This commit is contained in:
2025-11-24 00:15:32 +08:00
parent 9f7667a475
commit 0eeeb54784
256 changed files with 49494 additions and 0 deletions

View File

@@ -0,0 +1,454 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>160</width>
<height>560</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>160</width>
<height>560</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>2</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="titleFrame">
<property name="minimumSize">
<size>
<width>0</width>
<height>24</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="mainFrame">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>4</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>4</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<widget class="QFrame" name="iconTitleFrame">
<property name="minimumSize">
<size>
<width>0</width>
<height>16</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="iconFrame">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="iconFrame2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="thumbnailLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="thumbnailFrame">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>50</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>150</height>
</size>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>4</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="formFrame">
<property name="minimumSize">
<size>
<width>0</width>
<height>16</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_14">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="customWidgetFrame">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_9">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="previewButtons">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<property name="spacing">
<number>1</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="acceptButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>80</width>
<height>35</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>125</width>
<height>35</height>
</size>
</property>
<property name="font">
<font>
<family>Arial</family>
<pointsize>12</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="toolTip">
<string/>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="text">
<string>Apply</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="selectionSetButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>35</width>
<height>35</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>35</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>icons/selectionSet2.png</normaloff>icons/selectionSet2.png</iconset>
</property>
<property name="iconSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="labelSpacer">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>2</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
<slots>
<slot>snapshot()</slot>
<slot>apply()</slot>
<slot>edit()</slot>
<slot>showContextMenu()</slot>
</slots>
</ui>

View File

@@ -0,0 +1,304 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>167</width>
<height>473</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>160</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="windowTitle">
<string>Create Item</string>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>4</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="titleFrame">
<property name="minimumSize">
<size>
<width>0</width>
<height>24</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="thumbnailLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="thumbnailFrame">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>150</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>150</height>
</size>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QFrame" name="optionsFrame">
<property name="minimumSize">
<size>
<width>0</width>
<height>16</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>4</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>4</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="previewButtons">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<property name="spacing">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="acceptButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>80</width>
<height>35</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>140</width>
<height>35</height>
</size>
</property>
<property name="font">
<font>
<family>Arial</family>
<pointsize>12</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="toolTip">
<string/>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="text">
<string>Save</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="selectionSetButton">
<property name="minimumSize">
<size>
<width>35</width>
<height>35</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>5</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>icons/selectionSet2.png</normaloff>icons/selectionSet2.png</iconset>
</property>
<property name="iconSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>2</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
<slots>
<slot>snapshot()</slot>
<slot>save()</slot>
</slots>
</ui>

View File

@@ -0,0 +1,109 @@
# Studio Library Items
Items are used for loading and saving data.
### Pose Item
Saving and loading a pose items
```python
from studiolibrarymaya import poseitem
path = "/AnimLibrary/Characters/Malcolm/malcolm.pose"
objects = maya.cmds.ls(selection=True) or []
namespaces = []
# Saving a pose item
poseitem.save(path, objects=objects)
# Loading a pose item
poseitem.load(path, objects=objects, namespaces=namespaces, key=True, mirror=False)
```
### Animation Item
Saving and loading animation items
```python
from studiolibrarymaya import animitem
path = "/AnimLibrary/Characters/Malcolm/malcolm.anim"
objects = maya.cmds.ls(selection=True) or []
# Saving an animation item
animitem.save(path, objects=objects, frameRange=(0, 200), bakeConnected=False)
# Loading an animation item
animitem.load(path, objects=objects, option="replace all", connect=False, currentTime=False)
```
Loading an animation to multiple namespaces
```python
from studiolibrarymaya import animitem
animitem.load(path, namespaces=["character1", "character2"], option="replace all")
```
### Mirror Table Item
Saving and loading mirror tables
```python
from studiolibrarymaya import mirroritem
path = "/AnimLibrary/Characters/Malcolm/malcolm.mirror"
objects = maya.cmds.ls(selection=True) or []
# Saving a mirror table item
mirroritem.save(path, objects=objects, leftSide="Lf", rightSide="Rf")
# Loading a mirror table item
mirroritem.load(path, objects=objects, namespaces=[], option="swap", animation=True, time=None)
```
### Selection Set Item
Saving and loading selection sets
```python
from studiolibrarymaya import setsitem
path = "/AnimLibrary/Characters/Malcolm/malcolm.set"
objects = maya.cmds.ls(selection=True) or []
# Saving a selection sets item
setsitem.save(path, objects=objects)
# Loading a selection sets item
setsitem.load(path, objects=objects, namespaces=[])
```
### Maya File Item (Development)
Saving and loading a Maya file item
This item can be used to load and save any Maya nodes. For example:
locators and geometry.
```python
from studiolibrarymaya import mayafileitem
path = "/AnimLibrary/Characters/Malcolm/malcolm.mayafile"
objects = maya.cmds.ls(selection=True) or []
# Saving the item to disc
mayafileitem.save(path, objects=objects)
# Loading the item from disc
mayafileitem.load(path)
```
### Example Item
If you would like to create a custom item for saving and loading different data types, then please have a look at the [exampleitem.py](exampleitem.py)
When developing a new item you can "Shift + Click" on the shelf icon which will reload all Studio Library modules including your changes to the item.
Make sure you register any new items using either the "itemRegistry" key in the [config file](../studiolibrary/config/default.json) or by calling `studiolibrary.registerItem(cls)`.

View File

@@ -0,0 +1,11 @@
# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
#
# This library is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. This library is distributed in the
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.

View File

