diff --git a/common/lc_filter.cpp b/common/lc_filter.cpp new file mode 100644 index 00000000..bf29fb1a --- /dev/null +++ b/common/lc_filter.cpp @@ -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); +} diff --git a/common/lc_filter.h b/common/lc_filter.h new file mode 100644 index 00000000..d6d4f983 --- /dev/null +++ b/common/lc_filter.h @@ -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 mFilterParts; +}; diff --git a/common/lc_partselectionwidget.cpp b/common/lc_partselectionwidget.cpp index aee4a4d4..7c645cf4 100644 --- a/common/lc_partselectionwidget.cpp +++ b/common/lc_partselectionwidget.cpp @@ -11,6 +11,7 @@ #include "lc_glextensions.h" #include "lc_category.h" #include "lc_traintrack.h" +#include "lc_filter.h" Q_DECLARE_METATYPE(QList) @@ -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>& Parts, int ColorIndex) @@ -257,25 +258,21 @@ void lcPartSelectionListModel::SetCustomParts(const std::vector= 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,52 +286,19 @@ 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++; - } - - 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 && 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); + 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()); diff --git a/common/lc_partselectionwidget.h b/common/lc_partselectionwidget.h index 78e1b034..10aa8f21 100644 --- a/common/lc_partselectionwidget.h +++ b/common/lc_partselectionwidget.h @@ -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>& 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(); diff --git a/leocad.pro b/leocad.pro index 0c1fa9fa..e25a31ad 100644 --- a/leocad.pro +++ b/leocad.pro @@ -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 \