Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EMSUSD-1522 I want the ability to see and set the expansion rule of the collection #4015

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion lib/mayaUsd/resources/ae/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ if(MAYA_APP_VERSION VERSION_GREATER_EQUAL 2023)
install(FILES
${MAYAUSD_SHARED_COMPONENTS}/collection/__init__.py
${MAYAUSD_SHARED_COMPONENTS}/collection/widget.py
${MAYAUSD_SHARED_COMPONENTS}/collection/includeExcludeWidget.py
${MAYAUSD_SHARED_COMPONENTS}/collection/expressionWidget.py
${MAYAUSD_SHARED_COMPONENTS}/collection/expressionRulesMenu.py
DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/python/usd_shared_components/collection/
)

Expand All @@ -61,6 +64,24 @@ if(MAYA_APP_VERSION VERSION_GREATER_EQUAL 2023)
${MAYAUSD_SHARED_COMPONENTS}/common/persistentStorage.py
${MAYAUSD_SHARED_COMPONENTS}/common/resizable.py
${MAYAUSD_SHARED_COMPONENTS}/common/theme.py
${MAYAUSD_SHARED_COMPONENTS}/common/menuButton.py
DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/python/usd_shared_components/common/
)
endif()

set(LIB_ICONS
${MAYAUSD_SHARED_COMPONENTS}/icons/dark/menu
)
foreach(ICON_BASE ${LIB_ICONS})
# The _100.png files need to be installed without the _100. This is the
# base icon name that is used. Maya will automatically choose the _150/_200
# image if neeeded.
install(FILES "${ICON_BASE}_100.png"
DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/python/usd_shared_components/icons/dark"
RENAME "${ICON_BASE}.png"
)
install(FILES "${ICON_BASE}_150.png" "${ICON_BASE}_200.png"
DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/python/usd_shared_components/icons/dark"
)
endforeach()

endif()
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@

try:
from PySide6.QtWidgets import QMenu, QWidget # type: ignore
from PySide6.QtGui import QActionGroup, QAction # type: ignore
except ImportError:
from PySide2.QtWidgets import QMenu, QWidget # type: ignore
from PySide2.QtGui import QActionGroup # type: ignore

from pxr import Usd

EXPAND_PRIMS_MENU_OPTION = "Expand Prims"
EXPAND_PRIMS_PROPERTIES_MENU_OPTION = "Expand Prims and Properties"
EXPLICIT_ONLY_MENU_OPTION = "Explicit Only"

class ExpressionMenu(QMenu):
def __init__(self, collection, parent: QWidget):
super(ExpressionMenu, self).__init__(parent)
self._collection = collection

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)
expansionRulesMenu.addActions([self.expandPrimsAction, self.expandPrimsPropertiesAction, self.explicitOnlyAction])

self.update()

actionGroup = QActionGroup(self)
actionGroup.setExclusive(True)
for action in expansionRulesMenu.actions():
actionGroup.addAction(action)

self.triggered.connect(self.onExpressionSelected)
self.addMenu(expansionRulesMenu)

def update(self):
usdExpansionRule = self._collection.GetExpansionRuleAttr().Get()
if usdExpansionRule == Usd.Tokens.expandPrims:
self.expandPrimsAction.setChecked(True)
elif usdExpansionRule == Usd.Tokens.expandPrimsAndProperties:
self.expandPrimsPropertiesAction.setChecked(True)
elif usdExpansionRule == Usd.Tokens.explicitOnly:
self.explicitOnlyAction.setChecked(True)

def onExpressionSelected(self, menuOption):
usdExpansionRule = self._collection.GetExpansionRuleAttr().Get()
if menuOption == self.expandPrimsAction:
if usdExpansionRule != Usd.Tokens.expandPrims:
self._collection.CreateExpansionRuleAttr(Usd.Tokens.expandPrims)
elif menuOption == self.expandPrimsPropertiesAction:
if usdExpansionRule != Usd.Tokens.expandPrimsAndProperties:
self._collection.CreateExpansionRuleAttr(Usd.Tokens.expandPrimsAndProperties)
elif menuOption == self.explicitOnlyAction:
if usdExpansionRule != Usd.Tokens.explicitOnly:
self._collection.CreateExpansionRuleAttr(Usd.Tokens.explicitOnly)
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from .expressionRulesMenu import ExpressionMenu
from ..common.menuButton import MenuButton

try:
from PySide6.QtCore import QEvent, Qt # type: ignore
from PySide6.QtWidgets import QSizePolicy, QTextEdit, QWidget, QVBoxLayout, QHBoxLayout # type: ignore
except ImportError:
from PySide2.QtCore import QEvent, Qt # type: ignore
from PySide2.QtWidgets import QSizePolicy, QTextEdit, QWidget, QVBoxLayout, QHBoxLayout # type: ignore

from pxr import Usd, Sdf

class ExpressionWidget(QWidget):
def __init__(self, collection: Usd.CollectionAPI, parent: QWidget, expressionChangedCallback):
super(ExpressionWidget, self).__init__(parent)
self._collection = collection
self._expressionCallback = expressionChangedCallback