@@ -0,0 +1,273 @@
# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
#
# This library is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. This library is distributed in the
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
import logging
from studiolibrarymaya import baseitem
try:
import mutils
import mutils.gui
import maya.cmds
except ImportError as error:
print(error)
logger = logging.getLogger(__name__)
def save(path, *args, **kwargs):
"""Convenience function for saving an AnimItem."""
AnimItem(path).safeSave(*args, **kwargs)
def load(path, *args, **kwargs):
"""Convenience function for loading an AnimItem."""
AnimItem(path).load(*args, **kwargs)
class AnimItem(baseitem.BaseItem):
NAME = "Animation"
EXTENSION = ".anim"
ICON_PATH = os.path.join(os.path.dirname(__file__), "icons", "animation.png")
TRANSFER_CLASS = mutils.Animation
def imageSequencePath(self):
"""
Return the image sequence location for playing the animation preview.
:rtype: str
"""
return self.path() + "/sequence"
def loadSchema(self):
"""
Get schema used to load the animation item.
:rtype: list[dict]
"""
schema = super(AnimItem, self).loadSchema()
anim = mutils.Animation.fromPath(self.path())
startFrame = anim.startFrame() or 0
endFrame = anim.endFrame() or 0
value = "{0} - {1}".format(startFrame, endFrame)
schema.insert(3, {"name": "Range", "value": value})
schema.extend([
{
"name": "optionsGroup",
"title": "Options",
"type": "group",
"order": 2,
},
{
"name": "connect",
"type": "bool",
"inline": True,
"default": False,
"persistent": True,
"label": {"name": ""}
},
{
"name": "currentTime",
"type": "bool",
"inline": True,
"default": True,
"persistent": True,
"label": {"name": ""}
},
{
"name": "sourceTime",
"title": "source",
"type": "range",
"default": [startFrame, endFrame],
},
{
"name": "option",
"type": "enum",
"default": "replace all",
"items": ["replace", "replace all", "insert", "merge"],
"persistent": True,
},
])
return schema
def load(self, **kwargs):
"""
Load the animation for the given objects and options.
:type kwargs: dict
"""
anim = mutils.Animation.fromPath(self.path())
anim.load(
objects=kwargs.get("objects"),
namespaces=kwargs.get("namespaces"),
attrs=kwargs.get("attrs"),
startFrame=kwargs.get("startFrame"),
sourceTime=kwargs.get("sourceTime"),
option=kwargs.get("option"),
connect=kwargs.get("connect"),
mirrorTable=kwargs.get("mirrorTable"),
currentTime=kwargs.get("currentTime")
)
def saveSchema(self):
"""
Get the schema for saving an animation item.
:rtype: list[dict]
"""
start, end = (1, 100)
try:
start, end = mutils.currentFrameRange()
except NameError as error:
logger.exception(error)
return [
{
"name": "folder",
"type": "path",
"layout": "vertical",
"visible": False,
},
{
"name": "name",
"type": "string",
"layout": "vertical"
},
{
"name": "fileType",
"type": "enum",
"layout": "vertical",
"default": "mayaAscii",
"items": ["mayaAscii", "mayaBinary"],
"persistent": True
},
{
"name": "frameRange",
"type": "range",
"layout": "vertical",
"default": [start, end],
"actions": [
{
"name": "From Timeline",
"callback": mutils.playbackFrameRange
},
{
"name": "From Selected Timeline",
"callback": mutils.selectedFrameRange
},
{
"name": "From Selected Objects",
"callback": mutils.selectedObjectsFrameRange
},
]
},
{
"name": "byFrame",
"type": "int",
"default": 1,
"layout": "vertical",
"persistent": True
},
{
"name": "comment",
"type": "text",
"layout": "vertical"
},
{
"name": "bakeConnected",
"type": "bool",
"default": False,
"persistent": True,
"inline": True,
"label": {"visible": False}
},
{
"name": "objects",
"type": "objects",
"label": {
"visible": False
}
},
]
def saveValidator(self, **kwargs):
"""
The save validator is called when an input field has changed.
:type kwargs: dict
:rtype: list[dict]
"""
fields = super(AnimItem, self).saveValidator(**kwargs)
# Validate the by frame field
if kwargs.get("byFrame") == '' or kwargs.get("byFrame", 1) < 1:
fields.extend([
{
"name": "byFrame",
"error": "The by frame value cannot be less than 1!"
}
])
# Validate the frame range field
start, end = kwargs.get("frameRange", (0, 1))
if start >= end:
fields.extend([
{
"name": "frameRange",
"error": "The start frame cannot be greater "
"than or equal to the end frame!"
}
])
# Validate the current selection field
objects = kwargs.get("objects")
if objects and mutils.getDurationFromNodes(objects, time=[start, end]) <= 0:
fields.extend([
{
"name": "objects",
"error": "No animation was found on the selected object/s!"
"Please create a pose instead!",
}
])
return fields
def save(self, objects, sequencePath="", **kwargs):
"""
Save the animation from the given objects to the item path.
:type objects: list[str]
:type sequencePath: str
:type kwargs: dict
"""
super(AnimItem, self).save(**kwargs)
# Save the animation to the given path location on disc
mutils.saveAnim(
objects,
self.path(),
time=kwargs.get("frameRange"),
fileType=kwargs.get("fileType"),
iconPath=kwargs.get("thumbnail"),
metadata={"description": kwargs.get("comment", "")},
sequencePath=sequencePath,
bakeConnected=kwargs.get("bakeConnected")
)

View File

@@ -0,0 +1,445 @@
# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
#
# This library is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. This library is distributed in the
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
import shutil
import logging
from studiovendor.Qt import QtGui
from studiovendor.Qt import QtCore
import studiolibrary
from studiolibrarymaya import basesavewidget
from studiolibrarymaya import baseloadwidget
try:
import mutils
import mutils.gui
import maya.cmds
except ImportError as error:
print(error)
logger = logging.getLogger(__name__)
class BaseItemSignals(QtCore.QObject):
""""""
loadValueChanged = QtCore.Signal(object, object)
class BaseItem(studiolibrary.LibraryItem):
_baseItemSignals = BaseItemSignals()
loadValueChanged = _baseItemSignals.loadValueChanged
"""Base class for anim, pose, mirror and sets transfer items."""
SAVE_WIDGET_CLASS = basesavewidget.BaseSaveWidget
LOAD_WIDGET_CLASS = baseloadwidget.BaseLoadWidget
TRANSFER_CLASS = None
TRANSFER_BASENAME = ""
def createLoadWidget(self, parent=None):
widget = self.LOAD_WIDGET_CLASS(item=self, parent=parent)
return widget
@classmethod
def createSaveWidget(cls, parent=None, item=None):
item = item or cls()
widget = cls.SAVE_WIDGET_CLASS(item=item, parent=parent)
return widget
@classmethod
def showSaveWidget(cls, libraryWindow=None, item=None):
"""
Overriding this method to set the destination location
for the save widget.
Triggered when the user clicks the item action in the new item menu.
:type libraryWindow: studiolibrary.LibraryWindow
:type item: studiolibrary.LibraryItem or None
"""
item = item or cls()
widget = cls.SAVE_WIDGET_CLASS(item=item, parent=libraryWindow)
if libraryWindow:
path = libraryWindow.selectedFolderPath()
widget.setFolderPath(path)
widget.setLibraryWindow(libraryWindow)
libraryWindow.setCreateWidget(widget)
libraryWindow.folderSelectionChanged.connect(widget.setFolderPath)
def __init__(self, *args, **kwargs):
"""
Initialise a new instance for the given path.
:type path: str
:type args: list
:type kwargs: dict
"""
self._transferObject = None
self._currentLoadValues = {}
studiolibrary.LibraryItem.__init__(self, *args, **kwargs)
def emitLoadValueChanged(self, field, value):
"""
Emit the load value changed to be validated.
:type field: str
:type value: object
"""
self.loadValueChanged.emit(field, value)
def namespaces(self):
"""
Return the namesapces for this item depending on the namespace option.
:rtype: list[str] or None
"""
return self.currentLoadValue("namespaces")
def namespaceOption(self):
"""
Return the namespace option for this item.
:rtype: NamespaceOption or None
"""
return self.currentLoadValue("namespaceOption")
def doubleClicked(self):
"""
This method is called when the user double clicks the item.
:rtype: None
"""
self.loadFromCurrentValues()
def transferPath(self):
"""
Return the disc location to transfer path.
:rtype: str
"""
if self.TRANSFER_BASENAME:
return os.path.join(self.path(), self.TRANSFER_BASENAME)
else:
return self.path()
def transferObject(self):
"""
Return the transfer object used to read and write the data.
:rtype: mutils.TransferObject
"""
if not self._transferObject:
path = self.transferPath()
self._transferObject = self.TRANSFER_CLASS.fromPath(path)
return self._transferObject
def currentLoadValue(self, name):
"""
Get the current field value for the given name.
:type name: str
:rtype: object
"""
return self._currentLoadValues.get(name)
def setCurrentLoadValues(self, values):
"""
Set the current field values for the the item.
:type values: dict
"""
self._currentLoadValues = values
def loadFromCurrentValues(self):
"""Load the mirror table using the settings for this item."""
kwargs = self._currentLoadValues
objects = maya.cmds.ls(selection=True) or []
try:
self.load(objects=objects, **kwargs)
except Exception as error:
self.showErrorDialog("Item Error", str(error))
raise
def contextMenu(self, menu, items=None):
"""
This method is called when the user right clicks on this item.
:type menu: QtWidgets.QMenu
:type items: list[BaseItem]
:rtype: None
"""
from studiolibrarymaya import setsmenu
action = setsmenu.selectContentAction(self, parent=menu)
menu.addAction(action)
menu.addSeparator()
subMenu = self.createSelectionSetsMenu(menu, enableSelectContent=False)
menu.addMenu(subMenu)
menu.addSeparator()
studiolibrary.LibraryItem.contextMenu(self, menu, items=items)
def showSelectionSetsMenu(self, **kwargs):
"""
Show the selection sets menu for this item at the cursor position.
:rtype: QtWidgets.QAction
"""
menu = self.createSelectionSetsMenu(**kwargs)
position = QtGui.QCursor().pos()
action = menu.exec_(position)
return action
def createSelectionSetsMenu(self, parent=None, enableSelectContent=True):
"""
Get a new instance of the selection sets menu.
:type parent: QtWidgets.QWidget
:type enableSelectContent: bool
:rtype: QtWidgets.QMenu
"""
from . import setsmenu
parent = parent or self.libraryWindow()
namespaces = self.namespaces()
menu = setsmenu.SetsMenu(
item=self,
parent=parent,
namespaces=namespaces,
enableSelectContent=enableSelectContent,
)
return menu
def selectContent(self, namespaces=None, **kwargs):
"""
Select the contents of this item in the Maya scene.
:type namespaces: list[str]
"""
namespaces = namespaces or self.namespaces()
kwargs = kwargs or mutils.selectionModifiers()
msg = "Select content: Item.selectContent(namespacea={0}, kwargs={1})"
msg = msg.format(namespaces, kwargs)
logger.debug(msg)
try:
self.transferObject().select(namespaces=namespaces, **kwargs)
except Exception as error:
self.showErrorDialog("Item Error", str(error))
raise
def loadSchema(self):
"""
Get schema used to load the item.
:rtype: list[dict]
"""
modified = self.itemData().get("modified")
if modified:
modified = studiolibrary.timeAgo(modified)
count = self.transferObject().objectCount()
plural = "s" if count > 1 else ""
contains = str(count) + " Object" + plural
return [
{
"name": "infoGroup",
"title": "Info",
"type": "group",
"order": 1,
},
{
"name": "name",
"value": self.name(),
},
{
"name": "owner",
"value": self.transferObject().owner(),
},
{
"name": "created",
"value": modified,
},
{
"name": "contains",
"value": contains,
},
{
"name": "comment",
"value": self.transferObject().description() or "No comment",
},
{
"name": "namespaceGroup",
"title": "Namespace",
"type": "group",
"order": 10,
},
{
"name": "namespaceOption",
"title": "",
"type": "radio",
"value": "From file",
"items": ["From file", "From selection", "Use custom"],
"persistent": True,
"persistentKey": "BaseItem",
},
{
"name": "namespaces",
"title": "",
"type": "tags",
"value": [],
"items": mutils.namespace.getAll(),
"persistent": True,
"label": {"visible": False},
"persistentKey": "BaseItem",
},
]
def loadValidator(self, **values):
"""
Called when the load fields change.
:type values: dict
"""
namespaces = values.get("namespaces")
namespaceOption = values.get("namespaceOption")
if namespaceOption == "From file":
namespaces = self.transferObject().namespaces()
elif namespaceOption == "From selection":
namespaces = mutils.namespace.getFromSelection()
fieldChanged = values.get("fieldChanged")
if fieldChanged == "namespaces":
values["namespaceOption"] = "Use custom"
else:
values["namespaces"] = namespaces
self._currentLoadValues = values
return [
{
"name": "namespaces",
"value": values.get("namespaces"),
},
{
"name": "namespaceOption",
"value": values.get("namespaceOption"),
},
]
def load(self, **kwargs):
"""
Load the data from the transfer object.
:rtype: None
"""
logger.debug(u'Loading: {0}'.format(self.transferPath()))
self.transferObject().load(**kwargs)
logger.debug(u'Loading: {0}'.format(self.transferPath()))
def saveSchema(self):
"""
The base save schema.
:rtype: list[dict]
"""
return [
{
"name": "folder",
"type": "path",
"layout": "vertical",
"visible": False,
},
{
"name": "name",
"type": "string",
"layout": "vertical",
},
{
"name": "comment",
"type": "text",
"layout": "vertical"
},
{
"name": "objects",
"type": "objects",
"label": {"visible": False}
},
]
def saveValidator(self, **kwargs):
"""
The save validator is called when an input field has changed.
:type kwargs: dict
:rtype: list[dict]
"""
fields = []
if not kwargs.get("folder"):
fields.append({
"name": "folder",
"error": "No folder selected. Please select a destination folder.",
})
if not kwargs.get("name"):
fields.append({
"name": "name",
"error": "No name specified. Please set a name before saving.",
})
selection = maya.cmds.ls(selection=True) or []
msg = ""
if not selection:
msg = "No objects selected. Please select at least one object."
fields.append({
"name": "objects",
"value": selection,
"error": msg,
},
)
return fields
def save(self, thumbnail="", **kwargs):
"""
Save all the given object data to the item path on disc.
:type thumbnail: str
:type kwargs: dict
"""
# Copy the icon path to the given path
if thumbnail:
basename = os.path.basename(thumbnail)
shutil.copyfile(thumbnail, self.path() + "/" + basename)

