diff --git a/include/wx/msw/textctrl.h b/include/wx/msw/textctrl.h index 97f45501a8..dd56f32b01 100644 --- a/include/wx/msw/textctrl.h +++ b/include/wx/msw/textctrl.h @@ -44,6 +44,7 @@ public: virtual wxString GetRTFValue() const override; virtual void SetRTFValue(const wxString& val) override; virtual bool IsRTFSupported() override { return IsRich(); } + virtual wxTextSearchResult SearchText(const wxTextSearch& search) const override; #endif // wxUSE_RICHEDIT virtual bool IsEmpty() const; diff --git a/include/wx/textctrl.h b/include/wx/textctrl.h index 44956d4829..19f89bb36a 100644 --- a/include/wx/textctrl.h +++ b/include/wx/textctrl.h @@ -608,6 +608,72 @@ private: wxString m_listStyleName; }; +// ---------------------------------------------------------------------------- +// Search features for wxTextCtrl +// ---------------------------------------------------------------------------- + +// search options +// -------------- +struct wxTextSearch +{ + explicit wxTextSearch(const wxString& text = wxString{}) : m_searchValue(text) {} + + enum class Direction + { + Down, + Up + }; + + wxTextSearch& SearchValue(const wxString& value) + { + m_searchValue = value; + return *this; + } + + wxTextSearch& MatchCase(bool matchCase = true) + { + m_matchCase = matchCase; + return *this; + } + + wxTextSearch& MatchWholeWord(const bool matchWholeWord = true) + { + m_wholeWord = matchWholeWord; + return *this; + } + + wxTextSearch& SearchDirection(const Direction direction) + { + m_direction = direction; + return *this; + } + + wxTextSearch& Start(const long startPosition) + { + m_startingPosition = startPosition; + return *this; + } + + wxString m_searchValue; + long m_startingPosition = -1; + bool m_matchCase = false; + bool m_wholeWord = false; + Direction m_direction = Direction::Down; +}; + +// results from a search operation +// ------------------------------- +struct wxTextSearchResult +{ + explicit operator bool() const { return m_start != wxNOT_FOUND; } + explicit wxTextSearchResult(long startPos, long endPos) : + m_start(startPos), m_end(endPos) {} + wxTextSearchResult() = default; + + long m_start = wxNOT_FOUND; + long m_end = wxNOT_FOUND; +}; + // ---------------------------------------------------------------------------- // wxTextAreaBase: multiline text control specific methods // ---------------------------------------------------------------------------- @@ -698,6 +764,15 @@ public: virtual wxString GetRTFValue() const; virtual void SetRTFValue(const wxString& val); + // Searches for text. + // Base class implementations simply asserts, + // the port must override these functions to really implement them. + virtual wxTextSearchResult SearchText(const wxTextSearch& WXUNUSED(search)) const + { + wxFAIL_MSG("Text search not implemented for the current platform."); + return wxTextSearchResult(); + } + protected: // implementation of loading/saving virtual bool DoLoadFile(const wxString& file, int fileType); diff --git a/interface/wx/textctrl.h b/interface/wx/textctrl.h index f0fb0f97ce..ce4ea424a1 100644 --- a/interface/wx/textctrl.h +++ b/interface/wx/textctrl.h @@ -1128,6 +1128,112 @@ public: void operator=(const wxTextAttr& attr); }; +/** + Search options for wxTextCtrl::SearchText(). + + This is a builder class, where property functions can be + called during construction. For example: + + @code + wxTextSearchResult result = + textctrl->SearchText(wxTextSearch(L"Institutional Research"). + SearchDirection(wxTextSearch::Direction::Down). + MatchCase(). + MatchWholeWord()); + @endcode + + @since 3.3.0 +*/ +struct wxTextSearch +{ + /** + The string to search for. + */ + wxTextSearch(const wxString& text) : m_searchValue(text) {} + + /** + One of the following values can be passed to wxTextSearch::SearchDirection() to + control direction when searching a wxTextCtrl. + + @since 3.3.0 + */ + enum class Direction + { + Down, + Up + }; + + /** + The string to search for. + */ + wxTextSearch& SearchValue(const wxString& value) + { + m_searchValue = value; + return *this; + } + + /** + Whether the search should match case (i.e., be case sensitive). + + By default, this is @c false; search will be case insensitive. + */ + wxTextSearch& MatchCase(const bool matchCase = true) + { + m_matchCase = matchCase; + return *this; + } + + /** + Whether the search should match the whole word. + + By default, this is @c false; searching will not match by whole word. + */ + wxTextSearch& MatchWholeWord(const bool matchWholeWord = true) + { + m_wholeWord = matchWholeWord; + return *this; + } + + /** + Whether the search should go up or down in the text control. + + By default, search will go downward. + */ + wxTextSearch& SearchDirection(const wxTextSearch::Direction direction) + { + m_direction = direction; + return *this; + } + + /** + Where the search should start from. By default, if searching down, + then the search will start at 0. If searching up, then will start + at the end of control. + */ + wxTextSearch& Start(const long startPosition) + { + m_startingPosition = startPosition; + return *this; + } + + wxString m_searchValue; + long m_startingPosition = -1; + bool m_matchCase = true; + bool m_wholeWord = false; + Direction m_direction = Direction::Down; +}; + +/** Result from wxTextCtrl::SearchText(), specifying the range of the found text. + Range values will be @c wxNOT_FOUND if a match was not found. + + @since 3.3.0 +*/ +struct wxTextSearchResult +{ + long m_start = wxNOT_FOUND; + long m_end = wxNOT_FOUND; +}; + /** @class wxTextProofOptions @@ -1701,6 +1807,20 @@ public: */ void SetRTFValue(const wxString& val); + /** + Searches for a string in the control, using the provided search options. + + The range of the match will be returned as a wxTextSearchResult, which will + contain -1 values if no match was found. + + This is currently only implemented under wxMSW. + + @since 3.3.0 + + @onlyfor{wxmsw} + */ + wxTextSearchResult SearchText(const wxTextSearch& search) const; + /** Finds the position of the character at the specified point. diff --git a/src/msw/textctrl.cpp b/src/msw/textctrl.cpp index 3c9da618d0..3fe35c5420 100644 --- a/src/msw/textctrl.cpp +++ b/src/msw/textctrl.cpp @@ -1198,6 +1198,57 @@ void wxTextCtrl::SetRTFValue(const wxString& val) SetInsertionPoint(0); } +wxTextSearchResult wxTextCtrl::SearchText(const wxTextSearch& search) const +{ + // set up the flags + WPARAM flags = 0; + switch ( search.m_direction ) + { + case wxTextSearch::Direction::Down: + flags |= FR_DOWN; + break; + + case wxTextSearch::Direction::Up: + // Nothing to do this is (surprisingly) the default. + break; + } + if (search.m_wholeWord) + { + flags |= FR_WHOLEWORD; + } + if (search.m_matchCase) + { + flags |= FR_MATCHCASE; + } + + FINDTEXTEX findText; + findText.chrg.cpMin = (search.m_startingPosition != -1) ? + // user-provided start + search.m_startingPosition : + // if going down, then start from 0; otherwise, start from end + (search.m_direction == wxTextSearch::Direction::Down) ? 0 : GetLastPosition(); + if (search.m_direction == wxTextSearch::Direction::Down) + { + // go to the end of the text + findText.chrg.cpMax = -1; + } + else + { + // will search from the start of the selection and upward + // to the start of the text + findText.chrg.cpMax = 0; + } + + findText.lpstrText = search.m_searchValue.wc_str(); + + if (SendMessage(GetHwnd(), EM_FINDTEXTEXW, flags, (LPARAM)&findText) == -1) + { + return wxTextSearchResult(); + } + + return wxTextSearchResult{ findText.chrgText.cpMin, findText.chrgText.cpMax }; +} + #endif // wxUSE_RICHEDIT void wxTextCtrl::WriteText(const wxString& value) diff --git a/tests/controls/textctrltest.cpp b/tests/controls/textctrltest.cpp index a4bc72eb15..ae5b6e7397 100644 --- a/tests/controls/textctrltest.cpp +++ b/tests/controls/textctrltest.cpp @@ -1487,6 +1487,112 @@ TEST_CASE("wxTextCtrl::Get/SetRTFValue", "[wxTextCtrl][rtf]") } #endif +#ifdef __WXMSW__ +TEST_CASE("wxTextCtrl::SearchText", "[wxTextCtrl][search]") +{ + wxWindow* const parent = wxTheApp->GetTopWindow(); + + std::unique_ptr text(new wxTextCtrl(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_RICH2 | wxTE_MULTILINE)); + + text->SetValue(R"(Allows more than 30Kb of text +(on all Windows versions) +and a very very long line to test wxHSCROLL style + +And here is a link in quotation marks to test wxTE_AUTO_URL: "http://www.wxwidgets.org" + +First 10 characters should be in red +Next 10 characters should be in blue +Next 10 characters should be normal +And the next 10 characters should be green and italic +This text should be cyan on blue +And this should be in blue and the text you type should be in blue as well. + +And there is a mispeled word)"); + + text->SetSelection(0, 0); + auto results = text->SearchText(wxTextSearch(L"IMnotHERE!").SearchDirection(wxTextSearch::Direction::Down)); + CHECK_FALSE(results); + + results = text->SearchText(wxTextSearch(L"window").SearchDirection(wxTextSearch::Direction::Down).MatchCase()); + CHECK_FALSE(results); // case is different + + // ignore case + results = text->SearchText(wxTextSearch(L"window").SearchDirection(wxTextSearch::Direction::Down).MatchCase(false)); + CHECK(results); + CHECK(results.m_start == 38); + CHECK(results.m_end == 44); + + results = text->SearchText(wxTextSearch(L"Window").SearchDirection(wxTextSearch::Direction::Down).MatchCase()); + CHECK(results); + CHECK(results.m_start == 38); + CHECK(results.m_end == 44); + + results = text->SearchText(wxTextSearch(L"Window").SearchDirection(wxTextSearch::Direction::Down).MatchCase().MatchWholeWord()); + CHECK_FALSE(results); // whole word fails + + results = text->SearchText(wxTextSearch(L"Windows").SearchDirection(wxTextSearch::Direction::Down).MatchCase().MatchWholeWord()); + CHECK(results); + CHECK(results.m_start == 38); + CHECK(results.m_end == 45); + + + results = text->SearchText(wxTextSearch(L"very").SearchDirection(wxTextSearch::Direction::Down).MatchCase().MatchWholeWord()); + CHECK(results); + CHECK(results.m_start == 62); + CHECK(results.m_end == 66); + + // will find the same match + results = text->SearchText(wxTextSearch(L"very").SearchDirection(wxTextSearch::Direction::Down).MatchCase().MatchWholeWord().Start(results.m_start)); + CHECK(results); + CHECK(results.m_start == 62); + CHECK(results.m_end == 66); + + // goes to next match + results = text->SearchText(wxTextSearch(L"very").SearchDirection(wxTextSearch::Direction::Down).MatchCase().MatchWholeWord().Start(results.m_start + 1)); + CHECK(results); + CHECK(results.m_start == 67); + CHECK(results.m_end == 71); + + // no more matches going down + results = text->SearchText(wxTextSearch(L"very").SearchDirection(wxTextSearch::Direction::Down).MatchCase().MatchWholeWord().Start(results.m_start + 1)); + CHECK_FALSE(results); + + // go up from the end + results = text->SearchText(wxTextSearch(L"very").SearchDirection(wxTextSearch::Direction::Up).MatchCase().MatchWholeWord()); + CHECK(results); + CHECK(results.m_start == 67); + CHECK(results.m_end == 71); + + results = text->SearchText(wxTextSearch(L"very").SearchDirection(wxTextSearch::Direction::Up).MatchCase().MatchWholeWord().Start(results.m_start)); + CHECK(results); + CHECK(results.m_start == 62); + CHECK(results.m_end == 66); + + // no more going up + results = text->SearchText(wxTextSearch(L"very").SearchDirection(wxTextSearch::Direction::Up).MatchCase().MatchWholeWord().Start(results.m_start)); + CHECK_FALSE(results); + + // phrase + results = text->SearchText(wxTextSearch(L"Next 10 characters").SearchDirection(wxTextSearch::Direction::Down).MatchCase().MatchWholeWord()); + CHECK(results); + CHECK(results.m_start == 233); + CHECK(results.m_end == 251); + + // Edge cases + // last word + results = text->SearchText(wxTextSearch(L"word").SearchDirection(wxTextSearch::Direction::Up).MatchCase().MatchWholeWord()); + CHECK(results); + CHECK(results.m_start == 494); + CHECK(results.m_end == 498); + + // first word + results = text->SearchText(wxTextSearch(L"Allows").SearchDirection(wxTextSearch::Direction::Down).MatchCase().MatchWholeWord()); + CHECK(results); + CHECK(results.m_start == 0); + CHECK(results.m_end == 6); +} +#endif + TEST_CASE("wxTextCtrl::InitialCanUndo", "[wxTextCtrl][undo]") { long style = 0;