From 996f5fd7ad848fcbb2d2dce04fa6b5ffc4ea193b Mon Sep 17 00:00:00 2001 From: Chris Vigil Date: Sat, 5 Aug 2023 10:02:07 +0000 Subject: [PATCH] Resolve "Extend searching in the console with regex and toggleable case-sensitivity" --- AUTHORS.md | 1 + CHANGELOG.md | 1 + apps/openmw/mwgui/console.cpp | 186 ++++++++++++++++++++----- apps/openmw/mwgui/console.hpp | 19 ++- files/data/mygui/openmw_console.layout | 46 +++--- 5 files changed, 198 insertions(+), 55 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 4a277b9f46..99080fdebd 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -54,6 +54,7 @@ Programmers Cédric Mocquillon Chris Boyce (slothlife) Chris Robinson (KittyCat) + Chris Vigil Cody Glassman (Wazabear) Coleman Smith (olcoal) Cory F. Cohen (cfcohen) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b8e00af6f..7fac07c2ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,7 @@ Feature #7148: Optimize string literal lookup in mwscript Feature #7194: Ori to show texture paths Feature #7214: Searching in the in-game console + Feature #7284: Searching in the console with regex and toggleable case-sensitivity Feature #7477: NegativeLight Magic Effect flag Feature #7499: OpenMW-CS: Generate record filters by drag & dropping cell content to the filters field Task #7113: Move from std::atoi to std::from_char diff --git a/apps/openmw/mwgui/console.cpp b/apps/openmw/mwgui/console.cpp index 4ef454df7b..ca0b0674f3 100644 --- a/apps/openmw/mwgui/console.cpp +++ b/apps/openmw/mwgui/console.cpp @@ -7,7 +7,9 @@ #include #include +#include +#include #include #include #include @@ -144,6 +146,8 @@ namespace MWGui Console::Console(int w, int h, bool consoleOnlyScripts, Files::ConfigurationManager& cfgMgr) : WindowBase("openmw_console.layout") + , mCaseSensitiveSearch(false) + , mRegExSearch(false) , mCompilerContext(MWScript::CompilerContext::Type_Console) , mConsoleOnlyScripts(consoleOnlyScripts) , mCfgMgr(cfgMgr) @@ -155,6 +159,8 @@ namespace MWGui getWidget(mSearchTerm, "edit_SearchTerm"); getWidget(mNextButton, "button_Next"); getWidget(mPreviousButton, "button_Previous"); + getWidget(mCaseSensitiveToggleButton, "button_CaseSensitive"); + getWidget(mRegExSearchToggleButton, "button_RegExSearch"); // Set up the command line box mCommandLine->eventEditSelectAccept += newDelegate(this, &Console::acceptCommand); @@ -163,7 +169,9 @@ namespace MWGui // Set up the search term box mSearchTerm->eventEditSelectAccept += newDelegate(this, &Console::acceptSearchTerm); mNextButton->eventMouseButtonClick += newDelegate(this, &Console::findNextOccurrence); - mPreviousButton->eventMouseButtonClick += newDelegate(this, &Console::findPreviousOccurence); + mPreviousButton->eventMouseButtonClick += newDelegate(this, &Console::findPreviousOccurrence); + mCaseSensitiveToggleButton->eventMouseButtonClick += newDelegate(this, &Console::toggleCaseSensitiveSearch); + mRegExSearchToggleButton->eventMouseButtonClick += newDelegate(this, &Console::toggleRegExSearch); // Set up the log window mHistory->setOverflowToTheLeft(true); @@ -387,6 +395,37 @@ namespace MWGui execute(cm); } + void Console::toggleCaseSensitiveSearch(MyGUI::Widget* _sender) + { + mCaseSensitiveSearch = !mCaseSensitiveSearch; + + // Reset console search highlight position search parameters have changed + mCurrentOccurrenceIndex = std::string::npos; + + // Adjust color to reflect toggled status + const TextColours& textColours{ MWBase::Environment::get().getWindowManager()->getTextColours() }; + mCaseSensitiveToggleButton->setTextColour(mCaseSensitiveSearch ? textColours.link : textColours.normal); + } + + void Console::toggleRegExSearch(MyGUI::Widget* _sender) + { + mRegExSearch = !mRegExSearch; + + // Reset console search highlight position search parameters have changed + mCurrentOccurrenceIndex = std::string::npos; + + // Adjust color to reflect toggled status + const TextColours& textColours{ MWBase::Environment::get().getWindowManager()->getTextColours() }; + mRegExSearchToggleButton->setTextColour(mRegExSearch ? textColours.link : textColours.normal); + + // RegEx searches are always case sensitive + mCaseSensitiveSearch = mRegExSearch; + + // Dim case sensitive and set disabled if regex search toggled on, restore when toggled off + mCaseSensitiveToggleButton->setTextColour(mCaseSensitiveSearch ? textColours.linkPressed : textColours.normal); + mCaseSensitiveToggleButton->setEnabled(!mRegExSearch); + } + void Console::acceptSearchTerm(MyGUI::EditBox* _sender) { const std::string& searchTerm = mSearchTerm->getOnlyText(); @@ -396,43 +435,84 @@ namespace MWGui return; } - mCurrentSearchTerm = Utf8Stream::lowerCaseUtf8(searchTerm); - mCurrentOccurrence = std::string::npos; + std::string newSearchTerm = mCaseSensitiveSearch ? searchTerm : Utf8Stream::lowerCaseUtf8(searchTerm); + + // If new search term reset position, otherwise continue from current position + if (newSearchTerm != mCurrentSearchTerm) + { + mCurrentSearchTerm = newSearchTerm; + mCurrentOccurrenceIndex = std::string::npos; + } findNextOccurrence(nullptr); } + enum class Console::SearchDirection + { + Forward, + Reverse + }; + void Console::findNextOccurrence(MyGUI::Widget* _sender) { + findOccurrence(SearchDirection::Forward); + } + + void Console::findPreviousOccurrence(MyGUI::Widget* _sender) + { + findOccurrence(SearchDirection::Reverse); + } + + void Console::findOccurrence(const SearchDirection direction) + { + if (mCurrentSearchTerm.empty()) { return; } - const auto historyText = Utf8Stream::lowerCaseUtf8(mHistory->getOnlyText().asUTF8()); + const auto historyText{ mCaseSensitiveSearch ? mHistory->getOnlyText().asUTF8() + : Utf8Stream::lowerCaseUtf8(mHistory->getOnlyText().asUTF8()) }; - // Search starts at the beginning - size_t startIndex = 0; + // Setup default search range + size_t firstIndex{ 0 }; + size_t lastIndex{ historyText.length() }; - // If this is not the first search, we start right AFTER the last occurrence. - if (mCurrentOccurrence != std::string::npos && historyText.length() - mCurrentOccurrence > 1) + // If search is not the first adjust the range based on the direction and previous occurrence. + if (mCurrentOccurrenceIndex != std::string::npos) { - startIndex = mCurrentOccurrence + 1; + if (direction == SearchDirection::Forward && mCurrentOccurrenceIndex > 1) + { + firstIndex = mCurrentOccurrenceIndex + mCurrentOccurrenceLength; + } + else if (direction == SearchDirection::Reverse + && (historyText.length() - mCurrentOccurrenceIndex) > mCurrentOccurrenceLength) + { + lastIndex = mCurrentOccurrenceIndex - 1; + } } - mCurrentOccurrence = historyText.find(mCurrentSearchTerm, startIndex); + findInHistoryText(historyText, direction, firstIndex, lastIndex); - // If the last search did not find anything AND we didn't start at - // the beginning, we repeat the search one time for wrapping around the text. - if (mCurrentOccurrence == std::string::npos && startIndex != 0) + // If the last search did not find anything AND... + if (mCurrentOccurrenceIndex == std::string::npos) { - mCurrentOccurrence = historyText.find(mCurrentSearchTerm); + if (direction == SearchDirection::Forward && firstIndex != 0) + { + // ... We didn't start at the beginning, we apply the search to the other half of the text. + findInHistoryText(historyText, direction, 0, firstIndex); + } + else if (direction == SearchDirection::Reverse && lastIndex != historyText.length()) + { + // ... We didn't search to the end, we apply the search to the other half of the text. + findInHistoryText(historyText, direction, lastIndex, historyText.length()); + } } // Only scroll & select if we actually found something - if (mCurrentOccurrence != std::string::npos) + if (mCurrentOccurrenceIndex != std::string::npos) { - markOccurrence(mCurrentOccurrence, mCurrentSearchTerm.length()); + markOccurrence(mCurrentOccurrenceIndex, mCurrentOccurrenceLength); } else { @@ -440,44 +520,78 @@ namespace MWGui } } - void Console::findPreviousOccurence(MyGUI::Widget* _sender) + void Console::findInHistoryText(const std::string& historyText, const SearchDirection direction, + const size_t firstIndex, const size_t lastIndex) { - if (mCurrentSearchTerm.empty()) + if (mRegExSearch) { - return; + findWithRegex(historyText, direction, firstIndex, lastIndex); } + else + { + findWithStringSearch(historyText, direction, firstIndex, lastIndex); + } + } - const auto historyText = Utf8Stream::lowerCaseUtf8(mHistory->getOnlyText().asUTF8()); - - // Search starts at the end - size_t startIndex = historyText.length(); + void Console::findWithRegex(const std::string& historyText, const SearchDirection direction, + const size_t firstIndex, const size_t lastIndex) + { + // Search text for regex match in given interval + const std::regex pattern{ mCurrentSearchTerm }; + std::sregex_iterator match{ (historyText.cbegin() + firstIndex), (historyText.cbegin() + lastIndex), pattern }; + const std::sregex_iterator end{}; - // If this is not the first search, we start right BEFORE the last occurrence. - if (mCurrentOccurrence != std::string::npos && mCurrentOccurrence > 1) + // If reverse search get last result in interval + if (direction == SearchDirection::Reverse) { - startIndex = mCurrentOccurrence - 1; + std::sregex_iterator lastMatch{ end }; + while (match != end) + { + lastMatch = match; + ++match; + } + match = lastMatch; } - mCurrentOccurrence = historyText.rfind(mCurrentSearchTerm, startIndex); + // If regex match is found in text, set new current occurrence values + if (match != end) + { + mCurrentOccurrenceIndex = match->position() + firstIndex; + mCurrentOccurrenceLength = match->length(); + } + else + { + mCurrentOccurrenceIndex = std::string::npos; + mCurrentOccurrenceLength = 0; + } + } - // If the last search did not find anything AND we didn't start at - // the end, we repeat the search one time for wrapping around the text. - if (mCurrentOccurrence == std::string::npos && startIndex != historyText.length()) + void Console::findWithStringSearch(const std::string& historyText, const SearchDirection direction, + const size_t firstIndex, const size_t lastIndex) + { + // Search in given text interval for search term + const size_t substringLength{ (lastIndex - firstIndex) + 1 }; + const std::string_view historyTextView((historyText.c_str() + firstIndex), substringLength); + if (direction == SearchDirection::Forward) + { + mCurrentOccurrenceIndex = historyTextView.find(mCurrentSearchTerm); + } + else { - mCurrentOccurrence = historyText.rfind(mCurrentSearchTerm, historyText.length()); + mCurrentOccurrenceIndex = historyTextView.rfind(mCurrentSearchTerm); } - // Only scroll & select if we actually found something - if (mCurrentOccurrence != std::string::npos) + // If search term is found in text, set new current occurrence values + if (mCurrentOccurrenceIndex != std::string::npos) { - markOccurrence(mCurrentOccurrence, mCurrentSearchTerm.length()); + mCurrentOccurrenceIndex += firstIndex; + mCurrentOccurrenceLength = mCurrentSearchTerm.length(); } else { - markOccurrence(0, 0); + mCurrentOccurrenceLength = 0; } } - void Console::markOccurrence(const size_t textPosition, const size_t length) { if (textPosition == 0 && length == 0) diff --git a/apps/openmw/mwgui/console.hpp b/apps/openmw/mwgui/console.hpp index fb31d7477e..79d18847a4 100644 --- a/apps/openmw/mwgui/console.hpp +++ b/apps/openmw/mwgui/console.hpp @@ -31,6 +31,8 @@ namespace MWGui MyGUI::EditBox* mSearchTerm; MyGUI::Button* mNextButton; MyGUI::Button* mPreviousButton; + MyGUI::Button* mCaseSensitiveToggleButton; + MyGUI::Button* mRegExSearchToggleButton; typedef std::list StringList; @@ -83,12 +85,25 @@ namespace MWGui void commandBoxKeyPress(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char _char); void acceptCommand(MyGUI::EditBox* _sender); + enum class SearchDirection; + void toggleCaseSensitiveSearch(MyGUI::Widget* _sender); + void toggleRegExSearch(MyGUI::Widget* _sender); void acceptSearchTerm(MyGUI::EditBox* _sender); void findNextOccurrence(MyGUI::Widget* _sender); - void findPreviousOccurence(MyGUI::Widget* _sender); + void findPreviousOccurrence(MyGUI::Widget* _sender); + void findOccurrence(SearchDirection direction); + void findInHistoryText( + const std::string& historyText, SearchDirection direction, size_t firstIndex, size_t lastIndex); + void findWithRegex( + const std::string& historyText, SearchDirection direction, size_t firstIndex, size_t lastIndex); + void findWithStringSearch( + const std::string& historyText, SearchDirection direction, size_t firstIndex, size_t lastIndex); void markOccurrence(size_t textPosition, size_t length); - size_t mCurrentOccurrence = std::string::npos; + size_t mCurrentOccurrenceIndex = std::string::npos; + size_t mCurrentOccurrenceLength = 0; std::string mCurrentSearchTerm; + bool mCaseSensitiveSearch; + bool mRegExSearch; std::string complete(std::string input, std::vector& matches); diff --git a/files/data/mygui/openmw_console.layout b/files/data/mygui/openmw_console.layout index a4b02de585..efaf902790 100644 --- a/files/data/mygui/openmw_console.layout +++ b/files/data/mygui/openmw_console.layout @@ -6,7 +6,7 @@ - + @@ -17,26 +17,38 @@ - - - - - + + + + + - - - - + + + + - - - - + + + + + + + + + + - - - + + + + + + + + +