View File

@@ -0,0 +1,255 @@
# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
#
# This library is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. This library is distributed in the
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
import logging
from studiovendor.Qt import QtGui
from studiovendor.Qt import QtCore
from studiovendor.Qt import QtWidgets
import studioqt
import studiolibrary
import studiolibrary.widgets
try:
import mutils
import mutils.gui
import maya.cmds
except ImportError as error:
print(error)
logger = logging.getLogger(__name__)
class BaseLoadWidget(QtWidgets.QWidget):
"""Base widget for loading items."""
def __init__(self, item, parent=None):
"""
:type item: studiolibrarymaya.BaseItem
:type parent: QtWidgets.QWidget or None
"""
QtWidgets.QWidget.__init__(self, parent)
self.setObjectName("studioLibraryBaseLoadWidget")
self.setWindowTitle("Load Item")
self.loadUi()
self._item = item
self._scriptJob = None
self._formWidget = None
widget = self.createTitleWidget()
widget.ui.menuButton.clicked.connect(self.showMenu)
self.ui.titleFrame.layout().addWidget(widget)
# Create the icon group box
groupBox = studiolibrary.widgets.GroupBoxWidget("Icon", self.ui.iconFrame)
groupBox.setObjectName("iconGroupBoxWidget")
groupBox.setPersistent(True)
self.ui.iconTitleFrame.layout().addWidget(groupBox)
# Create the thumbnail widget and set the image
self.ui.thumbnailButton = studiolibrary.widgets.ImageSequenceWidget(self)
self.ui.thumbnailButton.setObjectName("thumbnailButton")
self.ui.thumbnailFrame.layout().insertWidget(0, self.ui.thumbnailButton)
if os.path.exists(item.imageSequencePath()):
self.ui.thumbnailButton.setPath(item.imageSequencePath())
elif os.path.exists(item.thumbnailPath()):
self.ui.thumbnailButton.setPath(item.thumbnailPath())
# Create the load widget and set the load schema
self._formWidget = studiolibrary.widgets.FormWidget(self)
self._formWidget.setObjectName(item.__class__.__name__ + "Form")
self._formWidget.setSchema(item.loadSchema())
self._formWidget.setValidator(self.loadValidator)
self._formWidget.validate()
self.ui.formFrame.layout().addWidget(self._formWidget)
try:
self.selectionChanged()
self.setScriptJobEnabled(True)
except NameError as error:
logger.exception(error)
self.updateThumbnailSize()
self._item.loadValueChanged.connect(self._itemValueChanged)
self.ui.acceptButton.clicked.connect(self.accept)
self.ui.selectionSetButton.clicked.connect(self.showSelectionSetsMenu)
def loadValidator(self, *args, **kwargs):
return self.item().loadValidator(*args, **kwargs)
def createTitleWidget(self):
"""
Create a new instance of the title bar widget.
:rtype: QtWidgets.QFrame
"""
class UI(object):
"""Proxy class for attaching ui widgets as properties."""
pass
titleWidget = QtWidgets.QFrame(self)
titleWidget.setObjectName("titleWidget")
titleWidget.ui = UI()
vlayout = QtWidgets.QVBoxLayout()
vlayout.setSpacing(0)
vlayout.setContentsMargins(0, 0, 0, 0)
hlayout = QtWidgets.QHBoxLayout()
hlayout.setSpacing(0)
hlayout.setContentsMargins(0, 0, 0, 0)
vlayout.addLayout(hlayout)
titleButton = QtWidgets.QLabel(self)
titleButton.setText(self.item().NAME)
titleButton.setObjectName("titleButton")
titleWidget.ui.titleButton = titleButton
hlayout.addWidget(titleButton)
menuButton = QtWidgets.QPushButton(self)
menuButton.setText("...")
menuButton.setObjectName("menuButton")
titleWidget.ui.menuButton = menuButton
hlayout.addWidget(menuButton)
titleWidget.setLayout(vlayout)
return titleWidget
def _itemValueChanged(self, field, value):
"""
Triggered when the a field value has changed.
:type field: str
:type value: object
"""
self._formWidget.setValue(field, value)
def showMenu(self):
"""
Show the edit menu at the current cursor position.
:rtype: QtWidgets.QAction
"""
menu = QtWidgets.QMenu(self)
self.item().contextEditMenu(menu)
point = QtGui.QCursor.pos()
point.setX(point.x() + 3)
point.setY(point.y() + 3)
return menu.exec_(point)
def loadUi(self):
"""Convenience method for loading the .ui file."""
studioqt.loadUi(self, cls=BaseLoadWidget)
def formWidget(self):
"""
Get the form widget instance.
:rtype: studiolibrary.widgets.formwidget.FormWidget
"""
return self._formWidget
def setCustomWidget(self, widget):
"""Convenience method for adding a custom widget when loading."""
self.ui.customWidgetFrame.layout().addWidget(widget)
def item(self):
"""
Get the library item to be created.
:rtype: studiolibrarymaya.BaseItem
"""
return self._item
def showSelectionSetsMenu(self):
"""Show the selection sets menu."""
item = self.item()
item.showSelectionSetsMenu()
def resizeEvent(self, event):
"""
Overriding to adjust the image size when the widget changes size.
:type event: QtCore.QSizeEvent
"""
self.updateThumbnailSize()
def updateThumbnailSize(self):
"""Update the thumbnail button to the size of the widget."""
width = self.width() - 10
if width > 250:
width = 250
size = QtCore.QSize(width, width)
self.ui.thumbnailButton.setIconSize(size)
self.ui.thumbnailButton.setMaximumSize(size)
self.ui.thumbnailFrame.setMaximumSize(size)
def close(self):
"""Overriding this method to disable the script job when closed."""
self.setScriptJobEnabled(False)
if self.formWidget():
self.formWidget().savePersistentValues()
QtWidgets.QWidget.close(self)
def scriptJob(self):
"""
Get the script job object used when the users selection changes.
:rtype: mutils.ScriptJob
"""
return self._scriptJob
def setScriptJobEnabled(self, enabled):
"""
Enable the script job used when the users selection changes.
:type enabled: bool
"""
if enabled:
if not self._scriptJob:
event = ['SelectionChanged', self.selectionChanged]
self._scriptJob = mutils.ScriptJob(event=event)
else:
sj = self.scriptJob()
if sj:
sj.kill()
self._scriptJob = None
def selectionChanged(self):
"""Triggered when the users Maya selection has changed."""
self.formWidget().validate()
def accept(self):
"""Called when the user clicks the apply button."""
self.item().loadFromCurrentValues()

