Support logical operators in the parts filter.

This commit is contained in:
Leonardo Zide
2025-07-24 16:48:04 -07:00
parent 842eff6cf9
commit c005c237b8
5 changed files with 359 additions and 100 deletions
+288
View File
@@ -0,0 +1,288 @@
#include "lc_global.h"
#include "lc_filter.h"
lcFilter::lcFilter(const std::string_view FilterString)
{
size_t TokenStart = std::string_view::npos;
FilterOp Op = FilterOp::Or;
bool InQuotes = false;
auto EndToken=[this, FilterString, &Op, &TokenStart, &InQuotes](size_t Index)
{
if (!InQuotes && TokenStart != std::string_view::npos)
{
std::string_view Token = FilterString.substr(TokenStart, Index - TokenStart);
if (Token == "AND")
Op = FilterOp::And;
else if (Token == "OR")
Op = FilterOp::Or;
else
{
mFilterParts.emplace_back(FilterPart{Op, Token.find_first_of("*?") != std::string_view::npos, std::string(Token)});
Op = FilterOp::And;
}
TokenStart = std::string_view::npos;
}
};
for (size_t Index = 0; Index < FilterString.size(); Index++)
{
switch (FilterString[Index])
{
case ' ':
EndToken(Index);
break;
case '-':
if (!InQuotes && TokenStart == std::string_view::npos)
{
if (Op == FilterOp::And)
Op = FilterOp::AndNot;
else if (Op == FilterOp::Or)
Op = FilterOp::OrNot;
}
break;
case '\"':
if (InQuotes)
{
std::string_view Token = FilterString.substr(TokenStart + 1, Index - TokenStart - 1);
if (!Token.empty())
mFilterParts.emplace_back(FilterPart{Op, Token.find_first_of("*?") != std::string_view::npos, std::string(Token)});
TokenStart = std::string_view::npos;
Op = FilterOp::And;
}
else
{
if (TokenStart == std::string_view::npos)
TokenStart = Index;
}
InQuotes = !InQuotes;
break;
default:
if (TokenStart == std::string_view::npos)
TokenStart = Index;
break;
}
}
EndToken(FilterString.size());
}
bool lcFilter::Match(const char* String) const
{
bool CurrentMatch = mFilterParts.empty();
for (const FilterPart& FilterPart : mFilterParts)
{
bool Match = FilterPart.Wildcard ? FastWildCompare(String, FilterPart.String.c_str()) : strcasestr(String, FilterPart.String.c_str());
switch (FilterPart.Op)
{
case FilterOp::And:
CurrentMatch &= Match;
break;
case FilterOp::AndNot:
CurrentMatch &= !Match;
break;
case FilterOp::Or:
CurrentMatch |= Match;
break;
case FilterOp::OrNot:
CurrentMatch |= !Match;
break;
}
}
return CurrentMatch;
}
// Copyright 2018 IBM Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Compares two text strings. Accepts '?' as a single-character wildcard.
// For each '*' wildcard, seeks out a matching sequence of any characters
// beyond it. Otherwise compares the strings a character at a time.
//
bool lcFilter::FastWildCompare(const char* Tame, const char* Wild)
{
const char* WildSequence; // Points to prospective wild string match after '*'
const char* TameSequence; // Points to prospective tame string match
auto NotEquals=[](char a, char b)
{
// Lowercase the characters to be compared.
if (a >= 'A' && a <= 'Z')
a += ('a' - 'A');
if (b >= 'A' && b <= 'Z')
b += ('a' - 'A');
return a != b;
};
// Find a first wildcard, if one exists, and the beginning of any
// prospectively matching sequence after it.
do
{
// Check for the end from the start. Get out fast, if possible.
if (!*Tame)
{
if (*Wild)
{
while (*(Wild++) == '*')
{
if (!(*Wild))
{
return true; // "ab" matches "ab*".
}
}
return false; // "abcd" doesn't match "abc".
}
else
{
return true; // "abc" matches "abc".
}
}
else if (*Wild == '*')
{
// Got wild: set up for the second loop and skip on down there.
while (*(++Wild) == '*')
{
continue;
}
if (!*Wild)
{
return true; // "abc*" matches "abcd".
}
// Search for the next prospective match.
if (*Wild != '?')
{
while (NotEquals(*Wild, *Tame))
{
if (!*(++Tame))
{
return false; // "a*bc" doesn't match "ab".
}
}
}
// Keep fallback positions for retry in case of incomplete match.
WildSequence = Wild;
TameSequence = Tame;
break;
}
else if (NotEquals(*Wild, *Tame) && *Wild != '?')
{
return false; // "abc" doesn't match "abd".
}
++Wild; // Everything's a match, so far.
++Tame;
} while (true);
// Find any further wildcards and any further matching sequences.
do
{
if (*Wild == '*')
{
// Got wild again.
while (*(++Wild) == '*')
{
continue;
}
if (!*Wild)
{
return true; // "ab*c*" matches "abcd".
}
if (!*Tame)
{
return false; // "*bcd*" doesn't match "abc".
}
// Search for the next prospective match.
if (*Wild != '?')
{
while (NotEquals(*Wild, *Tame))
{
if (!*(++Tame))
{
return false; // "a*b*c" doesn't match "ab".
}
}
}
// Keep the new fallback positions.
WildSequence = Wild;
TameSequence = Tame;
}
else if (NotEquals(*Wild, *Tame) && *Wild != '?')
{
// The equivalent portion of the upper loop is really simple.
if (!*Tame)
{
return false; // "*bcd" doesn't match "abc".
}
// A fine time for questions.
while (*WildSequence == '?')
{
++WildSequence;
++TameSequence;
}
Wild = WildSequence;
// Fall back, but never so far again.
while (NotEquals(*Wild, *(++TameSequence)))
{
if (!*TameSequence)
{
return false; // "*a*b" doesn't match "ac".
}
}
Tame = TameSequence;
}
// Another check for the end, at the end.
if (!*Tame)
{
if (!*Wild)
{
return true; // "*bc" matches "abc".
}
else
{
return false; // "*bc" doesn't match "abcd".
}
}
++Wild; // Everything's still a match.
++Tame;
} while (true);
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
class lcFilter
{
public:
lcFilter(const std::string_view FilterString);
~lcFilter() = default;
bool Match(const char* String) const;
protected:
static bool FastWildCompare(const char* Tame, const char* Wild);
enum class FilterOp
{
And,
AndNot,
Or,
OrNot
};
struct FilterPart
{
FilterOp Op;
bool Wildcard;
std::string String;
};
std::vector<FilterPart> mFilterParts;
};
+33 -91
View File
@@ -11,6 +11,7 @@
#include "lc_glextensions.h"
#include "lc_category.h"
#include "lc_traintrack.h"
#include "lc_filter.h"
Q_DECLARE_METATYPE(QList<int>)
@@ -78,7 +79,7 @@ void lcPartSelectionListModel::UpdateThumbnails()
endResetModel();
SetFilter(mFilter);
SetFilter(mFilterString);
}
void lcPartSelectionListModel::SetColorIndex(int ColorIndex)
@@ -149,7 +150,7 @@ void lcPartSelectionListModel::SetCategory(int CategoryIndex)
endResetModel();
SetFilter(mFilter);
SetFilter(mFilterString);
}
void lcPartSelectionListModel::SetModelsCategory()
@@ -175,7 +176,7 @@ void lcPartSelectionListModel::SetModelsCategory()
endResetModel();
SetFilter(mFilter);
SetFilter(mFilterString);
}
void lcPartSelectionListModel::SetPaletteCategory(int SetIndex)
@@ -203,7 +204,7 @@ void lcPartSelectionListModel::SetPaletteCategory(int SetIndex)
endResetModel();
SetFilter(mFilter);
SetFilter(mFilterString);
}
void lcPartSelectionListModel::SetCurrentModelCategory()
@@ -231,7 +232,7 @@ void lcPartSelectionListModel::SetCurrentModelCategory()
endResetModel();
SetFilter(mFilter);
SetFilter(mFilterString);
}
void lcPartSelectionListModel::SetCustomParts(const std::vector<std::pair<PieceInfo*, std::string>>& Parts, int ColorIndex)
@@ -257,25 +258,21 @@ void lcPartSelectionListModel::SetCustomParts(const std::vector<std::pair<PieceI
endResetModel();
SetFilter(mFilter);
SetFilter(mFilterString);
}
void lcPartSelectionListModel::SetFilter(const QString& Filter)
void lcPartSelectionListModel::SetFilter(const QString& FilterString)
{
mFilter = Filter.toLatin1();
mFilterString = FilterString;
bool DefaultFilter = mFileNameFilter && mPartDescriptionFilter;
bool WildcardFilter = mPartFilterType == lcPartFilterType::Wildcard;
bool FixedStringFilter = mPartFilterType == lcPartFilterType::FixedString;
lcFilter Filter(FilterString.toStdString());
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
const QString Pattern = WildcardFilter ? QRegularExpression::wildcardToRegularExpression(mFilter) : mFilter;
QRegularExpression::PatternOption PatternOptions = mCaseSensitiveFilter ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption;
QRegularExpression FilterRx = QRegularExpression(Pattern, PatternOptions);
QRegularExpression FilterRx = QRegularExpression(FilterString, PatternOptions);
#else
Qt::CaseSensitivity CaseSensitive = mCaseSensitiveFilter ? Qt::CaseSensitive : Qt::CaseInsensitive;
QRegExp::PatternSyntax PatternSyntax = WildcardFilter ? QRegExp::Wildcard : QRegExp::RegExp2;
QRegExp FilterRx(mFilter, CaseSensitive, PatternSyntax);
QRegExp FilterRx(FilterString, CaseSensitive, QRegExp::RegExp2);
#endif
for (size_t PartIdx = 0; PartIdx < mParts.size(); PartIdx++)
@@ -289,50 +286,17 @@ void lcPartSelectionListModel::SetFilter(const QString& Filter)
Visible = false;
else if (!mShowPartAliases && Info->m_strDescription[0] == '=')
Visible = false;
else if (!mFilter.isEmpty())
else if (mPartFilterType == lcPartFilterType::Default)
{
char Description[sizeof(Info->m_strDescription)];
const char* Src = mParts[PartIdx].Description.empty() ? Info->m_strDescription : mParts[PartIdx].Description.c_str();
char* Dst = Description;
const char* Description = mParts[PartIdx].Description.empty() ? Info->m_strDescription : mParts[PartIdx].Description.c_str();
for (;;)
{
*Dst = *Src;
if (*Src == ' ' && *(Src + 1) == ' ')
Src++;
else if (*Src == 0)
break;
Src++;
Dst++;
Visible = (mPartDescriptionFilter && Filter.Match(Description)) || (mFileNameFilter && Filter.Match(Info->mFileName));
}
else if (mPartFilterType == lcPartFilterType::RegularExpression)
{
QString Description = mParts[PartIdx].Description.empty() ? QString(Info->m_strDescription) : QString::fromStdString(mParts[PartIdx].Description);
if (FixedStringFilter)
{
if (DefaultFilter)
{
if (mCaseSensitiveFilter)
Visible = strstr(Description, mFilter) || strstr(Info->mFileName, mFilter);
else
Visible = strcasestr(Description, mFilter) || strcasestr(Info->mFileName, mFilter);
}
else if (mFileNameFilter)
Visible = mCaseSensitiveFilter ? strstr(Info->mFileName, mFilter) : strcasestr(Info->mFileName, mFilter);
else if (mPartDescriptionFilter)
Visible = mCaseSensitiveFilter ? strstr(Description, mFilter) : strcasestr(Description, mFilter);
}
else
{
QString DescriptionString = mParts[PartIdx].Description.empty() ? QString(Description) : QString::fromStdString(mParts[PartIdx].Description);
if (DefaultFilter)
Visible = DescriptionString.contains(FilterRx) || QString(Info->mFileName).contains(FilterRx);
else if (mFileNameFilter)
Visible = QString(Info->mFileName).contains(FilterRx);
else if (mPartDescriptionFilter)
Visible = DescriptionString.contains(FilterRx);
}
Visible = (mPartDescriptionFilter && Description.contains(FilterRx)) || (mFileNameFilter && QString(Info->mFileName).contains(FilterRx));
}
mListView->setRowHidden((int)PartIdx, !Visible);
@@ -468,7 +432,7 @@ void lcPartSelectionListModel::SetShowDecoratedParts(bool Show)
mShowDecoratedParts = Show;
SetFilter(mFilter);
SetFilter(mFilterString);
}
void lcPartSelectionListModel::SetShowPartAliases(bool Show)
@@ -478,7 +442,7 @@ void lcPartSelectionListModel::SetShowPartAliases(bool Show)
mShowPartAliases = Show;
SetFilter(mFilter);
SetFilter(mFilterString);
}
void lcPartSelectionListModel::SetPartFilterType(lcPartFilterType Option)
@@ -488,7 +452,7 @@ void lcPartSelectionListModel::SetPartFilterType(lcPartFilterType Option)
mPartFilterType = Option;
SetFilter(mFilter);
SetFilter(mFilterString);
}
void lcPartSelectionListModel::SetCaseSensitiveFilter(bool Option)
@@ -498,7 +462,7 @@ void lcPartSelectionListModel::SetCaseSensitiveFilter(bool Option)
mCaseSensitiveFilter = Option;
SetFilter(mFilter);
SetFilter(mFilterString);
}
void lcPartSelectionListModel::SetFileNameFilter(bool Option)
@@ -508,7 +472,7 @@ void lcPartSelectionListModel::SetFileNameFilter(bool Option)
mFileNameFilter = Option;
SetFilter(mFilter);
SetFilter(mFilterString);
}
void lcPartSelectionListModel::SetPartDescriptionFilter(bool Option)
@@ -518,7 +482,7 @@ void lcPartSelectionListModel::SetPartDescriptionFilter(bool Option)
mPartDescriptionFilter = Option;
SetFilter(mFilter);
SetFilter(mFilterString);
}
void lcPartSelectionListModel::SetIconSize(int Size)
@@ -534,7 +498,7 @@ void lcPartSelectionListModel::SetIconSize(int Size)
endResetModel();
SetFilter(mFilter);
SetFilter(mFilterString);
}
void lcPartSelectionListModel::SetShowPartNames(bool Show)
@@ -547,7 +511,7 @@ void lcPartSelectionListModel::SetShowPartNames(bool Show)
beginResetModel();
endResetModel();
SetFilter(mFilter);
SetFilter(mFilterString);
}
lcPartSelectionListView::lcPartSelectionListView(QWidget* Parent, lcPartSelectionWidget* PartSelectionWidget)
@@ -745,19 +709,10 @@ void lcPartSelectionListView::TogglePartAliases()
lcSetProfileInt(LC_PROFILE_PARTS_LIST_ALIASES, Show);
}
void lcPartSelectionListView::SetFixedStringFilter()
void lcPartSelectionListView::ToggleRegularExpressionFilter()
{
SetPartFilterType(lcPartFilterType::FixedString);
}
void lcPartSelectionListView::SetWildcardFilter()
{
SetPartFilterType(lcPartFilterType::Wildcard);
}
void lcPartSelectionListView::SetRegularExpressionFilter()
{
SetPartFilterType(lcPartFilterType::RegularExpression);
bool Option = mListModel->GetPartFilterType() == lcPartFilterType::RegularExpression;
SetPartFilterType(Option ? lcPartFilterType::Default : lcPartFilterType::RegularExpression);
}
void lcPartSelectionListView::ToggleCaseSensitiveFilter()
@@ -1238,29 +1193,16 @@ void lcPartSelectionWidget::OptionsMenuAboutToShow()
QMenu* FilterMenu = Menu->addMenu(tr("Filter"));
QActionGroup* FilterGroup = new QActionGroup(Menu);
QAction* PartFilterType = FilterMenu->addAction(tr("Fixed String"), mPartsWidget, &lcPartSelectionListView::SetFixedStringFilter);
PartFilterType->setCheckable(true);
PartFilterType->setChecked(ListModel->GetPartFilterType() == lcPartFilterType::FixedString);
FilterGroup->addAction(PartFilterType);
QAction* WildcardFilter = FilterMenu->addAction(tr("Wildcard"), mPartsWidget, &lcPartSelectionListView::SetWildcardFilter);
WildcardFilter->setCheckable(true);
WildcardFilter->setChecked(ListModel->GetPartFilterType() == lcPartFilterType::Wildcard);
FilterGroup->addAction(WildcardFilter);
QAction* RegularExpressionFilter = FilterMenu->addAction(tr("Regular Expression"), mPartsWidget, &lcPartSelectionListView::SetRegularExpressionFilter);
QAction* RegularExpressionFilter = FilterMenu->addAction(tr("Regular Expression"), mPartsWidget, &lcPartSelectionListView::ToggleRegularExpressionFilter);
RegularExpressionFilter->setCheckable(true);
RegularExpressionFilter->setChecked(ListModel->GetPartFilterType() == lcPartFilterType::RegularExpression);
FilterGroup->addAction(RegularExpressionFilter);
FilterMenu->addSeparator();
QAction* CaseSensitiveFilter = FilterMenu->addAction(tr("Match Case"), mPartsWidget, &lcPartSelectionListView::ToggleCaseSensitiveFilter);
CaseSensitiveFilter->setCheckable(true);
CaseSensitiveFilter->setChecked(ListModel->GetCaseSensitiveFilter());
FilterMenu->addSeparator();
QAction* FileNameFilter = FilterMenu->addAction(tr("Part ID"), mPartsWidget, &lcPartSelectionListView::ToggleFileNameFilter);
FileNameFilter->setCheckable(true);
FileNameFilter->setChecked(ListModel->GetFileNameFilter());
+4 -7
View File
@@ -25,8 +25,7 @@ enum class lcPartCategoryRole
enum class lcPartFilterType
{
FixedString,
Wildcard,
Default,
RegularExpression,
Count
};
@@ -152,7 +151,7 @@ public:
void SetPaletteCategory(int SetIndex);
void SetCurrentModelCategory();
void SetCustomParts(const std::vector<std::pair<PieceInfo*, std::string>>& Parts, int ColorIndex);
void SetFilter(const QString& Filter);
void SetFilter(const QString& FilterString);
void RequestThumbnail(int PartIndex);
void SetShowDecoratedParts(bool Show);
void SetShowPartAliases(bool Show);
@@ -182,7 +181,7 @@ protected:
bool mCaseSensitiveFilter;
bool mFileNameFilter;
bool mPartDescriptionFilter;
QByteArray mFilter;
QString mFilterString;
};
class lcPartSelectionListView : public QListView
@@ -232,9 +231,7 @@ public slots:
void SetMediumIcons();
void SetLargeIcons();
void SetExtraLargeIcons();
void SetFixedStringFilter();
void SetWildcardFilter();
void SetRegularExpressionFilter();
void ToggleRegularExpressionFilter();
void TogglePartNames();
void ToggleDecoratedParts();
void TogglePartAliases();
+2
View File
@@ -191,6 +191,7 @@ SOURCES += \
common/lc_doublespinbox.cpp \
common/lc_edgecolordialog.cpp \
common/lc_file.cpp \
common/lc_filter.cpp \
common/lc_findreplacewidget.cpp \
common/lc_glextensions.cpp \
common/lc_groupdialog.cpp \
@@ -267,6 +268,7 @@ HEADERS += \
common/lc_doublespinbox.h \
common/lc_edgecolordialog.h \
common/lc_file.h \
common/lc_filter.h \
common/lc_findreplacewidget.h \
common/lc_glextensions.h \
common/lc_global.h \