rewrite of the filter code to fix [LYN-4156][LYN-4545] (#1640)

* rewrite of the filter code to fix [LYN-4156][LYN-4545]

Signed-off-by: Alex Montgomery <alexmont@amazon.com>

* improved onRowCountChanged() per comments

Signed-off-by: Alex Montgomery <alexmont@amazon.com>
This commit is contained in:
alexmontAmazon
2021-06-29 17:31:27 -07:00
committed by GitHub
parent 9e85caa352
commit 69f2e06134
7 changed files with 217 additions and 161 deletions
@@ -140,7 +140,6 @@ namespace AzQtComponents
SearchTypeSelector::SearchTypeSelector(QWidget* parent /* = nullptr */)
: QMenu(parent)
, m_unfilteredData(nullptr)
{
Q_ASSERT(parent != nullptr);
@@ -201,7 +200,17 @@ namespace AzQtComponents
itemLayout->addWidget(m_tree);
m_model = new QStandardItemModel(this);
m_tree->setModel(m_model);
m_filterModel = new SearchTypeSelectorFilterModel(this);
m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_filterModel->setRecursiveFilteringEnabled(true);
m_filterModel->setSourceModel(m_model);
// make sure all entries enter the view expanded
connect(m_filterModel, &QAbstractItemModel::rowsInserted, this, [this]()
{
m_tree->expandAll();
});
m_tree->setModel(m_filterModel);
m_tree->setHeaderHidden(true);
connect(m_model, &QStandardItemModel::itemChanged, this, [this](QStandardItem* item)
@@ -210,11 +219,6 @@ namespace AzQtComponents
if (!m_settingUp)
{
int index = item->data().toInt();
if (index < m_filteredItemIndices.size())
{
index = m_filteredItemIndices[item->data().toInt()];
}
bool enabled = item->checkState() == Qt::Checked;
emit TypeToggled(index, enabled);
}
@@ -233,169 +237,116 @@ namespace AzQtComponents
QMenu::showEvent(e);
}
void SearchTypeSelector::resetData()
{
m_estimatedTableHeight = 0;
m_estimatedTableWidth = 0;
m_filteredItemIndices.clear();
m_model->clear();
}
void SearchTypeSelector::initItem(QStandardItem* item, const SearchTypeFilter& filter, int unfilteredDataIndex)
{
Q_UNUSED(filter);
Q_UNUSED(unfilteredDataIndex);
item->setData(unfilteredDataIndex);
item->setEditable(false);
item->setCheckable(true);
item->setCheckState(filter.enabled ? Qt::Checked : Qt::Unchecked);
}
bool SearchTypeSelector::filterItemOut(int unfilteredDataIndex, bool itemMatchesFilter, bool categoryMatchesFilter)
int SearchTypeSelector::getUnfilteredDataIndex(QStandardItem* item)
{
Q_UNUSED(unfilteredDataIndex);
return !itemMatchesFilter && !categoryMatchesFilter;
const QVariant itemData = item->data();
if (itemData.isValid())
{
return item->data().toInt();
}
return -1;
}
void SearchTypeSelector::RepopulateDataModel()
void SearchTypeSelector::RepopulateDataModel(const SearchTypeFilterList& unfilteredData)
{
resetData();
if (!m_unfilteredData)
{
return;
}
bool amFiltering = !m_filterString.isEmpty();
m_estimatedTableHeight = 0;
m_estimatedTableWidth = 0;
m_model->clear();
m_filterModel->setNoResultsMessageRow(-1); // reset the index for the "no results" message
QScopedValueRollback<bool> setupGuard(m_settingUp, true);
QMap<QString, QStandardItem*> categories;
QStandardItem* firstCategory = nullptr;
QStandardItem* firstItem = nullptr;
int numCategories = 0;
int numItems = 0;
int numItemsAdded = 0;
QVector<QString> categoriesInOrder; // categories in originally specified order
QMap<QString, QList<QStandardItem*>> categoryToEntryItems; // category name to sub-items that will be added
for (int unfilteredDataIndex = 0, length = m_unfilteredData->length(); unfilteredDataIndex < length; ++unfilteredDataIndex)
const int numItems = unfilteredData.length();
for (int unfilteredDataIndex = 0; unfilteredDataIndex < numItems; ++unfilteredDataIndex)
{
const SearchTypeFilter& filter = m_unfilteredData->at(unfilteredDataIndex);
bool addItem = true;
bool itemMatchesFilter = true;
bool categoryMatchesFilter = true;
if (amFiltering)
{
itemMatchesFilter = filter.displayName.contains(m_filterString, Qt::CaseSensitivity::CaseInsensitive);
categoryMatchesFilter = filter.category.contains(m_filterString, Qt::CaseSensitivity::CaseInsensitive);
}
if (filterItemOut(unfilteredDataIndex, itemMatchesFilter, categoryMatchesFilter))
{
addItem = false;
}
QStandardItem* categoryItem = nullptr;
if (categories.contains(filter.category))
{
categoryItem = categories[filter.category];
}
else
{
if (categoryMatchesFilter || addItem)
{
categoryItem = new QStandardItem(filter.category);
categories[filter.category] = categoryItem;
m_model->appendRow(categoryItem);
categoryItem->setEditable(false);
numCategories++;
if (!firstCategory)
{
firstCategory = firstCategory ? firstCategory : categoryItem;
}
}
}
// count the item even if we filter it out, so that the estimated height includes what it could be if the filter changes
numItems++;
if (!addItem)
{
continue;
}
numItemsAdded++;
m_filteredItemIndices.append(unfilteredDataIndex);
const SearchTypeFilter& filter = unfilteredData.at(unfilteredDataIndex);
// create each item, but don't add them yet, as we don't know if we will be adding to the category items,
// or to the model directly
QStandardItem* item = new QStandardItem(filter.displayName);
item->setData(unfilteredDataIndex);
item->setEditable(false);
initItem(item, filter, unfilteredDataIndex);
if (categoryItem)
categoryToEntryItems[filter.category].push_back(item);
if (categoriesInOrder.indexOf(filter.category) == -1)
{
categoryItem->appendRow(item);
}
else
{
m_model->appendRow(item);
// need to add these category items as they are first encountered so
// that the original category ordering is maintained
categoriesInOrder.push_back(filter.category);
}
int textWidth = fontMetrics().horizontalAdvance(filter.displayName);
if (textWidth > m_estimatedTableWidth)
{
m_estimatedTableWidth = textWidth;
m_estimatedTableWidth = textWidth;
}
}
const int numCategories = categoriesInOrder.size();
if (!firstItem)
// If there is only one category and its name is empty, discard it,
// and add its children directly to the model as one big column of N rows
if (numCategories == 1 && categoriesInOrder[0].isEmpty())
{
auto& entryItems = categoryToEntryItems.begin().value();
m_model->appendColumn(entryItems);
}
else
{
for (int categoryIndex = 0; categoryIndex < numCategories; ++categoryIndex)
{
firstItem = item;
const QString& currCategory = categoriesInOrder[categoryIndex];
QStandardItem* categoryItem = new QStandardItem(currCategory);
auto& entryItems = categoryToEntryItems[currCategory];
categoryItem->appendColumn(entryItems);
m_model->appendRow(categoryItem); // add the parent last, so the model just gets one row add per category
}
}
if (numItemsAdded == GetNumFixedItems())
{
QStandardItem* item = new QStandardItem(QObject::tr("<i>No result found.</i>"));
m_model->appendRow(item);
item->setEditable(false);
++numItems;
}
// add a special row that indicates that no categories (other than the fixed ones) match the filter
// this row itself will be filtered out when there are other matching categories
m_filterModel->setNoResultsMessageRow(m_model->rowCount());
QStandardItem* noResultsMessage = new QStandardItem(QObject::tr("<i>No result found.</i>"));
noResultsMessage->setEditable(false);
m_model->appendRow(new QStandardItem(QObject::tr("<i>No result found.</i>")));
// If there is only one category and its name is empty, let put everything at root.
if (categories.count() == 1 && categories.begin().key().isEmpty())
{
m_tree->setRootIndex(categories.begin().value()->index());
numCategories = 0;
}
estimateTableHeight(firstCategory, numCategories, firstItem, numItems);
estimateTableHeight(numCategories, numItems);
}
void SearchTypeSelector::estimateTableHeight(QStandardItem* firstCategory, int numCategories, QStandardItem* firstItem, int numItems)
void SearchTypeSelector::estimateTableHeight(int numCategories, int numItems)
{
m_tree->expandAll();
int totalCategoryHeight = 0;
int totalItemHeight = 0;
if (firstItem)
auto* theModel = m_tree->model();
QModelIndex firstIndex(theModel->index(0, 0));
QModelIndex firstChild(theModel->index(0, 0, firstIndex));
if (firstIndex.isValid())
{
QModelIndex index = m_model->indexFromItem(firstItem);
int itemHeight = m_tree->fetchRowHeight(index);
int itemHeight = m_tree->fetchRowHeight(firstIndex);
totalItemHeight += (itemHeight * numItems);
}
if (firstCategory)
if (firstChild.isValid())
{
QModelIndex index = m_model->indexFromItem(firstCategory);
int categoryHeight = m_tree->fetchRowHeight(index);
int categoryHeight = m_tree->fetchRowHeight(firstChild);
totalCategoryHeight += (categoryHeight * numCategories);
}
@@ -509,10 +460,7 @@ namespace AzQtComponents
void SearchTypeSelector::Setup(const SearchTypeFilterList& searchTypes)
{
m_unfilteredData = &searchTypes;
RepopulateDataModel();
RepopulateDataModel(searchTypes);
setFixedWidth(m_estimatedTableWidth + FilterWindowWidthPadding);
}
@@ -583,8 +531,75 @@ namespace AzQtComponents
void SearchTypeSelector::FilterTextChanged(const QString& newFilter)
{
m_filterString = newFilter;
m_filterModel->setFilterWildcard(m_filterString);
}
RepopulateDataModel();
SearchTypeSelectorFilterModel::SearchTypeSelectorFilterModel(SearchTypeSelector* searchTypeSelector)
: QSortFilterProxyModel(searchTypeSelector), m_searchTypeSelector(searchTypeSelector)
{
// use queued connections so that the normal sort/filter operations get a chance to finish the index remapping before we act
connect(this, &QAbstractItemModel::rowsInserted, this, &SearchTypeSelectorFilterModel::onRowCountChanged, Qt::QueuedConnection);
connect(this, &QAbstractItemModel::rowsRemoved, this, &SearchTypeSelectorFilterModel::onRowCountChanged, Qt::QueuedConnection);
}
void SearchTypeSelectorFilterModel::setNoResultsMessageRow(int row)
{
m_noResultsRow = row;
invalidateFilter();
}
void SearchTypeSelectorFilterModel::onRowCountChanged()
{
// see if we need to hide or show the "no results" message item
const int numLeafNodes = getNumLeafNodes();
// check if we're down to the (never filtered) fixed items, and should show the "no results" message,
// or if we were already showing that message, check if we have more than the fixed items plus the message itself
const bool hasResultsChanged = (m_showingNoResultsMessage ? numLeafNodes > m_searchTypeSelector->GetNumFixedItems() + 1
: numLeafNodes <= m_searchTypeSelector->GetNumFixedItems());
if (hasResultsChanged)
{
m_showingNoResultsMessage = !m_showingNoResultsMessage;
QModelIndex noResultsMessageIndex = sourceModel()->index(m_noResultsRow, 0);
// The no results row is in the source model, so trigger dataChanged to allow us to re-filter it
emit sourceModel()->dataChanged(noResultsMessageIndex, noResultsMessageIndex);
}
}
bool SearchTypeSelectorFilterModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
{
// if we're considering the "no results" item, accept it only if we're m_showingNoResultsMessage
if (!source_parent.isValid() && source_row == m_noResultsRow)
{
return m_showingNoResultsMessage;
}
const bool filteredByBase = ! QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
// allow searchTypeSelector to make the final decision whether to filter out this item
return !(m_searchTypeSelector->filterItemOut(m_searchTypeSelector->m_model->index(source_row, 0, source_parent), filteredByBase));
}
int SearchTypeSelectorFilterModel::getNumLeafNodes(const QModelIndex& theIndex)
{
// count the number of leaf nodes descending from this index, not including itself
int numLeafNodes = 0;
for (int childRow = 0, numChildRows = rowCount(theIndex); childRow < numChildRows; ++childRow)
{
QModelIndex childIndex = index(childRow, 0, theIndex);
if (rowCount(childIndex) == 0)
{
++numLeafNodes;
}
else
{
numLeafNodes += getNumLeafNodes(childIndex);
}
}
return numLeafNodes;
}
FilteredSearchWidget::Config FilteredSearchWidget::loadConfig(QSettings& settings)
@@ -934,7 +949,7 @@ namespace AzQtComponents
{
{
QSignalBlocker blocker(this);
ConfigHelpers::GroupGuard(&settings, widgetName);
ConfigHelpers::GroupGuard guard(&settings, widgetName);
const auto textFilter = settings.value(g_textFilterKey);
if (textFilter.isValid())
{
@@ -958,7 +973,7 @@ namespace AzQtComponents
void FilteredSearchWidget::writeSettings(QSettings& settings, const QString& widgetName)
{
ConfigHelpers::GroupGuard(&settings, widgetName);
ConfigHelpers::GroupGuard guard(&settings, widgetName);
settings.setValue(g_textFilterKey, textFilter());
const int size = m_typeFilters.size();
@@ -22,6 +22,8 @@
#include <QVariant>
#include <QMenu>
#include <QTimer>
#include <QSortFilterProxyModel>
#include <QStandardItem>
#include <AzCore/std/chrono/chrono.h>
#endif
@@ -33,9 +35,7 @@ namespace Ui
class FlowLayout;
class QTreeView;
class QSortFilterProxyModel;
class QStandardItemModel;
class QStandardItem;
class QSettings;
class QLineEdit;
class QToolButton;
@@ -48,6 +48,7 @@ namespace AzQtComponents
{
class Style;
class FilteredSearchItemDelegate;
class SearchTypeSelectorFilterModel;
class AZ_QT_COMPONENTS_API FilterCriteriaButton
: public QFrame
@@ -165,28 +166,27 @@ namespace AzQtComponents
void FilterTextChanged(const QString& newFilter);
protected:
void estimateTableHeight(QStandardItem* firstCategory, int numCategories, QStandardItem* firstItem, int numItems);
void resetData();
void estimateTableHeight(int numCategories, int numItems);
// can be used to override the logic when adding items in RepopulateDataModel
virtual bool filterItemOut(int index, bool itemMatchesFilter, bool categoryMatchesFilter);
// allows child classes to override the logic of accepting filter categories
virtual bool filterItemOut(const QModelIndex& sourceIndex, bool filteredByBase) { Q_UNUSED(sourceIndex); return filteredByBase; }
virtual void initItem(QStandardItem* item, const SearchTypeFilter& filter, int unfilteredDataIndex);
int getUnfilteredDataIndex(QStandardItem* item); // get the original filter index from the item itself
// Returns the number of items that always appear in the list, regardless of the filtering.
virtual int GetNumFixedItems() { return 0; }
void showEvent(QShowEvent* e) override;
virtual void RepopulateDataModel();
void RepopulateDataModel(const SearchTypeFilterList& unfilteredData);
void maximizeGeometryToFitScreen();
SearchTypeSelectorTreeView* m_tree;
QStandardItemModel* m_model;
const SearchTypeFilterList* m_unfilteredData;
AZ_PUSH_DISABLE_WARNING(4127 4251, "-Wunknown-warning-option") // conditional expression is constant, needs to have dll-interface to be used by clients of class 'AzQtComponents::SearchTypeSelector'
QVector<int> m_filteredItemIndices;
AZ_POP_DISABLE_WARNING
QString m_filterString;
friend class SearchTypeSelectorFilterModel;
SearchTypeSelectorFilterModel* m_filterModel;
QString m_filterString;
bool m_settingUp = false;
int m_fixedWidth = 256;
QLineEdit* m_searchField = nullptr;
@@ -198,6 +198,26 @@ namespace AzQtComponents
bool m_lineEditSearchVisible = true;
};
class SearchTypeSelectorFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
SearchTypeSelectorFilterModel(SearchTypeSelector* searchTypeSelector);
void setNoResultsMessageRow(int row); // row of specialized "no results" message in the source model
protected slots:
void onRowCountChanged();
protected:
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;
int getNumLeafNodes(const QModelIndex& theIndex = QModelIndex()); // gets the number of leaf node descendants of current index. Current index is *not* considered
SearchTypeSelector* m_searchTypeSelector = nullptr;
int m_noResultsRow = -1; // row of specialized "no results" message in the source model
bool m_showingNoResultsMessage = false;
};
class AZ_QT_COMPONENTS_API FilteredSearchWidget
: public QFrame
{
@@ -114,13 +114,13 @@ namespace AzQtComponents
void ReadColorGridConfig(QSettings& settings, const QString& name, ColorPicker::ColorGridConfig& colorGrid)
{
ConfigHelpers::GroupGuard(&settings, name);
ConfigHelpers::GroupGuard guard(&settings, name);
ConfigHelpers::read<QSize>(settings, QStringLiteral("MinimumSize"), colorGrid.minimumSize);
}
void ReadDialoButtonsConfig(QSettings& settings, const QString& name, ColorPicker::DialogButtonsConfig& dialogButtons)
{
ConfigHelpers::GroupGuard(&settings, name);
ConfigHelpers::GroupGuard guard(&settings, name);
ConfigHelpers::read<int>(settings, QStringLiteral("TopPadding"), dialogButtons.topPadding);
}
@@ -41,18 +41,29 @@ namespace AzToolsFramework
{
}
bool EntityOutlinerSearchTypeSelector::filterItemOut(int unfilteredDataIndex, bool itemMatchesFilter, bool categoryMatchesFilter)
bool EntityOutlinerSearchTypeSelector::filterItemOut(const QModelIndex& sourceIndex, bool filteredByBase)
{
bool unfilteredIndexInvalid = (unfilteredDataIndex >= aznumeric_cast<int>(EntityOutlinerSearchWidget::GlobalSearchCriteria::FirstRealFilter));
return SearchTypeSelector::filterItemOut(unfilteredDataIndex, itemMatchesFilter, categoryMatchesFilter) && unfilteredIndexInvalid;
auto* currItem = m_model->itemFromIndex(sourceIndex);
if (currItem != nullptr)
{
int unfilteredIndex = getUnfilteredDataIndex(currItem);
if (unfilteredIndex >= 0 && unfilteredIndex < aznumeric_cast<int>(EntityOutlinerSearchWidget::GlobalSearchCriteria::FirstRealFilter))
{
// never filter out the categories before FirstRealFilter (unlocked/locked, visible/hidden, etc.)
return false;
}
}
// no special case, return the result of the base filter
return filteredByBase;
}
void EntityOutlinerSearchTypeSelector::initItem(QStandardItem* item, const AzQtComponents::SearchTypeFilter& filter, int unfilteredDataIndex)
{
if (filter.displayName != "--------")
SearchTypeSelector::initItem(item, filter, unfilteredDataIndex);
if (filter.displayName == "--------")
{
item->setCheckable(true);
item->setCheckState(filter.enabled ? Qt::Checked : Qt::Unchecked);
SearchTypeSelector::initItem(item, filter, unfilteredDataIndex);
item->setCheckable(false);
}
if (unfilteredDataIndex < aznumeric_cast<int>(EntityOutlinerSearchWidget::GlobalSearchCriteria::FirstRealFilter))
@@ -44,8 +44,8 @@ namespace AzToolsFramework
EntityOutlinerSearchTypeSelector(QWidget* parent = nullptr);
protected:
// can be used to override the logic when adding items in RepopulateDataModel
bool filterItemOut(int unfilteredDataIndex, bool itemMatchesFilter, bool categoryMatchesFilter) override;
// override the logic of accepting filter categories
bool filterItemOut(const QModelIndex& sourceIndex, bool filteredByBase) override;
void initItem(QStandardItem* item, const AzQtComponents::SearchTypeFilter& filter, int unfilteredDataIndex) override;
int GetNumFixedItems() override;
};
@@ -37,21 +37,31 @@ namespace AzQtComponents
{
}
bool OutlinerSearchTypeSelector::filterItemOut(int unfilteredDataIndex, bool itemMatchesFilter, bool categoryMatchesFilter)
bool OutlinerSearchTypeSelector::filterItemOut(const QModelIndex& sourceIndex, bool filteredByBase)
{
bool unfilteredIndexInvalid = (unfilteredDataIndex >= static_cast<int>(OutlinerSearchWidget::GlobalSearchCriteria::FirstRealFilter));
return SearchTypeSelector::filterItemOut(unfilteredDataIndex, itemMatchesFilter, categoryMatchesFilter) && unfilteredIndexInvalid;
auto* currItem = m_model->itemFromIndex(sourceIndex);
if (currItem != nullptr)
{
int unfilteredIndex = getUnfilteredDataIndex(currItem);
if (unfilteredIndex >= 0 && unfilteredIndex < aznumeric_cast<int>(OutlinerSearchWidget::GlobalSearchCriteria::FirstRealFilter))
{
// never filter out the categories before FirstRealFilter (unlocked/locked, visible/hidden, etc.)
return false;
}
}
// no special case, return the result of the base filter
return filteredByBase;
}
void OutlinerSearchTypeSelector::initItem(QStandardItem* item, const SearchTypeFilter& filter, int unfilteredDataIndex)
{
if (filter.displayName != "--------")
SearchTypeSelector::initItem(item, filter, unfilteredDataIndex);
if (filter.displayName == "--------")
{
item->setCheckable(true);
item->setCheckState(filter.enabled ? Qt::Checked : Qt::Unchecked);
item->setCheckable(false);
}
if (unfilteredDataIndex < static_cast<int>(OutlinerSearchWidget::GlobalSearchCriteria::FirstRealFilter))
if (unfilteredDataIndex >= 0 && unfilteredDataIndex < static_cast<int>(OutlinerSearchWidget::GlobalSearchCriteria::FirstRealFilter))
{
item->setIcon(OutlinerIcons::GetInstance().GetIcon(unfilteredDataIndex));
}
@@ -42,8 +42,8 @@ namespace AzQtComponents
OutlinerSearchTypeSelector(QWidget* parent = nullptr);
protected:
// can be used to override the logic when adding items in RepopulateDataModel
bool filterItemOut(int unfilteredDataIndex, bool itemMatchesFilter, bool categoryMatchesFilter) override;
// override the logic of accepting filter categories
bool filterItemOut(const QModelIndex& sourceIndex, bool filteredByBase) override;
void initItem(QStandardItem* item, const SearchTypeFilter& filter, int unfilteredDataIndex) override;
int GetNumFixedItems() override;
};