1#
2# Copyright 2017 Pixar
3#
4# Licensed under the Apache License, Version 2.0 (the "Apache License")
5# with the following modification; you may not use this file except in
6# compliance with the Apache License and the following modification to it:
7# Section 6. Trademarks. is deleted and replaced with:
8#
9# 6. Trademarks. This License does not grant permission to use the trade
10#    names, trademarks, service marks, or product names of the Licensor
11#    and its affiliates, except as required to comply with Section 4(c) of
12#    the License and to reproduce the content of the NOTICE file.
13#
14# You may obtain a copy of the Apache License at
15#
16#     http://www.apache.org/licenses/LICENSE-2.0
17#
18# Unless required by applicable law or agreed to in writing, software
19# distributed under the Apache License with the above modification is
20# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21# KIND, either express or implied. See the Apache License for the specific
22# language governing permissions and limitations under the Apache License.
23#
24
25from collections import OrderedDict
26
27from pxr import Sdf, Gf
28from .qt import QtCore
29
30from .customAttributes import (ComputedPropertyNames, BoundingBoxAttribute,
31                               LocalToWorldXformAttribute, ComputedPropertyFactory)
32
33
34# Indicates that all instances of a prim are selected.
35ALL_INSTANCES = -1
36
37
38class Blocker:
39    """Object which can be used to temporarily block the execution of a body of
40    code. This object is a context manager, and enters a 'blocked' state when
41    used in a 'with' statement. The 'blocked()' method can be used to find if
42    the Blocker is in this 'blocked' state.
43    """
44
45    def __init__(self, exitCallback=lambda: None):
46
47        # A count is used rather than a 'blocked' flag to allow for nested
48        # blocking.
49        self._count = 0
50
51        # Fired when the Blocker's context is exited.
52        self._exitCallback = exitCallback
53
54    def __enter__(self):
55        """Enter the 'blocked' state until the context is exited."""
56
57        self._count += 1
58
59    def __exit__(self, *args):
60        """Exit the 'blocked' state."""
61
62        self._count -= 1
63
64        if not self.blocked():
65            self._exitCallback()
66
67    def blocked(self):
68        """Returns True if in the 'blocked' state, and False otherwise."""
69
70        return self._count > 0
71
72
73class _PrimSelection(object):
74    """This class keeps track of the core data for prim selection: paths and
75    instances. The methods here can be called in any order required without
76    corrupting the path selection state.
77    """
78
79    def __init__(self):
80
81        # The order of paths selected needs to be maintained so we can track the
82        # focus path. An OrderedDict is more efficient than a list here since it
83        # supports efficient removal of arbitrary paths but still maintains the
84        # path order. Sdf.Path objects are the keys in the OrderedDict, and a
85        # set of selected instances are the values. If all instances are
86        # selected, None is the value rather than a set.
87        self._selection = OrderedDict()
88
89        self._added = set()
90        self._removed = set()
91
92    def _clearPrimPath(self, path):
93        """Clears a path from the selection and updates the diff."""
94
95        if path in self._added:
96            # Path was added in this diff, but we are removing it again. Since
97            # there is no net change, it shouldn't be in _added or _removed.
98            self._added.discard(path)
99        else:
100            self._removed.add(path)
101
102        del self._selection[path]
103
104    def _discardInstance(self, path, instance):
105        """Discards an instance from the selection, then deletes the path from
106        the selection if it has no more instances.
107        """
108
109        instances = self._selection[path]
110        instances.discard(instance)
111        if len(instances) == 0:
112            # Last instance deselected, so path should be deselected.
113            self._clearPrimPath(path)
114
115    def _allInstancesSelected(self, path):
116        """Returns True if all instances of a specified path are selected and
117        False otherwise.
118        """
119
120        if path in self._selection:
121            return self._selection[path] == ALL_INSTANCES
122        else:
123            return False
124
125    def _noInstancesSelected(self, path):
126        """Returns True if all instances of a specified path are selected and
127        False otherwise.
128        """
129
130        return path not in self._selection
131
132    def clear(self):
133        """Clear the path selection."""
134
135        # _clearPrimPath modifies self._selection, so make a copy of keys
136        # before mutating it.
137        for path in list(self._selection.keys()):
138            self._clearPrimPath(path)
139
140    def removeMatchingPaths(self, matches):
141        """Remove any paths that pass the given predicate"""
142
143        # _clearPrimPath modifies self._selection, so make a copy of keys
144        # before mutating it.
145        for path in list(self._selection.keys()):
146            if matches(path):
147                self._clearPrimPath(path)
148
149    def addPrimPath(self, path, instance=ALL_INSTANCES):
150        """Add a path to the selection. If an instance is given, then only add
151        that instance. If all instances are selected when this happens then the
152        single instance will become the only selected one.
153        """
154
155        # If the path is not already in the selection, update the diff.
156        if path not in self._selection:
157            if path in self._removed:
158                # Path was removed in this diff, but is back now. Since there is
159                # no net change, it shouldn't be in _added or _removed.
160                self._removed.discard(path)
161            else:
162                self._added.add(path)
163
164        if instance == ALL_INSTANCES:
165            # Trying to add all instances, make sure all instances are selected.
166            self._selection[path] = ALL_INSTANCES
167        else:
168            # Trying to add a single instance.
169            if self._allInstancesSelected(path) or self._noInstancesSelected(path):
170                # Either all instances selected or none selected. Create an
171                # empty set of instances then add the target instance.
172                self._selection[path] = set()
173            self._selection[path].add(instance)
174
175    def removePrimPath(self, path, instance=ALL_INSTANCES):
176        """Remove a path from the selection. If an instance is given, then only
177        remove that instance. If all instances are selected when this happens,
178        deselect all instances. If the target does not exist in the selection,
179        do nothing.
180        """
181
182        if path in self._selection:
183            if instance == ALL_INSTANCES or self._allInstancesSelected(path):
184                # Want to deselect all instances or all instances are selected.
185                # Either way, deselect all.
186                self._clearPrimPath(path)
187            else:
188                # Some instances selected and want to deselect one of them.
189                self._discardInstance(path, instance)
190
191    def togglePrimPath(self, path, instance=ALL_INSTANCES):
192        """Toggle the selection of a path. If an instance is given, only toggle
193        that instance's selection.
194        """
195
196        if path not in self._selection:
197            self.addPrimPath(path, instance)
198            return
199
200        # Path is at least partially selected.
201        if instance == ALL_INSTANCES:
202            # Trying to toggle all instances.
203            if self._allInstancesSelected(path):
204                # All instances are already selected, so deselect the path.
205                self._clearPrimPath(path)
206            else:
207                # Only some instances are selected, select all all of them.
208                self._selection[path] = ALL_INSTANCES
209        else:
210            # Trying to toggle a single instance.
211            if self._allInstancesSelected(path):
212                # Currently all instances are selected. Switch selection to
213                # only the new instance.
214                self._selection[path] = set([instance])
215            else:
216                # Some instances already selected. Toggle the new instance
217                # in the selection.
218                instances = self._selection[path]
219                if instance in instances:
220                    self._discardInstance(path, instance)
221                else:
222                    instances.add(instance)
223
224    def getPrimPaths(self):
225        """Get a list of paths that are at least partially selected."""
226
227        return list(self._selection.keys())
228
229    def getPrimPathInstances(self):
230        """Get the full selection of paths and their corresponding selected
231        instances.
232        """
233
234        return OrderedDict(
235            (path, set(instances)) if isinstance(instances, set) else (path, instances)
236            for path, instances in self._selection.items())
237
238    def getDiff(self):
239        """Get the prims added to or removed from the selection since the last
240        time getDiff() was called.
241        """
242
243        diff = (self._added, self._removed)
244
245        self._added = set()
246        self._removed = set()
247
248        return diff
249
250
251class _PropSelection(object):
252    """This class keeps track of the state of property selection."""
253
254    def __init__(self):
255
256        self._selection = OrderedDict()
257
258    def clear(self):
259        """Clears the property selection."""
260
261        self._selection = OrderedDict()
262
263    def addPropPath(self, primPath, propName):
264        """Add a property to the selection."""
265
266        propTuple = (primPath, propName)
267
268        # If this property is already selected, remove it from the selection and
269        # re-add it so it becomes the focus property.
270        if propTuple in self._selection:
271            targets = self._selection[propTuple]
272            del self._selection[propTuple]
273            self._selection[propTuple] = targets
274        else:
275            self._selection[propTuple] = set()
276
277    def removePropPath(self, primPath, propName):
278        """Remove a property from the selection."""
279
280        propTuple = (primPath, propName)
281        if propTuple in self._selection:
282            del self._selection[propTuple]
283
284    def addTarget(self, primPath, propName, target):
285        """Add a target to the selection. Also add the target's property if it
286        is not already in the selection.
287        """
288
289        propTuple = (primPath, propName)
290
291        # If this property is already selected, remove it from the selection and
292        # re-add it so it becomes the focus property.
293        if propTuple in self._selection:
294            targets = self._selection[propTuple]
295            del self._selection[propTuple]
296            self._selection[propTuple] = targets
297        else:
298            targets = self._selection.setdefault(propTuple, set())
299
300        targets.add(target)
301
302    def removeTarget(self, primPath, propName, target):
303        """Remove a target from the selection. If the target or its property are
304        not already in the selection, nothing is changed.
305        """
306
307        propTuple = (primPath, propName)
308        if propTuple in self._selection:
309            self._selection[propTuple].discard(target)
310
311    def getPropPaths(self):
312        """Get the list of properties."""
313
314        return list(self._selection.keys())
315
316    def getTargets(self):
317        """Get a dictionary which maps selected properties to a set of their
318        selected targets or connections.
319        """
320
321        propTargets = OrderedDict()
322        for propTuple, targets in self._selection.items():
323            propTargets[propTuple] = set(targets)
324
325        return propTargets
326
327
328class SelectionDataModel(QtCore.QObject):
329    """Data model managing the current selection of prims and properties.
330    Please note that the owner of an instance of this class is
331    responsible for calling SelectionDataModel.removeUnpopulatedPrims() when
332    appropriate, lest methods like getPrims() return invalid prims."""
333
334    # Signals must be declared in the class, but each instance of the selection
335    # data model has its own unique signal instances.
336
337    # When emitted, includes two sets: one of newly selected prims, and one of
338    # newly deselected prims.
339    signalPrimSelectionChanged = QtCore.Signal(set, set)
340
341    signalPropSelectionChanged = QtCore.Signal()
342    signalComputedPropSelectionChanged = QtCore.Signal()
343
344    def __init__(self, rootDataModel, _computedPropFactory=None):
345        QtCore.QObject.__init__(self)
346
347        self._rootDataModel = rootDataModel
348
349        # _computedPropFactory may be passed explicitly for unit testing.
350        if _computedPropFactory is None:
351            self._computedPropFactory = ComputedPropertyFactory(
352                self._rootDataModel)
353        else:
354            self._computedPropFactory = _computedPropFactory
355
356        self.batchPrimChanges = Blocker(
357            exitCallback=self._primSelectionChanged)
358        self.batchPropChanges = Blocker(
359            exitCallback=self._propSelectionChanged)
360        self.batchComputedPropChanges = Blocker(
361            exitCallback=self._computedPropSelectionChanged)
362
363        self._pointSelection = Gf.Vec3f(0.0, 0.0, 0.0)
364
365        self._primSelection = _PrimSelection()
366        # The path selection should never be empty. If it ever is, we
367        # immediately add the root path before returning to user control (see
368        # _primSelectionChanged).
369        self._primSelection.addPrimPath(Sdf.Path.absoluteRootPath)
370        # Clear the prim selection diff so we don't get the absolute root in the
371        # first signal from signalPrimSelectionChanged.
372        self._primSelection.getDiff()
373
374        self._lcdPathSelection = [Sdf.Path.absoluteRootPath]
375
376        self._propSelection = _PropSelection()
377        self._computedPropSelection = _PropSelection()
378
379    ### Internal Operations ###
380    def _primSelectionChanged(self, emitSelChangedSignal=True):
381        """Should be called whenever a change is made to _primSelection. Some
382        final work is done then the prim selection changed signal is emitted.
383        """
384
385        # If updates are suppressed, do not emit a signal or do any
386        # pre-processing.
387        if self.batchPrimChanges.blocked():
388            return
389
390        # Make sure there is always at least one path selected.
391        if len(self.getPrimPaths()) == 0:
392            self._primSelection.addPrimPath(Sdf.Path.absoluteRootPath)
393
394        # Recalculate the LCD prims whenever the path selection changes.
395        paths = self._primSelection.getPrimPaths()
396        if len(paths) > 1:
397            paths = [path for path in paths
398                        if path != Sdf.Path.absoluteRootPath]
399        self._lcdPathSelection = Sdf.Path.RemoveDescendentPaths(paths)
400
401        # Finally, emit the changed signal.
402        added, removed = self._primSelection.getDiff()
403        if emitSelChangedSignal:
404            self.signalPrimSelectionChanged.emit(added, removed)
405
406    def _propSelectionChanged(self):
407        """Should be called whenever a change is made to _propSelection."""
408
409        # If updates are suppressed, do not emit a signal or do any
410        # pre-processing.
411        if self.batchPropChanges.blocked():
412            return
413
414        self.signalPropSelectionChanged.emit()
415
416    def _computedPropSelectionChanged(self):
417        """Should be called whenever a change is made to _computedPropSelection.
418        """
419
420        # If updates are suppressed, do not emit a signal or do any
421        # pre-processing.
422        if self.batchComputedPropChanges.blocked():
423            return
424
425        self.signalComputedPropSelectionChanged.emit()
426
427    def _ensureValidPrimPath(self, path):
428        """Validate an input path. If it is a string path, convert it to an
429        Sdf.Path object.
430        """
431
432        sdfPath = Sdf.Path(str(path))
433        if not sdfPath.IsAbsoluteRootOrPrimPath():
434            raise ValueError("Path must be a prim path, got: {}".format(
435                repr(sdfPath)))
436        return sdfPath
437
438    def _validateInstanceIndexParameter(self, instance):
439        """Validate an instance used as a parameter. This can be any positive
440        int or ALL_INSTANCES."""
441
442        validIndex = False
443        if isinstance(instance, int):
444            if instance >= 0 or instance == ALL_INSTANCES:
445                validIndex = True
446
447        if not validIndex:
448            raise ValueError(
449                "Instance must be a positive int or ALL_INSTANCES"
450                ", got: {}".format(repr(instance)))
451
452    def _ensureValidPropPath(self, prop):
453        """Validate a property."""
454
455        sdfPath = Sdf.Path(str(prop))
456        if not sdfPath.IsPropertyPath():
457            raise ValueError("Path must be a property path, got: {}".format(
458                repr(sdfPath)))
459        return sdfPath
460
461    def _ensureValidTargetPath(self, target):
462        """Validate a property target or connection."""
463
464        return Sdf.Path(str(target))
465
466    def _getPropFromPath(self, path):
467        """Get a Usd property object from a property path."""
468
469        prim = self._rootDataModel.stage.GetPrimAtPath(path.GetPrimPath())
470        return prim.GetProperty(path.name)
471
472    def _getTargetFromPath(self, path):
473        """Get the Usd object from a target path. It can be either a Usd prim or
474        Usd property.
475        """
476
477        if path.IsPropertyPath():
478            return self._getPropFromPath(path)
479        else:
480            return self._rootDataModel.stage.GetPrimAtPath(path)
481
482    def _requireNotBatchingPrims(self):
483        """Raise an error if we are currently batching prim selection changes.
484        We don't want to allow reading prim selection state in the middle of a
485        batch.
486        """
487
488        if self.batchPrimChanges.blocked():
489            raise RuntimeError(
490                "Cannot get prim selection state while batching changes.")
491
492    def _requireNotBatchingProps(self):
493        """Raise an error if we are currently batching prop selection changes.
494        We don't want to allow reading prop selection state in the middle of a
495        batch.
496        """
497
498        if self.batchPropChanges.blocked():
499            raise RuntimeError(
500                "Cannot get property selection state while batching changes.")
501
502    def _getComputedPropFromPath(self, primPath, propName):
503        """Get a CustomAttribute object from a prim path and property name.
504        Raise an error if the property name does not match any known
505        CustomAttribute.
506        """
507
508        prim = self._rootDataModel.stage.GetPrimAtPath(primPath)
509        return self._computedPropFactory.getComputedProperty(prim, propName)
510
511    def _requireNotBatchingComputedProps(self):
512        """Raise an error if we are currently batching prop selection changes.
513        We don't want to allow reading prop selection state in the middle of a
514        batch.
515        """
516
517        if self.batchComputedPropChanges.blocked():
518            raise RuntimeError("Cannot get computed property selection state "
519                "while batching changes.")
520
521    def _buildPropPath(self, primPath, propName):
522        """Build a new property path from a prim path and a property name."""
523
524        return Sdf.Path(str(primPath) + "." + propName)
525
526    def _validateComputedPropName(self, propName):
527        """Validate a computed property name."""
528
529        if propName not in ComputedPropertyNames:
530            raise ValueError("Invalid computed property name: {}".format(
531                repr(propName)))
532
533    def _switchProps(self, fromPrimPath, toPrimPath):
534        """Switch all selected properties from one prim to another. Only do this
535        if all properties currently belong to the "from" prim.
536        """
537
538        propTargets = self.getPropTargetPaths()
539        computedProps = self.getComputedPropPaths()
540
541        # Check that all properties belong to the "from" prim.
542        for propPath in propTargets:
543            if propPath.GetPrimPath() != fromPrimPath:
544                return
545        for propPrimPath, propName in computedProps:
546            if propPrimPath != fromPrimPath:
547                return
548
549        # Switch all properties to the "to" prim.
550        with self.batchPropChanges:
551
552            self.clearProps()
553
554            # Root prim cannot have non-computed properties. The property paths
555            # in this case are invalid.
556            if str(toPrimPath) != "/":
557                for propPath, targets in propTargets.items():
558                    newPropPath = self._buildPropPath(toPrimPath, propPath.name)
559                    self.addPropPath(newPropPath)
560                    for target in targets:
561                        self.addPropTargetPath(newPropPath, target)
562
563        with self.batchComputedPropChanges:
564
565            self.clearComputedProps()
566
567            for primPath, propName in computedProps:
568                self.addComputedPropPath(toPrimPath, propName)
569
570    ### General Operations ###
571
572    def clear(self):
573        """Clear all selections."""
574        self.clearPoint()
575        self.clearPrims()
576        self.clearProps()
577
578    def clearPoint(self):
579        self.setPoint(Gf.Vec3f(0.0, 0.0, 0.0))
580
581    def setPoint(self, point):
582        self._pointSelection = point
583
584    def getPoint(self):
585        return self._pointSelection
586
587    ### Prim Path Operations ###
588
589    def clearPrims(self):
590        """Clear the prim selection (same as path selection)."""
591
592        self._primSelection.clear()
593        self._primSelectionChanged()
594
595    def addPrimPath(self, path, instance=ALL_INSTANCES):
596        """Add a path to the path selection. If an instance is given, only add
597        that instance.
598        """
599        path = self._ensureValidPrimPath(path)
600        self._validateInstanceIndexParameter(instance)
601
602        self._primSelection.addPrimPath(path, instance)
603        self._primSelectionChanged()
604
605    def removePrimPath(self, path, instance=ALL_INSTANCES):
606        """Remove a path from the path selection. If an instance is given, only
607        remove that instance. If the target does not exist in the selection, do
608        nothing.
609        """
610
611        path = self._ensureValidPrimPath(path)
612        self._validateInstanceIndexParameter(instance)
613
614        self._primSelection.removePrimPath(path, instance)
615        self._primSelectionChanged()
616
617    def togglePrimPath(self, path, instance=ALL_INSTANCES):
618        """Toggle a path in the path selection. If an instance is given, only
619        that instance is toggled.
620        """
621
622        path = self._ensureValidPrimPath(path)
623        self._validateInstanceIndexParameter(instance)
624
625        self._primSelection.togglePrimPath(path, instance)
626        self._primSelectionChanged()
627
628    def setPrimPath(self, path, instance=ALL_INSTANCES):
629        """Clear the prim selection then add a single prim path back to the
630        selection. If an instance is given, only add that instance.
631        """
632        with self.batchPrimChanges:
633            self.clearPrims()
634            self.addPrimPath(path, instance)
635
636    def getFocusPrimPath(self):
637        """Get the path currently in focus."""
638
639        self._requireNotBatchingPrims()
640        return self._primSelection.getPrimPaths()[0]
641
642    def getPrimPaths(self):
643        """Get a list of all selected paths."""
644
645        self._requireNotBatchingPrims()
646        return self._primSelection.getPrimPaths()
647
648    def getLCDPaths(self):
649        """Get a list of paths from the selection who do not have an ancestor
650        that is also in the selection. The "Least Common Denominator" paths.
651        """
652
653        self._requireNotBatchingPrims()
654        return list(self._lcdPathSelection)
655
656    def getPrimPathInstances(self):
657        """Get a dictionary which maps each selected prim to a set of its
658        selected instances. If all of a path's instances are selected, the value
659        is ALL_INSTANCES rather than a set.
660        """
661
662        self._requireNotBatchingPrims()
663        return self._primSelection.getPrimPathInstances()
664
665    def switchToPrimPath(self, path, instance=ALL_INSTANCES):
666        """Select only the given prim path. If only a single prim was selected
667        before and all selected properties belong to this prim, select the
668        corresponding properties on the new prim instead. If an instance is
669        given, only select that instance.
670        """
671
672        path = self._ensureValidPrimPath(path)
673
674        oldPrimPaths = self.getPrimPaths()
675
676        with self.batchPrimChanges:
677            self.clearPrims()
678            self.addPrimPath(path, instance)
679
680        if len(oldPrimPaths) == 1:
681            self._switchProps(oldPrimPaths[0], path)
682
683    ### Prim Operations ###
684    # These are all convenience methods which just call their respective path
685    # operations.
686
687    def addPrim(self, prim, instance=ALL_INSTANCES):
688        """Add a prim's path to the path selection. If an instance is given,
689        only add that instance.
690        """
691
692        self.addPrimPath(prim.GetPath(), instance)
693
694    def removePrim(self, prim, instance=ALL_INSTANCES):
695        """Remove a prim from the prim selection. If an instance is given, only
696        remove that instance. If the target does not exist in the selection, do
697        nothing.
698        """
699
700        self.removePrimPath(prim.GetPath(), instance)
701
702    def togglePrim(self, prim, instance=ALL_INSTANCES):
703        """Toggle a prim's path in the path selection. If an instance is given,
704        only that instance is toggled.
705        """
706
707        self.togglePrimPath(prim.GetPath(), instance)
708
709    def setPrim(self, prim, instance=ALL_INSTANCES):
710        """Clear the prim selection then add a single prim back to the
711        selection. If an instance is given, only add that instance.
712        """
713
714        self.setPrimPath(prim.GetPath(), instance)
715
716    def getFocusPrim(self):
717        """Get the prim whose path is currently in focus."""
718
719        return self._rootDataModel.stage.GetPrimAtPath(self.getFocusPrimPath())
720
721    def getPrims(self):
722        """Get a list of all prims whose paths are selected."""
723
724        return [self._rootDataModel.stage.GetPrimAtPath(path)
725            for path in self.getPrimPaths()]
726
727    def getLCDPrims(self):
728        """Get a list of prims whose paths are both selected and do not have an
729        ancestor that is also in the selection. The "Least Common Denominator"
730        prims.
731        """
732
733        return [self._rootDataModel.stage.GetPrimAtPath(path)
734            for path in self.getLCDPaths()]
735
736    def getPrimInstances(self):
737        """Get a dictionary which maps each prim whose path is selected to a set
738        of its selected instances. If all of a path's instances are selected,
739        the value is ALL_INSTANCES rather than a set.
740        """
741
742        return OrderedDict(
743            (self._rootDataModel.stage.GetPrimAtPath(path), instance)
744            for path, instance in self.getPrimPathInstances().items())
745
746    def switchToPrim(self, prim, instance=ALL_INSTANCES):
747        """Select only the given prim. If only a single prim was selected before
748        and all selected properties belong to this prim, select the
749        corresponding properties on the new prim instead.
750        """
751
752        self.switchToPrimPath(prim.GetPath(), instance)
753
754    ### Prim Group Removal Operations ###
755    # Convenience methods for removing groups of prims
756
757    def removeInactivePrims(self):
758        """Remove all inactive prims"""
759        for prim in self.getPrims():
760            if not prim.IsActive():
761                self.removePrim(prim)
762
763    def removePrototypePrims(self):
764        """Remove all prototype prims"""
765        for prim in self.getPrims():
766            if prim.IsPrototype() or prim.IsInPrototype():
767                self.removePrim(prim)
768
769    def removeAbstractPrims(self):
770        """Remove all abstract prims"""
771        for prim in self.getPrims():
772            if prim.IsAbstract():
773                self.removePrim(prim)
774
775    def removeUndefinedPrims(self):
776        """Remove all undefined prims"""
777        for prim in self.getPrims():
778            if not prim.IsDefined():
779                self.removePrim(prim)
780
781    def removeUnpopulatedPrims(self):
782        """Remove all prim paths whose corresponding prims do not currently
783        exist on the stage.  It is the application's responsibility to
784        call this method while it is processing changes to the stage,
785        *before* querying this object for selections.  Because this is a
786        synchronization operation rather than an expression of GUI state
787        change, it does *not* perform any notifications/signals, which could
788        cause reentrant application change processing."""
789        stage = self._rootDataModel.stage
790        self._primSelection.removeMatchingPaths(lambda path: not stage.GetPrimAtPath(path))
791        self._primSelectionChanged(emitSelChangedSignal=False)
792
793    ### Property Path Operations ###
794
795    def clearProps(self):
796        """Clear the property selection."""
797
798        self._propSelection.clear()
799        self._propSelectionChanged()
800
801    def addPropPath(self, path):
802        """Add a property to the selection."""
803
804        path = self._ensureValidPropPath(path)
805
806        primPath = path.GetPrimPath()
807        propName = path.name
808
809        self._propSelection.addPropPath(primPath, propName)
810        self._propSelectionChanged()
811
812    def removePropPath(self, path):
813        """Remove a property from the selection."""
814
815        path = self._ensureValidPropPath(path)
816
817        primPath = path.GetPrimPath()
818        propName = path.name
819
820        self._propSelection.removePropPath(primPath, propName)
821        self._propSelectionChanged()
822
823    def setPropPath(self, path):
824        """Clear the property selection, then add a single property path back to
825        the selection.
826        """
827
828        path = self._ensureValidPropPath(path)
829
830        with self.batchPropChanges:
831            self.clearProps()
832            self.addPropPath(path)
833
834    def addPropTargetPath(self, path, targetPath):
835        """Select a property's target or connection."""
836
837        path = self._ensureValidPropPath(path)
838        targetPath = self._ensureValidTargetPath(targetPath)
839
840        primPath = path.GetPrimPath()
841        propName = path.name
842
843        self._propSelection.addTarget(primPath, propName, targetPath)
844        self._propSelectionChanged()
845
846    def removePropTargetPath(self, path, targetPath):
847        """Deselect a property's target or connection."""
848
849        path = self._ensureValidPropPath(path)
850        targetPath = self._ensureValidTargetPath(targetPath)
851
852        primPath = path.GetPrimPath()
853        propName = path.name
854
855        self._propSelection.removeTarget(primPath, propName, targetPath)
856        self._propSelectionChanged()
857
858    def setPropTargetPath(self, path, targetPath):
859        """Clear the property selection, then add a single property path back to
860        the selection with a target.
861        """
862
863        with self.batchPropChanges:
864            self.clearProps()
865            self.addPropTargetPath(path, targetPath)
866
867    def getFocusPropPath(self):
868        """Get the focus property from the property selection."""
869
870        self._requireNotBatchingProps()
871
872        propPaths = [self._buildPropPath(*propTuple)
873            for propTuple in self._propSelection.getPropPaths()]
874        if len(propPaths) > 0:
875            return propPaths[-1]
876        else:
877            return None
878
879    def getPropPaths(self):
880        """Get a list of all selected properties."""
881
882        self._requireNotBatchingProps()
883
884        propPaths = [self._buildPropPath(*propTuple)
885            for propTuple in self._propSelection.getPropPaths()]
886        return propPaths
887
888    def getPropTargetPaths(self):
889        """Get a dictionary which maps selected properties to a set of their
890        selected targets or connections.
891        """
892
893        self._requireNotBatchingProps()
894
895        return OrderedDict((self._buildPropPath(*propTuple), set(targets))
896            for propTuple, targets in self._propSelection.getTargets().items())
897
898    ### Property Operations ###
899
900    def addProp(self, prop):
901        """Add a property to the selection."""
902
903        self.addPropPath(prop.GetPath())
904
905    def removeProp(self, prop):
906        """Remove a property from the selection."""
907
908        self.removePropPath(prop.GetPath())
909
910    def setProp(self, prop):
911        """Clear the property selection, then add a single property back to the
912        selection.
913        """
914
915        self.setPropPath(prop.GetPath())
916
917    def addPropTarget(self, prop, target):
918        """Select a property's target or connection."""
919
920        self.addPropTargetPath(prop.GetPath(), target.GetPath())
921
922    def removePropTarget(self, prop, target):
923        """Deselect a property's target or connection."""
924
925        self.removePropTargetPath(prop.GetPath(), target.GetPath())
926
927    def setPropTarget(self, prop, target):
928        """Clear the property selection, then add a single property back to the
929        selection with a target.
930        """
931
932        self.removePropTargetPath(prop.GetPath(), target.GetPath())
933
934    def getFocusProp(self):
935        """Get the focus property from the property selection."""
936
937        focusPath = self.getFocusPropPath()
938        if focusPath is None:
939            return None
940
941        return self._getPropFromPath(focusPath)
942
943    def getProps(self):
944        """Get a list of all selected properties."""
945
946        return [self._getPropFromPath(path)
947            for path in self.getPropPaths()]
948
949    def getPropTargets(self):
950        """Get a dictionary which maps selected properties to a set of their
951        selected targets or connections.
952        """
953
954        propTargets = OrderedDict()
955        for propPath, targetPaths in self.getPropTargetPaths().items():
956
957            prop = self._getPropFromPath(propPath)
958            targets = {self._getTargetFromPath(target)
959                for target in targetPaths}
960
961            propTargets[prop] = targets
962
963        return propTargets
964
965    ### Computed Property Path Operations ###
966
967    def clearComputedProps(self):
968        """Clear the computed property selection."""
969
970        self._computedPropSelection.clear()
971        self._computedPropSelectionChanged()
972
973    def addComputedPropPath(self, primPath, propName):
974        """Add a computed property to the selection."""
975
976        primPath = self._ensureValidPrimPath(primPath)
977        self._validateComputedPropName(propName)
978
979        self._computedPropSelection.addPropPath(primPath, propName)
980        self._computedPropSelectionChanged()
981
982    def removeComputedPropPath(self, primPath, propName):
983        """Remove a computed property from the selection."""
984
985        primPath = self._ensureValidPrimPath(primPath)
986        self._validateComputedPropName(propName)
987
988        self._computedPropSelection.removePropPath(primPath, propName)
989        self._computedPropSelectionChanged()
990
991    def setComputedPropPath(self, primPath, propName):
992        """Clear the computed property selection, then add a single computed
993        property path back to the selection.
994        """
995
996        primPath = self._ensureValidPrimPath(primPath)
997        self._validateComputedPropName(propName)
998
999        with self.batchComputedPropChanges:
1000            self.clearComputedProps()
1001            self.addComputedPropPath(primPath, propName)
1002
1003    def getFocusComputedPropPath(self):
1004        """Get the focus computed property from the property selection."""
1005
1006        self._requireNotBatchingComputedProps()
1007
1008        propPaths = self._computedPropSelection.getPropPaths()
1009        if len(propPaths) > 0:
1010            return propPaths[-1]
1011        else:
1012            return (None, None)
1013
1014    def getComputedPropPaths(self):
1015        """Get a list of all selected computed properties."""
1016
1017        self._requireNotBatchingComputedProps()
1018
1019        return self._computedPropSelection.getPropPaths()
1020
1021    ### Computed Property Operations ###
1022
1023    def addComputedProp(self, prop):
1024        """Add a computed property to the selection."""
1025
1026        self.addComputedPropPath(prop.GetPrimPath(), prop.GetName())
1027
1028    def removeComputedProp(self, prop):
1029        """Remove a computed property from the selection."""
1030
1031        self.removeComputedPropPath(prop.GetPrimPath(), prop.GetName())
1032
1033    def setComputedProp(self, prop):
1034        """Clear the computed property selection, then add a single computed
1035        property back to the selection.
1036        """
1037
1038        self.setComputedPropPath(prop.GetPrimPath(), prop.GetName())
1039
1040    def getFocusComputedProp(self):
1041        """Get the focus computed property from the property selection."""
1042
1043        focusPath = self.getFocusComputedPropPath()
1044        if focusPath == (None, None):
1045            return None
1046
1047        return self._getComputedPropFromPath(*focusPath)
1048
1049    def getComputedProps(self):
1050        """Get a list of all selected computed properties."""
1051
1052        return [self._getComputedPropFromPath(*path)
1053            for path in self.getComputedPropPaths()]
1054