1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-03-28 19:21:04 +00:00

Resolve "Extend searching in the console with regex and toggleable case-sensitivity"

This commit is contained in:
Chris Vigil 2023-08-05 10:02:07 +00:00 committed by psi29a
parent d2f16774d9
commit 996f5fd7ad
5 changed files with 213 additions and 70 deletions

View File

@ -54,6 +54,7 @@ Programmers
Cédric Mocquillon Cédric Mocquillon
Chris Boyce (slothlife) Chris Boyce (slothlife)
Chris Robinson (KittyCat) Chris Robinson (KittyCat)
Chris Vigil
Cody Glassman (Wazabear) Cody Glassman (Wazabear)
Coleman Smith (olcoal) Coleman Smith (olcoal)
Cory F. Cohen (cfcohen) Cory F. Cohen (cfcohen)

View File

@ -85,6 +85,7 @@
Feature #7148: Optimize string literal lookup in mwscript Feature #7148: Optimize string literal lookup in mwscript
Feature #7194: Ori to show texture paths Feature #7194: Ori to show texture paths
Feature #7214: Searching in the in-game console 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 #7477: NegativeLight Magic Effect flag
Feature #7499: OpenMW-CS: Generate record filters by drag & dropping cell content to the filters field 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 Task #7113: Move from std::atoi to std::from_char

View File

