diff --git a/src/librssguard/CMakeLists.txt b/src/librssguard/CMakeLists.txt index e579601cf..58435fb9b 100644 --- a/src/librssguard/CMakeLists.txt +++ b/src/librssguard/CMakeLists.txt @@ -131,6 +131,8 @@ set(SOURCES gui/reusable/searchtextwidget.h gui/reusable/squeezelabel.cpp gui/reusable/squeezelabel.h + gui/reusable/searchlineedit.cpp + gui/reusable/searchlineedit.h gui/reusable/styleditemdelegatewithoutfocus.cpp gui/reusable/styleditemdelegatewithoutfocus.h gui/reusable/timespinbox.cpp diff --git a/src/librssguard/core/messagesproxymodel.cpp b/src/librssguard/core/messagesproxymodel.cpp index bf1713049..2050828aa 100644 --- a/src/librssguard/core/messagesproxymodel.cpp +++ b/src/librssguard/core/messagesproxymodel.cpp @@ -21,7 +21,7 @@ MessagesProxyModel::MessagesProxyModel(MessagesModel* source_model, QObject* par setSortCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); setFilterKeyColumn(-1); - setFilterRole(LOWER_TITLE_ROLE); + setFilterRole(Qt::ItemDataRole::EditRole); setDynamicSortFilter(false); setSourceModel(m_sourceModel); diff --git a/src/librssguard/gui/dialogs/formmain.cpp b/src/librssguard/gui/dialogs/formmain.cpp index 9ec783449..5dff26a22 100644 --- a/src/librssguard/gui/dialogs/formmain.cpp +++ b/src/librssguard/gui/dialogs/formmain.cpp @@ -16,8 +16,8 @@ #include "gui/messagebox.h" #include "gui/messagepreviewer.h" #include "gui/messagesview.h" -#include "gui/reusable/baselineedit.h" #include "gui/reusable/plaintoolbutton.h" +#include "gui/reusable/searchlineedit.h" #include "gui/systemtrayicon.h" #include "gui/tabbar.h" #include "gui/toolbars/feedstoolbar.h" diff --git a/src/librssguard/gui/feedmessageviewer.cpp b/src/librssguard/gui/feedmessageviewer.cpp index 905103197..434313e44 100644 --- a/src/librssguard/gui/feedmessageviewer.cpp +++ b/src/librssguard/gui/feedmessageviewer.cpp @@ -230,10 +230,7 @@ void FeedMessageViewer::displayMessage(const Message& message, RootItem* root) { void FeedMessageViewer::createConnections() { // Filtering & searching. - connect(m_toolBarMessages, - &MessagesToolBar::messageSearchPatternChanged, - m_messagesView, - &MessagesView::searchMessages); + connect(m_toolBarMessages, &MessagesToolBar::searchCriteriaChanged, m_messagesView, &MessagesView::searchMessages); connect(m_toolBarFeeds, &FeedsToolBar::feedsFilterPatternChanged, m_feedsView, &FeedsView::filterItems); connect(m_toolBarMessages, &MessagesToolBar::messageHighlighterChanged, diff --git a/src/librssguard/gui/messagesview.cpp b/src/librssguard/gui/messagesview.cpp index 4e51405bb..b22ede5eb 100644 --- a/src/librssguard/gui/messagesview.cpp +++ b/src/librssguard/gui/messagesview.cpp @@ -11,6 +11,7 @@ #include "gui/reusable/labelsmenu.h" #include "gui/reusable/styleditemdelegatewithoutfocus.h" #include "gui/reusable/treeviewcolumnsmenu.h" +#include "gui/toolbars/messagestoolbar.h" #include "miscellaneous/externaltool.h" #include "miscellaneous/feedreader.h" #include "miscellaneous/settings.h" @@ -756,10 +757,33 @@ void MessagesView::selectNextUnreadItem() { } } -void MessagesView::searchMessages(const QString& pattern) { - qDebugNN << LOGSEC_GUI << "Running search of messages with pattern" << QUOTE_W_SPACE_DOT(pattern); +void MessagesView::searchMessages(SearchLineEdit::SearchMode mode, + Qt::CaseSensitivity sensitivity, + int custom_criteria, + const QString& phrase) { + qDebugNN << LOGSEC_GUI << "Running search of messages with pattern" << QUOTE_W_SPACE_DOT(phrase); - m_proxyModel->setFilterRegularExpression(pattern.toLower()); + switch (mode) { + case SearchLineEdit::SearchMode::Wildcard: + m_proxyModel->setFilterWildcard(phrase); + break; + + case SearchLineEdit::SearchMode::RegularExpression: + m_proxyModel->setFilterRegularExpression(phrase); + break; + + case SearchLineEdit::SearchMode::FixedString: + default: + m_proxyModel->setFilterFixedString(phrase); + break; + } + + m_proxyModel->setFilterCaseSensitivity(sensitivity); + + MessagesToolBar::SearchFields where_search = MessagesToolBar::SearchFields(custom_criteria); + + m_proxyModel->setFilterKeyColumn(where_search == MessagesToolBar::SearchFields::SearchTitleOnly ? MSG_DB_TITLE_INDEX + : -1); if (selectionModel()->selectedRows().isEmpty()) { emit currentMessageRemoved(); diff --git a/src/librssguard/gui/messagesview.h b/src/librssguard/gui/messagesview.h index 3ed2e3a43..926c03903 100644 --- a/src/librssguard/gui/messagesview.h +++ b/src/librssguard/gui/messagesview.h @@ -7,6 +7,7 @@ #include "core/messagesmodel.h" #include "core/messagesproxymodel.h" +#include "gui/reusable/searchlineedit.h" #include "services/abstract/rootitem.h" #include @@ -61,7 +62,10 @@ class MessagesView : public BaseTreeView { void selectNextUnreadItem(); // Searchs the visible message according to given pattern. - void searchMessages(const QString& pattern); + void searchMessages(SearchLineEdit::SearchMode mode, + Qt::CaseSensitivity sensitivity, + int custom_criteria, + const QString& phrase); void highlightMessages(MessagesModel::MessageHighlighter highlighter); void changeFilter(MessagesProxyModel::MessageListFilter filter); diff --git a/src/librssguard/gui/reusable/plaintoolbutton.cpp b/src/librssguard/gui/reusable/plaintoolbutton.cpp index ddbb272d9..5b784b834 100644 --- a/src/librssguard/gui/reusable/plaintoolbutton.cpp +++ b/src/librssguard/gui/reusable/plaintoolbutton.cpp @@ -3,8 +3,9 @@ #include "gui/reusable/plaintoolbutton.h" #include -#include #include +#include +#include #include #include #include @@ -29,6 +30,22 @@ void PlainToolButton::paintEvent(QPaintEvent* e) { } icon().paint(&p, rect); + + if (menu() != nullptr) { + // Draw "dropdown" triangle. + QPainterPath path; + + const int triangle_width = int(rect.width() * 0.4); + const int triangle_height = int(triangle_width * 0.5); + const auto triangle_origin = rect.bottomRight() - QPoint(triangle_width, triangle_height); + + path.moveTo(triangle_origin); + path.lineTo(QPoint(rect.right(), triangle_origin.y())); + path.lineTo(triangle_origin + QPoint(triangle_width / 2, triangle_height)); + path.lineTo(triangle_origin); + + p.fillPath(path, QBrush(Qt::GlobalColor::black)); + } } int PlainToolButton::padding() const { diff --git a/src/librssguard/gui/reusable/searchlineedit.cpp b/src/librssguard/gui/reusable/searchlineedit.cpp new file mode 100755 index 000000000..5ffcb7851 --- /dev/null +++ b/src/librssguard/gui/reusable/searchlineedit.cpp @@ -0,0 +1,112 @@ +// For license of this file, see /LICENSE.md. + +#include "gui/reusable/searchlineedit.h" + +#include "3rd-party/boolinq/boolinq.h" +#include "gui/reusable/plaintoolbutton.h" +#include "miscellaneous/application.h" +#include "miscellaneous/iconfactory.h" + +#include +#include +#include + +SearchLineEdit::SearchLineEdit(const QList& choices, QWidget* parent) : BaseLineEdit(parent) { + QWidgetAction* act = new QWidgetAction(this); + PlainToolButton* btn = new PlainToolButton(this); + + m_tmrSearchPattern = new QTimer(this); + m_tmrSearchPattern->setSingleShot(true); + m_tmrSearchPattern->setInterval(300); + + m_menu = new QMenu(btn); + + m_actionGroupChoices = new QActionGroup(this); + m_actionGroupChoices->setExclusive(true); + + m_actionGroupModes = new QActionGroup(this); + m_actionGroupModes->setExclusive(true); + + m_actCaseSensitivity = m_menu->addAction(tr("Case-sensitive")); + m_actCaseSensitivity->setCheckable(true); + + m_menu->addSeparator(); + + // Setup tool button. + btn->setIcon(qApp->icons()->fromTheme(QSL("system-search"))); + btn->setPopupMode(QToolButton::ToolButtonPopupMode::InstantPopup); + btn->setMenu(m_menu); + + act->setDefaultWidget(btn); + + addAction(act, QLineEdit::ActionPosition::LeadingPosition); + + // Load predefined modes. + for (SearchMode mode : {SearchMode::FixedString, SearchMode::Wildcard, SearchMode::RegularExpression}) { + QAction* ac = m_actionGroupModes->addAction(m_menu->addAction(titleForMode(mode))); + + ac->setCheckable(true); + ac->setData(int(mode)); + } + + m_actionGroupModes->actions().first()->setChecked(true); + + if (!choices.isEmpty()) { + m_menu->addSeparator(); + + // Load custom coices. + for (const CustomSearchChoice& choice : choices) { + QAction* ac = m_actionGroupChoices->addAction(m_menu->addAction(choice.m_title)); + + ac->setCheckable(true); + ac->setData(choice.m_data); + } + + m_actionGroupChoices->actions().first()->setChecked(true); + } + + // NOTE: When any change is made, (re)start the timer which fires + // the signal with delay to avoid throttling. + connect(this, &SearchLineEdit::textChanged, m_tmrSearchPattern, QOverload<>::of(&QTimer::start)); + connect(m_menu, &QMenu::triggered, m_tmrSearchPattern, QOverload<>::of(&QTimer::start)); + connect(m_tmrSearchPattern, &QTimer::timeout, this, &SearchLineEdit::startSearch); +} + +void SearchLineEdit::startSearch() { + SearchMode mode = SearchMode(boolinq::from(m_actionGroupModes->actions()) + .first([](const QAction* act) { + return act->isChecked(); + }) + ->data() + .toInt()); + + int custom_criteria = boolinq::from(m_actionGroupChoices->actions()) + .first([](const QAction* act) { + return act->isChecked(); + }) + ->data() + .toInt(); + + bool case_sensitive = m_actCaseSensitivity->isChecked(); + + emit searchCriteriaChanged(mode, + case_sensitive ? Qt::CaseSensitivity::CaseSensitive : Qt::CaseSensitivity::CaseInsensitive, + custom_criteria, + text()); +} + +QString SearchLineEdit::titleForMode(SearchMode mode) { + switch (mode) { + case SearchLineEdit::SearchMode::FixedString: + return tr("Fixed text"); + + case SearchLineEdit::SearchMode::Wildcard: + return tr("Wildcard"); + + case SearchLineEdit::SearchMode::RegularExpression: + return tr("Regular expression"); + + default: + return {}; + } +} diff --git a/src/librssguard/gui/reusable/searchlineedit.h b/src/librssguard/gui/reusable/searchlineedit.h new file mode 100755 index 000000000..4be8a3e62 --- /dev/null +++ b/src/librssguard/gui/reusable/searchlineedit.h @@ -0,0 +1,46 @@ +// For license of this file, see /LICENSE.md. + +#ifndef SEARCHLINEEDIT_H +#define SEARCHLINEEDIT_H + +#include "gui/reusable/baselineedit.h" + +class QActionGroup; + +class SearchLineEdit : public BaseLineEdit { + Q_OBJECT + + public: + struct CustomSearchChoice { + public: + CustomSearchChoice(const QString& title, int data) : m_title(title), m_data(data) {} + + QString m_title; + int m_data; + }; + + enum class SearchMode { FixedString = 1, Wildcard = 2, RegularExpression = 4 }; + + explicit SearchLineEdit(const QList& choices, QWidget* parent = nullptr); + + private slots: + void startSearch(); + + signals: + void searchCriteriaChanged(SearchMode mode, + Qt::CaseSensitivity sensitivity, + int custom_criteria, + const QString& phrase); + + private: + QString titleForMode(SearchMode mode); + + private: + QTimer* m_tmrSearchPattern; + QMenu* m_menu; + QAction* m_actCaseSensitivity; + QActionGroup* m_actionGroupModes; + QActionGroup* m_actionGroupChoices; +}; + +#endif // SEARCHLINEEDIT_H diff --git a/src/librssguard/gui/toolbars/messagestoolbar.cpp b/src/librssguard/gui/toolbars/messagestoolbar.cpp index 6a8fd94ce..274fa73f3 100644 --- a/src/librssguard/gui/toolbars/messagestoolbar.cpp +++ b/src/librssguard/gui/toolbars/messagestoolbar.cpp @@ -4,7 +4,6 @@ #include "3rd-party/boolinq/boolinq.h" #include "definitions/definitions.h" -#include "gui/reusable/baselineedit.h" #include "miscellaneous/iconfactory.h" #include "miscellaneous/settings.h" @@ -151,10 +150,10 @@ void MessagesToolBar::handleMessageFilterChange(QAction* action) { } void MessagesToolBar::initializeSearchBox() { - m_tmrSearchPattern = new QTimer(this); - m_tmrSearchPattern->setSingleShot(true); - - m_txtSearchMessages = new BaseLineEdit(this); + m_txtSearchMessages = + new SearchLineEdit({SearchLineEdit::CustomSearchChoice(tr("Everywhere"), int(SearchFields::SearchAll)), + SearchLineEdit::CustomSearchChoice(tr("Titles only"), int(SearchFields::SearchTitleOnly))}, + this); m_txtSearchMessages->setSizePolicy(QSizePolicy::Policy::Expanding, m_txtSearchMessages->sizePolicy().verticalPolicy()); m_txtSearchMessages->setPlaceholderText(tr("Search articles (regex only)")); @@ -166,10 +165,7 @@ void MessagesToolBar::initializeSearchBox() { m_actionSearchMessages->setProperty("type", SEARCH_BOX_ACTION_NAME); m_actionSearchMessages->setProperty("name", tr("Article search box")); - connect(m_txtSearchMessages, &BaseLineEdit::textChanged, this, &MessagesToolBar::onSearchPatternChanged); - connect(m_tmrSearchPattern, &QTimer::timeout, this, [this]() { - emit messageSearchPatternChanged(m_searchPattern); - }); + connect(m_txtSearchMessages, &SearchLineEdit::searchCriteriaChanged, this, &MessagesToolBar::searchCriteriaChanged); } void MessagesToolBar::addActionToMenu(QMenu* menu, @@ -305,7 +301,7 @@ void MessagesToolBar::saveToolButtonSelection(const QString& button_name, const qApp->settings()->setValue(GROUP(GUI), GUI::MessagesToolbarDefaultButtons, action_names.join(QSL(","))); } -BaseLineEdit* MessagesToolBar::searchBox() const { +SearchLineEdit* MessagesToolBar::searchBox() const { return m_txtSearchMessages; } @@ -347,8 +343,3 @@ QStringList MessagesToolBar::savedActions() const { QString::SplitBehavior::SkipEmptyParts); #endif } - -void MessagesToolBar::onSearchPatternChanged(const QString& search_pattern) { - m_searchPattern = search_pattern; - m_tmrSearchPattern->start(search_pattern.isEmpty() ? 0ms : 300ms); -} diff --git a/src/librssguard/gui/toolbars/messagestoolbar.h b/src/librssguard/gui/toolbars/messagestoolbar.h index f2bbdaf91..24821cacd 100644 --- a/src/librssguard/gui/toolbars/messagestoolbar.h +++ b/src/librssguard/gui/toolbars/messagestoolbar.h @@ -7,17 +7,19 @@ #include "core/messagesmodel.h" #include "core/messagesproxymodel.h" +#include "gui/reusable/searchlineedit.h" -class BaseLineEdit; class QWidgetAction; class QToolButton; class QMenu; class QTimer; class MessagesToolBar : public BaseToolBar { - Q_OBJECT + Q_OBJECT public: + enum class SearchFields { SearchTitleOnly = 1, SearchAll = 2 }; + explicit MessagesToolBar(const QString& title, QWidget* parent = nullptr); virtual QList availableActions() const; @@ -28,24 +30,30 @@ class MessagesToolBar : public BaseToolBar { virtual QStringList defaultActions() const; virtual QStringList savedActions() const; - BaseLineEdit *searchBox() const; + SearchLineEdit* searchBox() const; signals: - void messageSearchPatternChanged(const QString& pattern); + void searchCriteriaChanged(SearchLineEdit::SearchMode mode, + Qt::CaseSensitivity sensitivity, + int custom_criteria, + const QString& phrase); void messageHighlighterChanged(MessagesModel::MessageHighlighter highlighter); void messageFilterChanged(MessagesProxyModel::MessageListFilter filter); private slots: - void onSearchPatternChanged(const QString& search_pattern); void handleMessageHighlighterChange(QAction* action); void handleMessageFilterChange(QAction* action); private: void initializeSearchBox(); - void addActionToMenu(QMenu* menu, const QIcon& icon, const QString& title, const QVariant& value, const QString& name); + void addActionToMenu(QMenu* menu, + const QIcon& icon, + const QString& title, + const QVariant& value, + const QString& name); void initializeHighlighter(); void activateAction(const QString& action_name, QWidgetAction* widget_action); - void saveToolButtonSelection(const QString& button_name, const QList &actions) const; + void saveToolButtonSelection(const QString& button_name, const QList& actions) const; private: QWidgetAction* m_actionMessageHighlighter; @@ -55,9 +63,7 @@ class MessagesToolBar : public BaseToolBar { QMenu* m_menuMessageHighlighter; QMenu* m_menuMessageFilter; QWidgetAction* m_actionSearchMessages; - BaseLineEdit* m_txtSearchMessages; - QTimer* m_tmrSearchPattern; - QString m_searchPattern; + SearchLineEdit* m_txtSearchMessages; }; #endif // NEWSTOOLBAR_H