View File

@@ -0,0 +1,488 @@
# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
#
# This library is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. This library is distributed in the
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
import logging
from studiovendor.Qt import QtGui
from studiovendor.Qt import QtCore
from studiovendor.Qt import QtWidgets
import studioqt
import studiolibrary.widgets
try:
import mutils
import mutils.gui
import maya.cmds
except ImportError as error:
print(error)
logger = logging.getLogger(__name__)
class BaseSaveWidget(QtWidgets.QWidget):
"""Base widget for saving new items."""
def __init__(self, item, parent=None):
"""
:type item: studiolibrarymaya.BaseItem
:type parent: QtWidgets.QWidget or None
"""
QtWidgets.QWidget.__init__(self, parent)
self.setObjectName("studioLibraryBaseSaveWidget")
self.setWindowTitle("Save Item")
studioqt.loadUi(self)
self._item = item
self._scriptJob = None
self._formWidget = None
widget = self.createTitleWidget()
widget.ui.menuButton.hide()
self.ui.titleFrame.layout().addWidget(widget)
self.ui.acceptButton.clicked.connect(self.accept)
self.ui.selectionSetButton.clicked.connect(self.showSelectionSetsMenu)
try:
self.setScriptJobEnabled(True)
except NameError as error:
logger.exception(error)
self.createSequenceWidget()
self.updateThumbnailSize()
self.setItem(item)
def showMenu(self):
"""
Show the edit menu at the current cursor position.
:rtype: QtWidgets.QAction
"""
raise NotImplementedError("The title menu is not implemented")
def createTitleWidget(self):
"""
Create a new instance of the title bar widget.
:rtype: QtWidgets.QFrame
"""
class UI(object):
"""Proxy class for attaching ui widgets as properties."""
pass
titleWidget = QtWidgets.QFrame(self)
titleWidget.setObjectName("titleWidget")
titleWidget.ui = UI()
vlayout = QtWidgets.QVBoxLayout()
vlayout.setSpacing(0)
vlayout.setContentsMargins(0, 0, 0, 0)
hlayout = QtWidgets.QHBoxLayout()
hlayout.setSpacing(0)
hlayout.setContentsMargins(0, 0, 0, 0)
vlayout.addLayout(hlayout)
titleButton = QtWidgets.QLabel(self)
titleButton.setText(self.item().NAME)
titleButton.setObjectName("titleButton")
titleWidget.ui.titleButton = titleButton
hlayout.addWidget(titleButton)
menuButton = QtWidgets.QPushButton(self)
menuButton.setText("...")
menuButton.setObjectName("menuButton")
titleWidget.ui.menuButton = menuButton
hlayout.addWidget(menuButton)
titleWidget.setLayout(vlayout)
return titleWidget
def createSequenceWidget(self):
"""Create a sequence widget to replace the static thumbnail widget."""
theme = None
if self.parent():
try:
theme = self.parent().theme()
except AttributeError as error:
logger.debug("Cannot find theme for parent.")
self.ui.thumbnailButton = studiolibrary.widgets.ImageSequenceWidget(self, theme=theme)
self.ui.thumbnailButton.setObjectName("thumbnailButton")
self.ui.thumbnailFrame.layout().insertWidget(0, self.ui.thumbnailButton)
self.ui.thumbnailButton.clicked.connect(self.thumbnailCapture)
text = "Click to capture a thumbnail from the current model panel.\n" \
"CTRL + Click to show the capture window for better framing."
self.ui.thumbnailButton.setToolTip(text)
path = studiolibrary.resource.get("icons", "camera.svg")
self.ui.thumbnailButton.addAction(
path,
"Capture new image",
"Capture new image",
self.thumbnailCapture
)
path = studiolibrary.resource.get("icons", "expand.svg")
self.ui.thumbnailButton.addAction(
path,
"Show Capture window",
"Show Capture window",
self.showCaptureWindow
)
path = studiolibrary.resource.get("icons", "folder.svg")
self.ui.thumbnailButton.addAction(
path,
"Load image from disk",
"Load image from disk",
self.showBrowseImageDialog
)
icon = studiolibrary.resource.icon("thumbnail_solid.png")
self.ui.thumbnailButton.setIcon(icon)
def setLibraryWindow(self, libraryWindow):
"""
Set the library widget for the item.
:type libraryWindow: studiolibrary.LibraryWindow
:rtype: None
"""
self.item().setLibraryWindow(libraryWindow)
def libraryWindow(self):
"""
Get the library widget for the item.
:rtype: libraryWindow: studiolibrary.LibraryWindow
"""
return self.item().libraryWindow()
def formWidget(self):
"""
Get the form widget instance.
:rtype: studiolibrary.widgets.formwidget.FormWidget
"""
return self._formWidget
def item(self):
"""
Get the library item to be created.
:rtype: studiolibrarymaya.BaseItem
"""
return self._item
def setItem(self, item):
"""
Set the item to be created.
:type item: studiolibrarymaya.BaseItem
"""
self._item = item
if os.path.exists(item.imageSequencePath()):
self.setThumbnailPath(item.imageSequencePath())
elif not item.isTHUMBNAIL_PATH():
self.setThumbnailPath(item.thumbnailPath())
schema = item.saveSchema()
if schema:
formWidget = studiolibrary.widgets.FormWidget(self)
formWidget.setSchema(schema)
formWidget.setValidator(item.saveValidator)
# Used when overriding the item
name = os.path.basename(item.path())
formWidget.setValues({"name": name})
self.ui.optionsFrame.layout().addWidget(formWidget)
self._formWidget = formWidget
formWidget.validate()
else:
self.ui.optionsFrame.setVisible(False)
def showSelectionSetsMenu(self):
"""
Show the selection sets menu for the current folder path.
:rtype: None
"""
from studiolibrarymaya import setsmenu
path = self.folderPath()
position = QtGui.QCursor().pos()
libraryWindow = self.libraryWindow()
menu = setsmenu.SetsMenu.fromPath(path, libraryWindow=libraryWindow)
menu.exec_(position)
def close(self):
"""Overriding the close method to disable the script job on close."""
self._formWidget.savePersistentValues()
self.setScriptJobEnabled(False)
QtWidgets.QWidget.close(self)
def scriptJob(self):
"""
Get the script job object used when the users selection changes.
:rtype: mutils.ScriptJob
"""
return self._scriptJob
def setScriptJobEnabled(self, enabled):
"""Set the script job used when the users selection changes."""
if enabled:
if not self._scriptJob:
event = ['SelectionChanged', self.selectionChanged]
self._scriptJob = mutils.ScriptJob(event=event)
else:
sj = self.scriptJob()
if sj:
sj.kill()
self._scriptJob = None
def resizeEvent(self, event):
"""
Overriding to adjust the image size when the widget changes size.
:type event: QtCore.QSizeEvent
"""
self.updateThumbnailSize()
def updateThumbnailSize(self):
"""Update the thumbnail button to the size of the widget."""
width = self.width() - 10
if width > 250:
width = 250
size = QtCore.QSize(width, width)
self.ui.thumbnailButton.setIconSize(size)
self.ui.thumbnailButton.setMaximumSize(size)
self.ui.thumbnailFrame.setMaximumSize(size)
def setFolderPath(self, path):
"""
Set the destination folder path.
:type path: str
"""
self.formWidget().setValue("folder", path)
def folderPath(self):
"""
Return the folder path.
:rtype: str
"""
return self.formWidget().value("folder")
def selectionChanged(self):
"""Triggered when the Maya selection changes."""
if self.formWidget():
self.formWidget().validate()
def showByFrameDialog(self):
"""
Show the by frame dialog.
:rtype: None or QtWidgets.QDialogButtonBox.StandardButton
"""
result = None
text = 'To help speed up the playblast you can set the "by frame" ' \
'to a number greater than 1. For example if the "by frame" ' \
'is set to 2 it will playblast every second frame.'
options = self.formWidget().values()
byFrame = options.get("byFrame", 1)
startFrame, endFrame = options.get("frameRange", [None, None])
duration = 1
if startFrame is not None and endFrame is not None:
duration = endFrame - startFrame
if duration > 100 and byFrame == 1:
buttons = [
QtWidgets.QDialogButtonBox.Ok,
QtWidgets.QDialogButtonBox.Cancel
]
result = studiolibrary.widgets.MessageBox.question(
self.libraryWindow(),
title="Playblast Tip",
text=text,
buttons=buttons,
enableDontShowCheckBox=True,
)
return result
def showBrowseImageDialog(self):
"""Show a file dialog for choosing an image from disc."""
fileDialog = QtWidgets.QFileDialog(
self,
caption="Open Image",
filter="Image Files (*.png *.jpg)"
)
fileDialog.fileSelected.connect(self.setThumbnailPath)
fileDialog.exec_()
def showCaptureWindow(self):
"""Show the capture window for framing."""
self.thumbnailCapture(show=True)
def setThumbnailPath(self, path):
"""
Set the path to the thumbnail image or the image sequence directory.
:type path: str
"""
filename, extension = os.path.splitext(path)
dst = studiolibrary.tempPath("thumbnail" + extension)
studiolibrary.copyPath(path, dst, force=True)
self.ui.thumbnailButton.setPath(dst)
def _capturedCallback(self, src):
"""
Triggered when capturing a thumbnail snapshot.
:type src: str
"""
path = os.path.dirname(src)
self.setThumbnailPath(path)
def thumbnailCapture(self, show=False):
"""Capture a playblast and save it to the temp thumbnail path."""
options = self.formWidget().values()
startFrame, endFrame = options.get("frameRange", [None, None])
step = options.get("byFrame", 1)
# Ignore the by frame dialog when the control modifier is pressed.
if not studioqt.isControlModifier():
result = self.showByFrameDialog()
if result == QtWidgets.QDialogButtonBox.Cancel:
return
try:
path = studiolibrary.tempPath("sequence", "thumbnail.jpg")
mutils.gui.thumbnailCapture(
show=show,
path=path,
startFrame=startFrame,
endFrame=endFrame,
step=step,
clearCache=True,
captured=self._capturedCallback,
)
except Exception as e:
title = "Error while capturing thumbnail"
studiolibrary.widgets.MessageBox.critical(self.libraryWindow(), title, str(e))
raise
def showThumbnailCaptureDialog(self):
"""
Ask the user if they would like to capture a thumbnail.
:rtype: int
"""
title = "Create a thumbnail"
text = "Would you like to capture a thumbnail?"
buttons = [
QtWidgets.QDialogButtonBox.Yes,
QtWidgets.QDialogButtonBox.Ignore,
QtWidgets.QDialogButtonBox.Cancel
]
parent = self.item().libraryWindow()
button = studiolibrary.widgets.MessageBox.question(
parent,
title,
text,
buttons=buttons
)
if button == QtWidgets.QDialogButtonBox.Yes:
self.thumbnailCapture()
return button
def accept(self):
"""Triggered when the user clicks the save button."""
try:
self.formWidget().validate()
if self.formWidget().hasErrors():
raise Exception("\n".join(self.formWidget().errors()))
hasFrames = self.ui.thumbnailButton.hasFrames()
if not hasFrames:
button = self.showThumbnailCaptureDialog()
if button == QtWidgets.QDialogButtonBox.Cancel:
return
name = self.formWidget().value("name")
folder = self.formWidget().value("folder")
path = folder + "/" + name
thumbnail = self.ui.thumbnailButton.firstFrame()
self.save(path=path, thumbnail=thumbnail)
except Exception as e:
studiolibrary.widgets.MessageBox.critical(
self.libraryWindow(),
"Error while saving",
str(e),
)
raise
def save(self, path, thumbnail):
"""
Save the item with the given objects to the given disc location path.
:type path: str
:type thumbnail: str
"""
kwargs = self.formWidget().values()
sequencePath = self.ui.thumbnailButton.dirname()
item = self.item()
item.setPath(path)
item.safeSave(
thumbnail=thumbnail,
sequencePath=sequencePath,
**kwargs
)
self.close()

