Skip to content

Commit

Permalink
EMSUSD-1856 Add filtering search to the collection widget
Browse files Browse the repository at this point in the history
- Add a FilteredStringListModel class.
- Use that model in the collection string list widgets.
- Add a text edit widget to enter the search text
- Connect the search changed signal to the filtered model.
- Add a button to clear the search filter.
- Add a flag on the filtered model to remember if it was entirely filtered out.
- Properly re-filter the model when a new list of text is given.
- Moved the filtered view in its own file.
- Give the resizable lists a sensible default size.
  • Loading branch information
pierrebai-adsk committed Nov 29, 2024
1 parent eaf38d0 commit cffff11
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 85 deletions.
2 changes: 2 additions & 0 deletions lib/mayaUsd/resources/ae/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ if(MAYA_APP_VERSION VERSION_GREATER_EQUAL 2023)

install(FILES
${MAYAUSD_SHARED_COMPONENTS}/common/__init__.py
${MAYAUSD_SHARED_COMPONENTS}/common/filteredStringListModel.py
${MAYAUSD_SHARED_COMPONENTS}/common/filteredStringListView.py
${MAYAUSD_SHARED_COMPONENTS}/common/list.py
${MAYAUSD_SHARED_COMPONENTS}/common/persistentStorage.py
${MAYAUSD_SHARED_COMPONENTS}/common/resizable.py
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
from .expressionRulesMenu import ExpressionMenu

try:
from PySide6.QtWidgets import QFrame, QWidget, QHBoxLayout, QVBoxLayout# type: ignore
from PySide6.QtWidgets import QFrame, QWidget, QHBoxLayout, QVBoxLayout, QLineEdit, QSizePolicy # type: ignore
except ImportError:
from PySide2.QtWidgets import QFrame, QWidget, QHBoxLayout, QVBoxLayout # type: ignore
from PySide2.QtWidgets import QFrame, QWidget, QHBoxLayout, QVBoxLayout, QLineEdit, QSizePolicy # type: ignore

from pxr import Usd

# TODO: support I8N
kSearchPlaceHolder = 'Search...'

class IncludeExcludeWidget(QWidget):
def __init__(self, collection: Usd.CollectionAPI = None, parent: QWidget = None):
super(IncludeExcludeWidget, self).__init__(parent)
Expand All @@ -35,27 +38,37 @@ def __init__(self, collection: Usd.CollectionAPI = None, parent: QWidget = None)
self._expressionMenu = ExpressionMenu(self._collection, self)
menuButton = MenuButton(self._expressionMenu, self)

self._filterWidget = QLineEdit()
self._filterWidget.setContentsMargins(0, 0, 0, 0)
self._filterWidget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self._filterWidget.setPlaceholderText(kSearchPlaceHolder)
self._filterWidget.setClearButtonEnabled(True)

separator = QFrame()
separator.setFrameShape(QFrame.VLine)

headerWidget = QWidget(self)
headerWidget.setContentsMargins(0, 0, 0, 0)
headerLayout = QHBoxLayout(headerWidget)
headerLayout.addStretch(1)
# Will need a separator for future designs
#headerLayout.addWidget(separator)
headerLayout.setContentsMargins(0, 2, 2, 0)
headerLayout.addWidget(self._filterWidget)
headerLayout.addWidget(separator)
headerLayout.addWidget(menuButton)
includeExcludeLayout.addWidget(headerWidget)

self._include = StringList(includes, "Include", "Include all", self)
self._include.cbIncludeAll.setChecked(shouldIncludeAll)
self._include.cbIncludeAll.stateChanged.connect(self.onIncludeAllToggle)
self._resizableInclude = Resizable(self._include, "USD_Light_Linking", "IncludeListHeight", self)
self._resizableInclude = Resizable(self._include, "USD_Light_Linking", "IncludeListHeight", self, defaultSize=80)
includeExcludeLayout.addWidget(self._resizableInclude)

self._exclude = StringList(excludes, "Exclude", "", self)
self._resizableExclude = Resizable(self._exclude, "USD_Light_Linking", "ExcludeListHeight", self)
self._resizableExclude = Resizable(self._exclude, "USD_Light_Linking", "ExcludeListHeight", self, defaultSize=80)
includeExcludeLayout.addWidget(self._resizableExclude)

self._filterWidget.textChanged.connect(self._include.list._model.setFilter)
self._filterWidget.textChanged.connect(self._exclude.list._model.setFilter)

self.setLayout(includeExcludeLayout)

def update(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from typing import Sequence

try:
from PySide6.QtCore import (
QStringListModel,
Signal,
)
except:
from PySide2.QtCore import (
QStringListModel,
Signal,
)


class FilteredStringListModel(QStringListModel):
'''
A Qt string list model that can be filtered.
'''
filterChanged = Signal()

def __init__(self, items: Sequence[str] = None, parent=None):
super(FilteredStringListModel, self).__init__(items if items else [], parent)
self._unfilteredItems = items
self._isFilteredEmpty = False
self._filter = ""

def setStringList(self, items: Sequence[str]):
'''
Override base class implementation to properly rebuild
the filtered list.
'''
self._unfilteredItems = items
self._isFilteredEmpty = False
super(FilteredStringListModel, self).setStringList(items)
self._rebuildFilteredModel()

def _rebuildFilteredModel(self):
'''
Rebuild the model by applying the filter.
'''
if not self._filter:
filteredItems = self._unfilteredItems
else:
filters = [filter.lower() for filter in self._filter.split('*')]
filteredItems = [item for item in self._unfilteredItems if self._isValidItem(item, filters)]
self._isFilteredEmpty = bool(self._unfilteredItems) and not bool(filteredItems)
# Note: don't call our own version, otehrwise we would get infinite recursion.
super(FilteredStringListModel, self).setStringList(filteredItems)

def _isValidItem(self, item: str, filters: Sequence[str]):
'''
Verify if the item passes all the filters.
We search each given filter in sequence, each one must match
somewhere in the item starting at the end of the point where
the preceeding filter ended:
'''
index = 0
item = item.lower()
for filter in filters:
newIndex = item.find(filter, index)
if newIndex < 0:
return False
index = newIndex + len(filter)
return True

def isFilteredEmpty(self):
'''
Verify if the model is empty because it was entirely filtered out.
'''
return self._isFilteredEmpty

def setFilter(self, filter: str):
'''
Set the filter to be applied to the model and rebuild the model.
'''
if filter == self._filter:
return
self._filter = filter
self._rebuildFilteredModel()
self.filterChanged.emit()
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from typing import Sequence, Union
from .theme import Theme
from .filteredStringListModel import FilteredStringListModel

try:
from PySide6.QtCore import (
QModelIndex,
QPersistentModelIndex,
QRect,
QSize,
QStringListModel,
Qt,
Signal,
)
from PySide6.QtGui import QPainter, QPaintEvent, QPen, QColor
from PySide6.QtWidgets import QStyleOptionViewItem, QStyledItemDelegate, QListView
except:
from PySide2.QtCore import (
QModelIndex,
QPersistentModelIndex,
QRect,
QSize,
QStringListModel,
Qt,
Signal,
)
from PySide2.QtGui import QPainter, QPaintEvent, QPen, QColor # type: ignore
from PySide2.QtWidgets import QStyleOptionViewItem, QStyledItemDelegate, QListView


kNoObjectFoundLabel = 'No objects found'

class FilteredStringListView(QListView):
selectedItemsChanged = Signal()

class Delegate(QStyledItemDelegate):
def __init__(self, model: QStringListModel, parent=None):
super(FilteredStringListView.Delegate, self).__init__(parent)
self._model = model

def sizeHint(self, option: QStyleOptionViewItem, index: Union[QModelIndex, QPersistentModelIndex]):
s: int = Theme.instance().uiScaled(24)
return QSize(s, s)

def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: Union[QModelIndex, QPersistentModelIndex]):
s: str = self._model.data(index, Qt.DisplayRole)
Theme.instance().paintStringListEntry(painter, option.rect, s)

def __init__(self, items: Sequence[str] = None, parent=None):
super(FilteredStringListView, self).__init__(parent)
self._model = FilteredStringListModel(items if items else [], self)
self.setModel(self._model)

self.setUniformItemSizes(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setTextElideMode(Qt.TextElideMode.ElideMiddle)
self.setSelectionBehavior(QListView.SelectRows)
self.setSelectionMode(QListView.MultiSelection)
self.setContentsMargins(0,0,0,0)

self.selectionModel().selectionChanged.connect(lambda: self.selectedItemsChanged.emit())

def drawFrame(self, painter: QPainter):
pass

def paintEvent(self, event: QPaintEvent):
super(FilteredStringListView, self).paintEvent(event)
if self._model.isFilteredEmpty():
painter = QPainter(self.viewport())
painter.setPen(QColor(128, 128, 128))
painter.drawText(self.rect(), Qt.AlignCenter, kNoObjectFoundLabel)

@property
def items(self) -> Sequence[str]:
return self._model.stringList

@items.setter
def items(self, items: Sequence[str]):
self._model.setStringList(items)

@property
def selectedItems(self) -> Sequence[str]:
return [index.data(Qt.DisplayRole) for index in self.selectedIndexes()]
Original file line number Diff line number Diff line change
@@ -1,89 +1,17 @@
from typing import Sequence, Union
from .theme import Theme
from typing import Sequence
from .filteredStringListView import FilteredStringListView

try:
from PySide6.QtCore import ( # type: ignore
QModelIndex,
QPersistentModelIndex,
QSize,
QStringListModel,
Qt,
Signal,
)
from PySide6.QtGui import QPainter # type: ignore
from PySide6.QtWidgets import ( # type: ignore
QStyleOptionViewItem,
QStyledItemDelegate,
QListView,
QLabel,
QVBoxLayout,
QHBoxLayout,
QWidget,
QCheckBox,
)
from PySide6.QtWidgets import QLabel, QVBoxLayout, QHBoxLayout, QWidget, QCheckBox
except:
from PySide2.QtCore import ( # type: ignore
QModelIndex,
QPersistentModelIndex,
QSize,
QStringListModel,
Qt,
Signal,
)
from PySide2.QtGui import QPainter # type: ignore
from PySide2.QtWidgets import QStyleOptionViewItem, QStyledItemDelegate, QListView, QLabel, QVBoxLayout, QHBoxLayout, QWidget, QCheckBox # type: ignore

class _StringList(QListView):
selectedItemsChanged = Signal()

class Delegate(QStyledItemDelegate):
def __init__(self, model: QStringListModel, parent=None):
super(_StringList.Delegate, self).__init__(parent)
self._model = model

def sizeHint(self, option: QStyleOptionViewItem, index: Union[QModelIndex, QPersistentModelIndex]):
s: int = Theme.instance().uiScaled(24)
return QSize(s, s)

def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: Union[QModelIndex, QPersistentModelIndex]):
s: str = self._model.data(index, Qt.DisplayRole)
Theme.instance().paintStringListEntry(painter, option.rect, s)

def __init__(self, items: Sequence[str] = [], parent=None):
super(_StringList, self).__init__(parent)
self._model = QStringListModel(items, self)
self.setModel(self._model)

self.setUniformItemSizes(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setTextElideMode(Qt.TextElideMode.ElideMiddle)
self.setSelectionBehavior(QListView.SelectRows)
self.setSelectionMode(QListView.MultiSelection)
self.setContentsMargins(0,0,0,0)

self.selectionModel().selectionChanged.connect(lambda: self.selectedItemsChanged.emit())

def drawFrame(self, painter: QPainter):
pass

@property
def items(self) -> Sequence[str]:
return self._model.stringList

@items.setter
def items(self, items: Sequence[str]):
self._model.setStringList(items)

@property
def selectedItems(self) -> Sequence[str]:
return [index.data(Qt.DisplayRole) for index in self.selectedIndexes()]
from PySide2.QtWidgets import QLabel, QVBoxLayout, QHBoxLayout, QWidget, QCheckBox # type: ignore


class StringList(QWidget):

def __init__(self, items: Sequence[str] = [], headerTitle: str = "", toggleTitle: str = "", parent=None):
def __init__(self, items: Sequence[str] = None, headerTitle: str = "", toggleTitle: str = "", parent=None):
super().__init__()
self.list = _StringList(items, self)
self.list = FilteredStringListView(items if items else [], self)

layout = QVBoxLayout(self)
LEFT_RIGHT_MARGINS = 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ def __init__(
persistentStorageGroup: str = None,
persistentStorageKey: str = None,
parent: QWidget = None,
defaultSize = -1,
):
super(Resizable, self).__init__(parent)

Expand All @@ -127,6 +128,8 @@ def __init__(
self._widget: QWidget = None

self.loadPersistentStorage()
if self._contentSize < 0 and defaultSize > 0:
self._contentSize = defaultSize

if w is not None:
self.widget = w
Expand Down

0 comments on commit cffff11

Please sign in to comment.