Skip to content

Commit 2781089

Browse files
authored
Merge pull request #46 from mottosso/fix45
Fix disconnect/connect with Undo
2 parents c112597 + 3d02f1c commit 2781089

File tree

1 file changed

+168
-46
lines changed

1 file changed

+168
-46
lines changed

cmdx.py

Lines changed: 168 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,5 @@
11
# -*- coding: utf-8 -*-
22

3-
# Hack for static typing analysis:
4-
# the test below only passes in the IDE (eg VsCode); Maya doesn't know or care about typing.
5-
MYPY = False
6-
if MYPY:
7-
from typing import *
8-
# fake-declare some Python2-only types to fix Pylance analyzer false positives (Pylance is Python3-only)
9-
basestring = unicode = str
10-
long = int
11-
buffer = bytearray
12-
file = object
13-
del MYPY
14-
153
import os
164
import sys
175
import json
@@ -28,7 +16,7 @@
2816
from maya.api import OpenMaya as om, OpenMayaAnim as oma, OpenMayaUI as omui
2917
from maya import OpenMaya as om1, OpenMayaMPx as ompx1, OpenMayaUI as omui1
3018

31-
__version__ = "0.4.9"
19+
__version__ = "0.4.10"
3220

3321
PY3 = sys.version_info[0] == 3
3422

@@ -73,6 +61,21 @@
7361
if not IGNORE_VERSION:
7462
assert __maya_version__ >= 2015, "Requires Maya 2015 or newer"
7563

64+
# Hack for static typing analysis
65+
#
66+
# the test below only passes in the IDE such as VSCode
67+
# Maya doesn't know or care about the `typing` library
68+
MYPY = False
69+
if MYPY:
70+
from typing import *
71+
# Fake-declare some Python2-only types to fix Pylance
72+
# analyzer false positives (Pylance is Python3-only)
73+
basestring = unicode = str
74+
long = int
75+
buffer = bytearray
76+
file = object
77+
del MYPY
78+
7679
self = sys.modules[__name__]
7780
self.installed = False
7881
log = logging.getLogger("cmdx")
@@ -282,7 +285,13 @@ def __call__(self, enum):
282285

283286

284287
def AngleUiUnit():
285-
"""Unlike other angle units, this can be modified by the user at run-time"""
288+
"""Dynamic angle UI unit
289+
290+
Unlike other angle units, this can be modified by the user at run-time
291+
hence it needs to be a function rather than a variable.
292+
293+
"""
294+
286295
return _Unit(om.MAngle, om.MAngle.uiUnit())
287296

288297

@@ -298,7 +307,13 @@ def AngleUiUnit():
298307

299308

300309
def DistanceUiUnit():
301-
"""Unlike other distance units, this can be modified by the user at run-time"""
310+
"""Dynamic distance UI unit
311+
312+
Unlike other distance units, this can be modified by the user at run-time
313+
hence it needs to be a function rather than a variable.
314+
315+
"""
316+
302317
return _Unit(om.MDistance, om.MDistance.uiUnit())
303318

304319

@@ -1005,7 +1020,11 @@ def dump(self, ignore_error=True, preserve_order=False):
10051020

10061021
def dumps(self, indent=4, sort_keys=True, preserve_order=False):
10071022
"""Return a JSON compatible dictionary of all attributes"""
1008-
return json.dumps(self.dump(preserve_order), indent=indent, sort_keys=sort_keys)
1023+
return json.dumps(
1024+
self.dump(preserve_order),
1025+
indent=indent,
1026+
sort_keys=sort_keys
1027+
)
10091028

