From 55a16a3b05068e7f509eab757aa9375e6d6cc651 Mon Sep 17 00:00:00 2001 From: Pierre Baillargeon Date: Thu, 30 Jan 2025 10:42:14 -0500 Subject: [PATCH 1/2] EMSUSD-2111 print collection membership - Add a printCollection function to the CollectionData class. - Implement this function in the UsdCollectionData class. - Print the collection by calling computeMembership and printing the result. - Add a menu item to print the collection. Work around a bug in USD: when both a membership expression and explicit inclusions or exclusions are used, we need to call a different less efficient USD function to compute the membership because the simpler, faster function is not working correctly. --- .../collection/expressionRulesMenu.py | 10 ++++- .../data/collectionData.py | 6 +++ .../usdData/usdCollectionData.py | 38 ++++++++++++++++++- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/expressionRulesMenu.py b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/expressionRulesMenu.py index 6d99af623..bfa26c9bb 100644 --- a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/expressionRulesMenu.py +++ b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/expressionRulesMenu.py @@ -15,6 +15,7 @@ INCLUDE_EXCLUDE_LABEL = "Include/Exclude" REMOVE_ALL_LABEL = "Remove All" CLEAR_OPINIONS_LABEL = "Clear Opinions from Target Layer" +PRINT_PRIMS_LABEL = "Print Prims to Script Editor" class ExpressionMenu(QMenu): @@ -27,10 +28,14 @@ def __init__(self, data: CollectionData, parent: QWidget): self._removeAllAction = QAction(REMOVE_ALL_LABEL, self) self._clearOpinionsAction = QAction(CLEAR_OPINIONS_LABEL, self) - self.addActions([self._removeAllAction, self._clearOpinionsAction]) + prePrintSeparator = QAction() + prePrintSeparator.setSeparator(True) + self._printPrimsAction = QAction(PRINT_PRIMS_LABEL, self) + self.addActions([self._removeAllAction, self._clearOpinionsAction, prePrintSeparator, self._printPrimsAction]) self._removeAllAction.triggered.connect(self._onRemoveAll) self._clearOpinionsAction.triggered.connect(self._onClearOpinions) + self._printPrimsAction.triggered.connect(self._onPrintPrims) self._collData.dataChanged.connect(self._onDataChanged) expansionRulesMenu = QMenu("Expansion Rules", self) @@ -64,6 +69,9 @@ def _onRemoveAll(self): def _onClearOpinions(self): self._collData.clearIncludeExcludeOpinions() + def _onPrintPrims(self): + self._collData.printCollection() + def onExpressionSelected(self, menuOption): if menuOption == self.expandPrimsAction: self._collData.setExpansionRule(Usd.Tokens.expandPrims) diff --git a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/data/collectionData.py b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/data/collectionData.py index 8b4f5c88e..782cdad8a 100644 --- a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/data/collectionData.py +++ b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/data/collectionData.py @@ -30,6 +30,12 @@ def getStage(self): ''' return None + def printCollection(self): + ''' + Prints the collection to the host logging system. + ''' + pass + # Include and exclude def includesAll(self) -> bool: diff --git a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/usdData/usdCollectionData.py b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/usdData/usdCollectionData.py index efc968021..72263ee73 100644 --- a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/usdData/usdCollectionData.py +++ b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/usdData/usdCollectionData.py @@ -6,6 +6,10 @@ from pxr import Sdf, Tf, Usd +PRINT_PRIMS_MSG = "{count} prims included in collection {collName} on {primName}:" +PRINT_PRIMS_CONFLICT_MSG = "Both Include/Exclude rules and Expressions are currently defined. Expressions will be ignored." + + class UsdCollectionData(CollectionData): def __init__(self, prim: Usd.Prim, collection: Usd.CollectionAPI): super().__init__() @@ -85,6 +89,24 @@ def getStage(self): ''' return self._prim.GetStage() + @validateCollection('') + def printCollection(self): + ''' + Prints the collection to the host logging system. + ''' + members = list(map(str, self.computeMembership())) + msgHeader = PRINT_PRIMS_MSG.format( + count=len(members), + collName=self._collection.GetName(), + primName=self._prim.GetPath()) + + from ..common.host import Host, MessageType + Host.instance().reportMessage(msgHeader, MessageType.INFO) + Host.instance().reportMessage('\n'.join(members), MessageType.INFO) + + if (self.hasDataConflict()): + Host.instance().reportMessage(PRINT_PRIMS_CONFLICT_MSG, MessageType.INFO) + # Include and exclude @validateCollection(False) @@ -214,4 +236,18 @@ def computeMembership(self) -> Sequence[str]: query = self._collection.ComputeMembershipQuery() if not query: return [] - return Usd.CollectionAPI.ComputeIncludedPaths(query, self._prim.GetStage()) + + # When there are both explicit inclusions/exclusions and a membership + # expression, the CollectionAPI docs states that the expression should + # be ignored. Unfortunately, ComputeIncludedPaths does not handle this + # correctly, so we have to do the work by hand by calling IsPAthIncluded + # for each path, as that function works correctly. + if self.hasDataConflict(): + members = [] + for prim in self._prim.GetStage().TraverseAll(): + path = prim.GetPath() + if query.IsPathIncluded(path): + members.append(path.pathString) + return members + else: + return Usd.CollectionAPI.ComputeIncludedPaths(query, self._prim.GetStage()) From 26ce0e918e24deb6e3e62b7207fb6299db659e84 Mon Sep 17 00:00:00 2001 From: Pierre Baillargeon Date: Mon, 3 Feb 2025 10:07:18 -0500 Subject: [PATCH 2/2] EMSUSD-2111 make labels themeable --- .../collection/expressionRulesMenu.py | 15 +++--- .../collection/includeExcludeWidget.py | 50 +++++++++++-------- .../common/filteredStringListView.py | 3 +- .../usdSharedComponents/common/theme.py | 3 ++ 4 files changed, 42 insertions(+), 29 deletions(-) diff --git a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/expressionRulesMenu.py b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/expressionRulesMenu.py index bfa26c9bb..891e821e8 100644 --- a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/expressionRulesMenu.py +++ b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/expressionRulesMenu.py @@ -1,4 +1,5 @@ from ..data.collectionData import CollectionData +from ..common.theme import Theme try: from PySide6.QtWidgets import QMenu, QWidget # type: ignore @@ -23,14 +24,16 @@ def __init__(self, data: CollectionData, parent: QWidget): super(ExpressionMenu, self).__init__(parent) self._collData = data + theme = Theme.instance() + # Note: this is necessary to avoid the separator not show up. self.setSeparatorsCollapsible(False) - self._removeAllAction = QAction(REMOVE_ALL_LABEL, self) - self._clearOpinionsAction = QAction(CLEAR_OPINIONS_LABEL, self) + self._removeAllAction = QAction(theme.themeLabel(REMOVE_ALL_LABEL), self) + self._clearOpinionsAction = QAction(theme.themeLabel(CLEAR_OPINIONS_LABEL), self) prePrintSeparator = QAction() prePrintSeparator.setSeparator(True) - self._printPrimsAction = QAction(PRINT_PRIMS_LABEL, self) + self._printPrimsAction = QAction(theme.themeLabel(PRINT_PRIMS_LABEL), self) self.addActions([self._removeAllAction, self._clearOpinionsAction, prePrintSeparator, self._printPrimsAction]) self._removeAllAction.triggered.connect(self._onRemoveAll) @@ -39,9 +42,9 @@ def __init__(self, data: CollectionData, parent: QWidget): self._collData.dataChanged.connect(self._onDataChanged) expansionRulesMenu = QMenu("Expansion Rules", self) - self.expandPrimsAction = QAction(EXPAND_PRIMS_MENU_OPTION, expansionRulesMenu, checkable=True) - self.expandPrimsPropertiesAction = QAction(EXPAND_PRIMS_PROPERTIES_MENU_OPTION, expansionRulesMenu, checkable=True) - self.explicitOnlyAction = QAction(EXPLICIT_ONLY_MENU_OPTION, expansionRulesMenu, checkable=True) + self.expandPrimsAction = QAction(theme.themeLabel(EXPAND_PRIMS_MENU_OPTION), expansionRulesMenu, checkable=True) + self.expandPrimsPropertiesAction = QAction(theme.themeLabel(EXPAND_PRIMS_PROPERTIES_MENU_OPTION), expansionRulesMenu, checkable=True) + self.explicitOnlyAction = QAction(theme.themeLabel(EXPLICIT_ONLY_MENU_OPTION), expansionRulesMenu, checkable=True) expansionRulesMenu.addActions([self.expandPrimsAction, self.expandPrimsPropertiesAction, self.explicitOnlyAction]) actionGroup = QActionGroup(self) diff --git a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/includeExcludeWidget.py b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/includeExcludeWidget.py index c3c6ca0fa..f20dc6bef 100644 --- a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/includeExcludeWidget.py +++ b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/includeExcludeWidget.py @@ -46,6 +46,8 @@ def __init__( super(IncludeExcludeWidget, self).__init__(parent) self._collData = data + theme = Theme.instance() + mainLayout = QVBoxLayout(self) mainLayout.setContentsMargins(0, 0, 0, 0) @@ -63,7 +65,7 @@ def __init__( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed ) self._filterWidget.setMaximumWidth(Theme.instance().uiScaled(165)) - self._filterWidget.setPlaceholderText(SEARCH_PLACEHOLDER_LABEL) + self._filterWidget.setPlaceholderText(theme.themeLabel(SEARCH_PLACEHOLDER_LABEL)) self._filterWidget.setClearButtonEnabled(True) separator = QFrame() @@ -77,24 +79,24 @@ def __init__( headerLayout.setContentsMargins(margin, topMargin, margin, margin) addBtn = QToolButton(headerWidget) - addBtn.setToolTip(ADD_OBJECTS_TOOLTIP) - addBtn.setIcon(Theme.instance().icon("add")) + addBtn.setToolTip(theme.themeLabel(ADD_OBJECTS_TOOLTIP)) + addBtn.setIcon(theme.icon("add")) addBtn.setPopupMode(QToolButton.InstantPopup) Theme.instance().themeMenuButton(addBtn, True) self._addBtnMenu = QMenu(addBtn) if Host.instance().canPick: - self._addBtnMenu.addAction(INCLUDE_OBJECTS_LABEL, self.onAddToIncludePrimClicked) - self._addBtnMenu.addAction(EXCLUDE_OBJECTS_LABEL, self.onAddToExcludePrimClicked) + self._addBtnMenu.addAction(theme.themeLabel(INCLUDE_OBJECTS_LABEL), self.onAddToIncludePrimClicked) + self._addBtnMenu.addAction(theme.themeLabel(EXCLUDE_OBJECTS_LABEL), self.onAddToExcludePrimClicked) self._addBtnMenu.addSeparator() - self._addBtnMenu.addAction(ADD_SELECTION_TO_INCLUDE_LABEL, self._onAddSelectionToInclude) - self._addBtnMenu.addAction(ADD_SELECTION_TO_EXCLUDE_LABEL, self._onAddSelectionToExclude) + self._addBtnMenu.addAction(theme.themeLabel(ADD_SELECTION_TO_INCLUDE_LABEL), self._onAddSelectionToInclude) + self._addBtnMenu.addAction(theme.themeLabel(ADD_SELECTION_TO_EXCLUDE_LABEL), self._onAddSelectionToExclude) self._addBtnMenu.aboutToShow.connect(self._onAboutToShowAddMenu) addBtn.setMenu(self._addBtnMenu) self._deleteBtn = QToolButton(headerWidget) - self._deleteBtn.setToolTip(REMOVE_OBJECTS_TOOLTIP) - self._deleteBtn.setIcon(Theme.instance().icon("delete")) + self._deleteBtn.setToolTip(theme.themeLabel(REMOVE_OBJECTS_TOOLTIP)) + self._deleteBtn.setIcon(theme.icon("delete")) self._deleteBtn.setPopupMode(QToolButton.InstantPopup) self._deleteBtn.setEnabled(False) @@ -103,12 +105,12 @@ def __init__( REMOVE_FROM_INCLUDES_LABEL, self.onRemoveSelectionFromInclude ) self._deleteBtnMenu.addAction( - REMOVE_FROM_EXCLUDES_LABEL, self.onRemoveSelectionFromExclude + theme.themeLabel(REMOVE_FROM_EXCLUDES_LABEL), self.onRemoveSelectionFromExclude ) self._deleteBtn.setMenu(self._deleteBtnMenu) self._selectBtn = QToolButton(headerWidget) - self._selectBtn.setToolTip(SELECT_OBJECTS_TOOLTIP) + self._selectBtn.setToolTip(theme.themeLabel(SELECT_OBJECTS_TOOLTIP)) self._selectBtn.setIcon(Theme.instance().icon("selector")) self._selectBtn.setEnabled(False) self._selectBtn.clicked.connect(self._onSelectItemsClicked) @@ -134,7 +136,7 @@ def __init__( mainLayout.addWidget(headerWidget) - self._include = StringListPanel(data.getIncludeData(), True, INCLUDE_LABEL, self) + self._include = StringListPanel(data.getIncludeData(), True, theme.themeLabel(INCLUDE_LABEL), self) self._include.cbIncludeAll.stateChanged.connect(self.onIncludeAllToggle) self._resizableInclude = Resizable( self._include, @@ -146,7 +148,7 @@ def __init__( self._resizableInclude.minContentSize = Theme.instance().uiScaled(44) mainLayout.addWidget(self._resizableInclude) - self._exclude = StringListPanel(data.getExcludeData(), False, EXCLUDE_LABEL, self) + self._exclude = StringListPanel(data.getExcludeData(), False, theme.themeLabel(EXCLUDE_LABEL), self) self._resizableExclude = Resizable( self._exclude, "USD_Light_Linking", @@ -187,14 +189,16 @@ def onAddToIncludePrimClicked(self): stage = self._collData.getStage() if not stage: return - items = Host.instance().pick(stage, dialogTitle=ADD_INCLUDE_OBJECTS_TITLE) + theme = Theme.instance() + items = Host.instance().pick(stage, dialogTitle=theme.themeLabel(ADD_INCLUDE_OBJECTS_TITLE)) self._collData.getIncludeData().addStrings(map(lambda x: str(x.GetPath()), items)) def onAddToExcludePrimClicked(self): stage = self._collData.getStage() if not stage: return - items = Host.instance().pick(stage, dialogTitle=ADD_EXCLUDE_OBJECTS_TITLE) + theme = Theme.instance() + items = Host.instance().pick(stage, dialogTitle=theme.themeLabel(ADD_EXCLUDE_OBJECTS_TITLE)) self._collData.getExcludeData().addStrings(map(lambda x: str(x.GetPath()), items)) def _hasValidSelection(self, stringList: StringListData) -> bool: @@ -204,9 +208,10 @@ def _hasValidSelection(self, stringList: StringListData) -> bool: return False def _onAboutToShowAddMenu(self): + theme = Theme.instance() labelsAndDatas = [ - (ADD_SELECTION_TO_INCLUDE_LABEL, self._collData.getIncludeData()), - (ADD_SELECTION_TO_EXCLUDE_LABEL, self._collData.getExcludeData()), + (theme.themeLabel(ADD_SELECTION_TO_INCLUDE_LABEL), self._collData.getIncludeData()), + (theme.themeLabel(ADD_SELECTION_TO_EXCLUDE_LABEL), self._collData.getExcludeData()), ] for label, data in labelsAndDatas: action = self._findAction(label) @@ -258,10 +263,11 @@ def onListSelectionChanged(self): self._deleteBtn.setEnabled(includesSelected or excludeSelected) self._selectBtn.setEnabled(includesSelected or excludeSelected) - deleteFromIncludesAction = self._findAction(REMOVE_FROM_INCLUDES_LABEL) + theme = Theme.instance() + deleteFromIncludesAction = self._findAction(theme.themeLabel(REMOVE_FROM_INCLUDES_LABEL)) if deleteFromIncludesAction: deleteFromIncludesAction.setEnabled(includesSelected) - deleteFromExcludesAction = self._findAction(REMOVE_FROM_EXCLUDES_LABEL) + deleteFromExcludesAction = self._findAction(theme.themeLabel(REMOVE_FROM_EXCLUDES_LABEL)) if deleteFromExcludesAction: deleteFromExcludesAction.setEnabled(excludeSelected) @@ -270,16 +276,16 @@ def onListSelectionChanged(self): self._deleteBtnPressedConnectedTo = None if includesSelected and excludeSelected: - self._deleteBtn.setToolTip(REMOVE_OBJECTS_TOOLTIP) + self._deleteBtn.setToolTip(theme.themeLabel(REMOVE_OBJECTS_TOOLTIP)) self._deleteBtn.setPopupMode(QToolButton.InstantPopup) Theme.instance().themeMenuButton(self._deleteBtn, True) else: if includesSelected: - self._deleteBtn.setToolTip(REMOVE_FROM_INCLUDE_TOOLTIP) + self._deleteBtn.setToolTip(theme.themeLabel(REMOVE_FROM_INCLUDE_TOOLTIP)) self._deleteBtn.pressed.connect(self.onRemoveSelectionFromInclude) self._deleteBtnPressedConnectedTo = self.onRemoveSelectionFromInclude elif excludeSelected: - self._deleteBtn.setToolTip(REMOVE_FROM_EXCLUDE_TOOLTIP) + self._deleteBtn.setToolTip(theme.themeLabel(REMOVE_FROM_EXCLUDE_TOOLTIP)) self._deleteBtn.pressed.connect(self.onRemoveSelectionFromExclude) self._deleteBtnPressedConnectedTo = self.onRemoveSelectionFromExclude self._deleteBtn.setPopupMode(QToolButton.DelayedPopup) diff --git a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/filteredStringListView.py b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/filteredStringListView.py index cd480cbc3..db24f0dd5 100644 --- a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/filteredStringListView.py +++ b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/filteredStringListView.py @@ -159,7 +159,8 @@ def paintEvent(self, event: QPaintEvent): self.placeholder_label.hide() def _paintPlaceHolder(self, placeHolderText): - self.placeholder_label.setText(placeHolderText) + theme = Theme.instance() + self.placeholder_label.setText(theme.themeLabel(placeHolderText)) self.placeholder_label.setGeometry(self.viewport().geometry()) self.placeholder_label.show() diff --git a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/theme.py b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/theme.py index 27d3a3fa8..48b004c59 100644 --- a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/theme.py +++ b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/theme.py @@ -103,6 +103,9 @@ def themedIcon(self, name: str, theme: str) -> QIcon: return result + def themeLabel(self, label: str) -> str: + return label + def themeTab(self, tab: QTabWidget): tab.setDocumentMode(True) tab.tabBar().setCursor(Qt.ArrowCursor)