View File

@@ -0,0 +1,92 @@
# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
#
# This library is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. This library is distributed in the
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
"""
NOTE: Make sure you register this item in the config.
"""
import os
import logging
from studiolibrarymaya import baseitem
logger = logging.getLogger(__name__)
class ExampleItem(baseitem.BaseItem):
NAME = "Example"
EXTENSION = ".example"
ICON_PATH = os.path.join(os.path.dirname(__file__), "icons", "pose.png")
def loadSchema(self, **kwargs):
"""
Get the schema used for loading the example item.
:rtype: list[dict]
"""
return [
{
"name": "option",
"type": "bool",
"default": False,
"persistent": True,
},
]
def load(self, **kwargs):
"""
The load method is called with the user values from the load schema.
:type kwargs: dict
"""
logger.info("Loading %s %s", self.path(), kwargs)
raise NotImplementedError("The load method is not implemented!")
def saveSchema(self, **kwargs):
"""
Get the schema used for saving the example item.
:rtype: list[dict]
"""
return [
# The 'name' field and the 'folder' field are both required by
# the BaseItem. How this is handled may change in the future.
{
"name": "folder",
"type": "path",
"layout": "vertical",
"visible": False,
},
{
"name": "name",
"type": "string",
"layout": "vertical"
},
{
"name": "fileType",
"type": "enum",
"layout": "vertical",
"default": "mayaAscii",
"items": ["mayaAscii", "mayaBinary"],
"persistent": True
},
]
def save(self, **kwargs):
"""
The save method is called with the user values from the save schema.
:type kwargs: dict
"""
logger.info("Saving %s %s", self.path(), kwargs)
raise NotImplementedError("The save method is not implemented!")

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,105 @@
# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
#
# This library is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. This library is distributed in the
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
"""
NOTE: Make sure you register this item in the config.
"""
import os
import logging
import maya.cmds
from studiolibrarymaya import baseitem
logger = logging.getLogger(__name__)
class MayaFileItem(baseitem.BaseItem):
NAME = "Maya File"
TYPE = NAME
EXTENSION = ".mayafile"
ICON_PATH = os.path.join(os.path.dirname(__file__), "icons", "file.png")
def transferPath(self):
return self.path() + "/mayafile.ma"
def loadSchema(self, **kwargs):
"""
Get the schema used for loading the example item.
:rtype: list[dict]
"""
return []
def load(self, **kwargs):
"""
The load method is called with the user values from the load schema.
:type kwargs: dict
"""
logger.info("Loading %s %s", self.path(), kwargs)
maya.cmds.file(
self.transferPath(),
i=True,
type="mayaAscii",
options="v=0;",
preserveReferences=True,
mergeNamespacesOnClash=False,
)
def saveSchema(self, **kwargs):
"""
Get the schema used for saving the example item.
:rtype: list[dict]
"""
return [
# The 'name' field and the 'folder' field are both required by
# the BaseItem. How this is handled may change in the future.
{
"name": "folder",
"type": "path",
"layout": "vertical",
"visible": False,
},
{
"name": "name",
"type": "string",
"layout": "vertical"
},
{
"name": "objects",
"type": "objects",
"layout": "vertical"
},
]
def save(self, **kwargs):
"""
The save method is called with the user values from the save schema.
:type kwargs: dict
"""
logger.info("Saving %s %s", self.path(), kwargs)
super(MayaFileItem, self).save(**kwargs)
maya.cmds.file(
self.transferPath(),
type="mayaAscii",
options="v=0;",
preserveReferences=True,
exportSelected=True
)