self._expressionText = QTextEdit(self)
self._expressionText.setContentsMargins(0,0,0,0)
self._expressionText.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
self._expressionText.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self._expressionText.setPlaceholderText("Type an expression here...")

self._expressionMenu = ExpressionMenu(collection, self)
menuButton = MenuButton(self._expressionMenu, self)

menuWidget = QWidget(self)
menuLayout = QHBoxLayout(menuWidget)
menuLayout.addStretch(1)
menuLayout.addWidget(menuButton)

mainLayout = QVBoxLayout(self)
mainLayout.setContentsMargins(0,0,0,0)
mainLayout.addWidget(menuWidget)
mainLayout.addWidget(self._expressionText)

self._expressionText.installEventFilter(self)
self.setLayout(mainLayout)
self.update()

def update(self):
usdExpressionAttr = self._collection.GetMembershipExpressionAttr().Get()
if usdExpressionAttr != None:
self._expressionText.setPlainText(usdExpressionAttr.GetText())

self._expressionMenu.update()

def submitExpression(self):
usdExpressionAttr = self._collection.GetMembershipExpressionAttr().Get()
usdExpression = ""
if usdExpressionAttr != None:
usdExpression = usdExpressionAttr.GetText()

textExpression = self._expressionText.toPlainText()
if usdExpression != textExpression:
# assign default value if text is empty
if textExpression == "":
self._collection.CreateMembershipExpressionAttr()
pierrebai-adsk marked this conversation as resolved.
Show resolved Hide resolved
else:
self._collection.CreateMembershipExpressionAttr(Sdf.PathExpression(textExpression))

if self._expressionCallback != None:
self._expressionCallback()

def eventFilter(self, obj, event):
pierrebai-adsk marked this conversation as resolved.
Show resolved Hide resolved
if event.type() == QEvent.KeyPress and obj is self._expressionText:
if event.key() == Qt.Key_Return and self._expressionText.hasFocus():
self._expressionText.clearFocus()
return True
elif event.type() == QEvent.FocusOut:
self.submitExpression()

return super().eventFilter(obj, event)
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from ..common.list import StringList
from ..common.resizable import Resizable
from ..common.menuButton import MenuButton
from .expressionRulesMenu import ExpressionMenu

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

from pxr import Usd

class IncludeExcludeWidget(QWidget):
def __init__(self, collection: Usd.CollectionAPI = None, parent: QWidget = None):
super(IncludeExcludeWidget, self).__init__(parent)
self._collection = collection

shouldIncludeAll = False
includes = []
excludes = []

if self._collection is not None:
includeRootAttribute = self._collection.GetIncludeRootAttr()
if includeRootAttribute.IsAuthored():
shouldIncludeAll = self._collection.GetIncludeRootAttr().Get()

for p in self._collection.GetIncludesRel().GetTargets():
includes.append(p.pathString)
for p in self._collection.GetExcludesRel().GetTargets():
excludes.append(p.pathString)

includeExcludeLayout = QVBoxLayout(self)
includeExcludeLayout.setContentsMargins(0,0,0,0)

self._expressionMenu = ExpressionMenu(self._collection, self)
menuButton = MenuButton(self._expressionMenu, self)

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

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

self._include = StringList(includes, "Include", "Include all", self)
Copy link
Collaborator

@pierrebai-adsk pierrebai-adsk Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have like the widget class to have names that reflect they are UI element. StringList and Resizable are very generic, at first I did not understand what was going on. For example StringListView.

(Also, its file is just "list.py" which triggers my consistency demon. :) )

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those were there on the initial version of this widget that was moved to maya. So I kept them with the same name for ease of updating

self._include.cbIncludeAll.setChecked(shouldIncludeAll)
self._include.cbIncludeAll.stateChanged.connect(self.onIncludeAllToggle)
self._resizableInclude = Resizable(self._include, "USD_Light_Linking", "IncludeListHeight", self)
includeExcludeLayout.addWidget(self._resizableInclude)

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

self.setLayout(includeExcludeLayout)

def update(self):
self._expressionMenu.update()

def getIncludedItems(self):
return self._include.list.items()

def getExcludedItems(self):
return self._exclude.list.items()

def getIncludeAll(self):
return self._include.cbIncludeAll.isChecked()

def setIncludeAll(self, value: bool):
self._include.cbIncludeAll.setChecked(value)

def onIncludeAllToggle(self):
self._collection.GetIncludeRootAttr().Set(self._include.cbIncludeAll.isChecked())
Original file line number Diff line number Diff line change
@@ -1,48 +1,47 @@
from ..common.list import StringList
from ..common.resizable import Resizable
from .includeExcludeWidget import IncludeExcludeWidget
from .expressionWidget import ExpressionWidget

try:
from PySide6.QtWidgets import QWidget, QVBoxLayout
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QWidget, QTabWidget, QVBoxLayout
except ImportError:
from PySide2.QtWidgets import QWidget, QVBoxLayout # type: ignore
from PySide2.QtGui import QIcon # type: ignore
from PySide2.QtWidgets import QWidget, QTabWidget, QVBoxLayout # type: ignore