@ -7,7 +7,9 @@
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <regex>
#include <apps/openmw/mwgui/textcolours.hpp>
#include <components/compiler/exception.hpp> #include <components/compiler/exception.hpp>
#include <components/compiler/extensions0.hpp> #include <components/compiler/extensions0.hpp>
#include <components/compiler/lineparser.hpp> #include <components/compiler/lineparser.hpp>
@ -144,6 +146,8 @@ namespace MWGui
Console::Console(int w, int h, bool consoleOnlyScripts, Files::ConfigurationManager& cfgMgr) Console::Console(int w, int h, bool consoleOnlyScripts, Files::ConfigurationManager& cfgMgr)
: WindowBase("openmw_console.layout") : WindowBase("openmw_console.layout")
, mCaseSensitiveSearch(false)
, mRegExSearch(false)
, mCompilerContext(MWScript::CompilerContext::Type_Console) , mCompilerContext(MWScript::CompilerContext::Type_Console)
, mConsoleOnlyScripts(consoleOnlyScripts) , mConsoleOnlyScripts(consoleOnlyScripts)
, mCfgMgr(cfgMgr) , mCfgMgr(cfgMgr)
@ -155,6 +159,8 @@ namespace MWGui
getWidget(mSearchTerm, "edit_SearchTerm"); getWidget(mSearchTerm, "edit_SearchTerm");
getWidget(mNextButton, "button_Next"); getWidget(mNextButton, "button_Next");
getWidget(mPreviousButton, "button_Previous"); getWidget(mPreviousButton, "button_Previous");
getWidget(mCaseSensitiveToggleButton, "button_CaseSensitive");
getWidget(mRegExSearchToggleButton, "button_RegExSearch");
// Set up the command line box // Set up the command line box
mCommandLine->eventEditSelectAccept += newDelegate(this, &Console::acceptCommand); mCommandLine->eventEditSelectAccept += newDelegate(this, &Console::acceptCommand);
@ -163,7 +169,9 @@ namespace MWGui
// Set up the search term box // Set up the search term box
mSearchTerm->eventEditSelectAccept += newDelegate(this, &Console::acceptSearchTerm); mSearchTerm->eventEditSelectAccept += newDelegate(this, &Console::acceptSearchTerm);
mNextButton->eventMouseButtonClick += newDelegate(this, &Console::findNextOccurrence); 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 // Set up the log window
mHistory->setOverflowToTheLeft(true); mHistory->setOverflowToTheLeft(true);
@ -387,6 +395,37 @@ namespace MWGui
execute(cm); 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) void Console::acceptSearchTerm(MyGUI::EditBox* _sender)
{ {
const std::string& searchTerm = mSearchTerm->getOnlyText(); const std::string& searchTerm = mSearchTerm->getOnlyText();
@ -396,81 +435,84 @@ namespace MWGui
return; return;
} }
mCurrentSearchTerm = Utf8Stream::lowerCaseUtf8(searchTerm); std::string newSearchTerm = mCaseSensitiveSearch ? searchTerm : Utf8Stream::lowerCaseUtf8(searchTerm);
mCurrentOccurrence = std::string::npos;
// If new search term reset position, otherwise continue from current position
if (newSearchTerm != mCurrentSearchTerm)
{
mCurrentSearchTerm = newSearchTerm;
mCurrentOccurrenceIndex = std::string::npos;
}
findNextOccurrence(nullptr); findNextOccurrence(nullptr);
} }
enum class Console::SearchDirection
{
Forward,
Reverse
};
void Console::findNextOccurrence(MyGUI::Widget* _sender) void Console::findNextOccurrence(MyGUI::Widget* _sender)
{ {
if (mCurrentSearchTerm.empty()) findOccurrence(SearchDirection::Forward);
{
return;
}
const auto historyText = Utf8Stream::lowerCaseUtf8(mHistory->getOnlyText().asUTF8());
// Search starts at the beginning
size_t startIndex = 0;
// If this is not the first search, we start right AFTER the last occurrence.
if (mCurrentOccurrence != std::string::npos && historyText.length() - mCurrentOccurrence > 1)
{
startIndex = mCurrentOccurrence + 1;
}
mCurrentOccurrence = historyText.find(mCurrentSearchTerm, startIndex);
// 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)
{
mCurrentOccurrence = historyText.find(mCurrentSearchTerm);
}
// Only scroll & select if we actually found something
if (mCurrentOccurrence != std::string::npos)
{
markOccurrence(mCurrentOccurrence, mCurrentSearchTerm.length());
}
else
{
markOccurrence(0, 0);
}
} }
void Console::findPreviousOccurence(MyGUI::Widget* _sender) void Console::findPreviousOccurrence(MyGUI::Widget* _sender)
{ {
findOccurrence(SearchDirection::Reverse);
}
void Console::findOccurrence(const SearchDirection direction)
{
if (mCurrentSearchTerm.empty()) if (mCurrentSearchTerm.empty())
{ {
return; 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 end // Setup default search range
size_t startIndex = historyText.length(); size_t firstIndex{ 0 };
size_t lastIndex{ historyText.length() };
// If this is not the first search, we start right BEFORE the last occurrence. // If search is not the first adjust the range based on the direction and previous occurrence.
if (mCurrentOccurrence != std::string::npos && mCurrentOccurrence > 1) 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.rfind(mCurrentSearchTerm, startIndex); findInHistoryText(historyText, direction, firstIndex, lastIndex);
// If the last search did not find anything AND we didn't start at // If the last search did not find anything AND...
// the end, we repeat the search one time for wrapping around the text. if (mCurrentOccurrenceIndex == std::string::npos)
if (mCurrentOccurrence == std::string::npos && startIndex != historyText.length())
{ {
mCurrentOccurrence = historyText.rfind(mCurrentSearchTerm, historyText.length()); 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 // 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 else
{ {
@ -478,6 +520,78 @@ namespace MWGui
} }
} }
void Console::findInHistoryText(const std::string& historyText, const SearchDirection direction,
const size_t firstIndex, const size_t lastIndex)
{
if (mRegExSearch)
{
findWithRegex(historyText, direction, firstIndex, lastIndex);
}
else
{
findWithStringSearch(historyText, direction, firstIndex, lastIndex);
}
}
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 reverse search get last result in interval
if (direction == SearchDirection::Reverse)
{
std::sregex_iterator lastMatch{ end };
while (match != end)
{
lastMatch = match;
++match;
}
match = lastMatch;
}
// 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;
}
}
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
{
mCurrentOccurrenceIndex = historyTextView.rfind(mCurrentSearchTerm);
}
// If search term is found in text, set new current occurrence values
if (mCurrentOccurrenceIndex != std::string::npos)
{
mCurrentOccurrenceIndex += firstIndex;
mCurrentOccurrenceLength = mCurrentSearchTerm.length();
}
else
{
mCurrentOccurrenceLength = 0;
}
}
void Console::markOccurrence(const size_t textPosition, const size_t length) void Console::markOccurrence(const size_t textPosition, const size_t length)
{ {
if (textPosition == 0 && length == 0) if (textPosition == 0 && length == 0)

View File

@ -31,6 +31,8 @@ namespace MWGui
MyGUI::EditBox* mSearchTerm; MyGUI::EditBox* mSearchTerm;
MyGUI::Button* mNextButton; MyGUI::Button* mNextButton;
MyGUI::Button* mPreviousButton; MyGUI::Button* mPreviousButton;
MyGUI::Button* mCaseSensitiveToggleButton;
MyGUI::Button* mRegExSearchToggleButton;
typedef std::list<std::string> StringList; typedef std::list<std::string> StringList;
@ -83,12 +85,25 @@ namespace MWGui
void commandBoxKeyPress(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char _char); void commandBoxKeyPress(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char _char);
void acceptCommand(MyGUI::EditBox* _sender); void acceptCommand(MyGUI::EditBox* _sender);
enum class SearchDirection;
void toggleCaseSensitiveSearch(MyGUI::Widget* _sender);
void toggleRegExSearch(MyGUI::Widget* _sender);
void acceptSearchTerm(MyGUI::EditBox* _sender); void acceptSearchTerm(MyGUI::EditBox* _sender);
void findNextOccurrence(MyGUI::Widget* _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); 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; std::string mCurrentSearchTerm;
bool mCaseSensitiveSearch;
bool mRegExSearch;
std::string complete(std::string input, std::vector<std::string>& matches); std::string complete(std::string input, std::vector<std::string>& matches);

View File

@ -6,7 +6,7 @@
<Property key="Visible" value="false"/> <Property key="Visible" value="false"/>
<!-- Log window --> <!-- Log window -->
<Widget type="EditBox" skin="MW_TextBoxEdit" position="5 5 380 328" align="Stretch" name="list_History"> <Widget type="EditBox" skin="MW_TextBoxEdit" position="5 30 380 303" align="Stretch" name="list_History">
<Property key="MultiLine" value="1"/> <Property key="MultiLine" value="1"/>
<Property key="ReadOnly" value="true"/> <Property key="ReadOnly" value="true"/>
<Property key="FontName" value="MonoFont"/> <Property key="FontName" value="MonoFont"/>
@ -17,26 +17,38 @@
<Property key="MaxTextLength" value="10000"/> <Property key="MaxTextLength" value="10000"/>
</Widget> </Widget>
<Widget type="HBox" position="1 0 379 28" align="Left Top HStretch">
<!-- "Previous" button -->
<Widget type="Button" skin="MW_Button" position="0 0 28 28" name="button_Previous" align="Top Right">
<Property key="Caption" value="<"/>
</Widget>
<!-- "Next" button -->
<Widget type="Button" skin="MW_Button" position="0 0 28 28" name="button_Next" align="Top Right">
<Property key="Caption" value=">"/>
</Widget>
<!-- Search box -->
<Widget type="EditBox" skin="MW_ConsoleCommand" position="0 0 0 28" align="Top Right" name="edit_SearchTerm">
<Property key="InvertSelected" value="false"/>
<UserString key="HStretch" value="true"/>
</Widget>
<!-- "CaseSensitive" toggle -->
<Widget type="Button" skin="MW_Button" position="0 0 28 28" name="button_CaseSensitive" align="Top Right">
<Property key="Caption" value="Aa"/>
</Widget>
<!-- "RegEx" toggle -->
<Widget type="Button" skin="MW_Button" position="0 0 28 28" name="button_RegExSearch" align="Top Right">
<Property key="Caption" value=".*"/>
</Widget>
</Widget>
<!-- Command line --> <!-- Command line -->
<Widget type="EditBox" skin="MW_ConsoleCommand" position="0 338 384 28" align="HStretch Bottom" name="edit_Command"> <Widget type="EditBox" skin="MW_ConsoleCommand" position="0 338 384 28" align="HStretch Bottom" name="edit_Command">
<Property key="InvertSelected" value="false"/> <Property key="InvertSelected" value="false"/>
<UserString key="AcceptTab" value="true"/> <UserString key="AcceptTab" value="true"/>
</Widget> </Widget>
<!-- Search box -->
<Widget type="EditBox" skin="MW_ConsoleCommand" position="250 1 115 28" align="Top Right" name="edit_SearchTerm">
<Property key="InvertSelected" value="false"/>
</Widget>
<!-- "Next" button -->
<Widget type="Button" skin="MW_Button" position="220 0 28 28" name="button_Next" align="Top Right">
<Property key="Caption" value=">"/>
</Widget>
<!-- "Previous" button -->
<Widget type="Button" skin="MW_Button" position="190 0 28 28" name="button_Previous" align="Top Right">
<Property key="Caption" value="<"/>
</Widget>
</Widget> </Widget>
</MyGUI> </MyGUI>