View File

@@ -0,0 +1,195 @@
# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
#
# This library is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. This library is distributed in the
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import uuid
import logging
import maya.cmds
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
import studiolibrary
from studiolibrary import librarywindow
import mutils
logger = logging.getLogger(__name__)
_mayaCloseScriptJob = None
def enableMayaClosedEvent():
"""
Create a Maya script job to trigger on the event "quitApplication".
Enable the Maya closed event to save the library settings on close
:rtype: None
"""
global _mayaCloseScriptJob
if not _mayaCloseScriptJob:
event = ['quitApplication', mayaClosedEvent]
try:
_mayaCloseScriptJob = mutils.ScriptJob(event=event)
logger.debug("Maya close event enabled")
except NameError as error:
logging.exception(error)
def disableMayaClosedEvent():
"""Disable the maya closed event."""
global _mayaCloseScriptJob
if _mayaCloseScriptJob:
_mayaCloseScriptJob.kill()
_mayaCloseScriptJob = None
logger.debug("Maya close event disabled")
def mayaClosedEvent():
"""
Create a Maya script job to trigger on the event "quitApplication".
:rtype: None
"""
for libraryWindow in librarywindow.LibraryWindow.instances():
libraryWindow.saveSettings()
class MayaLibraryWindow(MayaQWidgetDockableMixin, librarywindow.LibraryWindow):
def destroy(self):
"""
Overriding this method to avoid multiple script jobs when developing.
"""
disableMayaClosedEvent()
librarywindow.LibraryWindow.destroy(self)
def setObjectName(self, name):
"""
Overriding to ensure the widget has a unique name for Maya.
:type name: str
:rtype: None
"""
name = '{0}_{1}'.format(name, uuid.uuid4())
librarywindow.LibraryWindow.setObjectName(self, name)
def tabWidget(self):
"""
Return the tab widget for the library widget.
:rtype: QtWidgets.QTabWidget or None
"""
if self.isDockable():
return self.parent().parent().parent()
else:
return None
def workspaceControlName(self):
"""
Return the workspaceControl name for the widget.
:rtype: str or None
"""
if self.isDockable() and self.parent():
return self.parent().objectName()
else:
return None
def isDocked(self):
"""
Convenience method to return if the widget is docked.
:rtype: bool
"""
return not self.isFloating()
def isFloating(self):
"""
Return True if the widget is a floating window.
:rtype: bool
"""
name = self.workspaceControlName()
if name:
try:
return maya.cmds.workspaceControl(name, q=True, floating=True)
except AttributeError:
msg = 'The "maya.cmds.workspaceControl" ' \
'command is not supported!'
logger.warning(msg)
return True
def window(self):
"""
Overriding this method to return itself when docked.
This is used for saving the correct window position and size settings.
:rtype: QWidgets.QWidget
"""
if self.isDocked():
return self
else:
return librarywindow.LibraryWindow.window(self)
def show(self, **kwargs):
"""
Show the library widget as a dockable window.
Set dockable=False in kwargs if you want to show the widget as a floating window.
:rtype: None
"""
dockable = kwargs.get('dockable', True)
MayaQWidgetDockableMixin.show(self, dockable=dockable)
self.raise_()
self.fixBorder()
def resizeEvent(self, event):
"""
Override method to remove the border when the window size has changed.
:type event: QtCore.QEvent
:rtype: None
"""
if event.isAccepted():
if not self.isLoaded():
self.fixBorder()
def floatingChanged(self, isFloating):
"""
Override method to remove the grey border when the parent has changed.
Only supported/triggered in Maya 2018
:rtype: None
"""
self.fixBorder()
def fixBorder(self):
"""
Remove the grey border around the tab widget.
:rtype: None
"""
if self.tabWidget():
self.tabWidget().setStyleSheet("border:0px;")
enableMayaClosedEvent()

View File

@@ -0,0 +1,231 @@
# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
#
# This library is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. This library is distributed in the
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
import logging
from studiolibrarymaya import baseitem
try:
import mutils
import maya.cmds
except ImportError as error:
print(error)
logger = logging.getLogger(__name__)
def save(path, *args, **kwargs):
"""Convenience function for saving a MirrorItem."""
MirrorItem(path).safeSave(*args, **kwargs)
def load(path, *args, **kwargs):
"""Convenience function for loading a MirrorItem."""
MirrorItem(path).load(*args, **kwargs)
class MirrorItem(baseitem.BaseItem):
NAME = "Mirror Table"
EXTENSION = ".mirror"
ICON_PATH = os.path.join(os.path.dirname(__file__), "icons", "mirrortable.png")
TRANSFER_CLASS = mutils.MirrorTable
TRANSFER_BASENAME = "mirrortable.json"
def __init__(self, *args, **kwargs):
"""
:type args: list
:type kwargs: dict
"""
super(MirrorItem, self).__init__(*args, **kwargs)
self._validatedObjects = []
def loadSchema(self):
"""
Get schema used to load the mirror table item.
:rtype: list[dict]
"""
schema = super(MirrorItem, self).loadSchema()
mt = self.transferObject()
schema.insert(2, {"name": "Left", "value": mt.leftSide()})
schema.insert(3, {"name": "Right", "value": mt.rightSide()})
schema.extend([
{
"name": "optionsGroup",
"title": "Options",
"type": "group",
"order": 2,
},
{
"name": "keysOption",
"title": "Keys",
"type": "radio",
"value": "Selected Range",
"items": ["All Keys", "Selected Range"],
"persistent": True,
},
{
"name": "option",
"type": "enum",
"default": "swap",
"items": ["swap", "left to right", "right to left"],
"persistent": True
},
])
return schema
def load(self, **kwargs):
"""
Load the current mirror table to the given objects.
:type kwargs: dict
"""
mt = mutils.MirrorTable.fromPath(self.path() + "/mirrortable.json")
mt.load(
objects=kwargs.get("objects"),
namespaces=kwargs.get("namespaces"),
option=kwargs.get("option"),
keysOption=kwargs.get("keysOption"),
time=kwargs.get("time")
)
def saveSchema(self):
"""
Get the fields used to save the item.
:rtype: list[dict]
"""
return [
{
"name": "folder",
"type": "path",
"layout": "vertical",
"visible": False,
},
{
"name": "name",
"type": "string",
"layout": "vertical"
},
{
"name": "mirrorPlane",
"type": "buttonGroup",
"default": "YZ",
"layout": "vertical",
"items": ["YZ", "XY", "XZ"],
},
{
"name": "leftSide",
"type": "string",
"layout": "vertical",
"menu": {
"name": "0"
}
},
{
"name": "rightSide",
"type": "string",
"layout": "vertical",
"menu": {
"name": "0"
}
},
{
"name": "comment",
"type": "text",
"layout": "vertical"
},
{
"name": "objects",
"type": "objects",
"label": {
"visible": False
}
},
]
def saveValidator(self, **kwargs):
"""
The save validator is called when an input field has changed.
:type kwargs: dict
:rtype: list[dict]
"""
results = super(MirrorItem, self).saveValidator(**kwargs)
objects = maya.cmds.ls(selection=True) or []
dirty = kwargs.get("fieldChanged") in ["leftSide", "rightSide"]
dirty = dirty or self._validatedObjects != objects
if dirty:
self._validatedObjects = objects
leftSide = kwargs.get("leftSide", "")
if not leftSide:
leftSide = mutils.MirrorTable.findLeftSide(objects)
rightSide = kwargs.get("rightSide", "")
if not rightSide:
rightSide = mutils.MirrorTable.findRightSide(objects)
mt = mutils.MirrorTable.fromObjects(
[],
leftSide=leftSide,
rightSide=rightSide
)
results.extend([
{
"name": "leftSide",
"value": leftSide,
"menu": {
"name": str(mt.leftCount(objects))
}
},
{
"name": "rightSide",
"value": rightSide,
"menu": {
"name": str(mt.rightCount(objects))
}
},
])
return results
def save(self, objects, **kwargs):
"""
Save the given objects to the item path on disc.
:type objects: list[str]
:type kwargs: dict
"""
super(MirrorItem, self).save(**kwargs)
# Save the mirror table to the given location
mutils.saveMirrorTable(
self.path() + "/mirrortable.json",
objects,
metadata={"description": kwargs.get("comment", "")},
leftSide=kwargs.get("leftSide"),
rightSide=kwargs.get("rightSide"),
mirrorPlane=kwargs.get("mirrorPlane"),
)