10101029
def type(self):
10111030
"""Return type name
@@ -2730,14 +2749,17 @@ def typeClass(self):
27302749
k = om.MFnNumericAttribute(attr).numericType()
27312750
if k == om.MFnNumericData.kBoolean:
27322751
return Boolean
2733-
elif k in (om.MFnNumericData.kLong, om.MFnNumericData.kInt):
2752+
elif k in (om.MFnNumericData.kLong,
2753+
om.MFnNumericData.kInt):
27342754
return Long
27352755
elif k == om.MFnNumericData.kDouble:
27362756
return Double
27372757

2738-
elif k in (om.MFn.kDoubleAngleAttribute, om.MFn.kFloatAngleAttribute):
2758+
elif k in (om.MFn.kDoubleAngleAttribute,
2759+
om.MFn.kFloatAngleAttribute):
27392760
return Angle
2740-
elif k in (om.MFn.kDoubleLinearAttribute, om.MFn.kFloatLinearAttribute):
2761+
elif k in (om.MFn.kDoubleLinearAttribute,
2762+
om.MFn.kFloatLinearAttribute):
27412763
return Distance
27422764
elif k == om.MFn.kTimeAttribute:
27432765
return Time
@@ -2762,7 +2784,8 @@ def typeClass(self):
27622784

27632785
elif k == om.MFn.kCompoundAttribute:
27642786
return Compound
2765-
elif k in (om.Mfn.kMatrixAttribute, om.MFn.kFloatMatrixAttribute):
2787+
elif k in (om.Mfn.kMatrixAttribute,
2788+
om.MFn.kFloatMatrixAttribute):
27662789
return Matrix
27672790
elif k == om.MFn.kMessageAttribute:
27682791
return Message
@@ -4237,7 +4260,10 @@ def addAttr(self, node, plug):
42374260
def deleteAttr(self, plug):
42384261
node = plug.node()
42394262
node.clear()
4240-
return self._modifier.removeAttribute(node._mobject, plug._mplug.attribute())
4263+
4264+
return self._modifier.removeAttribute(
4265+
node._mobject, plug._mplug.attribute()
4266+
)
42414267

42424268
@record_history
42434269
def setAttr(self, plug, value):
@@ -4254,6 +4280,32 @@ def resetAttr(self, plug):
42544280

42554281
@record_history
42564282
def connect(self, src, dst, force=True):
4283+
"""Connect one attribute to another, with undo
4284+
4285+
Examples:
4286+
>>> tm = createNode("transform")
4287+
>>> with DagModifier() as mod:
4288+
... mod.connect(tm["rx"], tm["ry"])
4289+
...
4290+
>>> tx = createNode("animCurveTL")
4291+
4292+
# Connect without undo
4293+
>>> tm["tx"] << tx["output"]
4294+
>>> tm["tx"].connection() is tx
4295+
True
4296+
4297+
# Automatically disconnects any connected attribute
4298+
>>> with DagModifier() as mod:
4299+
... mod.connect(tm["sx"], tm["tx"])
4300+
...
4301+
>>> tm["tx"].connection() is tm
4302+
True
4303+
>>> cmds.undo() if ENABLE_UNDO else DoNothing
4304+
>>> tm["tx"].connection() is tx
4305+
True
4306+
4307+
"""
4308+
42574309
if isinstance(src, Plug):
42584310
src = src._mplug
42594311

@@ -4262,21 +4314,23 @@ def connect(self, src, dst, force=True):
42624314

42634315
if force:
42644316
# Disconnect any plug connected to `other`
4317+
disconnected = False
4318+
42654319
for plug in dst.connectedTo(True, False):
4266-
self.disconnect(plug, dst)
4320+
self.disconnect(a=plug, b=dst)
4321+
disconnected = True
4322+
4323+
if disconnected:
4324+
# Connecting after disconnecting breaks undo,
4325+
# unless we do it first.
4326+
self.doIt()
42674327

42684328
self._modifier.connect(src, dst)
42694329

42704330
@record_history
42714331
def disconnect(self, a, b=None, source=True, destination=True):
42724332
"""Disconnect `a` from `b`
42734333
4274-
Arguments:
4275-
a (Plug): Starting point of a connection
4276-
b (Plug, optional): End point of a connection, defaults to all
4277-
source (bool, optional): Disconnect b, if it is a source
4278-
destination (bool, optional): Disconnect b, if it is a destination
4279-
42804334
Normally, Maya only performs a disconnect if the
42814335
connection is incoming. Bidirectional
42824336
@@ -4292,6 +4346,63 @@ def disconnect(self, a, b=None, source=True, destination=True):
42924346
| nodeA o---->o nodeB |
42934347
|__________| |_________|
42944348
4349+
Examples:
4350+
>>> tm1 = createNode("transform", name="tm1")
4351+
>>> tm2 = createNode("transform", name="tm2")
4352+
>>> tm3 = createNode("transform", name="tm3")
4353+
4354+
# Disconnects of unconnected attributes are ignored
4355+
>>> with DagModifier() as mod:
4356+
... _ = mod.disconnect(tm1["rx"], tm1["ry"])
4357+
... _ = mod.disconnect(tm1["rx"], tm1["rz"])
4358+
... _ = mod.disconnect(tm1["rx"], tm1["sy"])
4359+
... _ = mod.disconnect(tm1["rx"], tm1["ty"])
4360+
...
4361+
4362+
# This doesn't throw an error
4363+
>>> cmds.undo() if ENABLE_UNDO else DoNothing
4364+
4365+
# Disconnect either source or destination, only
4366+
>>> a, b = tm1["tx"], tm2["ty"]
4367+
>>> a << b
4368+
>>> a.connection() is tm2
4369+
True
4370+
4371+
# b is a source, not a destination, so this does nothing
4372+
>>> a.disconnect(b, source=False, destination=True)
4373+
>>> a.connection() is tm2
4374+
True
4375+
4376+
# This on the other hand..
4377+
>>> a.disconnect(b, source=True, destination=False)
4378+
>>> a.connection() is tm2
4379+
False
4380+
>>> a.connection() is None
4381+
True
4382+
4383+
# Default is to disconnect both
4384+
>>> tm1["tx"] >> tm2["tx"]
4385+
>>> tm2["tx"] >> tm3["tx"]
4386+
>>> tm1["tx"].connection() is tm2
4387+
True
4388+
>>> tm3["tx"].connection() is tm2
4389+
True
4390+
>>> tm2["tx"].disconnect()
4391+
>>> tm1["tx"].connection() is tm2
4392+
False
4393+
>>> tm3["tx"].connection() is tm2
4394+
False
4395+
4396+
Arguments:
4397+
a (Plug): Starting point of a connection
4398+
b (Plug, optional): End point of a connection, defaults to all
4399+
source (bool, optional): Disconnect b, if it is a source
4400+
destination (bool, optional): Disconnect b, if it
4401+
is a destination
4402+
4403+
Returns:
4404+
count (int): Number of disconnected attributes
4405+
42954406
"""
42964407

42974408
if isinstance(a, Plug):
@@ -4300,22 +4411,29 @@ def disconnect(self, a, b=None, source=True, destination=True):
43004411
if isinstance(b, Plug):
43014412
b = b._mplug
43024413

4303-
if b is None:
4304-
# Disconnect any plug connected to `other`
4305-
if source:
4306-
for plug in a.connectedTo(True, False):
4307-
self._modifier.disconnect(plug, a)
4414+
count = 0
4415+
incoming = (True, False)
4416+
outgoing = (False, True)
43084417

4309-
if destination:
4310-
for plug in a.connectedTo(False, True):
4311-
self._modifier.disconnect(a, plug)
4418+
if source:
4419+
for other in a.connectedTo(*incoming):
43124420

4313-
else:
4314-
if source:
4315-
self._modifier.disconnect(a, b)
4421+
# Limit disconnects to the attribute provided
4422+
if b is not None and other != b:
4423+
continue
4424+
4425+
self._modifier.disconnect(other, a)
4426+
count += 1
4427+
4428+
if destination:
4429+
for other in a.connectedTo(*outgoing):
4430+
if b is not None and other != b:
4431+
continue
4432+
4433+
self._modifier.disconnect(a, other)
4434+
count += 1
43164435

4317-
if destination:
4318-
self._modifier.disconnect(b, a)
4436+
return count
43194437

43204438
if ENABLE_PEP8:
43214439
do_it = doIt
@@ -4479,14 +4597,15 @@ def __enter__(self):
44794597
return self
44804598
else:
44814599
cmds.error(
4482-
"'%s' does not support context manager functionality for Maya 2017 "
4483-
"and below" % self.__class__.__name__
4600+
"'%s' does not support context manager functionality "
4601+
"for Maya 2017 and below" % self.__class__.__name__
44844602
)
44854603

44864604
def __exit__(self, exc_type, exc_value, tb):
44874605
if self._previousContext:
44884606
self._previousContext.makeCurrent()
44894607

4608+
44904609
# Alias
44914610
Context = DGContext
44924611

@@ -5189,7 +5308,10 @@ def __init__(self, label, **kwargs):
51895308
kwargs.pop("name", None)
51905309
kwargs.pop("fields", None)
51915310
kwargs.pop("label", None)
5192-
super(Divider, self).__init__(label, fields=(label,), label=" ", **kwargs)
5311+
5312+
super(Divider, self).__init__(
5313+
label, fields=(label,), label=" ", **kwargs
5314+
)
51935315

51945316

51955317
class String(_AbstractAttribute):
@@ -5333,7 +5455,7 @@ def default(self, cls=None):
53335455

53345456
class Compound(_AbstractAttribute):
53355457
"""One or more nested attributes
5336-
5458+
53375459
Examples:
53385460
>>> _ = cmds.file(new=True, force=True)
53395461
>>> node = createNode("transform")
@@ -5346,7 +5468,7 @@ class Compound(_AbstractAttribute):
53465468
1.0
53475469
>>> node["compoundAttr"]["child2"].read()
53485470
5.0
5349-
5471+
53505472
# Also supports nested attributes
53515473
>>> node.addAttr(
53525474
... Compound("parent", children=[

0 commit comments

Comments
 (0)