from pxr import Usd

class CollectionWidget(QWidget):
def __init__(self, collection: Usd.CollectionAPI = None, parent: QWidget = None):
super(CollectionWidget, self).__init__(parent)

self._collection: Usd.CollectionAPI = collection
mainLayout = QVBoxLayout()
mainLayout.setContentsMargins(0,0,0,0)

includes = []
excludes = []
shouldIncludeAll = False
self._includeExcludeWidget = IncludeExcludeWidget(collection, self)

if self._collection is not None:
includeRootAttribute = self._collection.GetIncludeRootAttr()
if includeRootAttribute.IsAuthored():
shouldIncludeAll = self._collection.GetIncludeRootAttr().Get()
# only create tab when usd version is greater then 23.11
if Usd.GetVersion() >= (0, 23, 11):
tabWidget = QTabWidget()
tabWidget.currentChanged.connect(self.onTabChanged)

for p in self._collection.GetIncludesRel().GetTargets():
includes.append(p.pathString)
for p in self._collection.GetExcludesRel().GetTargets():
excludes.append(p.pathString)
self._expressionWidget = ExpressionWidget(collection, tabWidget, self.onExpressionChanged)
tabWidget.addTab(self._includeExcludeWidget, QIcon(), "Include/Exclude")
tabWidget.addTab(self._expressionWidget, QIcon(), "Expression")

self.mainLayout = QVBoxLayout(self)
self.mainLayout.setContentsMargins(0,0,0,0)
mainLayout.addWidget(tabWidget)
else:
mainLayout.addWidget(self._includeExcludeWidget)

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.setLayout(mainLayout)

self.mainLayout.addWidget(self._resizableInclude)

self._exclude = StringList( excludes, "Exclude", "", self)
self._resizableExclude = Resizable(self._exclude, "USD_Light_Linking", "ExcludeListHeight", self)
if Usd.GetVersion() >= (0, 23, 11):
def onTabChanged(self, index):
self._includeExcludeWidget.update()
self._expressionWidget.update()

self.mainLayout.addWidget(self._resizableExclude)
self.mainLayout.addStretch(1)

def onIncludeAllToggle(self):
self._collection.GetIncludeRootAttr().Set(self._include.cbIncludeAll.isChecked())
def onExpressionChanged(self):
updateIncludeAll = len(self._includeExcludeWidget.getIncludedItems()) == 0 and len(self._includeExcludeWidget.getIncludedItems()) == 0 and self._includeExcludeWidget.getIncludeAll()
if updateIncludeAll:
self._includeExcludeWidget.setIncludeAll(False)
print('"Include All" has been disabled for the expression to take effect.')
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@
from .theme import Theme

try:
from PySide6.QtCore import (
from PySide6.QtCore import ( # type: ignore
QModelIndex,
QPersistentModelIndex,
QSize,
QStringListModel,
Qt,
Signal,
)
from PySide6.QtGui import QPainter
from PySide6.QtWidgets import QStyleOptionViewItem, QStyledItemDelegate, QListView, QLabel, QVBoxLayout, QHBoxLayout, QWidget, QCheckBox
from PySide6.QtGui import QPainter # type: ignore
from PySide6.QtWidgets import ( # type: ignore
QStyleOptionViewItem,
QStyledItemDelegate,
QListView,
QLabel,
QVBoxLayout,
QHBoxLayout,
QWidget,
QCheckBox,
)
except:
from PySide2.QtCore import (
from PySide2.QtCore import ( # type: ignore
pierrebai-adsk marked this conversation as resolved.
Show resolved Hide resolved
QModelIndex,
QPersistentModelIndex,
QSize,
Expand Down Expand Up @@ -99,4 +108,4 @@ def __init__(self, items: Sequence[str] = [], headerTitle: str = "", toggleTitle
layout.addWidget(headerWidget)
layout.addWidget(self.list)

self.setLayout(layout)
self.setLayout(layout)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from .theme import Theme
try:
from PySide6.QtCore import Qt # type: ignore
from PySide6.QtWidgets import QSizePolicy, QToolButton, QWidget, QMenu # type: ignore
except ImportError:
from PySide2.QtCore import Qt # type: ignore
from PySide2.QtWidgets import QSizePolicy, QToolButton, QWidget, QMenu # type: ignore

class MenuButton(QToolButton):
def __init__(self, menu: QMenu, parent: QWidget = None):
super(MenuButton, self).__init__(parent)
self.setIcon(Theme.instance().icon("menu"))
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
self.setArrowType(Qt.NoArrow)
self.setPopupMode(QToolButton.InstantPopup)
self.setStyleSheet("""
QToolButton::menu-indicator { width: 0px; }
""")
self.setMenu(menu)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
try:
from PySide6.QtCore import QSettings
from PySide6.QtCore import QSettings # type: ignore
except:
from PySide2.QtCore import QSettings # type: ignore

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I discovered that this writes files in a random folder. I think we will need to have some DCC-specific callback to save the data in DCC-specific locations. (In another PR)

Expand Down
Loading
Loading