View File

@@ -0,0 +1,392 @@
# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
#
# This library is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. This library is distributed in the
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
import logging
import studiolibrary
from studiovendor.Qt import QtGui
from studiovendor.Qt import QtCore
from studiovendor.Qt import QtWidgets
from studiolibrarymaya import baseitem
from studiolibrarymaya import baseloadwidget
try:
import mutils
import maya.cmds
except ImportError as error:
print(error)
logger = logging.getLogger(__name__)
def save(path, *args, **kwargs):
"""Convenience function for saving a PoseItem."""
PoseItem(path).safeSave(*args, **kwargs)
def load(path, *args, **kwargs):
"""Convenience function for loading a PoseItem."""
PoseItem(path).load(*args, **kwargs)
class PoseLoadWidget(baseloadwidget.BaseLoadWidget):
@classmethod
def createFromPath(cls, path, theme=None):
item = PoseItem(path)
widget = cls(item)
if not theme:
import studiolibrary.widgets
theme = studiolibrary.widgets.Theme()
widget.setStyleSheet(theme.styleSheet())
widget.show()
def __init__(self, *args, **kwargs):
super(PoseLoadWidget, self).__init__(*args, **kwargs)
self._options = None
self._pose = mutils.Pose.fromPath(self.item().transferPath())
self.ui.blendFrame = QtWidgets.QFrame(self)
layout = QtWidgets.QHBoxLayout()
self.ui.blendFrame.setLayout(layout)
if self.item().libraryWindow():
self.item().libraryWindow().itemsWidget().itemSliderMoved.connect(self._sliderMoved)
self.item().libraryWindow().itemsWidget().itemSliderReleased.connect(self._sliderReleased)
self.item().libraryWindow().itemsWidget().itemDoubleClicked.connect(self._itemDoubleClicked)
self.ui.blendSlider = QtWidgets.QSlider(self)
self.ui.blendSlider.setObjectName("blendSlider")
self.ui.blendSlider.setMinimum(-30)
self.ui.blendSlider.setMaximum(130)
self.ui.blendSlider.setOrientation(QtCore.Qt.Horizontal)
self.ui.blendSlider.sliderMoved.connect(self._sliderMoved)
self.ui.blendSlider.sliderReleased.connect(self._sliderReleased)
self.ui.blendEdit = QtWidgets.QLineEdit(self)
self.ui.blendEdit.setObjectName("blendEdit")
self.ui.blendEdit.setText("0")
self.ui.blendEdit.editingFinished.connect(self._blendEditChanged)
validator = QtGui.QIntValidator(-200, 200, self)
self.ui.blendEdit.setValidator(validator)
layout.addWidget(self.ui.blendSlider)
layout.addWidget(self.ui.blendEdit)
self.setCustomWidget(self.ui.blendFrame)
def _itemDoubleClicked(self):
"""Triggered when the user double-clicks a pose."""
self.accept()
def _sliderMoved(self, value):
"""Triggered when the user moves the slider handle."""
self.load(blend=value, batchMode=True)
def _sliderReleased(self):
"""Triggered when the user releases the slider handle."""
try:
self.load(blend=self.ui.blendSlider.value(), refresh=False)
self._options = {}
except Exception as error:
self.item().showErrorDialog("Item Error", str(error))
raise
def _blendEditChanged(self, *args):
"""Triggered when the user changes the blend edit value."""
try:
self._options = {}
self.load(blend=int(self.ui.blendEdit.text()), clearSelection=False)
except Exception as error:
self.item().showErrorDialog("Item Error", str(error))
raise
def loadValidator(self, *args, **kwargs):
self._options = {}
return super(PoseLoadWidget, self).loadValidator(*args, **kwargs)
def accept(self):
"""Triggered when the user clicks the apply button."""
try:
self._options = {}
self.load(clearCache=True, clearSelection=False)
except Exception as error:
self.item().showErrorDialog("Item Error", str(error))
raise
def load(
self,
blend=100.0,
refresh=True,
batchMode=False,
clearCache=False,
clearSelection=True,
):
"""
Load the pose item with the current user settings from disc.
:type blend: float
:type refresh: bool
:type batchMode: bool
:type clearCache: bool
:type clearSelection: bool
"""
if batchMode:
self.formWidget().setValidatorEnabled(False)
else:
self.formWidget().setValidatorEnabled(True)
if not self._options:
self._options = self.formWidget().values()
self._options['mirrorTable'] = self.item().mirrorTable()
self._options['objects'] = maya.cmds.ls(selection=True) or []
if not self._options["searchAndReplaceEnabled"]:
self._options["searchAndReplace"] = None
del self._options["namespaceOption"]
del self._options["searchAndReplaceEnabled"]
self.ui.blendEdit.blockSignals(True)
self.ui.blendSlider.setValue(blend)
self.ui.blendEdit.setText(str(int(blend)))
self.ui.blendEdit.blockSignals(False)
if self.item().libraryWindow():
self.item().libraryWindow().itemsWidget().blockSignals(True)
self.item().setSliderValue(blend)
self.item().libraryWindow().itemsWidget().blockSignals(False)
if batchMode:
self.item().libraryWindow().showToastMessage("Blend: {0}%".format(blend))
try:
self._pose.load(
blend=blend,
refresh=refresh,
batchMode=batchMode,
clearCache=clearCache,
clearSelection=clearSelection,
**self._options
)
finally:
self.item().setSliderDown(batchMode)
def findMirrorTable(path):
"""
Get the mirror table object for this item.
:rtype: mutils.MirrorTable or None
"""
mirrorTable = None
mirrorTablePaths = list(studiolibrary.walkup(
path,
match=lambda path: path.endswith(".mirror"),
depth=10,
)
)
if mirrorTablePaths:
mirrorTablePath = mirrorTablePaths[0]
path = os.path.join(mirrorTablePath, "mirrortable.json")
if path:
mirrorTable = mutils.MirrorTable.fromPath(path)
return mirrorTable
class PoseItem(baseitem.BaseItem):
NAME = "Pose"
EXTENSION = ".pose"
ICON_PATH = os.path.join(os.path.dirname(__file__), "icons", "pose.png")
LOAD_WIDGET_CLASS = PoseLoadWidget
TRANSFER_CLASS = mutils.Pose
TRANSFER_BASENAME = "pose.json"
def __init__(self, *args, **kwargs):
"""
Create a new instance of the pose item from the given path.
:type path: str
:type args: list
:type kwargs: dict
"""
super(PoseItem, self).__init__(*args, **kwargs)
self.setSliderEnabled(True)
def mirrorTableSearchAndReplace(self):
"""
Get the values for search and replace from the mirror table.
:rtype: (str, str)
"""
mirrorTable = findMirrorTable(self.path())
return mirrorTable.leftSide(), mirrorTable.rightSide()
def switchSearchAndReplace(self):
"""
Switch the values of the search and replace field.
:rtype: (str, str)
"""
values = self.currentLoadValue("searchAndReplace")
return values[1], values[0]
def clearSearchAndReplace(self):
"""
Clear the search and replace field.
:rtype: (str, str)
"""
return '', ''
def doubleClicked(self):
"""Triggered when the user double-click the item."""
pass
def loadSchema(self):
"""
Get schema used to load the pose item.
:rtype: list[dict]
"""
schema = [
{
"name": "optionsGroup",
"title": "Options",
"type": "group",
"order": 2,
},
{
"name": "key",
"type": "bool",
"inline": True,
"default": False,
"persistent": True,
},
{
"name": "mirror",
"type": "bool",
"inline": True,
"default": False,
"persistent": True,
},
{
"name": "additive",
"type": "bool",
"inline": True,
"default": False,
"persistent": True,
},
{
"name": "searchAndReplaceEnabled",
"title": "Search and Replace",
"type": "bool",
"inline": True,
"default": False,
"persistent": True,
},
{
"name": "searchAndReplace",
"title": "",
"type": "stringDouble",
"default": ("", ""),
"placeholder": ("search", "replace"),
"persistent": True,
"actions": [
{
"name": "Switch",
"callback": self.switchSearchAndReplace,
},
{
"name": "Clear",
"callback": self.clearSearchAndReplace,
},
{
"name": "From Mirror Table",
"enabled": bool(findMirrorTable(self.path())),
"callback": self.mirrorTableSearchAndReplace,
},
]
},
]
schema.extend(super(PoseItem, self).loadSchema())
return schema
def mirrorTable(self):
return findMirrorTable(self.path())
def loadValidator(self, **values):
"""
Using the validator to change the state of the mirror option.
:type values: dict
:rtype: list[dict]
"""
# Mirror check box
mirrorTip = "Cannot find a mirror table!"
mirrorTable = findMirrorTable(self.path())
if mirrorTable:
mirrorTip = "Using mirror table: %s" % mirrorTable.path()
fields = [
{
"name": "mirror",
"toolTip": mirrorTip,
"enabled": mirrorTable is not None,
},
{
"name": "searchAndReplace",
"visible": values.get("searchAndReplaceEnabled")
},
]
fields.extend(super(PoseItem, self).loadValidator(**values))
return fields
def save(self, objects, **kwargs):
"""
Save all the given object data to the item path on disc.
:type objects: list[str]
:type kwargs: dict
"""
super(PoseItem, self).save(**kwargs)
# Save the pose to the temp location
mutils.savePose(
self.transferPath(),
objects,
metadata={"description": kwargs.get("comment", "")}
)

