diff --git a/src/commandlineoptions.cpp b/src/commandlineoptions.cpp index 34a08f6d..14090e38 100644 --- a/src/commandlineoptions.cpp +++ b/src/commandlineoptions.cpp @@ -5,28 +5,36 @@ #include #include -#define TR(x) QCoreApplication::translate("main", (x)) +#include + +using vnotex::MainWindow; CommandLineOptions::ParseResult CommandLineOptions::parse(const QStringList &p_arguments) { QCommandLineParser parser; - parser.setApplicationDescription(TR("A pleasant note-taking platform.")); + parser.setApplicationDescription(MainWindow::tr("A pleasant note-taking platform.")); const auto helpOpt = parser.addHelpOption(); const auto versionOpt = parser.addVersionOption(); // Positional arguments. - parser.addPositionalArgument("paths", TR("Files or folders to open.")); + parser.addPositionalArgument("paths", MainWindow::tr("Files or folders to open.")); - const QCommandLineOption verboseOpt("verbose", TR("Print more logs.")); + const QCommandLineOption verboseOpt("verbose", MainWindow::tr("Print more logs.")); parser.addOption(verboseOpt); // WebEngine options. // No need to handle them. Just add them to the parser to avoid parse error. - QCommandLineOption webRemoteDebuggingPortOpt("remote-debugging-port", - TR("WebEngine remote debugging port."), - "port_number"); - webRemoteDebuggingPortOpt.setFlags(QCommandLineOption::HiddenFromHelp); - parser.addOption(webRemoteDebuggingPortOpt); + { + QCommandLineOption webRemoteDebuggingPortOpt("remote-debugging-port", + MainWindow::tr("WebEngine remote debugging port."), + "port_number"); + webRemoteDebuggingPortOpt.setFlags(QCommandLineOption::HiddenFromHelp); + parser.addOption(webRemoteDebuggingPortOpt); + + QCommandLineOption webNoSandboxOpt("no-sandbox", MainWindow::tr("WebEngine without sandbox.")); + webNoSandboxOpt.setFlags(QCommandLineOption::HiddenFromHelp); + parser.addOption(webNoSandboxOpt); + } if (!parser.parse(p_arguments)) { m_errorMsg = parser.errorText(); diff --git a/src/core/core.pri b/src/core/core.pri index 2eccfd18..f5407f21 100644 --- a/src/core/core.pri +++ b/src/core/core.pri @@ -42,6 +42,7 @@ HEADERS += \ $$PWD/filelocator.h \ $$PWD/fileopenparameters.h \ $$PWD/htmltemplatehelper.h \ + $$PWD/location.h \ $$PWD/logger.h \ $$PWD/mainconfig.h \ $$PWD/markdowneditorconfig.h \ diff --git a/src/core/coreconfig.h b/src/core/coreconfig.h index 78613c1f..c9817700 100644 --- a/src/core/coreconfig.h +++ b/src/core/coreconfig.h @@ -23,6 +23,8 @@ namespace vnotex CloseTab, NavigationDock, OutlineDock, + SearchDock, + LocationListDock, NavigationMode, LocateNode, VerticalSplit, diff --git a/src/core/fileopenparameters.h b/src/core/fileopenparameters.h index ae50b03c..d32818ed 100644 --- a/src/core/fileopenparameters.h +++ b/src/core/fileopenparameters.h @@ -29,6 +29,9 @@ namespace vnotex // Open as read-only. bool m_readOnly = false; + + // If m_lineNumber > -1, it indicates the line to scroll to after opening the file. + int m_lineNumber = -1; }; } diff --git a/src/core/global.h b/src/core/global.h index f05e12a9..a828442e 100644 --- a/src/core/global.h +++ b/src/core/global.h @@ -64,12 +64,14 @@ namespace vnotex enum FindOption { - None = 0, + FindNone = 0, FindBackward = 0x1U, CaseSensitive = 0x2U, WholeWordOnly = 0x4U, RegularExpression = 0x8U, - IncrementalSearch = 0x10U + IncrementalSearch = 0x10U, + // Used in full-text search. + FuzzySearch = 0x20U }; Q_DECLARE_FLAGS(FindOptions, FindOption); diff --git a/src/core/location.h b/src/core/location.h new file mode 100644 index 00000000..8003fa53 --- /dev/null +++ b/src/core/location.h @@ -0,0 +1,72 @@ +#ifndef LOCATION_H +#define LOCATION_H + +#include + +namespace vnotex +{ + struct Location + { + friend QDebug operator<<(QDebug p_dbg, const Location &p_loc) + { + QDebugStateSaver saver(p_dbg); + p_dbg.nospace() << p_loc.m_path << ":" << p_loc.m_lineNumber; + return p_dbg; + } + + // TODO: support encoding like buffer/notebook. + QString m_path; + + QString m_displayPath; + + int m_lineNumber = -1; + }; + + enum class LocationType + { + Buffer, + File, + Folder, + Notebook + }; + + struct ComplexLocation + { + struct Line + { + Line() = default; + + Line(int p_lineNumber, const QString &p_text) + : m_lineNumber(p_lineNumber), + m_text(p_text) + { + } + + int m_lineNumber = -1; + + QString m_text; + }; + + void addLine(int p_lineNumber, const QString &p_text) + { + m_lines.push_back(Line(p_lineNumber, p_text)); + } + + friend QDebug operator<<(QDebug p_dbg, const ComplexLocation &p_loc) + { + QDebugStateSaver saver(p_dbg); + p_dbg.nospace() << static_cast(p_loc.m_type) << p_loc.m_path << p_loc.m_displayPath; + return p_dbg; + } + + LocationType m_type = LocationType::File; + + QString m_path; + + QString m_displayPath; + + QVector m_lines; + }; +} + +#endif // LOCATION_H diff --git a/src/core/sessionconfig.cpp b/src/core/sessionconfig.cpp index 58280373..034eed1b 100644 --- a/src/core/sessionconfig.cpp +++ b/src/core/sessionconfig.cpp @@ -62,6 +62,8 @@ void SessionConfig::init() } m_exportOption.fromJson(sessionJobj[QStringLiteral("export_option")].toObject()); + + m_searchOption.fromJson(sessionJobj[QStringLiteral("search_option")].toObject()); } void SessionConfig::loadCore(const QJsonObject &p_session) @@ -177,6 +179,7 @@ QJsonObject SessionConfig::toJson() const obj[QStringLiteral("notebooks")] = saveNotebooks(); obj[QStringLiteral("state_geometry")] = saveStateAndGeometry(); obj[QStringLiteral("export_option")] = m_exportOption.toJson(); + obj[QStringLiteral("search_option")] = m_searchOption.toJson(); return obj; } @@ -289,6 +292,16 @@ void SessionConfig::setExportOption(const ExportOption &p_option) updateConfig(m_exportOption, p_option, this); } +const SearchOption &SessionConfig::getSearchOption() const +{ + return m_searchOption; +} + +void SessionConfig::setSearchOption(const SearchOption &p_option) +{ + updateConfig(m_searchOption, p_option, this); +} + void SessionConfig::loadStateAndGeometry(const QJsonObject &p_session) { const auto obj = p_session.value(QStringLiteral("state_geometry")).toObject(); diff --git a/src/core/sessionconfig.h b/src/core/sessionconfig.h index 16ba4236..c468a11b 100644 --- a/src/core/sessionconfig.h +++ b/src/core/sessionconfig.h @@ -7,6 +7,7 @@ #include #include +#include namespace vnotex { @@ -87,6 +88,9 @@ namespace vnotex const ExportOption &getExportOption() const; void setExportOption(const ExportOption &p_option); + const SearchOption &getSearchOption() const; + void setSearchOption(const SearchOption &p_option); + private: void loadCore(const QJsonObject &p_session); @@ -123,6 +127,8 @@ namespace vnotex int m_minimizeToSystemTray = -1; ExportOption m_exportOption; + + SearchOption m_searchOption; }; } // ns vnotex diff --git a/src/core/vnotex.cpp b/src/core/vnotex.cpp index cfaecd9e..2f581fe3 100644 --- a/src/core/vnotex.cpp +++ b/src/core/vnotex.cpp @@ -8,6 +8,7 @@ #include "buffermgr.h" #include "configmgr.h" #include "coreconfig.h" +#include "location.h" #include "fileopenparameters.h" diff --git a/src/core/vnotex.h b/src/core/vnotex.h index aa870834..0872a6cd 100644 --- a/src/core/vnotex.h +++ b/src/core/vnotex.h @@ -16,6 +16,7 @@ namespace vnotex struct FileOpenParameters; class Event; class Notebook; + struct ComplexLocation; class VNoteX : public QObject { diff --git a/src/core/widgetconfig.h b/src/core/widgetconfig.h index 0da1ce85..3de57a58 100644 --- a/src/core/widgetconfig.h +++ b/src/core/widgetconfig.h @@ -39,7 +39,7 @@ namespace vnotex private: int m_outlineAutoExpandedLevel = 6; - FindOptions m_findAndReplaceOptions = FindOption::None; + FindOptions m_findAndReplaceOptions = FindOption::FindNone; int m_nodeExplorerViewOrder = 0; diff --git a/src/data/core/core.qrc b/src/data/core/core.qrc index bf3f485a..8624bbfa 100644 --- a/src/data/core/core.qrc +++ b/src/data/core/core.qrc @@ -35,8 +35,8 @@ icons/remove_notebook.svg icons/close_notebook.svg icons/recycle_bin.svg - icons/saved.svg icons/save_editor.svg + icons/buffer.svg icons/attachment_editor.svg icons/attachment_full_editor.svg icons/split_menu.svg @@ -74,6 +74,8 @@ icons/stay_on_top.svg icons/outline_editor.svg icons/find_replace_editor.svg + icons/search.svg + icons/cancel.svg icons/section_number_editor.svg icons/sort.svg logo/vnote.svg diff --git a/src/data/core/icons/buffer.svg b/src/data/core/icons/buffer.svg new file mode 100644 index 00000000..f0aaaa8a --- /dev/null +++ b/src/data/core/icons/buffer.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/src/data/core/icons/cancel.svg b/src/data/core/icons/cancel.svg new file mode 100644 index 00000000..980efb8b --- /dev/null +++ b/src/data/core/icons/cancel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/data/core/icons/saved.svg b/src/data/core/icons/saved.svg deleted file mode 100644 index 97ffd9ff..00000000 --- a/src/data/core/icons/saved.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/data/core/icons/search.svg b/src/data/core/icons/search.svg new file mode 100644 index 00000000..0927bfb2 --- /dev/null +++ b/src/data/core/icons/search.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/data/core/vnotex.json b/src/data/core/vnotex.json index f45b65f2..189fe07f 100644 --- a/src/data/core/vnotex.json +++ b/src/data/core/vnotex.json @@ -17,6 +17,8 @@ "CloseTab" : "Ctrl+G, X", "NavigationDock" : "Ctrl+G, 1", "OutlineDock" : "Ctrl+G, 2", + "SearchDock" : "Ctrl+G, 3", + "LocationListDock" : "Ctrl+G, 4", "NavigationMode" : "Ctrl+G, W", "LocateNode" : "Ctrl+G, D", "VerticalSplit" : "Ctrl+G, \\", @@ -260,7 +262,7 @@ "//comment" : "Whether insert the file name as title on new file", "insert_file_name_as_title" : true, "//comment" : "none/read/edit", - "section_number" : "read", + "section_number" : "none", "//comment" : "Base level to start section numbering", "section_number_base_level" : 2, "//comment" : "Style of the section number in edit mode", diff --git a/src/data/extra/themes/moonlight/interface.qss b/src/data/extra/themes/moonlight/interface.qss index 6a32e096..634d0ba4 100644 --- a/src/data/extra/themes/moonlight/interface.qss +++ b/src/data/extra/themes/moonlight/interface.qss @@ -412,6 +412,24 @@ vnotex--MainWindow QLabel#MainWindowTipsLabel { } /* QLineEdit */ +QLineEdit[EmbeddedLineEdit="true"] { + border: none; + padding: 0px; + margin: 0px; + color: @widgets#qlineedit#fg; + background-color: transparent; +} + +QLineEdit[EmbeddedLineEdit="true"]:focus { + border: none; + background-color: @widgets#qlineedit#focus#bg; +} + +QLineEdit[EmbeddedLineEdit="true"]:hover { + border: none; + background-color: @widgets#qlineedit#hover#bg; +} + QLineEdit { border: 1px solid @widgets#qlineedit#border; padding: 3px; diff --git a/src/data/extra/themes/moonlight/palette.json b/src/data/extra/themes/moonlight/palette.json index b22f6820..0daaf67e 100644 --- a/src/data/extra/themes/moonlight/palette.json +++ b/src/data/extra/themes/moonlight/palette.json @@ -233,6 +233,11 @@ "fg" : "@base#icon#inactive#fg" } }, + "locationlist" : { + "node_icon" : { + "fg" : "@base#icon#fg" + } + }, "viewsplit" : { "action_button" : { "fg" : "@base#icon#inactive#fg", diff --git a/src/data/extra/themes/native/palette.json b/src/data/extra/themes/native/palette.json index 96ff0826..91967386 100644 --- a/src/data/extra/themes/native/palette.json +++ b/src/data/extra/themes/native/palette.json @@ -92,6 +92,11 @@ "fg" : "@base#icon#disabled#fg" } }, + "locationlist" : { + "node_icon" : { + "fg" : "@base#icon#fg" + } + }, "viewsplit" : { "action_button" : { "fg" : "#808080", diff --git a/src/data/extra/themes/pure/interface.qss b/src/data/extra/themes/pure/interface.qss index 6a32e096..634d0ba4 100644 --- a/src/data/extra/themes/pure/interface.qss +++ b/src/data/extra/themes/pure/interface.qss @@ -412,6 +412,24 @@ vnotex--MainWindow QLabel#MainWindowTipsLabel { } /* QLineEdit */ +QLineEdit[EmbeddedLineEdit="true"] { + border: none; + padding: 0px; + margin: 0px; + color: @widgets#qlineedit#fg; + background-color: transparent; +} + +QLineEdit[EmbeddedLineEdit="true"]:focus { + border: none; + background-color: @widgets#qlineedit#focus#bg; +} + +QLineEdit[EmbeddedLineEdit="true"]:hover { + border: none; + background-color: @widgets#qlineedit#hover#bg; +} + QLineEdit { border: 1px solid @widgets#qlineedit#border; padding: 3px; diff --git a/src/data/extra/themes/pure/palette.json b/src/data/extra/themes/pure/palette.json index e90bc92f..ec677a37 100644 --- a/src/data/extra/themes/pure/palette.json +++ b/src/data/extra/themes/pure/palette.json @@ -229,6 +229,11 @@ "fg" : "@base#icon#inactive#fg" } }, + "locationlist" : { + "node_icon" : { + "fg" : "@base#icon#fg" + } + }, "viewsplit" : { "action_button" : { "fg" : "@base#icon#inactive#fg", diff --git a/src/main.cpp b/src/main.cpp index 6fb3ee5c..a24f1f24 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -89,8 +89,9 @@ int main(int argc, char *argv[]) break; case CommandLineOptions::Error: - showMessageOnCommandLineIfAvailable(cmdOptions.m_errorMsg); - return -1; + fprintf(stderr, "%s\n", qPrintable(cmdOptions.m_errorMsg)); + // Arguments to WebEngineView will be unknown ones. So just let it go. + break; case CommandLineOptions::VersionRequested: { @@ -244,6 +245,6 @@ void showMessageOnCommandLineIfAvailable(const QString &p_msg) MessageBoxHelper::notify(MessageBoxHelper::Information, QString("
%1
").arg(p_msg)); #else - printf("%s\n", qPrintable(p_msg)); + fprintf(stderr, "%s\n", qPrintable(p_msg)); #endif } diff --git a/src/search/filesearchengine.cpp b/src/search/filesearchengine.cpp new file mode 100644 index 00000000..20324251 --- /dev/null +++ b/src/search/filesearchengine.cpp @@ -0,0 +1,259 @@ +#include "filesearchengine.h" + +#include +#include +#include + +#include "searchresultitem.h" + +using namespace vnotex; + +FileSearchEngineWorker::FileSearchEngineWorker(QObject *p_parent) + : QThread(p_parent) +{ +} + +void FileSearchEngineWorker::setData(const QVector &p_items, + const QSharedPointer &p_option, + const SearchToken &p_token) +{ + m_items = p_items; + m_option = p_option; + m_token = p_token; +} + +void FileSearchEngineWorker::stop() +{ + m_askedToStop.store(1); +} + +bool FileSearchEngineWorker::isAskedToStop() const +{ + return m_askedToStop.load() == 1; +} + +void FileSearchEngineWorker::run() +{ + const int c_batchSize = 100; + + QMimeDatabase mimeDatabase; + m_state = SearchState::Busy; + + m_results.clear(); + int nr = 0; + for (const auto &item : m_items) { + if (isAskedToStop()) { + m_state = SearchState::Stopped; + break; + } + + const QMimeType mimeType = mimeDatabase.mimeTypeForFile(item.m_filePath); + if (mimeType.isValid() && !mimeType.inherits(QStringLiteral("text/plain"))) { + appendError(tr("Skip binary file (%1)").arg(item.m_filePath)); + continue; + } + + searchFile(item.m_filePath, item.m_displayPath); + + if (++nr >= c_batchSize) { + nr = 0; + processBatchResults(); + } + } + + processBatchResults(); + + if (m_state == SearchState::Busy) { + m_state = SearchState::Finished; + } +} + +void FileSearchEngineWorker::appendError(const QString &p_err) +{ + m_errors.append(p_err); +} + +void FileSearchEngineWorker::searchFile(const QString &p_filePath, const QString &p_displayPath) +{ + QFile file(p_filePath); + if (!file.open(QIODevice::ReadOnly)) { + return; + } + + const bool shouldStartBatchMode = m_token.shouldStartBatchMode(); + if (shouldStartBatchMode) { + m_token.startBatchMode(); + } + + QSharedPointer resultItem; + + int lineNum = 1; + QTextStream ins(&file); + while (!ins.atEnd()) { + if (isAskedToStop()) { + m_state = SearchState::Stopped; + break; + } + + const auto lineText = ins.readLine(); + bool matched = false; + if (!shouldStartBatchMode) { + matched = m_token.matched(lineText); + } else { + matched = m_token.matchedInBatchMode(lineText); + } + + if (matched) { + if (resultItem) { + resultItem->addLine(lineNum, lineText); + } else { + resultItem = SearchResultItem::createFileItem(p_filePath, p_displayPath, lineNum, lineText); + } + } + + if (shouldStartBatchMode && m_token.readyToEndBatchMode()) { + break; + } + + ++lineNum; + } + + if (shouldStartBatchMode) { + bool allMatched = m_token.readyToEndBatchMode(); + m_token.endBatchMode(); + + if (!allMatched) { + // This file does not meet all the tokens. + resultItem.reset(); + } + } + + if (resultItem) { + m_results.append(resultItem); + } +} + +void FileSearchEngineWorker::processBatchResults() +{ + if (!m_results.isEmpty()) { + emit resultItemsReady(m_results); + m_results.clear(); + } +} + +FileSearchEngine::FileSearchEngine() +{ +} + +FileSearchEngine::~FileSearchEngine() +{ + stopInternal(); + clearInternal(); +} + +void FileSearchEngine::search(const QSharedPointer &p_option, + const SearchToken &p_token, + const QVector &p_items) +{ + int numThread = QThread::idealThreadCount(); + if (numThread < 1) { + numThread = 1; + } + + Q_ASSERT(!p_items.isEmpty()); + if (p_items.size() < numThread) { + numThread = 1; + } + + clearWorkers(); + m_workers.reserve(numThread); + const int totalSize = p_items.size(); + const int step = totalSize / numThread; + int remain = totalSize % numThread; + int start = 0; + for (int i = 0; i < numThread && start < totalSize; ++i) { + int len = step; + if (remain) { + ++len; + --remain; + } + + if (start + len > totalSize) { + len = totalSize - start; + } + + auto th = QSharedPointer::create(); + th->setData(p_items.mid(start, len), p_option, p_token); + connect(th.data(), &FileSearchEngineWorker::finished, + this, &FileSearchEngine::handleWorkerFinished); + connect(th.data(), &FileSearchEngineWorker::resultItemsReady, + this, &FileSearchEngine::resultItemsAdded); + + m_workers.append(th); + th->start(); + + start += len; + } +} + +void FileSearchEngine::stop() +{ + stopInternal(); +} + +void FileSearchEngine::stopInternal() +{ + for (const auto &th : m_workers) { + th->stop(); + } +} + +void FileSearchEngine::clear() +{ + clearInternal(); +} + +void FileSearchEngine::clearInternal() +{ + clearWorkers(); +} + +void FileSearchEngine::clearWorkers() +{ + for (const auto &th : m_workers) { + th->quit(); + th->wait(); + } + + m_workers.clear(); + m_numOfFinishedWorkers = 0; +} + +void FileSearchEngine::handleWorkerFinished() +{ + ++m_numOfFinishedWorkers; + if (m_numOfFinishedWorkers == m_workers.size()) { + SearchState state = SearchState::Finished; + + for (const auto &th : m_workers) { + if (th->m_state == SearchState::Failed) { + if (state != SearchState::Stopped) { + state = SearchState::Failed; + } + } else if (th->m_state == SearchState::Stopped) { + state = SearchState::Stopped; + } + + for (const auto &err : th->m_errors) { + emit logRequested(err); + } + + Q_ASSERT(th->isFinished()); + } + + m_workers.clear(); + m_numOfFinishedWorkers = 0; + + emit finished(state); + } +} diff --git a/src/search/filesearchengine.h b/src/search/filesearchengine.h new file mode 100644 index 00000000..e0c81173 --- /dev/null +++ b/src/search/filesearchengine.h @@ -0,0 +1,98 @@ +#ifndef SEARCHENGINE_H +#define SEARCHENGINE_H + +#include "isearchengine.h" + +#include +#include +#include +#include + +#include "searchtoken.h" +#include "searchdata.h" + +namespace vnotex +{ + struct SearchResultItem; + + class FileSearchEngineWorker : public QThread + { + Q_OBJECT + friend class FileSearchEngine; + public: + explicit FileSearchEngineWorker(QObject *p_parent = nullptr); + + ~FileSearchEngineWorker() = default; + + void setData(const QVector &p_items, + const QSharedPointer &p_option, + const SearchToken &p_token); + + public slots: + void stop(); + + signals: + void resultItemsReady(const QVector> &p_items); + + protected: + void run() Q_DECL_OVERRIDE; + + private: + void appendError(const QString &p_err); + + void searchFile(const QString &p_filePath, const QString &p_displayPath); + + void processBatchResults(); + + bool isAskedToStop() const; + + QAtomicInt m_askedToStop = 0; + + QVector m_items; + + SearchToken m_token; + + QSharedPointer m_option; + + SearchState m_state = SearchState::Idle; + + QStringList m_errors; + + QVector> m_results; + }; + + class FileSearchEngine : public ISearchEngine + { + Q_OBJECT + public: + FileSearchEngine(); + + ~FileSearchEngine(); + + void search(const QSharedPointer &p_option, + const SearchToken &p_token, + const QVector &p_items); + + void stop() Q_DECL_OVERRIDE; + + void clear() Q_DECL_OVERRIDE; + + private slots: + void handleWorkerFinished(); + + private: + void clearWorkers(); + + // Need non-virtual version of this. + void stopInternal(); + + // Need non-virtual version of this. + void clearInternal(); + + int m_numOfFinishedWorkers = 0; + + QVector> m_workers; + }; +} + +#endif // SEARCHENGINE_H diff --git a/src/search/isearchengine.h b/src/search/isearchengine.h new file mode 100644 index 00000000..57cb2a7e --- /dev/null +++ b/src/search/isearchengine.h @@ -0,0 +1,57 @@ +#ifndef ISEARCHENGINE_H +#define ISEARCHENGINE_H + +#include +#include +#include +#include + +#include "searchdata.h" + +namespace vnotex +{ + struct SearchResultItem; + + class SearchToken; + + struct SearchSecondPhaseItem + { + SearchSecondPhaseItem() = default; + + SearchSecondPhaseItem(const QString &p_filePath, const QString &p_displayPath) + : m_filePath(p_filePath), + m_displayPath(p_displayPath) + { + } + + QString m_filePath; + + QString m_displayPath; + }; + + class ISearchEngine : public QObject + { + Q_OBJECT + public: + ISearchEngine() = default; + + virtual ~ISearchEngine() = default; + + virtual void search(const QSharedPointer &p_option, + const SearchToken &p_token, + const QVector &p_items) = 0; + + virtual void stop() = 0; + + virtual void clear() = 0; + + signals: + void finished(SearchState p_state); + + void resultItemsAdded(const QVector> &p_items); + + void logRequested(const QString &p_log); + }; +} + +#endif // ISEARCHENGINE_H diff --git a/src/search/search.pri b/src/search/search.pri new file mode 100644 index 00000000..d6a7005c --- /dev/null +++ b/src/search/search.pri @@ -0,0 +1,17 @@ +QT += widgets + +HEADERS += \ + $$PWD/filesearchengine.h \ + $$PWD/isearchengine.h \ + $$PWD/searchdata.h \ + $$PWD/searcher.h \ + $$PWD/searchresultitem.h \ + $$PWD/searchtoken.h + +SOURCES += \ + $$PWD/filesearchengine.cpp \ + $$PWD/searchdata.cpp \ + $$PWD/searcher.cpp \ + $$PWD/searchresultitem.cpp \ + $$PWD/searchtoken.cpp + diff --git a/src/search/searchdata.cpp b/src/search/searchdata.cpp new file mode 100644 index 00000000..51830650 --- /dev/null +++ b/src/search/searchdata.cpp @@ -0,0 +1,47 @@ +#include "searchdata.h" + +#include + +using namespace vnotex; + +SearchOption::SearchOption() + : m_objects(SearchObject::SearchName | SearchObject::SearchContent), + m_targets(SearchTarget::SearchFile | SearchTarget::SearchFolder) +{ +} + +QJsonObject SearchOption::toJson() const +{ + QJsonObject obj; + obj["file_pattern"] = m_filePattern; + obj["scope"] = static_cast(m_scope); + obj["objects"] = static_cast(m_objects); + obj["targets"] = static_cast(m_targets); + obj["engine"] = static_cast(m_engine); + obj["find_options"] = static_cast(m_findOptions); + return obj; +} + +void SearchOption::fromJson(const QJsonObject &p_obj) +{ + if (p_obj.isEmpty()) { + return; + } + + m_filePattern = p_obj["file_pattern"].toString(); + m_scope = static_cast(p_obj["scope"].toInt()); + m_objects = static_cast(p_obj["objects"].toInt()); + m_targets = static_cast(p_obj["targets"].toInt()); + m_engine = static_cast(p_obj["engine"].toInt()); + m_findOptions = static_cast(p_obj["find_options"].toInt()); +} + +bool SearchOption::operator==(const SearchOption &p_other) const +{ + return m_filePattern == p_other.m_filePattern + && m_scope == p_other.m_scope + && m_objects == p_other.m_objects + && m_targets == p_other.m_targets + && m_engine == p_other.m_engine + && m_findOptions == p_other.m_findOptions; +} diff --git a/src/search/searchdata.h b/src/search/searchdata.h new file mode 100644 index 00000000..d81cff42 --- /dev/null +++ b/src/search/searchdata.h @@ -0,0 +1,110 @@ +#ifndef SEARCHOPTION_H +#define SEARCHOPTION_H + +#include +#include + +#include + +namespace vnotex +{ + enum class SearchState + { + Idle = 0, + Busy, + Finished, + Failed, + Stopped + }; + + class SearchTranslate : public QObject + { + Q_OBJECT + }; + + inline QString SearchStateToString(SearchState p_state) + { + switch (p_state) { + case SearchState::Idle: + return SearchTranslate::tr("Idle"); + + case SearchState::Busy: + return SearchTranslate::tr("Busy"); + + case SearchState::Finished: + return SearchTranslate::tr("Finished"); + + case SearchState::Failed: + return SearchTranslate::tr("Failed"); + + case SearchState::Stopped: + return SearchTranslate::tr("Stopped"); + } + + return QString(); + } + + enum class SearchScope + { + Buffers = 0, + CurrentFolder, + CurrentNotebook, + AllNotebooks + }; + + enum SearchObject + { + ObjectNone = 0, + SearchName = 0x1UL, + SearchContent = 0x2UL, + SearchOutline = 0x4UL, + SearchTag = 0x8UL, + SearchPath = 0x10UL + }; + Q_DECLARE_FLAGS(SearchObjects, SearchObject); + + enum SearchTarget + { + TargetNone = 0, + SearchFile = 0x1UL, + SearchFolder = 0x2UL, + SearchNotebook = 0x4UL + }; + Q_DECLARE_FLAGS(SearchTargets, SearchTarget); + + enum class SearchEngine + { + Internal = 0 + }; + + struct SearchOption + { + SearchOption(); + + QJsonObject toJson() const; + void fromJson(const QJsonObject &p_obj); + + bool operator==(const SearchOption &p_other) const; + + QString m_keyword; + + QString m_filePattern; + + SearchScope m_scope = SearchScope::CurrentNotebook; + + // *nix requests to init in the constructor. + SearchObjects m_objects; + + // *nix requests to init in the constructor. + SearchTargets m_targets; + + SearchEngine m_engine = SearchEngine::Internal; + + FindOptions m_findOptions = FindOption::FindNone; + }; +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(vnotex::SearchObjects); +Q_DECLARE_OPERATORS_FOR_FLAGS(vnotex::SearchTargets); + +#endif // SEARCHOPTION_H diff --git a/src/search/searcher.cpp b/src/search/searcher.cpp new file mode 100644 index 00000000..96f31b3e --- /dev/null +++ b/src/search/searcher.cpp @@ -0,0 +1,501 @@ +#include "searcher.h" + +#include +#include + +#include +#include +#include +#include + +#include "searchresultitem.h" +#include "filesearchengine.h" + +using namespace vnotex; + +Searcher::Searcher(QObject *p_parent) + : QObject(p_parent) +{ +} + +void Searcher::clear() +{ + m_option.clear(); + + if (m_engine) { + m_engine->clear(); + m_engine.reset(); + } + + m_askedToStop = false; +} + +void Searcher::stop() +{ + m_askedToStop = true; + + if (m_engine) { + m_engine->stop(); + } +} + +SearchState Searcher::search(const QSharedPointer &p_option, const QList &p_buffers) +{ + if (!(p_option->m_targets & SearchTarget::SearchFile)) { + // Only File target is applicable. + return SearchState::Finished; + } + + if (!prepare(p_option)) { + return SearchState::Failed; + } + + emit logRequested(tr("Searching %n buffer(s)", "", p_buffers.size())); + + emit progressUpdated(0, p_buffers.size()); + for (int i = 0; i < p_buffers.size(); ++i) { + if (!p_buffers[i]) { + continue; + } + + if (isAskedToStop()) { + return SearchState::Stopped; + } + + auto file = p_buffers[i]->getFile(); + if (!firstPhaseSearch(file.data())) { + return SearchState::Failed; + } + + emit progressUpdated(i + 1, p_buffers.size()); + } + + return SearchState::Finished; +} + +SearchState Searcher::search(const QSharedPointer &p_option, Node *p_folder) +{ + Q_ASSERT(p_folder->isContainer()); + if (!(p_option->m_targets & (SearchTarget::SearchFile | SearchTarget::SearchFolder))) { + // Only File/Folder target is applicable. + return SearchState::Finished; + } + + if (!prepare(p_option)) { + return SearchState::Failed; + } + + emit logRequested(tr("Searching folder (%1)").arg(p_folder->getName())); + + QVector secondPhaseItems; + if (!firstPhaseSearchFolder(p_folder, secondPhaseItems)) { + return SearchState::Failed; + } + + if (isAskedToStop()) { + return SearchState::Stopped; + } + + if (!secondPhaseItems.isEmpty()) { + // Do second phase search. + if (!secondPhaseSearch(secondPhaseItems)) { + return SearchState::Failed; + } + + if (isAskedToStop()) { + return SearchState::Stopped; + } + + return SearchState::Busy; + } + + return SearchState::Finished; +} + +SearchState Searcher::search(const QSharedPointer &p_option, const QVector &p_notebooks) +{ + if (!prepare(p_option)) { + return SearchState::Failed; + } + + QVector secondPhaseItems; + + emit progressUpdated(0, p_notebooks.size()); + for (int i = 0; i < p_notebooks.size(); ++i) { + if (isAskedToStop()) { + return SearchState::Stopped; + } + + emit logRequested(tr("Searching notebook (%1)").arg(p_notebooks[i]->getName())); + + if (!firstPhaseSearch(p_notebooks[i], secondPhaseItems)) { + return SearchState::Failed; + } + + emit progressUpdated(i + 1, p_notebooks.size()); + } + + if (isAskedToStop()) { + return SearchState::Stopped; + } + + if (!secondPhaseItems.isEmpty()) { + // Do second phase search. + if (!secondPhaseSearch(secondPhaseItems)) { + return SearchState::Failed; + } + + if (isAskedToStop()) { + return SearchState::Stopped; + } + + return SearchState::Busy; + } + + return SearchState::Finished; +} + +bool Searcher::prepare(const QSharedPointer &p_option) +{ + Q_ASSERT(!m_option); + m_option = p_option; + + if (!SearchToken::compile(m_option->m_keyword, m_option->m_findOptions, m_token)) { + emit logRequested(tr("Failed to compile tokens (%1)").arg(m_option->m_keyword)); + return false; + } + + if (m_option->m_filePattern.isEmpty()) { + m_filePattern = QRegularExpression(); + } else { + m_filePattern = QRegularExpression(QRegularExpression::wildcardToRegularExpression(m_option->m_filePattern), QRegularExpression::CaseInsensitiveOption); + } + + return true; +} + +bool Searcher::isAskedToStop() const +{ + QCoreApplication::sendPostedEvents(); + return m_askedToStop; +} + +static QString tryGetRelativePath(const File *p_file) +{ + const auto node = p_file->getNode(); + if (node) { + return node->fetchPath(); + } + return p_file->getFilePath(); +} + +bool Searcher::firstPhaseSearch(const File *p_file) +{ + if (!p_file) { + return true; + } + + Q_ASSERT(testTarget(SearchTarget::SearchFile)); + + const auto name = p_file->getName(); + if (!isFilePatternMatched(name)) { + return true; + } + + const auto filePath = p_file->getFilePath(); + const auto relativePath = tryGetRelativePath(p_file); + + if (testObject(SearchObject::SearchName)) { + if (isTokenMatched(name)) { + emit resultItemAdded(SearchResultItem::createBufferItem(filePath, relativePath, -1, name)); + } + } + + if (testObject(SearchObject::SearchPath)) { + if (isTokenMatched(relativePath)) { + emit resultItemAdded(SearchResultItem::createBufferItem(filePath, relativePath, -1, name)); + } + } + + if (testObject(SearchObject::SearchOutline)) { + emit logRequested(tr("Searching outline is not supported yet")); + } + + if (testObject(SearchObject::SearchTag)) { + emit logRequested(tr("Searching tag is not supported yet")); + } + + // Make SearchContent always the last one to check. + if (testObject(SearchObject::SearchContent)) { + if (!searchContent(p_file)) { + return false; + } + } + + return true; +} + +bool Searcher::isFilePatternMatched(const QString &p_name) const +{ + if (m_option->m_filePattern.isEmpty()) { + return true; + } + + return m_filePattern.match(p_name).hasMatch(); +} + +bool Searcher::testTarget(SearchTarget p_target) const +{ + return m_option->m_targets & p_target; +} + +bool Searcher::testObject(SearchObject p_object) const +{ + return m_option->m_objects & p_object; +} + +bool Searcher::isTokenMatched(const QString &p_text) const +{ + return m_token.matched(p_text); +} + +bool Searcher::searchContent(const File *p_file) +{ + const auto content = p_file->read(); + if (content.isEmpty()) { + return true; + } + + const bool shouldStartBatchMode = m_token.shouldStartBatchMode(); + if (shouldStartBatchMode) { + m_token.startBatchMode(); + } + + const auto filePath = p_file->getFilePath(); + const auto relativePath = tryGetRelativePath(p_file); + + QSharedPointer resultItem; + + int lineNum = 1; + int pos = 0; + int contentSize = content.size(); + QRegularExpression newlineRegExp("\\n|\\r\\n|\\r"); + while (pos < contentSize) { + if (isAskedToStop()) { + break; + } + + QRegularExpressionMatch match; + int idx = content.indexOf(newlineRegExp, pos, &match); + if (idx == -1) { + idx = contentSize; + } + + if (idx > pos) { + QString lineText = content.mid(pos, idx - pos); + bool matched = false; + if (!shouldStartBatchMode) { + matched = m_token.matched(lineText); + } else { + matched = m_token.matchedInBatchMode(lineText); + } + + if (matched) { + if (resultItem) { + resultItem->addLine(lineNum, lineText); + } else { + resultItem = SearchResultItem::createBufferItem(filePath, relativePath, lineNum, lineText); + } + } + } + + if (idx == contentSize) { + break; + } + + if (shouldStartBatchMode && m_token.readyToEndBatchMode()) { + break; + } + + pos = idx + match.capturedLength(); + ++lineNum; + } + + if (shouldStartBatchMode) { + bool allMatched = m_token.readyToEndBatchMode(); + m_token.endBatchMode(); + + if (!allMatched) { + // This file does not meet all the tokens. + resultItem.reset(); + } + } + + if (resultItem) { + emit resultItemAdded(resultItem); + } + + return true; +} + +bool Searcher::firstPhaseSearchFolder(Node *p_node, QVector &p_secondPhaseItems) +{ + if (!p_node) { + return true; + } + + Q_ASSERT(p_node->isContainer()); + Q_ASSERT(testTarget(SearchTarget::SearchFile) || testTarget(SearchTarget::SearchFolder)); + + p_node->load(); + + if (testTarget(SearchTarget::SearchFolder)) { + const auto name = p_node->getName(); + const auto folderPath = p_node->fetchAbsolutePath(); + const auto relativePath = p_node->fetchPath(); + if (testObject(SearchObject::SearchName)) { + if (isTokenMatched(name)) { + emit resultItemAdded(SearchResultItem::createFolderItem(folderPath, relativePath)); + } + } + + if (testObject(SearchObject::SearchPath)) { + if (isTokenMatched(relativePath)) { + emit resultItemAdded(SearchResultItem::createFolderItem(folderPath, relativePath)); + } + } + } + + // Search children. + const auto &children = p_node->getChildrenRef(); + for (const auto &child : children) { + if (isAskedToStop()) { + return true; + } + + if (child->hasContent() && testTarget(SearchTarget::SearchFile)) { + if (!firstPhaseSearch(child.data(), p_secondPhaseItems)) { + return false; + } + } + + if (child->isContainer()) { + if (!firstPhaseSearchFolder(child.data(), p_secondPhaseItems)) { + return false; + } + } + } + + return true; +} + +bool Searcher::firstPhaseSearch(Node *p_node, QVector &p_secondPhaseItems) +{ + if (!p_node) { + return true; + } + + Q_ASSERT(testTarget(SearchTarget::SearchFile)); + + const auto name = p_node->getName(); + if (!isFilePatternMatched(name)) { + return true; + } + + const auto filePath = p_node->fetchAbsolutePath(); + const auto relativePath = p_node->fetchPath(); + + if (testObject(SearchObject::SearchName)) { + if (isTokenMatched(name)) { + emit resultItemAdded(SearchResultItem::createFileItem(filePath, relativePath, -1, name)); + } + } + + if (testObject(SearchObject::SearchPath)) { + if (isTokenMatched(relativePath)) { + emit resultItemAdded(SearchResultItem::createFileItem(filePath, relativePath, -1, name)); + } + } + + if (testObject(SearchObject::SearchOutline)) { + emit logRequested(tr("Searching outline is not supported yet")); + } + + if (testObject(SearchObject::SearchTag)) { + emit logRequested(tr("Searching tag is not supported yet")); + } + + if (testObject(SearchObject::SearchContent)) { + p_secondPhaseItems.push_back(SearchSecondPhaseItem(filePath, relativePath)); + } + + return true; +} + +bool Searcher::firstPhaseSearch(Notebook *p_notebook, QVector &p_secondPhaseItems) +{ + if (!p_notebook) { + return true; + } + + if (testTarget(SearchTarget::SearchNotebook)) { + if (testObject(SearchObject::SearchName)) { + const auto name = p_notebook->getName(); + if (isTokenMatched(name)) { + emit resultItemAdded(SearchResultItem::createNotebookItem(p_notebook->getRootFolderAbsolutePath(), + name)); + } + } + } + + if (!testTarget(SearchTarget::SearchFile) && !testTarget(SearchTarget::SearchFolder)) { + return true; + } + + auto rootNode = p_notebook->getRootNode(); + Q_ASSERT(rootNode->isLoaded()); + const auto &children = rootNode->getChildrenRef(); + for (const auto &child : children) { + if (isAskedToStop()) { + return true; + } + + if (child->hasContent() && testTarget(SearchTarget::SearchFile)) { + if (!firstPhaseSearch(child.data(), p_secondPhaseItems)) { + return false; + } + } + + if (child->isContainer()) { + if (!firstPhaseSearchFolder(child.data(), p_secondPhaseItems)) { + return false; + } + } + } + + return true; +} + +bool Searcher::secondPhaseSearch(const QVector &p_secondPhaseItems) +{ + Q_ASSERT(!p_secondPhaseItems.isEmpty()); + qDebug() << "secondPhaseSearch" << p_secondPhaseItems.size(); + + createSearchEngine(); + + m_engine->search(m_option, m_token, p_secondPhaseItems); + connect(m_engine.data(), &ISearchEngine::finished, + this, &Searcher::finished); + connect(m_engine.data(), &ISearchEngine::logRequested, + this, &Searcher::logRequested); + connect(m_engine.data(), &ISearchEngine::resultItemsAdded, + this, &Searcher::resultItemsAdded); + return true; +} + +void Searcher::createSearchEngine() +{ + Q_ASSERT(m_option->m_engine == SearchEngine::Internal); + + m_engine.reset(new FileSearchEngine()); +} diff --git a/src/search/searcher.h b/src/search/searcher.h new file mode 100644 index 00000000..4cc9e6ab --- /dev/null +++ b/src/search/searcher.h @@ -0,0 +1,93 @@ +#ifndef SEARCHER_H +#define SEARCHER_H + +#include +#include +#include +#include + +#include "searchdata.h" +#include "searchtoken.h" +#include "isearchengine.h" + +namespace vnotex +{ + class Buffer; + class File; + struct SearchResultItem; + class Node; + class Notebook; + + class Searcher : public QObject + { + Q_OBJECT + public: + explicit Searcher(QObject *p_parent = nullptr); + + void clear(); + + void stop(); + + SearchState search(const QSharedPointer &p_option, const QList &p_buffers); + + SearchState search(const QSharedPointer &p_option, Node *p_folder); + + SearchState search(const QSharedPointer &p_option, const QVector &p_notebooks); + + signals: + void progressUpdated(int p_val, int p_maximum); + + void logRequested(const QString &p_log); + + void resultItemAdded(const QSharedPointer &p_item); + + void resultItemsAdded(const QVector> &p_items); + + void finished(SearchState p_state); + + private: + bool isAskedToStop() const; + + bool prepare(const QSharedPointer &p_option); + + // Return false if there is failure. + // Always search content at first phase. + bool firstPhaseSearch(const File *p_file); + + // Return false if there is failure. + bool firstPhaseSearchFolder(Node *p_node, QVector &p_secondPhaseItems); + + // Return false if there is failure. + bool firstPhaseSearch(Node *p_node, QVector &p_secondPhaseItems); + + // Return false if there is failure. + bool firstPhaseSearch(Notebook *p_notebook, QVector &p_secondPhaseItems); + + // Return false if there is failure. + bool secondPhaseSearch(const QVector &p_secondPhaseItems); + + bool isFilePatternMatched(const QString &p_name) const; + + bool testTarget(SearchTarget p_target) const; + + bool testObject(SearchObject p_object) const; + + bool isTokenMatched(const QString &p_text) const; + + bool searchContent(const File *p_file); + + void createSearchEngine(); + + QSharedPointer m_option; + + SearchToken m_token; + + QRegularExpression m_filePattern; + + bool m_askedToStop = false; + + QScopedPointer m_engine; + }; +} + +#endif // SEARCHER_H diff --git a/src/search/searchresultitem.cpp b/src/search/searchresultitem.cpp new file mode 100644 index 00000000..1d374e9c --- /dev/null +++ b/src/search/searchresultitem.cpp @@ -0,0 +1,54 @@ +#include "searchresultitem.h" + +using namespace vnotex; + +QSharedPointer SearchResultItem::createBufferItem(const QString &p_targetPath, + const QString &p_displayPath, + int p_lineNumber, + const QString &p_text) +{ + auto item = QSharedPointer::create(); + item->m_location.m_type = LocationType::Buffer; + item->m_location.m_path = p_targetPath; + item->m_location.m_displayPath = p_displayPath; + item->m_location.addLine(p_lineNumber, p_text); + return item; +} + +QSharedPointer SearchResultItem::createFileItem(const QString &p_targetPath, + const QString &p_displayPath, + int p_lineNumber, + const QString &p_text) +{ + auto item = QSharedPointer::create(); + item->m_location.m_type = LocationType::File; + item->m_location.m_path = p_targetPath; + item->m_location.m_displayPath = p_displayPath; + item->m_location.addLine(p_lineNumber, p_text); + return item; +} + +QSharedPointer SearchResultItem::createFolderItem(const QString &p_targetPath, + const QString &p_displayPath) +{ + auto item = QSharedPointer::create(); + item->m_location.m_type = LocationType::Folder; + item->m_location.m_path = p_targetPath; + item->m_location.m_displayPath = p_displayPath; + return item; +} + +QSharedPointer SearchResultItem::createNotebookItem(const QString &p_targetPath, + const QString &p_displayPath) +{ + auto item = QSharedPointer::create(); + item->m_location.m_type = LocationType::Notebook; + item->m_location.m_path = p_targetPath; + item->m_location.m_displayPath = p_displayPath; + return item; +} + +void SearchResultItem::addLine(int p_lineNumber, const QString &p_text) +{ + m_location.addLine(p_lineNumber, p_text); +} diff --git a/src/search/searchresultitem.h b/src/search/searchresultitem.h new file mode 100644 index 00000000..ec804eb6 --- /dev/null +++ b/src/search/searchresultitem.h @@ -0,0 +1,42 @@ +#ifndef SEARCHRESULTITEM_H +#define SEARCHRESULTITEM_H + +#include +#include +#include + +#include + +namespace vnotex +{ + struct SearchResultItem + { + friend QDebug operator<<(QDebug p_dbg, const SearchResultItem &p_item) + { + p_dbg << p_item.m_location; + return p_dbg; + } + + void addLine(int p_lineNumber, const QString &p_text); + + static QSharedPointer createBufferItem(const QString &p_targetPath, + const QString &p_displayPath, + int p_lineNumber, + const QString &p_text); + + static QSharedPointer createFileItem(const QString &p_targetPath, + const QString &p_displayPath, + int p_lineNumber, + const QString &p_text); + + static QSharedPointer createFolderItem(const QString &p_targetPath, + const QString &p_displayPath); + + static QSharedPointer createNotebookItem(const QString &p_targetPath, + const QString &p_displayPath); + + ComplexLocation m_location; + }; +} + +#endif // SEARCHRESULTITEM_H diff --git a/src/search/searchtoken.cpp b/src/search/searchtoken.cpp new file mode 100644 index 00000000..4cf576bf --- /dev/null +++ b/src/search/searchtoken.cpp @@ -0,0 +1,247 @@ +#include "searchtoken.h" + +#include +#include + +#include +#include + +using namespace vnotex; + +QScopedPointer SearchToken::s_parser; + +void SearchToken::clear() +{ + m_type = Type::PlainText; + m_operator = Operator::And; + m_caseSensitivity = Qt::CaseInsensitive; + m_keywords.clear(); + m_regularExpressions.clear(); + m_matchedConstraintsInBatchMode.clear(); + m_matchedConstraintsCountInBatchMode = 0; +} + +void SearchToken::append(const QString &p_text) +{ + m_keywords.append(p_text); +} + +void SearchToken::append(const QRegularExpression &p_regExp) +{ + m_regularExpressions.append(p_regExp); +} + +bool SearchToken::matched(const QString &p_text) const +{ + const int consSize = constraintSize(); + if (consSize == 0) { + return false; + } + + bool isMatched = m_operator == Operator::And ? true : false; + for (int i = 0; i < consSize; ++i) { + bool consMatched = false; + if (m_type == Type::PlainText) { + consMatched = p_text.contains(m_keywords[i], m_caseSensitivity); + } else { + consMatched = p_text.contains(m_regularExpressions[i]); + } + + if (consMatched) { + if (m_operator == Operator::Or) { + isMatched = true; + break; + } + } else if (m_operator == Operator::And) { + isMatched = false; + break; + } + } + + return isMatched; +} + +int SearchToken::constraintSize() const +{ + return (m_type == Type::PlainText ? m_keywords.size() : m_regularExpressions.size()); +} + +bool SearchToken::shouldStartBatchMode() const +{ + return constraintSize() > 1; +} + +void SearchToken::startBatchMode() +{ + m_matchedConstraintsInBatchMode.fill(false, constraintSize()); + m_matchedConstraintsCountInBatchMode = 0; +} + +bool SearchToken::matchedInBatchMode(const QString &p_text) +{ + bool isMatched = false; + const int consSize = m_matchedConstraintsInBatchMode.size(); + for (int i = 0; i < consSize; ++i) { + if (m_matchedConstraintsInBatchMode[i]) { + continue; + } + + bool consMatched = false; + if (m_type == Type::PlainText) { + consMatched = p_text.contains(m_keywords[i], m_caseSensitivity); + } else { + consMatched = p_text.contains(m_regularExpressions[i]); + } + + if (consMatched) { + m_matchedConstraintsInBatchMode[i] = true; + ++m_matchedConstraintsCountInBatchMode; + isMatched = true; + } + } + + return isMatched; +} + +bool SearchToken::readyToEndBatchMode() const +{ + if (m_operator == Operator::And) { + // We need all the tokens matched. + if (m_matchedConstraintsCountInBatchMode == m_matchedConstraintsInBatchMode.size()) { + return true; + } + } else { + // We only need one match. + if (m_matchedConstraintsCountInBatchMode > 0) { + return true; + } + } + + return false; +} + +void SearchToken::endBatchMode() +{ + m_matchedConstraintsInBatchMode.clear(); + m_matchedConstraintsCountInBatchMode = 0; +} + +bool SearchToken::isEmpty() const +{ + return constraintSize() == 0; +} + +void SearchToken::createCommandLineParser() +{ + if (s_parser) { + return; + } + + s_parser.reset(new QCommandLineParser()); + s_parser->setApplicationDescription(SearchPanel::tr("Full-text search.")); + + QCommandLineOption caseSensitiveOpt(QStringList() << "c" << "case-sensitive", SearchPanel::tr("Search in case sensitive.")); + s_parser->addOption(caseSensitiveOpt); + + QCommandLineOption regularExpressionOpt(QStringList() << "r" << "regular-expression", SearchPanel::tr("Search by regular expression.")); + s_parser->addOption(regularExpressionOpt); + + QCommandLineOption wholeWordOnlyOpt(QStringList() << "w" << "whole-word-only", SearchPanel::tr("Search whole word only.")); + s_parser->addOption(wholeWordOnlyOpt); + + QCommandLineOption fuzzySearchOpt(QStringList() << "f" << "fuzzy-search", SearchPanel::tr("Do a fuzzy search (not applicable to content search).")); + s_parser->addOption(fuzzySearchOpt); + + QCommandLineOption orOpt(QStringList() << "o" << "or", SearchPanel::tr("Do an OR combination of keywords.")); + s_parser->addOption(orOpt); + + s_parser->addPositionalArgument("keywords", SearchPanel::tr("Keywords to search.")); +} + +bool SearchToken::compile(const QString &p_keyword, FindOptions p_options, SearchToken &p_token) +{ + + p_token.clear(); + + if (p_keyword.isEmpty()) { + return false; + } + + createCommandLineParser(); + + auto caseSensitivity = p_options & FindOption::CaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive; + bool isRegularExpression = p_options & FindOption::RegularExpression; + bool isWholeWordOnly = p_options & FindOption::WholeWordOnly; + bool isFuzzySearch = p_options & FindOption::FuzzySearch; + + auto args = ProcessUtils::parseCombinedArgString(p_keyword); + // The parser needs the first arg to be the application name. + args.prepend("vnotex"); + if (!s_parser->parse(args)) + { + return false; + } + + if (s_parser->isSet("c")) { + caseSensitivity = Qt::CaseSensitive; + } + if (s_parser->isSet("r")) { + isRegularExpression = true; + } + if (s_parser->isSet("w")) { + isWholeWordOnly = true; + } + if (s_parser->isSet("f")) { + isFuzzySearch = true; + } + + args = s_parser->positionalArguments(); + if (args.isEmpty()) { + return false; + } + + p_token.m_caseSensitivity = caseSensitivity; + if (isRegularExpression || isWholeWordOnly || isFuzzySearch) { + p_token.m_type = Type::RegularExpression; + } else { + p_token.m_type = Type::PlainText; + } + p_token.m_operator = s_parser->isSet("o") ? Operator::Or : Operator::And; + + auto patternOptions = caseSensitivity == Qt::CaseInsensitive ? QRegularExpression::CaseInsensitiveOption + : QRegularExpression::NoPatternOption; + for (const auto &ar : args) { + if (ar.isEmpty()) { + continue; + } + + if (isRegularExpression) { + p_token.append(QRegularExpression(ar, patternOptions)); + } else if (isFuzzySearch) { + // ABC -> *A*B*C*. + QString wildcardText(ar.size() * 2 + 1, '*'); + for (int i = 0, j = 1; i < ar.size(); ++i, j += 2) { + wildcardText[j] = ar[i]; + } + + p_token.append(QRegularExpression(QRegularExpression::wildcardToRegularExpression(wildcardText), + patternOptions)); + } else if (isWholeWordOnly) { + auto pattern = QRegularExpression::escape(ar); + pattern = "\\b" + pattern + "\\b"; + p_token.append(QRegularExpression(pattern, patternOptions)); + } else { + p_token.append(ar); + } + } + + return !p_token.isEmpty(); +} + +QString SearchToken::getHelpText() +{ + createCommandLineParser(); + auto text = s_parser->helpText(); + // Skip the first line containing the application name. + return text.mid(text.indexOf('\n') + 1); +} diff --git a/src/search/searchtoken.h b/src/search/searchtoken.h new file mode 100644 index 00000000..d6dd3e02 --- /dev/null +++ b/src/search/searchtoken.h @@ -0,0 +1,86 @@ +#ifndef SEARCHTOKEN_H +#define SEARCHTOKEN_H + +#include +#include +#include +#include +#include +#include + +#include + +class QCommandLineParser; + +namespace vnotex +{ + class SearchToken + { + public: + enum class Type + { + PlainText, + RegularExpression + }; + + enum class Operator + { + And, + Or + }; + + void clear(); + + void append(const QString &p_text); + + void append(const QRegularExpression &p_regExp); + + // Whether @p_text is matched. + bool matched(const QString &p_text) const; + + int constraintSize() const; + + bool isEmpty() const; + + bool shouldStartBatchMode() const; + + // Batch Mode: use a list of text string to match the same token. + void startBatchMode(); + + // Match one string in batch mode. + // Return true if @p_text is matched. + bool matchedInBatchMode(const QString &p_text); + + bool readyToEndBatchMode() const; + + void endBatchMode(); + + // Compile tokens from keyword. + // Support some magic switchs in the keyword which will suppress the given options. + static bool compile(const QString &p_keyword, FindOptions p_options, SearchToken &p_token); + + static QString getHelpText(); + + private: + static void createCommandLineParser(); + + Type m_type = Type::PlainText; + + Operator m_operator = Operator::And; + + Qt::CaseSensitivity m_caseSensitivity = Qt::CaseInsensitive; + + QStringList m_keywords; + + QVector m_regularExpressions; + + // [i] is true only if m_keywords[i] or m_regularExpressions[i] is matched. + QBitArray m_matchedConstraintsInBatchMode; + + int m_matchedConstraintsCountInBatchMode = 0; + + static QScopedPointer s_parser; + }; +} + +#endif // SEARCHTOKEN_H diff --git a/src/src.pro b/src/src.pro index fd716c69..8887401a 100644 --- a/src/src.pro +++ b/src/src.pro @@ -45,6 +45,8 @@ include($$PWD/utils/utils.pri) include($$PWD/export/export.pri) +include($$PWD/search/search.pri) + include($$PWD/core/core.pri) include($$PWD/widgets/widgets.pri) diff --git a/src/utils/widgetutils.cpp b/src/utils/widgetutils.cpp index 8ec95643..af7da07a 100644 --- a/src/utils/widgetutils.cpp +++ b/src/utils/widgetutils.cpp @@ -22,6 +22,9 @@ #include #include #include +#include + +#include using namespace vnotex; @@ -361,3 +364,8 @@ void WidgetUtils::selectBaseName(QLineEdit *p_lineEdit) int dotIndex = text.lastIndexOf(QLatin1Char('.')); p_lineEdit->setSelection(0, (dotIndex == -1) ? text.size() : dotIndex); } + +void WidgetUtils::setContentsMargins(QLayout *p_layout) +{ + p_layout->setContentsMargins(CONTENTS_MARGIN, CONTENTS_MARGIN, CONTENTS_MARGIN, CONTENTS_MARGIN); +} diff --git a/src/utils/widgetutils.h b/src/utils/widgetutils.h index a00b28be..9163e833 100644 --- a/src/utils/widgetutils.h +++ b/src/utils/widgetutils.h @@ -18,6 +18,7 @@ class QListView; class QMenu; class QShortcut; class QLineEdit; +class QLayout; namespace vnotex { @@ -81,6 +82,8 @@ namespace vnotex // Select the base name part of the line edit content. static void selectBaseName(QLineEdit *p_lineEdit); + static void setContentsMargins(QLayout *p_layout); + private: static void resizeToHideScrollBar(QScrollArea *p_scroll, bool p_vertical, bool p_horizontal); }; diff --git a/src/widgets/attachmentpopup.cpp b/src/widgets/attachmentpopup.cpp index 4a85438a..5bdd6019 100644 --- a/src/widgets/attachmentpopup.cpp +++ b/src/widgets/attachmentpopup.cpp @@ -204,7 +204,7 @@ void AttachmentPopup::setupUI() QToolButton *AttachmentPopup::createButton() { auto btn = new QToolButton(this); - btn->setProperty(PropertyDefs::s_actionToolButton, true); + btn->setProperty(PropertyDefs::c_actionToolButton, true); return btn; } diff --git a/src/widgets/dialogs/dialog.cpp b/src/widgets/dialogs/dialog.cpp index a7cf6f3a..c3609bfc 100644 --- a/src/widgets/dialogs/dialog.cpp +++ b/src/widgets/dialogs/dialog.cpp @@ -26,7 +26,7 @@ void Dialog::setCentralWidget(QWidget *p_widget) { Q_ASSERT(!m_centralWidget && p_widget); m_centralWidget = p_widget; - m_centralWidget->setProperty(PropertyDefs::s_dialogCentralWidget, true); + m_centralWidget->setProperty(PropertyDefs::c_dialogCentralWidget, true); m_layout->addWidget(m_centralWidget); } @@ -115,7 +115,7 @@ void Dialog::setInformationText(const QString &p_text, InformationLevel p_level) break; } - WidgetUtils::setPropertyDynamically(m_infoTextEdit, PropertyDefs::s_state, level); + WidgetUtils::setPropertyDynamically(m_infoTextEdit, PropertyDefs::c_state, level); if (needResize) { WidgetUtils::updateSize(this); } diff --git a/src/widgets/dialogs/exportdialog.cpp b/src/widgets/dialogs/exportdialog.cpp index cd1631a0..f07a7cfe 100644 --- a/src/widgets/dialogs/exportdialog.cpp +++ b/src/widgets/dialogs/exportdialog.cpp @@ -387,8 +387,8 @@ void ExportDialog::rejectedButtonClicked() { if (m_exportOngoing) { // Just cancel the export. - appendLog(tr("Cancelling the export.")); - m_exporter->stop(); + appendLog(tr("Cancelling the export")); + getExporter()->stop(); } else { Dialog::rejectedButtonClicked(); } diff --git a/src/widgets/dialogs/managenotebooksdialog.cpp b/src/widgets/dialogs/managenotebooksdialog.cpp index fbf8c71f..2a8b3d2b 100644 --- a/src/widgets/dialogs/managenotebooksdialog.cpp +++ b/src/widgets/dialogs/managenotebooksdialog.cpp @@ -71,7 +71,7 @@ void ManageNotebooksDialog::setupUI() }); m_deleteNotebookBtn = new QPushButton(tr("Delete (DANGER)"), infoWidget); - WidgetUtils::setPropertyDynamically(m_deleteNotebookBtn, PropertyDefs::s_dangerButton, true); + WidgetUtils::setPropertyDynamically(m_deleteNotebookBtn, PropertyDefs::c_dangerButton, true); btnLayout->addWidget(m_deleteNotebookBtn); connect(m_deleteNotebookBtn, &QPushButton::clicked, this, [this]() { diff --git a/src/widgets/dialogs/scrolldialog.cpp b/src/widgets/dialogs/scrolldialog.cpp index 3f25e002..c0b27622 100644 --- a/src/widgets/dialogs/scrolldialog.cpp +++ b/src/widgets/dialogs/scrolldialog.cpp @@ -29,7 +29,7 @@ void ScrollDialog::setCentralWidget(QWidget *p_widget) { Q_ASSERT(!m_centralWidget && p_widget); m_centralWidget = p_widget; - m_centralWidget->setProperty(PropertyDefs::s_dialogCentralWidget, true); + m_centralWidget->setProperty(PropertyDefs::c_dialogCentralWidget, true); m_scrollArea->setWidget(p_widget); } diff --git a/src/widgets/findandreplacewidget.cpp b/src/widgets/findandreplacewidget.cpp index b27f1b3e..e14e65c1 100644 --- a/src/widgets/findandreplacewidget.cpp +++ b/src/widgets/findandreplacewidget.cpp @@ -55,7 +55,7 @@ void FindAndReplaceWidget::setupUI() const auto &themeMgr = VNoteX::getInst().getThemeMgr(); auto iconFile = themeMgr.getIconFile(QStringLiteral("close.svg")); auto closeBtn = new QToolButton(this); - closeBtn->setProperty(PropertyDefs::s_actionToolButton, true); + closeBtn->setProperty(PropertyDefs::c_actionToolButton, true); titleLayout->addWidget(closeBtn); auto closeAct = new QAction(IconUtils::fetchIcon(iconFile), QString(), closeBtn); @@ -223,7 +223,7 @@ void FindAndReplaceWidget::updateFindOptions() return; } - FindOptions options = FindOption::None; + FindOptions options = FindOption::FindNone; if (m_caseSensitiveCheckBox->isChecked()) { options |= FindOption::CaseSensitive; diff --git a/src/widgets/findandreplacewidget.h b/src/widgets/findandreplacewidget.h index 73e48844..b44c2c47 100644 --- a/src/widgets/findandreplacewidget.h +++ b/src/widgets/findandreplacewidget.h @@ -77,7 +77,7 @@ namespace vnotex QCheckBox *m_incrementalSearchCheckBox = nullptr; - FindOptions m_options = FindOption::None; + FindOptions m_options = FindOption::FindNone; QTimer *m_findTextTimer = nullptr; diff --git a/src/widgets/locationlist.cpp b/src/widgets/locationlist.cpp new file mode 100644 index 00000000..7ce3d984 --- /dev/null +++ b/src/widgets/locationlist.cpp @@ -0,0 +1,180 @@ +#include "locationlist.h" + +#include +#include + +#include "treewidget.h" +#include "widgetsfactory.h" +#include "titlebar.h" + +#include +#include + +using namespace vnotex; + +QIcon LocationList::s_bufferIcon; + +QIcon LocationList::s_fileIcon; + +QIcon LocationList::s_folderIcon; + +QIcon LocationList::s_notebookIcon; + +LocationList::LocationList(QWidget *p_parent) + : QFrame(p_parent) +{ + setupUI(); +} + +void LocationList::setupUI() +{ + auto mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + { + setupTitleBar(QString(), this); + mainLayout->addWidget(m_titleBar); + } + + m_tree = new TreeWidget(TreeWidget::Flag::None, this); + // When updated, pay attention to the Columns enum. + m_tree->setHeaderLabels(QStringList() << tr("Path") << tr("Line") << tr("Text")); + TreeWidget::showHorizontalScrollbar(m_tree); + connect(m_tree, &QTreeWidget::itemActivated, + this, [this](QTreeWidgetItem *p_item, int p_col) { + Q_UNUSED(p_col); + if (!m_callback) { + return; + } + m_callback(getItemLocation(p_item)); + }); + mainLayout->addWidget(m_tree); + + setFocusProxy(m_tree); +} + +const QIcon &LocationList::getItemIcon(LocationType p_type) +{ + if (s_bufferIcon.isNull()) { + // Init. + const QString nodeIconFgName = "widgets#locationlist#node_icon#fg"; + const auto &themeMgr = VNoteX::getInst().getThemeMgr(); + const auto fg = themeMgr.paletteColor(nodeIconFgName); + + s_bufferIcon = IconUtils::fetchIcon(themeMgr.getIconFile("buffer.svg"), fg); + s_fileIcon = IconUtils::fetchIcon(themeMgr.getIconFile("file_node.svg"), fg); + s_folderIcon = IconUtils::fetchIcon(themeMgr.getIconFile("folder_node.svg"), fg); + s_notebookIcon = IconUtils::fetchIcon(themeMgr.getIconFile("notebook_default.svg"), fg); + } + + switch (p_type) { + case LocationType::Buffer: + return s_bufferIcon; + + case LocationType::File: + return s_fileIcon; + + case LocationType::Folder: + return s_folderIcon; + + case LocationType::Notebook: + Q_FALLTHROUGH(); + default: + return s_notebookIcon; + } +} + +NavigationModeWrapper *LocationList::getNavigationModeWrapper() +{ + if (!m_navigationWrapper) { + m_navigationWrapper.reset(new NavigationModeWrapper(m_tree)); + } + return m_navigationWrapper.data(); +} + +void LocationList::setupTitleBar(const QString &p_title, QWidget *p_parent) +{ + m_titleBar = new TitleBar(p_title, true, TitleBar::Action::None, p_parent); + + { + auto clearBtn = m_titleBar->addActionButton(QStringLiteral("clear.svg"), tr("Clear")); + connect(clearBtn, &QToolButton::triggered, + this, &LocationList::clear); + } +} + +void LocationList::clear() +{ + m_tree->clear(); + + m_callback = LocationCallback(); + + updateItemsCountLabel(); +} + +void LocationList::setItemLocationLineAndText(QTreeWidgetItem *p_item, const ComplexLocation::Line &p_line) +{ + p_item->setData(Columns::LineColumn, Qt::UserRole, p_line.m_lineNumber); + if (p_line.m_lineNumber != -1) { + p_item->setText(Columns::LineColumn, QString::number(p_line.m_lineNumber)); + } + p_item->setText(Columns::TextColumn, p_line.m_text); +} + +void LocationList::addLocation(const ComplexLocation &p_location) +{ + auto item = new QTreeWidgetItem(m_tree); + item->setText(Columns::PathColumn, p_location.m_displayPath); + item->setData(Columns::PathColumn, Qt::UserRole, p_location.m_path); + + item->setIcon(Columns::PathColumn, getItemIcon(p_location.m_type)); + + if (p_location.m_lines.size() == 1) { + setItemLocationLineAndText(item, p_location.m_lines[0]); + } else if (p_location.m_lines.size() > 1) { + // Add sub items. + for (const auto &line : p_location.m_lines) { + auto subItem = new QTreeWidgetItem(item); + setItemLocationLineAndText(subItem, line); + } + + item->setExpanded(true); + } + + updateItemsCountLabel(); +} + +void LocationList::startSession(const LocationCallback &p_callback) +{ + m_callback = p_callback; +} + +Location LocationList::getItemLocation(const QTreeWidgetItem *p_item) const +{ + Location loc; + + if (!p_item) { + return loc; + } + + auto paItem = p_item->parent() ? p_item->parent() : p_item; + loc.m_path = paItem->data(Columns::PathColumn, Qt::UserRole).toString(); + loc.m_displayPath = paItem->text(Columns::PathColumn); + + auto lineNumberData = p_item->data(Columns::LineColumn, Qt::UserRole); + if (lineNumberData.isValid()) { + loc.m_lineNumber = lineNumberData.toInt(); + } + return loc; +} + +void LocationList::updateItemsCountLabel() +{ + const auto cnt = m_tree->topLevelItemCount(); + if (cnt == 0) { + m_titleBar->setInfoLabel(""); + } else { + m_titleBar->setInfoLabel(tr("%n Item(s)", "", m_tree->topLevelItemCount())); + } +} diff --git a/src/widgets/locationlist.h b/src/widgets/locationlist.h new file mode 100644 index 00000000..b987bf98 --- /dev/null +++ b/src/widgets/locationlist.h @@ -0,0 +1,74 @@ +#ifndef LOCATIONLIST_H +#define LOCATIONLIST_H + +#include + +#include +#include +#include +#include + +#include + +#include "navigationmodewrapper.h" + +namespace vnotex +{ + class TitleBar; + + class LocationList : public QFrame + { + Q_OBJECT + public: + typedef std::function LocationCallback; + + explicit LocationList(QWidget *p_parent = nullptr); + + NavigationModeWrapper *getNavigationModeWrapper(); + + void clear(); + + void addLocation(const ComplexLocation &p_location); + + // Start a new session of the location list to set a callback for activation handling. + void startSession(const LocationCallback &p_callback); + + private: + enum Columns + { + PathColumn = 0, + LineColumn, + TextColumn + }; + + void setupUI(); + + void setupTitleBar(const QString &p_title, QWidget *p_parent = nullptr); + + void setItemLocationLineAndText(QTreeWidgetItem *p_item, const ComplexLocation::Line &p_line); + + const QIcon &getItemIcon(LocationType p_type); + + Location getItemLocation(const QTreeWidgetItem *p_item) const; + + void updateItemsCountLabel(); + + TitleBar *m_titleBar = nullptr; + + QTreeWidget *m_tree = nullptr; + + QScopedPointer> m_navigationWrapper; + + LocationCallback m_callback; + + static QIcon s_bufferIcon; + + static QIcon s_fileIcon; + + static QIcon s_folderIcon; + + static QIcon s_notebookIcon; + }; +} + +#endif // LOCATIONLIST_H diff --git a/src/widgets/mainwindow.cpp b/src/widgets/mainwindow.cpp index 114fa1dd..4b3515e2 100644 --- a/src/widgets/mainwindow.cpp +++ b/src/widgets/mainwindow.cpp @@ -38,6 +38,10 @@ #include "messageboxhelper.h" #include "systemtrayhelper.h" #include "titletoolbar.h" +#include "locationlist.h" +#include "searchpanel.h" +#include +#include "searchinfoprovider.h" using namespace vnotex; @@ -193,10 +197,14 @@ void MainWindow::setupDocks() setupOutlineDock(); + setupSearchDock(); + for (int i = 1; i < m_docks.size(); ++i) { tabifyDockWidget(m_docks[i - 1], m_docks[i]); } + setupLocationListDock(); + for (auto dock : m_docks) { connect(dock, &QDockWidget::visibilityChanged, this, [this]() { @@ -205,8 +213,7 @@ void MainWindow::setupDocks() }); } - // Activate the first dock. - activateDock(m_docks[0]); + activateDock(m_docks[DockIndex::NavigationDock]); } void MainWindow::activateDock(QDockWidget *p_dock) @@ -257,6 +264,53 @@ void MainWindow::setupOutlineDock() addDockWidget(Qt::LeftDockWidgetArea, dock); } +void MainWindow::setupSearchDock() +{ + auto dock = new QDockWidget(tr("Search"), this); + m_docks.push_back(dock); + + dock->setObjectName(QStringLiteral("SearchDock.vnotex")); + dock->setAllowedAreas(Qt::AllDockWidgetAreas); + + setupSearchPanel(); + dock->setWidget(m_searchPanel); + dock->setFocusProxy(m_searchPanel); + addDockWidget(Qt::LeftDockWidgetArea, dock); +} + +void MainWindow::setupSearchPanel() +{ + m_searchPanel = new SearchPanel( + QSharedPointer::create(m_viewArea, + m_notebookExplorer, + &VNoteX::getInst().getNotebookMgr()), + this); + m_searchPanel->setObjectName("SearchPanel.vnotex"); +} + +void MainWindow::setupLocationListDock() +{ + auto dock = new QDockWidget(tr("Location List"), this); + m_docks.push_back(dock); + + dock->setObjectName(QStringLiteral("LocationListDock.vnotex")); + dock->setAllowedAreas(Qt::AllDockWidgetAreas); + + setupLocationList(); + dock->setWidget(m_locationList); + dock->setFocusProxy(m_locationList); + addDockWidget(Qt::BottomDockWidgetArea, dock); + dock->hide(); +} + +void MainWindow::setupLocationList() +{ + m_locationList = new LocationList(this); + m_locationList->setObjectName("LocationList.vnotex"); + + NavigationModeMgr::getInst().registerNavigationTarget(m_locationList->getNavigationModeWrapper()); +} + void MainWindow::setupNavigationToolBox() { m_navigationToolBox = new ToolBox(this); @@ -522,34 +576,30 @@ void MainWindow::closeOnQuit() void MainWindow::setupShortcuts() { const auto &coreConfig = ConfigMgr::getInst().getCoreConfig(); - // Focus Navigation dock. - { - auto keys = coreConfig.getShortcut(CoreConfig::Shortcut::NavigationDock); - auto shortcut = WidgetUtils::createShortcut(keys, this); - if (shortcut) { - auto dock = m_docks[DockIndex::NavigationDock]; - dock->setToolTip(QString("%1\t%2").arg(dock->windowTitle(), - QKeySequence(keys).toString(QKeySequence::NativeText))); - connect(shortcut, &QShortcut::activated, - this, [this]() { - activateDock(m_docks[DockIndex::NavigationDock]); - }); - } - } - // Focus Outline dock. - { - auto keys = coreConfig.getShortcut(CoreConfig::Shortcut::OutlineDock); - auto shortcut = WidgetUtils::createShortcut(keys, this); - if (shortcut) { - auto dock = m_docks[DockIndex::OutlineDock]; - dock->setToolTip(QString("%1\t%2").arg(dock->windowTitle(), - QKeySequence(keys).toString(QKeySequence::NativeText))); - connect(shortcut, &QShortcut::activated, - this, [this]() { - activateDock(m_docks[DockIndex::OutlineDock]); - }); - } + setupDockActivateShortcut(m_docks[DockIndex::NavigationDock], + coreConfig.getShortcut(CoreConfig::Shortcut::NavigationDock)); + + setupDockActivateShortcut(m_docks[DockIndex::OutlineDock], + coreConfig.getShortcut(CoreConfig::Shortcut::OutlineDock)); + + setupDockActivateShortcut(m_docks[DockIndex::SearchDock], + coreConfig.getShortcut(CoreConfig::Shortcut::SearchDock)); + + setupDockActivateShortcut(m_docks[DockIndex::LocationListDock], + coreConfig.getShortcut(CoreConfig::Shortcut::LocationListDock)); +} + +void MainWindow::setupDockActivateShortcut(QDockWidget *p_dock, const QString &p_keys) +{ + auto shortcut = WidgetUtils::createShortcut(p_keys, this); + if (shortcut) { + p_dock->setToolTip(QString("%1\t%2").arg(p_dock->windowTitle(), + QKeySequence(p_keys).toString(QKeySequence::NativeText))); + connect(shortcut, &QShortcut::activated, + this, [this, p_dock]() { + activateDock(p_dock); + }); } } @@ -684,3 +734,17 @@ void MainWindow::setTipsAreaVisible(bool p_visible) m_tipsLabel->hide(); } } + +LocationList *MainWindow::getLocationList() const +{ + return m_locationList; +} + +void MainWindow::setLocationListVisible(bool p_visible) +{ + if (p_visible) { + activateDock(m_docks[DockIndex::LocationListDock]); + } else { + m_docks[DockIndex::LocationListDock]->hide(); + } +} diff --git a/src/widgets/mainwindow.h b/src/widgets/mainwindow.h index a570b824..5a92f7f2 100644 --- a/src/widgets/mainwindow.h +++ b/src/widgets/mainwindow.h @@ -19,6 +19,8 @@ namespace vnotex class ViewArea; class Event; class OutlineViewer; + class LocationList; + class SearchPanel; enum { RESTART_EXIT_CODE = 1000 }; @@ -55,6 +57,10 @@ namespace vnotex void openFiles(const QStringList &p_files); + LocationList *getLocationList() const; + + void setLocationListVisible(bool p_visible); + signals: void mainWindowStarted(); @@ -85,7 +91,9 @@ namespace vnotex enum DockIndex { NavigationDock = 0, - OutlineDock + OutlineDock, + SearchDock, + LocationListDock }; void setupUI(); @@ -100,6 +108,14 @@ namespace vnotex void setupOutlineDock(); + void setupSearchDock(); + + void setupSearchPanel(); + + void setupLocationListDock(); + + void setupLocationList(); + void setupNotebookExplorer(QWidget *p_parent = nullptr); void setupDocks(); @@ -129,6 +145,8 @@ namespace vnotex void setTipsAreaVisible(bool p_visible); + void setupDockActivateShortcut(QDockWidget *p_dock, const QString &p_keys); + ToolBarHelper m_toolBarHelper; StatusBarHelper m_statusBarHelper; @@ -143,6 +161,10 @@ namespace vnotex OutlineViewer *m_outlineViewer = nullptr; + LocationList *m_locationList = nullptr; + + SearchPanel *m_searchPanel = nullptr; + QVector m_docks; bool m_layoutReset = false; diff --git a/src/widgets/markdownviewwindow.cpp b/src/widgets/markdownviewwindow.cpp index 0e40e07a..8152f49e 100644 --- a/src/widgets/markdownviewwindow.cpp +++ b/src/widgets/markdownviewwindow.cpp @@ -840,7 +840,9 @@ void MarkdownViewWindow::zoom(bool p_zoomIn) void MarkdownViewWindow::handleFindTextChanged(const QString &p_text, FindOptions p_options) { if (m_mode == Mode::Read) { - adapter()->findText(p_text, p_options); + if (p_options & FindOption::IncrementalSearch) { + adapter()->findText(p_text, p_options); + } } else { TextViewWindowHelper::handleFindTextChanged(this, p_text, p_options); } @@ -849,9 +851,7 @@ void MarkdownViewWindow::handleFindTextChanged(const QString &p_text, FindOption void MarkdownViewWindow::handleFindNext(const QString &p_text, FindOptions p_options) { if (m_mode == Mode::Read) { - if (p_options & FindOption::IncrementalSearch) { - adapter()->findText(p_text, p_options); - } + adapter()->findText(p_text, p_options); } else { TextViewWindowHelper::handleFindNext(this, p_text, p_options); } @@ -880,7 +880,7 @@ void MarkdownViewWindow::handleFindAndReplaceWidgetClosed() if (m_editor) { TextViewWindowHelper::handleFindAndReplaceWidgetClosed(this); } else { - adapter()->findText("", FindOption::None); + adapter()->findText("", FindOption::FindNone); } } diff --git a/src/widgets/notebookexplorer.cpp b/src/widgets/notebookexplorer.cpp index 050c57c1..993c6ef2 100644 --- a/src/widgets/notebookexplorer.cpp +++ b/src/widgets/notebookexplorer.cpp @@ -45,7 +45,7 @@ NotebookExplorer::NotebookExplorer(QWidget *p_parent) void NotebookExplorer::setupUI() { auto mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); + WidgetUtils::setContentsMargins(mainLayout); // Title bar. auto titleBar = setupTitleBar(this); @@ -88,6 +88,7 @@ TitleBar *NotebookExplorer::setupTitleBar(QWidget *p_parent) const auto &widgetConfig = ConfigMgr::getInst().getWidgetConfig(); auto titleBar = new TitleBar(tr("Notebook"), + false, TitleBar::Action::Menu, p_parent); titleBar->setWhatsThis(tr("This title bar contains buttons and menu to manage notebooks and notes.")); diff --git a/src/widgets/notebooknodeexplorer.cpp b/src/widgets/notebooknodeexplorer.cpp index 8744549d..0f466e4b 100644 --- a/src/widgets/notebooknodeexplorer.cpp +++ b/src/widgets/notebooknodeexplorer.cpp @@ -941,7 +941,7 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent) case Action::Properties: act = new QAction(generateMenuActionIcon("properties.svg"), - tr("&Properties"), + tr("&Properties (Rename)"), p_parent); connect(act, &QAction::triggered, this, [this]() { diff --git a/src/widgets/outlinepopup.cpp b/src/widgets/outlinepopup.cpp index d252e14b..14e350ae 100644 --- a/src/widgets/outlinepopup.cpp +++ b/src/widgets/outlinepopup.cpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include "outlineviewer.h" using namespace vnotex; @@ -22,6 +24,7 @@ OutlinePopup::OutlinePopup(QToolButton *p_btn, QWidget *p_parent) void OutlinePopup::setupUI() { auto mainLayout = new QVBoxLayout(this); + WidgetUtils::setContentsMargins(mainLayout); m_viewer = new OutlineViewer(tr("Outline"), this); mainLayout->addWidget(m_viewer); diff --git a/src/widgets/outlineviewer.cpp b/src/widgets/outlineviewer.cpp index 37bf20af..3a4174b2 100644 --- a/src/widgets/outlineviewer.cpp +++ b/src/widgets/outlineviewer.cpp @@ -9,6 +9,8 @@ #include #include +#include + #include "treewidget.h" #include "titlebar.h" @@ -50,8 +52,7 @@ OutlineViewer::OutlineViewer(const QString &p_title, QWidget *p_parent) void OutlineViewer::setupUI(const QString &p_title) { auto mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(0); + WidgetUtils::setContentsMargins(mainLayout); { auto titleBar = setupTitleBar(p_title, this); @@ -88,7 +89,7 @@ NavigationModeWrapper *OutlineViewer::getNavigatio TitleBar *OutlineViewer::setupTitleBar(const QString &p_title, QWidget *p_parent) { - auto titleBar = new TitleBar(p_title, TitleBar::Action::None, p_parent); + auto titleBar = new TitleBar(p_title, false, TitleBar::Action::None, p_parent); auto decreaseBtn = titleBar->addActionButton(QStringLiteral("decrease_outline_level.svg"), tr("Decrease Expansion Level")); connect(decreaseBtn, &QToolButton::clicked, diff --git a/src/widgets/outlineviewer.h b/src/widgets/outlineviewer.h index 5d5c71dc..58926709 100644 --- a/src/widgets/outlineviewer.h +++ b/src/widgets/outlineviewer.h @@ -22,7 +22,7 @@ namespace vnotex { Q_OBJECT public: - explicit OutlineViewer(const QString &p_title, QWidget *p_parent = nullptr); + OutlineViewer(const QString &p_title, QWidget *p_parent = nullptr); void setOutlineProvider(const QSharedPointer &p_provider); diff --git a/src/widgets/propertydefs.cpp b/src/widgets/propertydefs.cpp index 09affe95..cc9f3d5f 100644 --- a/src/widgets/propertydefs.cpp +++ b/src/widgets/propertydefs.cpp @@ -2,18 +2,20 @@ using namespace vnotex; -const char *PropertyDefs::s_actionToolButton = "ActionToolButton"; +const char *PropertyDefs::c_actionToolButton = "ActionToolButton"; -const char *PropertyDefs::s_toolButtonWithoutMenuIndicator = "NoMenuIndicator"; +const char *PropertyDefs::c_toolButtonWithoutMenuIndicator = "NoMenuIndicator"; -const char *PropertyDefs::s_dangerButton = "DangerButton"; +const char *PropertyDefs::c_dangerButton = "DangerButton"; -const char *PropertyDefs::s_dialogCentralWidget = "DialogCentralWidget"; +const char *PropertyDefs::c_dialogCentralWidget = "DialogCentralWidget"; -const char *PropertyDefs::s_viewSplitCornerWidget = "ViewSplitCornerWidget"; +const char *PropertyDefs::c_viewSplitCornerWidget = "ViewSplitCornerWidget"; -const char *PropertyDefs::s_state = "State"; +const char *PropertyDefs::c_state = "State"; -const char *PropertyDefs::s_viewWindowToolBar = "ViewWindowToolBar"; +const char *PropertyDefs::c_viewWindowToolBar = "ViewWindowToolBar"; -const char *PropertyDefs::s_consoleTextEdit = "ConsoleTextEdit"; +const char *PropertyDefs::c_consoleTextEdit = "ConsoleTextEdit"; + +const char *PropertyDefs::c_embeddedLineEdit = "EmbeddedLineEdit"; diff --git a/src/widgets/propertydefs.h b/src/widgets/propertydefs.h index 6ba3046b..7e2df928 100644 --- a/src/widgets/propertydefs.h +++ b/src/widgets/propertydefs.h @@ -9,22 +9,24 @@ namespace vnotex public: PropertyDefs() = delete; - static const char *s_actionToolButton; + static const char *c_actionToolButton; - static const char *s_toolButtonWithoutMenuIndicator; + static const char *c_toolButtonWithoutMenuIndicator; - static const char *s_dangerButton; + static const char *c_dangerButton; - static const char *s_dialogCentralWidget; + static const char *c_dialogCentralWidget; - static const char *s_viewSplitCornerWidget; + static const char *c_viewSplitCornerWidget; - static const char *s_viewWindowToolBar; + static const char *c_viewWindowToolBar; - static const char *s_consoleTextEdit; + static const char *c_consoleTextEdit; + + static const char *c_embeddedLineEdit; // Values: info/warning/error. - static const char *s_state; + static const char *c_state; }; } diff --git a/src/widgets/searchinfoprovider.cpp b/src/widgets/searchinfoprovider.cpp new file mode 100644 index 00000000..636bcb53 --- /dev/null +++ b/src/widgets/searchinfoprovider.cpp @@ -0,0 +1,43 @@ +#include "searchinfoprovider.h" + +#include "viewarea.h" +#include "notebookexplorer.h" +#include "notebookmgr.h" + +using namespace vnotex; + +SearchInfoProvider::SearchInfoProvider(const ViewArea *p_viewArea, + const NotebookExplorer *p_notebookExplorer, + const NotebookMgr *p_notebookMgr) + : m_viewArea(p_viewArea), + m_notebookExplorer(p_notebookExplorer), + m_notebookMgr(p_notebookMgr) +{ +} + +QList SearchInfoProvider::getBuffers() const +{ + return m_viewArea->getAllBuffersInViewSplits(); +} + +Node *SearchInfoProvider::getCurrentFolder() const +{ + return m_notebookExplorer->currentExploredFolderNode(); +} + +Notebook *SearchInfoProvider::getCurrentNotebook() const +{ + return m_notebookExplorer->currentNotebook().data(); +} + +QVector SearchInfoProvider::getNotebooks() const +{ + auto notebooks = m_notebookMgr->getNotebooks(); + QVector nbs; + nbs.reserve(notebooks.size()); + for (const auto &nb : notebooks) { + nbs.push_back(nb.data()); + } + + return nbs; +} diff --git a/src/widgets/searchinfoprovider.h b/src/widgets/searchinfoprovider.h new file mode 100644 index 00000000..94b18985 --- /dev/null +++ b/src/widgets/searchinfoprovider.h @@ -0,0 +1,36 @@ +#ifndef SEARCHINFOPROVIDER_H +#define SEARCHINFOPROVIDER_H + +#include "searchpanel.h" + +namespace vnotex +{ + class ViewArea; + class NotebookExplorer; + class NotebookMgr; + + class SearchInfoProvider : public ISearchInfoProvider + { + public: + SearchInfoProvider(const ViewArea *p_viewArea, + const NotebookExplorer *p_notebookExplorer, + const NotebookMgr *p_notebookMgr); + + QList getBuffers() const Q_DECL_OVERRIDE; + + Node *getCurrentFolder() const Q_DECL_OVERRIDE; + + Notebook *getCurrentNotebook() const Q_DECL_OVERRIDE; + + QVector getNotebooks() const Q_DECL_OVERRIDE; + + private: + const ViewArea *m_viewArea = nullptr; + + const NotebookExplorer *m_notebookExplorer = nullptr; + + const NotebookMgr *m_notebookMgr = nullptr; + }; +} + +#endif // SEARCHINFOPROVIDER_H diff --git a/src/widgets/searchpanel.cpp b/src/widgets/searchpanel.cpp new file mode 100644 index 00000000..1a488337 --- /dev/null +++ b/src/widgets/searchpanel.cpp @@ -0,0 +1,526 @@ +#include "searchpanel.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include "widgetsfactory.h" +#include "titlebar.h" +#include "propertydefs.h" +#include "mainwindow.h" +#include +#include +#include +#include "locationlist.h" + +using namespace vnotex; + +SearchPanel::SearchPanel(const QSharedPointer &p_provider, QWidget *p_parent) + : QFrame(p_parent), + m_provider(p_provider) +{ + qRegisterMetaType>>("QVector>"); + + setupUI(); + + initOptions(); + + restoreFields(*m_option); +} + +void SearchPanel::setupUI() +{ + auto mainLayout = new QVBoxLayout(this); + WidgetUtils::setContentsMargins(mainLayout); + + { + auto titleBar = setupTitleBar(QString(), this); + mainLayout->addWidget(titleBar); + } + + auto inputsLayout = new QFormLayout(); + mainLayout->addLayout(inputsLayout); + + m_keywordComboBox = WidgetsFactory::createComboBox(this); + m_keywordComboBox->setToolTip(SearchToken::getHelpText()); + m_keywordComboBox->setEditable(true); + m_keywordComboBox->setLineEdit(WidgetsFactory::createLineEdit(this)); + m_keywordComboBox->lineEdit()->setProperty(PropertyDefs::c_embeddedLineEdit, true); + m_keywordComboBox->completer()->setCaseSensitivity(Qt::CaseSensitive); + connect(m_keywordComboBox->lineEdit(), &QLineEdit::returnPressed, + this, [this]() { + m_searchBtn->animateClick(); + }); + inputsLayout->addRow(tr("Keyword:"), m_keywordComboBox); + + m_searchScopeComboBox = WidgetsFactory::createComboBox(this); + m_searchScopeComboBox->addItem(tr("Buffers"), static_cast(SearchScope::Buffers)); + m_searchScopeComboBox->addItem(tr("Current Folder"), static_cast(SearchScope::CurrentFolder)); + m_searchScopeComboBox->addItem(tr("Current Notebook"), static_cast(SearchScope::CurrentNotebook)); + m_searchScopeComboBox->addItem(tr("All Notebooks"), static_cast(SearchScope::AllNotebooks)); + inputsLayout->addRow(tr("Scope:"), m_searchScopeComboBox); + + setupSearchObject(inputsLayout); + + setupSearchTarget(inputsLayout); + + m_filePatternComboBox = WidgetsFactory::createComboBox(this); + m_filePatternComboBox->setEditable(true); + m_filePatternComboBox->setLineEdit(WidgetsFactory::createLineEdit(this)); + m_filePatternComboBox->lineEdit()->setPlaceholderText(tr("Wildcard pattern of files and folders to search")); + m_filePatternComboBox->lineEdit()->setProperty(PropertyDefs::c_embeddedLineEdit, true); + m_filePatternComboBox->completer()->setCaseSensitivity(Qt::CaseSensitive); + inputsLayout->addRow(tr("File pattern:"), m_filePatternComboBox); + + setupFindOption(inputsLayout); + + { + m_progressBar = new QProgressBar(this); + m_progressBar->setRange(0, 0); + m_progressBar->hide(); + mainLayout->addWidget(m_progressBar); + } + + mainLayout->addStretch(); +} + +TitleBar *SearchPanel::setupTitleBar(const QString &p_title, QWidget *p_parent) +{ + auto titleBar = new TitleBar(p_title, false, TitleBar::Action::None, p_parent); + titleBar->setActionButtonsAlwaysShown(true); + + { + m_searchBtn = titleBar->addActionButton(QStringLiteral("search.svg"), tr("Search")); + connect(m_searchBtn, &QToolButton::triggered, + this, &SearchPanel::startSearch); + + auto cancelBtn = titleBar->addActionButton(QStringLiteral("cancel.svg"), tr("Cancel")); + connect(cancelBtn, &QToolButton::triggered, + this, &SearchPanel::stopSearch); + + auto closeLocationListBtn = titleBar->addActionButton(QStringLiteral("close.svg"), tr("Close Location List")); + connect(closeLocationListBtn, &QToolButton::triggered, + this, [this]() { + + }); + } + + return titleBar; +} + +void SearchPanel::setupSearchObject(QFormLayout *p_layout) +{ + auto gridLayout = new QGridLayout(); + p_layout->addRow(tr("Object:"), gridLayout); + + m_searchObjectNameCheckBox = WidgetsFactory::createCheckBox(tr("Name"), this); + gridLayout->addWidget(m_searchObjectNameCheckBox, 0, 0); + + m_searchObjectContentCheckBox = WidgetsFactory::createCheckBox(tr("Content"), this); + gridLayout->addWidget(m_searchObjectContentCheckBox, 0, 1); + + m_searchObjectOutlineCheckBox = WidgetsFactory::createCheckBox(tr("Outline"), this); + gridLayout->addWidget(m_searchObjectOutlineCheckBox, 0, 2); + + m_searchObjectTagCheckBox = WidgetsFactory::createCheckBox(tr("Tag"), this); + gridLayout->addWidget(m_searchObjectTagCheckBox, 1, 0); + + m_searchObjectPathCheckBox = WidgetsFactory::createCheckBox(tr("Path"), this); + gridLayout->addWidget(m_searchObjectPathCheckBox, 1, 1); +} + +void SearchPanel::setupSearchTarget(QFormLayout *p_layout) +{ + auto gridLayout = new QGridLayout(); + p_layout->addRow(tr("Target:"), gridLayout); + + m_searchTargetFileCheckBox = WidgetsFactory::createCheckBox(tr("File"), this); + gridLayout->addWidget(m_searchTargetFileCheckBox, 0, 0); + + m_searchTargetFolderCheckBox = WidgetsFactory::createCheckBox(tr("Folder"), this); + gridLayout->addWidget(m_searchTargetFolderCheckBox, 0, 1); + + m_searchTargetNotebookCheckBox = WidgetsFactory::createCheckBox(tr("Notebook"), this); + gridLayout->addWidget(m_searchTargetNotebookCheckBox, 0, 2); +} + +void SearchPanel::setupFindOption(QFormLayout *p_layout) +{ + auto gridLayout = new QGridLayout(); + p_layout->addRow(tr("Option:"), gridLayout); + + m_caseSensitiveCheckBox = WidgetsFactory::createCheckBox(tr("&Case sensitive"), this); + gridLayout->addWidget(m_caseSensitiveCheckBox, 0, 0); + + { + QButtonGroup *btnGroup = new QButtonGroup(this); + + m_plainTextRadioBtn = WidgetsFactory::createRadioButton(tr("&Plain text"), this); + btnGroup->addButton(m_plainTextRadioBtn); + gridLayout->addWidget(m_plainTextRadioBtn, 1, 0); + + m_wholeWordOnlyRadioBtn = WidgetsFactory::createRadioButton(tr("&Whole word only"), this); + btnGroup->addButton(m_wholeWordOnlyRadioBtn); + gridLayout->addWidget(m_wholeWordOnlyRadioBtn, 1, 1); + + m_fuzzySearchRadioBtn = WidgetsFactory::createRadioButton(tr("&Fuzzy search"), this); + btnGroup->addButton(m_fuzzySearchRadioBtn); + gridLayout->addWidget(m_fuzzySearchRadioBtn, 2, 0); + + m_regularExpressionRadioBtn = WidgetsFactory::createRadioButton(tr("Re&gular expression"), this); + btnGroup->addButton(m_regularExpressionRadioBtn); + gridLayout->addWidget(m_regularExpressionRadioBtn, 2, 1); + } +} + +void SearchPanel::initOptions() +{ + // Read it from config. + m_option = QSharedPointer::create(ConfigMgr::getInst().getSessionConfig().getSearchOption()); + + connect(VNoteX::getInst().getMainWindow(), &MainWindow::mainWindowClosedOnQuit, + this, [this]() { + saveFields(*m_option); + ConfigMgr::getInst().getSessionConfig().setSearchOption(*m_option); + }); +} + +void SearchPanel::restoreFields(const SearchOption &p_option) +{ + m_keywordComboBox->setEditText(p_option.m_keyword); + m_filePatternComboBox->setEditText(p_option.m_filePattern); + + { + int idx = m_searchScopeComboBox->findData(static_cast(p_option.m_scope)); + if (idx != -1) { + m_searchScopeComboBox->setCurrentIndex(idx); + } + } + + { + m_searchObjectNameCheckBox->setChecked(p_option.m_objects & SearchObject::SearchName); + m_searchObjectContentCheckBox->setChecked(p_option.m_objects & SearchObject::SearchContent); + m_searchObjectOutlineCheckBox->setChecked(p_option.m_objects & SearchObject::SearchOutline); + m_searchObjectTagCheckBox->setChecked(p_option.m_objects & SearchObject::SearchTag); + m_searchObjectPathCheckBox->setChecked(p_option.m_objects & SearchObject::SearchPath); + } + + { + m_searchTargetFileCheckBox->setChecked(p_option.m_targets & SearchTarget::SearchFile); + m_searchTargetFolderCheckBox->setChecked(p_option.m_targets & SearchTarget::SearchFolder); + m_searchTargetNotebookCheckBox->setChecked(p_option.m_targets & SearchTarget::SearchNotebook); + } + + { + m_plainTextRadioBtn->setChecked(true); + + m_caseSensitiveCheckBox->setChecked(p_option.m_findOptions & FindOption::CaseSensitive); + m_wholeWordOnlyRadioBtn->setChecked(p_option.m_findOptions & FindOption::WholeWordOnly); + m_fuzzySearchRadioBtn->setChecked(p_option.m_findOptions & FindOption::FuzzySearch); + m_regularExpressionRadioBtn->setChecked(p_option.m_findOptions & FindOption::RegularExpression); + } +} + +void SearchPanel::updateUIOnSearch() +{ + if (m_searchOngoing) { + m_progressBar->setMaximum(0); + m_progressBar->show(); + } else { + m_progressBar->hide(); + } +} + +void SearchPanel::startSearch() +{ + if (m_searchOngoing) { + return; + } + + // On start. + { + clearLog(); + m_searchOngoing = true; + updateUIOnSearch(); + + prepareLocationList(); + } + + saveFields(*m_option); + + auto state = search(m_option); + + // On end. + handleSearchFinished(state); +} + +void SearchPanel::handleSearchFinished(SearchState p_state) +{ + Q_ASSERT(m_searchOngoing); + Q_ASSERT(p_state != SearchState::Idle); + + if (p_state != SearchState::Busy) { + appendLog(tr("Search finished: %1").arg(SearchStateToString(p_state))); + + getSearcher()->clear(); + m_searchOngoing = false; + updateUIOnSearch(); + } +} + +void SearchPanel::stopSearch() +{ + if (!m_searchOngoing) { + return; + } + + getSearcher()->stop(); +} + +void SearchPanel::appendLog(const QString &p_text) +{ + if (p_text.isEmpty()) { + return; + } + + if (!m_infoTextEdit) { + m_infoTextEdit = WidgetsFactory::createPlainTextConsole(this); + m_infoTextEdit->setMaximumHeight(m_infoTextEdit->minimumSizeHint().height()); + static_cast(layout())->insertWidget(layout()->count() - 1, m_infoTextEdit); + } + + m_infoTextEdit->appendPlainText(">>> " + p_text); + m_infoTextEdit->ensureCursorVisible(); + m_infoTextEdit->show(); + + QCoreApplication::sendPostedEvents(); +} + +void SearchPanel::clearLog() +{ + if (!m_infoTextEdit) { + return; + } + + m_infoTextEdit->clear(); + m_infoTextEdit->hide(); +} + +void SearchPanel::saveFields(SearchOption &p_option) +{ + p_option.m_keyword = m_keywordComboBox->currentText().trimmed(); + p_option.m_filePattern = m_filePatternComboBox->currentText().trimmed(); + p_option.m_scope = static_cast(m_searchScopeComboBox->currentData().toInt()); + + { + p_option.m_objects = SearchObject::ObjectNone; + if (m_searchObjectNameCheckBox->isChecked()) { + p_option.m_objects |= SearchObject::SearchName; + } + if (m_searchObjectContentCheckBox->isChecked()) { + p_option.m_objects |= SearchObject::SearchContent; + } + if (m_searchObjectOutlineCheckBox->isChecked()) { + p_option.m_objects |= SearchObject::SearchOutline; + } + if (m_searchObjectTagCheckBox->isChecked()) { + p_option.m_objects |= SearchObject::SearchTag; + } + if (m_searchObjectPathCheckBox->isChecked()) { + p_option.m_objects |= SearchObject::SearchPath; + } + } + + { + p_option.m_targets = SearchTarget::TargetNone; + if (m_searchTargetFileCheckBox->isChecked()) { + p_option.m_targets |= SearchTarget::SearchFile; + } + if (m_searchTargetFolderCheckBox->isChecked()) { + p_option.m_targets |= SearchTarget::SearchFolder; + } + if (m_searchTargetNotebookCheckBox->isChecked()) { + p_option.m_targets |= SearchTarget::SearchNotebook; + } + } + + p_option.m_engine = SearchEngine::Internal; + + { + p_option.m_findOptions = FindOption::FindNone; + if (m_caseSensitiveCheckBox->isChecked()) { + p_option.m_findOptions |= FindOption::CaseSensitive; + } + if (m_wholeWordOnlyRadioBtn->isChecked()) { + p_option.m_findOptions |= FindOption::WholeWordOnly; + } + if (m_fuzzySearchRadioBtn->isChecked()) { + p_option.m_findOptions |= FindOption::FuzzySearch; + } + if (m_regularExpressionRadioBtn->isChecked()) { + p_option.m_findOptions |= FindOption::RegularExpression; + } + } +} + +SearchState SearchPanel::search(const QSharedPointer &p_option) +{ + if (!isSearchOptionValid(*p_option)) { + return SearchState::Failed; + } + + SearchState state = SearchState::Finished; + + switch (p_option->m_scope) { + case SearchScope::Buffers: + { + auto buffers = m_provider->getBuffers(); + if (buffers.isEmpty()) { + break; + } + state = getSearcher()->search(p_option, buffers); + break; + } + + case SearchScope::CurrentFolder: + { + auto notebook = m_provider->getCurrentNotebook(); + if (!notebook) { + break; + } + auto folder = m_provider->getCurrentFolder(); + if (folder && (folder->isRoot() || notebook->isRecycleBinNode(folder))) { + folder = nullptr; + } + if (!folder) { + break; + } + + state = getSearcher()->search(p_option, folder); + break; + } + + case SearchScope::CurrentNotebook: + { + auto notebook = m_provider->getCurrentNotebook(); + if (!notebook) { + break; + } + + QVector notebooks; + notebooks.push_back(notebook); + state = getSearcher()->search(p_option, notebooks); + break; + } + + case SearchScope::AllNotebooks: + { + auto notebooks = m_provider->getNotebooks(); + if (notebooks.isEmpty()) { + break; + } + + state = getSearcher()->search(p_option, notebooks); + break; + } + } + + return state; +} + +bool SearchPanel::isSearchOptionValid(const SearchOption &p_option) +{ + if (p_option.m_keyword.isEmpty()) { + appendLog(tr("Invalid keyword")); + return false; + } + + if (p_option.m_objects == SearchObject::ObjectNone) { + appendLog(tr("No object specified")); + return false; + } + + if (p_option.m_targets == SearchTarget::TargetNone) { + appendLog(tr("No target specified")); + return false; + } + + if (p_option.m_findOptions & FindOption::FuzzySearch + && p_option.m_objects & SearchObject::SearchContent) { + appendLog(tr("Fuzzy search is not allowed when searching content")); + return false; + } + + return true; +} + +Searcher *SearchPanel::getSearcher() +{ + if (!m_searcher) { + m_searcher = new Searcher(this); + connect(m_searcher, &Searcher::progressUpdated, + this, &SearchPanel::updateProgress); + connect(m_searcher, &Searcher::logRequested, + this, &SearchPanel::appendLog); + connect(m_searcher, &Searcher::resultItemAdded, + this, [this](const QSharedPointer &p_item) { + m_locationList->addLocation(p_item->m_location); + }); + connect(m_searcher, &Searcher::resultItemsAdded, + this, [this](const QVector> &p_items) { + for (const auto &item : p_items) { + m_locationList->addLocation(item->m_location); + } + }); + connect(m_searcher, &Searcher::finished, + this, &SearchPanel::handleSearchFinished); + } + return m_searcher; +} + +void SearchPanel::updateProgress(int p_val, int p_maximum) +{ + m_progressBar->setMaximum(p_maximum); + m_progressBar->setValue(p_val); +} + +void SearchPanel::prepareLocationList() +{ + auto mainWindow = VNoteX::getInst().getMainWindow(); + mainWindow->setLocationListVisible(true); + + if (!m_locationList) { + m_locationList = mainWindow->getLocationList(); + } + + m_locationList->clear(); + m_locationList->startSession([this](const Location &p_location) { + handleLocationActivated(p_location); + }); +} + +void SearchPanel::handleLocationActivated(const Location &p_location) +{ + qDebug() << "location activated" << p_location; + // TODO: decode the path of location and handle different types of destination. + auto paras = QSharedPointer::create(); + paras->m_lineNumber = p_location.m_lineNumber; + emit VNoteX::getInst().openFileRequested(p_location.m_path, paras); +} diff --git a/src/widgets/searchpanel.h b/src/widgets/searchpanel.h new file mode 100644 index 00000000..b00dd764 --- /dev/null +++ b/src/widgets/searchpanel.h @@ -0,0 +1,144 @@ +#ifndef SEARCHPANEL_H +#define SEARCHPANEL_H + +#include +#include +#include + +#include +#include + +class QComboBox; +class QCheckBox; +class QFormLayout; +class QProgressBar; +class QToolButton; +class QPlainTextEdit; +class QRadioButton; +class QButtonGroup; + +namespace vnotex +{ + class TitleBar; + class Buffer; + class Node; + class Notebook; + class LocationList; + struct Location; + + class ISearchInfoProvider + { + public: + ISearchInfoProvider() = default; + + virtual ~ISearchInfoProvider() = default; + + virtual QList getBuffers() const = 0; + + virtual Node *getCurrentFolder() const = 0; + + virtual Notebook *getCurrentNotebook() const = 0; + + virtual QVector getNotebooks() const = 0; + }; + + class SearchPanel : public QFrame + { + Q_OBJECT + public: + explicit SearchPanel(const QSharedPointer &p_provider, QWidget *p_parent = nullptr); + + private slots: + void startSearch(); + + void stopSearch(); + + void handleSearchFinished(SearchState p_state); + + void updateProgress(int p_val, int p_maximum); + + void appendLog(const QString &p_text); + + private: + void setupUI(); + + TitleBar *setupTitleBar(const QString &p_title, QWidget *p_parent = nullptr); + + void setupSearchObject(QFormLayout *p_layout); + + void setupSearchTarget(QFormLayout *p_layout); + + void setupFindOption(QFormLayout *p_layout); + + void initOptions(); + + void restoreFields(const SearchOption &p_option); + + void saveFields(SearchOption &p_option); + + void updateUIOnSearch(); + + void clearLog(); + + SearchState search(const QSharedPointer &p_option); + + bool isSearchOptionValid(const SearchOption &p_option); + + Searcher *getSearcher(); + + void prepareLocationList(); + + void handleLocationActivated(const Location &p_location); + + QSharedPointer m_provider; + + QToolButton *m_searchBtn = nullptr; + + QComboBox *m_keywordComboBox = nullptr; + + QComboBox *m_searchScopeComboBox = nullptr; + + QCheckBox *m_searchObjectNameCheckBox = nullptr; + + QCheckBox *m_searchObjectContentCheckBox = nullptr; + + QCheckBox *m_searchObjectOutlineCheckBox = nullptr; + + QCheckBox *m_searchObjectTagCheckBox = nullptr; + + QCheckBox *m_searchObjectPathCheckBox = nullptr; + + QCheckBox *m_searchTargetFileCheckBox = nullptr; + + QCheckBox *m_searchTargetFolderCheckBox = nullptr; + + QCheckBox *m_searchTargetNotebookCheckBox = nullptr; + + QComboBox *m_filePatternComboBox = nullptr; + + QCheckBox *m_caseSensitiveCheckBox = nullptr; + + // WholeWordOnly/RegularExpression/FuzzySearch is exclusive. + QRadioButton *m_plainTextRadioBtn = nullptr; + + QRadioButton *m_wholeWordOnlyRadioBtn = nullptr; + + QRadioButton *m_fuzzySearchRadioBtn = nullptr; + + QRadioButton *m_regularExpressionRadioBtn = nullptr; + + QProgressBar *m_progressBar = nullptr; + + QPlainTextEdit *m_infoTextEdit = nullptr; + + QSharedPointer m_option; + + bool m_searchOngoing = false; + + Searcher *m_searcher = nullptr; + + LocationList *m_locationList = nullptr; + }; +} + +#endif // SEARCHPANEL_H diff --git a/src/widgets/titlebar.cpp b/src/widgets/titlebar.cpp index fcc68ecc..43295fb7 100644 --- a/src/widgets/titlebar.cpp +++ b/src/widgets/titlebar.cpp @@ -23,19 +23,21 @@ const QString TitleBar::c_menuIconForegroundName = "widgets#titlebar#menu_icon#f const QString TitleBar::c_menuIconDisabledForegroundName = "widgets#titlebar#menu_icon#disabled#fg"; TitleBar::TitleBar(const QString &p_title, + bool p_hasInfoLabel, TitleBar::Actions p_actionFlags, QWidget *p_parent) : QWidget(p_parent) { - setupUI(p_title, p_actionFlags); + setupUI(p_title, p_hasInfoLabel, p_actionFlags); } -void TitleBar::setupUI(const QString &p_title, TitleBar::Actions p_actionFlags) +void TitleBar::setupUI(const QString &p_title, bool p_hasInfoLabel, TitleBar::Actions p_actionFlags) { auto mainLayout = new QHBoxLayout(this); mainLayout->setContentsMargins(0, 0, 0, 0); // Title label. + // Should always add it even if title is empty. Otherwise, we could not catch the hover event to show actions. { auto titleLabel = new QLabel(p_title, this); titleLabel->setProperty(c_titleProp, true); @@ -56,13 +58,20 @@ void TitleBar::setupUI(const QString &p_title, TitleBar::Actions p_actionFlags) setActionButtonsVisible(false); } + // Info label. + if (p_hasInfoLabel) { + m_infoLabel = new QLabel(this); + m_infoLabel->setProperty(c_titleProp, true); + mainLayout->addWidget(m_infoLabel); + } + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); } QToolButton *TitleBar::newActionButton(const QString &p_iconName, const QString &p_text, QWidget *p_parent) { auto btn = new QToolButton(p_parent); - btn->setProperty(PropertyDefs::s_actionToolButton, true); + btn->setProperty(PropertyDefs::c_actionToolButton, true); const auto &themeMgr = VNoteX::getInst().getThemeMgr(); auto iconFile = themeMgr.getIconFile(p_iconName); @@ -99,7 +108,7 @@ void TitleBar::enterEvent(QEvent *p_event) void TitleBar::leaveEvent(QEvent *p_event) { QWidget::leaveEvent(p_event); - setActionButtonsVisible(m_alwaysShowActionButtons); + setActionButtonsVisible(m_actionButtonsForcedShown || m_actionButtonsAlwaysShown); } void TitleBar::setActionButtonsVisible(bool p_visible) @@ -165,12 +174,12 @@ QToolButton *TitleBar::addActionButton(const QString &p_iconName, const QString btn->setMenu(p_menu); connect(p_menu, &QMenu::aboutToShow, this, [this]() { - m_alwaysShowActionButtons = true; + m_actionButtonsForcedShown = true; setActionButtonsVisible(true); }); connect(p_menu, &QMenu::aboutToHide, this, [this]() { - m_alwaysShowActionButtons = false; + m_actionButtonsForcedShown = false; setActionButtonsVisible(false); }); return btn; @@ -180,3 +189,17 @@ QHBoxLayout *TitleBar::actionButtonLayout() const { return static_cast(m_buttonWidget->layout()); } + +void TitleBar::setInfoLabel(const QString &p_info) +{ + Q_ASSERT(m_infoLabel); + if (m_infoLabel) { + m_infoLabel->setText(p_info); + } +} + +void TitleBar::setActionButtonsAlwaysShown(bool p_shown) +{ + m_actionButtonsAlwaysShown = p_shown; + setActionButtonsVisible(m_actionButtonsForcedShown || m_actionButtonsAlwaysShown); +} diff --git a/src/widgets/titlebar.h b/src/widgets/titlebar.h index c5ac4d35..563be58e 100644 --- a/src/widgets/titlebar.h +++ b/src/widgets/titlebar.h @@ -7,6 +7,7 @@ class QToolButton; class QHBoxLayout; +class QLabel; namespace vnotex { @@ -23,6 +24,7 @@ namespace vnotex Q_DECLARE_FLAGS(Actions, Action) TitleBar(const QString &p_title, + bool p_hasInfoLabel, TitleBar::Actions p_actionFlags, QWidget *p_parent = nullptr); @@ -46,13 +48,17 @@ namespace vnotex void addMenuSeparator(); + void setInfoLabel(const QString &p_info); + + void setActionButtonsAlwaysShown(bool p_shown); + protected: void enterEvent(QEvent *p_event) Q_DECL_OVERRIDE; void leaveEvent(QEvent *p_event) Q_DECL_OVERRIDE; private: - void setupUI(const QString &p_title, TitleBar::Actions p_actionFlags); + void setupUI(const QString &p_title, bool p_hasInfoLabel, TitleBar::Actions p_actionFlags); void setupActionButtons(TitleBar::Actions p_actionFlags); @@ -64,11 +70,15 @@ namespace vnotex static QIcon generateMenuActionIcon(const QString &p_iconName); + QLabel *m_infoLabel = nullptr; + QVector m_actionButtons; QWidget *m_buttonWidget = nullptr; - bool m_alwaysShowActionButtons = false; + bool m_actionButtonsAlwaysShown = false; + + bool m_actionButtonsForcedShown = false; QMenu *m_menu = nullptr; diff --git a/src/widgets/titletoolbar.cpp b/src/widgets/titletoolbar.cpp index b5e25088..d15e7a6a 100644 --- a/src/widgets/titletoolbar.cpp +++ b/src/widgets/titletoolbar.cpp @@ -92,7 +92,7 @@ void TitleToolBar::addTitleBarIcons(const QIcon &p_minimizeIcon, m_window->close(); }); auto btn = static_cast(widgetForAction(closeAct)); - btn->setProperty(PropertyDefs::s_dangerButton, true); + btn->setProperty(PropertyDefs::c_dangerButton, true); } updateMaximizeAct(); diff --git a/src/widgets/toolbarhelper.cpp b/src/widgets/toolbarhelper.cpp index 5c979d48..47c7bdda 100644 --- a/src/widgets/toolbarhelper.cpp +++ b/src/widgets/toolbarhelper.cpp @@ -48,8 +48,9 @@ QToolBar *ToolBarHelper::setupFileToolBar(MainWindow *p_win, QToolBar *p_toolBar auto toolBtn = dynamic_cast(tb->widgetForAction(act)); Q_ASSERT(toolBtn); + toolBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); toolBtn->setPopupMode(QToolButton::InstantPopup); - toolBtn->setProperty(PropertyDefs::s_toolButtonWithoutMenuIndicator, true); + toolBtn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true); auto newMenu = WidgetsFactory::createMenu(tb); toolBtn->setMenu(newMenu); @@ -149,7 +150,7 @@ QToolBar *ToolBarHelper::setupFileToolBar(MainWindow *p_win, QToolBar *p_toolBar auto btn = dynamic_cast(tb->widgetForAction(act)); Q_ASSERT(btn); btn->setPopupMode(QToolButton::InstantPopup); - btn->setProperty(PropertyDefs::s_toolButtonWithoutMenuIndicator, true); + btn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true); auto newMenu = WidgetsFactory::createMenu(tb); btn->setMenu(newMenu); @@ -269,7 +270,7 @@ QToolBar *ToolBarHelper::setupSettingsToolBar(MainWindow *p_win, QToolBar *p_too auto btn = dynamic_cast(tb->widgetForAction(act)); Q_ASSERT(btn); btn->setPopupMode(QToolButton::InstantPopup); - btn->setProperty(PropertyDefs::s_toolButtonWithoutMenuIndicator, true); + btn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true); auto menu = WidgetsFactory::createMenu(tb); btn->setMenu(menu); @@ -348,7 +349,7 @@ QToolBar *ToolBarHelper::setupSettingsToolBar(MainWindow *p_win, QToolBar *p_too auto btn = dynamic_cast(tb->widgetForAction(act)); Q_ASSERT(btn); btn->setPopupMode(QToolButton::InstantPopup); - btn->setProperty(PropertyDefs::s_toolButtonWithoutMenuIndicator, true); + btn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true); auto menu = WidgetsFactory::createMenu(tb); btn->setMenu(menu); diff --git a/src/widgets/viewarea.cpp b/src/widgets/viewarea.cpp index caf3758e..87b9b318 100644 --- a/src/widgets/viewarea.cpp +++ b/src/widgets/viewarea.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "viewwindow.h" #include "mainwindow.h" @@ -655,9 +656,7 @@ bool ViewArea::removeWorkspaceInViewSplit(ViewSplit *p_split, bool p_insertNew) { // Close all the ViewWindows. setCurrentViewSplit(p_split, true); - auto wins = getAllViewWindows(p_split, [](ViewWindow *) { - return true; - }); + auto wins = getAllViewWindows(p_split); for (const auto win : wins) { if (!closeViewWindow(win, false, false)) { return false; @@ -963,7 +962,7 @@ void ViewArea::dropEvent(QDropEvent *p_event) QWidget::dropEvent(p_event); } -QVector ViewArea::getAllViewWindows(ViewSplit *p_split, const ViewSplit::ViewWindowSelector &p_func) +QVector ViewArea::getAllViewWindows(ViewSplit *p_split, const ViewSplit::ViewWindowSelector &p_func) const { QVector wins; p_split->forEachViewWindow([p_func, &wins](ViewWindow *p_win) { @@ -974,3 +973,24 @@ QVector ViewArea::getAllViewWindows(ViewSplit *p_split, const View }); return wins; } + +QVector ViewArea::getAllViewWindows(ViewSplit *p_split) const +{ + return getAllViewWindows(p_split, [](ViewWindow *) { + return true; + }); +} + +QList ViewArea::getAllBuffersInViewSplits() const +{ + QSet bufferSet; + + for (auto split : m_splits) { + auto wins = getAllViewWindows(split); + for (auto win : wins) { + bufferSet.insert(win->getBuffer()); + } + } + + return bufferSet.values(); +} diff --git a/src/widgets/viewarea.h b/src/widgets/viewarea.h index f6b6afc4..2710c754 100644 --- a/src/widgets/viewarea.h +++ b/src/widgets/viewarea.h @@ -68,6 +68,9 @@ namespace vnotex QSize sizeHint() const Q_DECL_OVERRIDE; + // Not all Workspace. Just all ViewSplits. + QList getAllBuffersInViewSplits() const; + public slots: void openBuffer(Buffer *p_buffer, const QSharedPointer &p_paras); @@ -198,7 +201,9 @@ namespace vnotex void checkCurrentViewWindowChange(); - QVector getAllViewWindows(ViewSplit *p_split, const ViewSplit::ViewWindowSelector &p_func); + QVector getAllViewWindows(ViewSplit *p_split, const ViewSplit::ViewWindowSelector &p_func) const; + + QVector getAllViewWindows(ViewSplit *p_split) const; QLayout *m_mainLayout = nullptr; diff --git a/src/widgets/viewsplit.cpp b/src/widgets/viewsplit.cpp index 97637fb7..6653f92a 100644 --- a/src/widgets/viewsplit.cpp +++ b/src/widgets/viewsplit.cpp @@ -109,7 +109,7 @@ void ViewSplit::setupCornerWidget() // Container. auto widget = new QWidget(this); - widget->setProperty(PropertyDefs::s_viewSplitCornerWidget, true); + widget->setProperty(PropertyDefs::c_viewSplitCornerWidget, true); auto layout = new QHBoxLayout(widget); layout->setContentsMargins(0, 0, 0, 0); @@ -117,7 +117,7 @@ void ViewSplit::setupCornerWidget() { m_windowListButton = new QToolButton(this); m_windowListButton->setPopupMode(QToolButton::InstantPopup); - m_windowListButton->setProperty(PropertyDefs::s_actionToolButton, true); + m_windowListButton->setProperty(PropertyDefs::c_actionToolButton, true); auto act = new QAction(s_windowListIcon, tr("Windows List"), m_windowListButton); m_windowListButton->setDefaultAction(act); @@ -141,7 +141,7 @@ void ViewSplit::setupCornerWidget() { m_menuButton = new QToolButton(this); m_menuButton->setPopupMode(QToolButton::InstantPopup); - m_menuButton->setProperty(PropertyDefs::s_actionToolButton, true); + m_menuButton->setProperty(PropertyDefs::c_actionToolButton, true); auto act = new QAction(s_menuIcon, tr("Workspaces and Splits"), m_menuButton); m_menuButton->setDefaultAction(act); @@ -607,7 +607,7 @@ void ViewSplit::mousePressEvent(QMouseEvent *p_event) emit focused(this); } -bool ViewSplit::forEachViewWindow(const ViewWindowSelector &p_func) +bool ViewSplit::forEachViewWindow(const ViewWindowSelector &p_func) const { int cnt = getViewWindowCount(); for (int i = 0; i < cnt; ++i) { diff --git a/src/widgets/viewsplit.h b/src/widgets/viewsplit.h index f2526b41..262d5cfd 100644 --- a/src/widgets/viewsplit.h +++ b/src/widgets/viewsplit.h @@ -56,7 +56,7 @@ namespace vnotex // @p_func: return true if going well, return false to stop the iteration. // Return false if there is a break. - bool forEachViewWindow(const ViewWindowSelector &p_func); + bool forEachViewWindow(const ViewWindowSelector &p_func) const; QVector getNavigationModeInfo() const; diff --git a/src/widgets/viewwindow.cpp b/src/widgets/viewwindow.cpp index 0e417a05..ed43806d 100644 --- a/src/widgets/viewwindow.cpp +++ b/src/widgets/viewwindow.cpp @@ -128,7 +128,7 @@ void ViewWindow::initIcons() } const auto &themeMgr = VNoteX::getInst().getThemeMgr(); - const QString savedIconName("saved.svg"); + const QString savedIconName("buffer.svg"); const QString unsavedIconFg("base#icon#warning#fg"); s_savedIcon = IconUtils::fetchIcon(themeMgr.getIconFile(savedIconName)); s_modifiedIcon = IconUtils::fetchIcon(themeMgr.getIconFile(savedIconName), @@ -1083,6 +1083,6 @@ void ViewWindow::read(bool p_save) QToolBar *ViewWindow::createToolBar(QWidget *p_parent) { auto toolBar = new QToolBar(p_parent); - toolBar->setProperty(PropertyDefs::s_viewWindowToolBar, true); + toolBar->setProperty(PropertyDefs::c_viewWindowToolBar, true); return toolBar; } diff --git a/src/widgets/viewwindowtoolbarhelper.cpp b/src/widgets/viewwindowtoolbarhelper.cpp index c71da160..7487d4f2 100644 --- a/src/widgets/viewwindowtoolbarhelper.cpp +++ b/src/widgets/viewwindowtoolbarhelper.cpp @@ -121,7 +121,7 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action) auto toolBtn = dynamic_cast(p_tb->widgetForAction(act)); Q_ASSERT(toolBtn); toolBtn->setPopupMode(QToolButton::InstantPopup); - toolBtn->setProperty(PropertyDefs::s_toolButtonWithoutMenuIndicator, true); + toolBtn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true); auto menu = WidgetsFactory::createMenu(p_tb); @@ -282,7 +282,7 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action) auto toolBtn = dynamic_cast(p_tb->widgetForAction(act)); Q_ASSERT(toolBtn); toolBtn->setPopupMode(QToolButton::InstantPopup); - toolBtn->setProperty(PropertyDefs::s_toolButtonWithoutMenuIndicator, true); + toolBtn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true); auto menu = new AttachmentPopup(toolBtn, p_tb); toolBtn->setMenu(menu); @@ -297,7 +297,7 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action) auto toolBtn = dynamic_cast(p_tb->widgetForAction(act)); Q_ASSERT(toolBtn); toolBtn->setPopupMode(QToolButton::InstantPopup); - toolBtn->setProperty(PropertyDefs::s_toolButtonWithoutMenuIndicator, true); + toolBtn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true); addButtonShortcut(toolBtn, editorConfig.getShortcut(Shortcut::Outline), viewWindow); @@ -322,7 +322,7 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action) auto toolBtn = dynamic_cast(p_tb->widgetForAction(act)); Q_ASSERT(toolBtn); toolBtn->setPopupMode(QToolButton::InstantPopup); - toolBtn->setProperty(PropertyDefs::s_toolButtonWithoutMenuIndicator, true); + toolBtn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true); auto menu = WidgetsFactory::createMenu(p_tb); diff --git a/src/widgets/widgets.pri b/src/widgets/widgets.pri index 14460e73..98c11012 100644 --- a/src/widgets/widgets.pri +++ b/src/widgets/widgets.pri @@ -43,6 +43,7 @@ SOURCES += \ $$PWD/lineedit.cpp \ $$PWD/lineeditdelegate.cpp \ $$PWD/listwidget.cpp \ + $$PWD/locationlist.cpp \ $$PWD/mainwindow.cpp \ $$PWD/markdownviewwindow.cpp \ $$PWD/navigationmodemgr.cpp \ @@ -50,6 +51,8 @@ SOURCES += \ $$PWD/outlineprovider.cpp \ $$PWD/outlineviewer.cpp \ $$PWD/propertydefs.cpp \ + $$PWD/searchinfoprovider.cpp \ + $$PWD/searchpanel.cpp \ $$PWD/systemtrayhelper.cpp \ $$PWD/textviewwindow.cpp \ $$PWD/toolbarhelper.cpp \ @@ -130,6 +133,7 @@ HEADERS += \ $$PWD/lineedit.h \ $$PWD/lineeditdelegate.h \ $$PWD/listwidget.h \ + $$PWD/locationlist.h \ $$PWD/mainwindow.h \ $$PWD/markdownviewwindow.h \ $$PWD/navigationmodemgr.h \ @@ -138,6 +142,8 @@ HEADERS += \ $$PWD/outlineprovider.h \ $$PWD/outlineviewer.h \ $$PWD/propertydefs.h \ + $$PWD/searchinfoprovider.h \ + $$PWD/searchpanel.h \ $$PWD/systemtrayhelper.h \ $$PWD/textviewwindow.h \ $$PWD/textviewwindowhelper.h \ diff --git a/src/widgets/widgetsfactory.cpp b/src/widgets/widgetsfactory.cpp index a3a9710d..9ede5c92 100644 --- a/src/widgets/widgetsfactory.cpp +++ b/src/widgets/widgetsfactory.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "lineedit.h" #include "combobox.h" @@ -52,6 +53,11 @@ QCheckBox *WidgetsFactory::createCheckBox(const QString &p_text, QWidget *p_pare return new QCheckBox(p_text, p_parent); } +QRadioButton *WidgetsFactory::createRadioButton(const QString &p_text, QWidget *p_parent) +{ + return new QRadioButton(p_text, p_parent); +} + QSpinBox *WidgetsFactory::createSpinBox(QWidget *p_parent) { return new QSpinBox(p_parent); diff --git a/src/widgets/widgetsfactory.h b/src/widgets/widgetsfactory.h index 68de9c65..4bb9e5a6 100644 --- a/src/widgets/widgetsfactory.h +++ b/src/widgets/widgetsfactory.h @@ -12,6 +12,7 @@ class QToolButton; class QDoubleSpinBox; class QFormLayout; class QPlainTextEdit; +class QRadioButton; namespace vnotex { @@ -32,6 +33,8 @@ namespace vnotex static QCheckBox *createCheckBox(const QString &p_text, QWidget *p_parent = nullptr); + static QRadioButton *createRadioButton(const QString &p_text, QWidget *p_parent = nullptr); + static QSpinBox *createSpinBox(QWidget *p_parent = nullptr); static QDoubleSpinBox *createDoubleSpinBox(QWidget *p_parent = nullptr); diff --git a/tests/test_core/test_notebook/test_notebook.pro b/tests/test_core/test_notebook/test_notebook.pro index 065582cc..5b6140f2 100644 --- a/tests/test_core/test_notebook/test_notebook.pro +++ b/tests/test_core/test_notebook/test_notebook.pro @@ -18,6 +18,7 @@ include($$CORE_FOLDER/core.pri) include($$SRC_FOLDER/widgets/widgets.pri) include($$SRC_FOLDER/utils/utils.pri) include($$SRC_FOLDER/export/export.pri) +include($$SRC_FOLDER/search/search.pri) SOURCES += \ test_notebook.cpp