View File

@@ -0,0 +1,65 @@
# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
#
# This library is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. This library is distributed in the
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
try:
import mutils
except ImportError as error:
print(error)
from studiolibrarymaya import baseitem
def save(path, *args, **kwargs):
"""Convenience function for saving a SetsItem."""
SetsItem(path).safeSave(*args, **kwargs)
def load(path, *args, **kwargs):
"""Convenience function for loading a SetsItem."""
SetsItem(path).load(*args, **kwargs)
class SetsItem(baseitem.BaseItem):
NAME = "Selection Set"
EXTENSION = ".set"
ICON_PATH = os.path.join(os.path.dirname(__file__), "icons", "selectionSet.png")
TRANSFER_CLASS = mutils.SelectionSet
TRANSFER_BASENAME = "set.json"
def loadFromCurrentValues(self):
"""Load the selection set using the settings for this item."""
self.load(namespaces=self.namespaces())
def load(self, namespaces=None):
"""
:type namespaces: list[str] | None
"""
self.selectContent(namespaces=namespaces)
def save(self, objects, **kwargs):
"""
Save all the given object data to the item path on disc.
:type objects: list[str]
:type kwargs: dict
"""
super(SetsItem, self).save(**kwargs)
# Save the selection set to the given path
mutils.saveSelectionSet(
self.path() + "/set.json",
objects,
metadata={"description": kwargs.get("comment", "")}
)

View File

@@ -0,0 +1,167 @@
# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
#
# This library is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. This library is distributed in the
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
import logging
from functools import partial
from studiovendor.Qt import QtGui
from studiovendor.Qt import QtWidgets
import studiolibrary
from studiolibrarymaya import setsitem
logger = logging.getLogger(__name__)
DIRNAME = os.path.dirname(__file__)
ARROW_ICON_PATH = os.path.join(DIRNAME, "icons", "arrow.png")
def selectContentAction(item, parent=None):
"""
:param item: mayabaseitem.MayaBaseItem
:param parent: QtWidgets.QMenu
"""
arrowIcon = QtGui.QIcon(ARROW_ICON_PATH)
action = QtWidgets.QAction(arrowIcon, "Select content", parent)
action.triggered.connect(item.selectContent)
return action
def showSetsMenu(path, **kwargs):
"""
Show the frame range menu at the current cursor position.
:type path: str
:rtype: QtWidgets.QAction
"""
menu = SetsMenu.fromPath(path, **kwargs)
position = QtGui.QCursor().pos()
action = menu.exec_(position)
return action
class SetsMenu(QtWidgets.QMenu):
@classmethod
def fromPath(cls, path, parent=None, libraryWindow=None, **kwargs):
"""
Return a new SetMenu instance from the given path.
:type path: str
:type parent: QtWidgets.QMenu or None
:type libraryWindow: studiolibrary.LibraryWindow or None
:type kwargs: dict
:rtype: QtWidgets.QAction
"""
item = setsitem.SetsItem(path, libraryWindow=libraryWindow)
return cls(item, parent, enableSelectContent=False, **kwargs)
def __init__(
self,
item,
parent=None,
namespaces=None,
enableSelectContent=True,
):
"""
:type item: studiolibrarymaya.BaseItem
:type parent: QtWidgets.QMenu or None
:type namespaces: list[str] or None
:type enableSelectContent: bool
"""
parent = parent or item.libraryWindow()
QtWidgets.QMenu.__init__(self, "Selection Sets", parent)
icon = QtGui.QIcon(setsitem.SetsItem.ICON_PATH)
self.setIcon(icon)
self._item = item
self._namespaces = namespaces
self._enableSelectContent = enableSelectContent
self.reload()
def item(self):
"""
:rtype: mayabaseitem.MayaBaseItem
"""
return self._item
def namespaces(self):
"""
:rtype: list[str]
"""
return self._namespaces
def selectContent(self):
"""
:rtype: None
"""
self.item().selectContent(namespaces=self.namespaces())
def selectionSets(self):
"""
:rtype: list[setsitem.SetsItem]
"""
path = self.item().path()
paths = studiolibrary.walkup(
path,
match=lambda path: path.endswith(".set"),
depth=10,
)
items = []
paths = list(paths)
libraryWindow = self.item().libraryWindow()
for path in paths:
item = setsitem.SetsItem(path)
item.setLibraryWindow(libraryWindow)
items.append(item)
return items
def reload(self):
"""
:rtype: None
"""
self.clear()
if self._enableSelectContent:
action = selectContentAction(item=self.item(), parent=self)
self.addAction(action)
self.addSeparator()
selectionSets = self.selectionSets()
if selectionSets:
for selectionSet in selectionSets:
dirname = os.path.basename(os.path.dirname(selectionSet.path()))
basename = os.path.basename(selectionSet.path())
basename = basename.replace(selectionSet.EXTENSION, "")
nicename = dirname + ": " + basename
action = QtWidgets.QAction(nicename, self)
callback = partial(selectionSet.load, namespaces=self.namespaces())
action.triggered.connect(callback)
self.addAction(action)
else:
action = QtWidgets.QAction("No selection sets found!", self)
action.setEnabled(False)
self.addAction(action)