1#
2# Copyright 2016 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#
24from __future__ import print_function
25
26from .qt import QtCore, QtGui, QtWidgets
27import os, time, sys, platform, math
28from pxr import Ar, Tf, Sdf, Kind, Usd, UsdGeom, UsdShade
29from .customAttributes import CustomAttribute
30from pxr.UsdUtils.constantsGroup import ConstantsGroup
31
32DEBUG_CLIPPING = "USDVIEWQ_DEBUG_CLIPPING"
33
34class ClearColors(ConstantsGroup):
35    """Names of available background colors."""
36    BLACK = "Black"
37    DARK_GREY = "Grey (Dark)"
38    LIGHT_GREY = "Grey (Light)"
39    WHITE = "White"
40
41class DefaultFontFamily(ConstantsGroup):
42    """Names of the default font family and monospace font family to be used
43    with usdview"""
44    FONT_FAMILY = "Roboto"
45    MONOSPACE_FONT_FAMILY = "Roboto Mono"
46
47class HighlightColors(ConstantsGroup):
48    """Names of available highlight colors for selected objects."""
49    WHITE = "White"
50    YELLOW = "Yellow"
51    CYAN = "Cyan"
52
53class UIBaseColors(ConstantsGroup):
54    RED = QtGui.QBrush(QtGui.QColor(230, 132, 131))
55    LIGHT_SKY_BLUE = QtGui.QBrush(QtGui.QColor(135, 206, 250))
56    DARK_YELLOW = QtGui.QBrush(QtGui.QColor(222, 158, 46))
57
58class UIPrimTypeColors(ConstantsGroup):
59    HAS_ARCS = UIBaseColors.DARK_YELLOW
60    NORMAL = QtGui.QBrush(QtGui.QColor(227, 227, 227))
61    INSTANCE = UIBaseColors.LIGHT_SKY_BLUE
62    PROTOTYPE = QtGui.QBrush(QtGui.QColor(118, 136, 217))
63
64class UIPrimTreeColors(ConstantsGroup):
65    SELECTED = QtGui.QBrush(QtGui.QColor(189, 155, 84))
66    SELECTED_HOVER = QtGui.QBrush(QtGui.QColor(227, 186, 101))
67    ANCESTOR_OF_SELECTED = QtGui.QBrush(QtGui.QColor(189, 155, 84, 50))
68    ANCESTOR_OF_SELECTED_HOVER = QtGui.QBrush(QtGui.QColor(189, 155, 84, 100))
69    UNSELECTED_HOVER = QtGui.QBrush(QtGui.QColor(70, 70, 70))
70
71class UIPropertyValueSourceColors(ConstantsGroup):
72    FALLBACK = UIBaseColors.DARK_YELLOW
73    TIME_SAMPLE = QtGui.QBrush(QtGui.QColor(177, 207, 153))
74    DEFAULT = UIBaseColors.LIGHT_SKY_BLUE
75    NONE = QtGui.QBrush(QtGui.QColor(140, 140, 140))
76    VALUE_CLIPS = QtGui.QBrush(QtGui.QColor(230, 150, 230))
77
78class UIFonts(ConstantsGroup):
79    # Font constants.  We use font in the prim browser to distinguish
80    # "resolved" prim specifier
81    # XXX - the use of weight here may need to be revised depending on font family
82    BASE_POINT_SIZE = 10
83
84    ITALIC = QtGui.QFont()
85    ITALIC.setWeight(QtGui.QFont.Light)
86    ITALIC.setItalic(True)
87
88    NORMAL = QtGui.QFont()
89    NORMAL.setWeight(QtGui.QFont.Normal)
90
91    BOLD = QtGui.QFont()
92    BOLD.setWeight(QtGui.QFont.Bold)
93
94    BOLD_ITALIC = QtGui.QFont()
95    BOLD_ITALIC.setWeight(QtGui.QFont.Bold)
96    BOLD_ITALIC.setItalic(True)
97
98    OVER_PRIM = ITALIC
99    DEFINED_PRIM = BOLD
100    ABSTRACT_PRIM = NORMAL
101
102    INHERITED = QtGui.QFont()
103    INHERITED.setPointSize(BASE_POINT_SIZE * 0.8)
104    INHERITED.setWeight(QtGui.QFont.Normal)
105    INHERITED.setItalic(True)
106
107class KeyboardShortcuts(ConstantsGroup):
108    FramingKey = QtCore.Qt.Key_F
109
110class PropertyViewIndex(ConstantsGroup):
111    TYPE, NAME, VALUE = range(3)
112
113ICON_DIR_ROOT = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'icons')
114
115# We use deferred loading because icons can't be constructed before
116# application initialization time.
117_icons = {}
118def _DeferredIconLoad(path):
119    fullPath = os.path.join(ICON_DIR_ROOT, path)
120    try:
121        icon = _icons[fullPath]
122    except KeyError:
123        icon = QtGui.QIcon(fullPath)
124        _icons[fullPath] = icon
125    return icon
126
127class PropertyViewIcons(ConstantsGroup):
128    ATTRIBUTE                  = lambda: _DeferredIconLoad('usd-attr-plain-icon.png')
129    ATTRIBUTE_WITH_CONNECTIONS = lambda: _DeferredIconLoad('usd-attr-with-conn-icon.png')
130    RELATIONSHIP               = lambda: _DeferredIconLoad('usd-rel-plain-icon.png')
131    RELATIONSHIP_WITH_TARGETS  = lambda: _DeferredIconLoad('usd-rel-with-target-icon.png')
132    TARGET                     = lambda: _DeferredIconLoad('usd-target-icon.png')
133    CONNECTION                 = lambda: _DeferredIconLoad('usd-conn-icon.png')
134    COMPOSED                   = lambda: _DeferredIconLoad('usd-cmp-icon.png')
135
136class PropertyViewDataRoles(ConstantsGroup):
137    ATTRIBUTE = "Attr"
138    RELATIONSHIP = "Rel"
139    ATTRIBUTE_WITH_CONNNECTIONS = "Attr_"
140    RELATIONSHIP_WITH_TARGETS = "Rel_"
141    TARGET = "Tgt"
142    CONNECTION = "Conn"
143    COMPOSED = "Cmp"
144
145class RenderModes(ConstantsGroup):
146    # Render modes
147    WIREFRAME = "Wireframe"
148    WIREFRAME_ON_SURFACE = "WireframeOnSurface"
149    SMOOTH_SHADED = "Smooth Shaded"
150    FLAT_SHADED = "Flat Shaded"
151    POINTS = "Points"
152    GEOM_ONLY = "Geom Only"
153    GEOM_FLAT = "Geom Flat"
154    GEOM_SMOOTH = "Geom Smooth"
155    HIDDEN_SURFACE_WIREFRAME = "Hidden Surface Wireframe"
156
157class ShadedRenderModes(ConstantsGroup):
158    # Render modes which use shading
159    SMOOTH_SHADED = RenderModes.SMOOTH_SHADED
160    FLAT_SHADED = RenderModes.FLAT_SHADED
161    WIREFRAME_ON_SURFACE = RenderModes.WIREFRAME_ON_SURFACE
162    GEOM_FLAT = RenderModes.GEOM_FLAT
163    GEOM_SMOOTH = RenderModes.GEOM_SMOOTH
164
165class ColorCorrectionModes(ConstantsGroup):
166    # Color correction used when render is presented to screen
167    # These strings should match HdxColorCorrectionTokens
168    DISABLED = "disabled"
169    SRGB = "sRGB"
170    OPENCOLORIO = "openColorIO"
171
172class PickModes(ConstantsGroup):
173    # Pick modes
174    PRIMS = "Prims"
175    MODELS = "Models"
176    INSTANCES = "Instances"
177    PROTOTYPES = "Prototypes"
178
179class SelectionHighlightModes(ConstantsGroup):
180    # Selection highlight modes
181    NEVER = "Never"
182    ONLY_WHEN_PAUSED = "Only when paused"
183    ALWAYS = "Always"
184
185class CameraMaskModes(ConstantsGroup):
186    NONE = "none"
187    PARTIAL = "partial"
188    FULL = "full"
189
190class IncludedPurposes(ConstantsGroup):
191    DEFAULT = UsdGeom.Tokens.default_
192    PROXY = UsdGeom.Tokens.proxy
193    GUIDE = UsdGeom.Tokens.guide
194    RENDER = UsdGeom.Tokens.render
195
196def _PropTreeWidgetGetRole(tw):
197    return tw.data(PropertyViewIndex.TYPE, QtCore.Qt.ItemDataRole.WhatsThisRole)
198
199def PropTreeWidgetTypeIsRel(tw):
200    role = _PropTreeWidgetGetRole(tw)
201    return role in (PropertyViewDataRoles.RELATIONSHIP,
202                    PropertyViewDataRoles.RELATIONSHIP_WITH_TARGETS)
203
204def _UpdateLabelText(text, substring, mode):
205    return text.replace(substring, '<'+mode+'>'+substring+'</'+mode+'>')
206
207def ItalicizeLabelText(text, substring):
208    return _UpdateLabelText(text, substring, 'i')
209
210def BoldenLabelText(text, substring):
211    return _UpdateLabelText(text, substring, 'b')
212
213def ColorizeLabelText(text, substring, r, g, b):
214    return _UpdateLabelText(text, substring,
215                            "span style=\"color:rgb(%d, %d, %d);\"" % (r, g, b))
216
217def PrintWarning(title, description):
218    msg = sys.stderr
219    print("------------------------------------------------------------", file=msg)
220    print("WARNING: %s" % title, file=msg)
221    print(description, file=msg)
222    print("------------------------------------------------------------", file=msg)
223
224def GetValueAndDisplayString(prop, time):
225    """If `prop` is a timeSampled Sdf.AttributeSpec, compute a string specifying
226    how many timeSamples it possesses.  Otherwise, compute the single default
227    value, or targets for a relationship, or value at 'time' for a
228    Usd.Attribute.  Return a tuple of a parameterless function that returns the
229    resolved value at 'time', and the computed brief string for display.  We
230    return a value-producing function rather than the value itself because for
231    an Sdf.AttributeSpec with multiple timeSamples, the resolved value is
232    *all* of the timeSamples, which can be expensive to compute, and is
233    rarely needed.
234    """
235    def _ValAndStr(val):
236        return (lambda: val, GetShortStringForValue(prop, val))
237
238    if isinstance(prop, Usd.Relationship):
239        return _ValAndStr(prop.GetTargets())
240    elif isinstance(prop, (Usd.Attribute, CustomAttribute)):
241        return _ValAndStr(prop.Get(time))
242    elif isinstance(prop, Sdf.AttributeSpec):
243        if time == Usd.TimeCode.Default():
244            return _ValAndStr(prop.default)
245        else:
246            numTimeSamples = prop.layer.GetNumTimeSamplesForPath(prop.path)
247            if numTimeSamples == 0:
248                return _ValAndStr(prop.default)
249            else:
250                def _GetAllTimeSamples(attrSpec):
251                    l = attrSpec.layer
252                    p = attrSpec.path
253                    ordinates = l.ListTimeSamplesForPath(p)
254                    return [(o, l.QueryTimeSample(p, o)) for o in ordinates]
255
256                if numTimeSamples == 1:
257                    valStr = "1 time sample"
258                else:
259                    valStr = str(numTimeSamples) + " time samples"
260
261                return (lambda prop=prop: _GetAllTimeSamples(prop), valStr)
262
263    elif isinstance(prop, Sdf.RelationshipSpec):
264        return _ValAndStr(prop.targetPathList)
265
266    return (lambda: None, "unrecognized property type")
267
268
269def GetShortStringForValue(prop, val):
270    if isinstance(prop, Usd.Relationship):
271        val = ", ".join(str(p) for p in val)
272    elif isinstance(prop, Sdf.RelationshipSpec):
273        return str(prop.targetPathList)
274
275    # If there is no value opinion, we do not want to display anything,
276    # since python 'None' has a different meaning than usda-authored None,
277    # which is how we encode attribute value blocks (which evaluate to
278    # Sdf.ValueBlock)
279    if val is None:
280        return ''
281
282    from .scalarTypes import GetScalarTypeFromAttr
283    scalarType, isArray = GetScalarTypeFromAttr(prop)
284    result = ''
285    if isArray and not isinstance(val, Sdf.ValueBlock):
286        def arrayToStr(a):
287            from itertools import chain
288            elems = a if len(a) <= 6 else chain(a[:3], ['...'], a[-3:])
289            return '[' + ', '.join(map(str, elems)) + ']'
290        if val is not None and len(val):
291            result = "%s[%d]: %s" % (scalarType, len(val), arrayToStr(val))
292        else:
293            result = "%s[]" % scalarType
294    else:
295        result = str(val)
296
297    return result[:500]
298
299# Return a string that reports size in metric units (units of 1000, not 1024).
300def ReportMetricSize(sizeInBytes):
301    if sizeInBytes == 0:
302       return "0 B"
303    sizeSuffixes = ("B", "KB", "MB", "GB", "TB", "PB", "EB")
304    i = int(math.floor(math.log(sizeInBytes, 1000)))
305    if i >= len(sizeSuffixes):
306        i = len(sizeSuffixes) - 1
307    p = math.pow(1000, i)
308    s = round(sizeInBytes / p, 2)
309    return "%s %s" % (s, sizeSuffixes[i])
310
311# Return attribute status at a certian frame (is it using the default, or the
312# fallback? Is it authored at this frame? etc.
313def _GetAttributeStatus(attribute, frame):
314    return attribute.GetResolveInfo(frame).GetSource()
315
316# Return a Font corresponding to certain attribute properties.
317# Currently this only applies italicization on interpolated time samples.
318def GetPropertyTextFont(prop, frame):
319    if not isinstance(prop, Usd.Attribute):
320        # Early-out for non-attribute properties.
321        return None
322
323    frameVal = frame.GetValue()
324    bracketing = prop.GetBracketingTimeSamples(frameVal)
325
326    # Note that some attributes return an empty tuple, some None, from
327    # GetBracketingTimeSamples(), but all will be fed into this function.
328    if bracketing and (len(bracketing) == 2) and (bracketing[0] != frameVal):
329        return UIFonts.ITALIC
330
331    return None
332
333# Helper function that takes attribute status and returns the display color
334def GetPropertyColor(prop, frame, hasValue=None, hasAuthoredValue=None,
335                      valueIsDefault=None):
336    if not isinstance(prop, Usd.Attribute):
337        # Early-out for non-attribute properties.
338        return UIBaseColors.RED.color()
339
340    statusToColor = {Usd.ResolveInfoSourceFallback   : UIPropertyValueSourceColors.FALLBACK,
341                     Usd.ResolveInfoSourceDefault    : UIPropertyValueSourceColors.DEFAULT,
342                     Usd.ResolveInfoSourceValueClips : UIPropertyValueSourceColors.VALUE_CLIPS,
343                     Usd.ResolveInfoSourceTimeSamples: UIPropertyValueSourceColors.TIME_SAMPLE,
344                     Usd.ResolveInfoSourceNone       : UIPropertyValueSourceColors.NONE}
345
346    valueSource = _GetAttributeStatus(prop, frame)
347
348    return statusToColor[valueSource].color()
349
350# Gathers information about a layer used as a subLayer, including its
351# position in the layerStack hierarchy.
352class SubLayerInfo(object):
353    def __init__(self, sublayer, offset, containingLayer, prefix):
354        self.layer = sublayer
355        self.offset = offset
356        self.parentLayer = containingLayer
357        self._prefix = prefix
358
359    def GetOffsetString(self):
360        o = self.offset.offset
361        s = self.offset.scale
362        if o == 0:
363            if s == 1:
364                return ""
365            else:
366                return str.format("(scale = {})", s)
367        elif s == 1:
368            return str.format("(offset = {})", o)
369        else:
370            return str.format("(offset = {0}; scale = {1})", o, s)
371
372    def GetHierarchicalDisplayString(self):
373        return self._prefix + self.layer.GetDisplayName()
374
375def _AddSubLayers(layer, layerOffset, prefix, parentLayer, layers):
376    offsets = layer.subLayerOffsets
377    layers.append(SubLayerInfo(layer, layerOffset, parentLayer, prefix))
378    for i, l in enumerate(layer.subLayerPaths):
379        offset = offsets[i] if offsets is not None and len(offsets) > i else Sdf.LayerOffset()
380        subLayer = Sdf.Layer.FindRelativeToLayer(layer, l)
381        # Due to an unfortunate behavior of the Pixar studio resolver,
382        # FindRelativeToLayer() may fail to resolve certain paths.  We will
383        # remove this extra Find() call as soon as we can retire the behavior;
384        # in the meantime, the extra call does not hurt (but should not, in
385        # general, be necessary)
386        if not subLayer:
387            subLayer = Sdf.Layer.Find(l)
388
389        if subLayer:
390            # This gives a 'tree'-ish presentation, but it looks sad in
391            # a QTableWidget.  Just use spaces for now
392            # addedPrefix = "|-- " if parentLayer is None else "|    "
393            addedPrefix = "     "
394            _AddSubLayers(subLayer, offset, addedPrefix + prefix, layer, layers)
395        else:
396            print("Could not find layer " + l)
397
398def GetRootLayerStackInfo(layer):
399    layers = []
400    _AddSubLayers(layer, Sdf.LayerOffset(), "", None, layers)
401    return layers
402
403def PrettyFormatSize(sz):
404    k = 1024
405    meg = k * 1024
406    gig = meg * 1024
407    ter = gig * 1024
408
409    sz = float(sz)
410    if sz > ter:
411        return "%.1fT" % (sz/float(ter))
412    elif sz > gig:
413        return "%.1fG" % (sz/float(gig))
414    elif sz > meg:
415        return "%.1fM" % (sz/float(meg))
416    elif sz > k:
417        return "%.1fK" % (sz/float(k))
418    else:
419        return "%db" % sz
420
421
422class Timer(object):
423    """Use as a context object with python's "with" statement, like so:
424       with Timer() as t:
425           doSomeStuff()
426       t.PrintTime("did some stuff")
427    """
428    def __enter__(self):
429        self._start = time.time()
430        self.interval = 0
431        return self
432
433    def __exit__(self, *args):
434        self._end = time.time()
435        self.interval = self._end - self._start
436
437    def PrintTime(self, action):
438        print("Time to %s: %2.3fs" % (action, self.interval))
439
440
441class BusyContext(object):
442    """When used as a context object with python's "with" statement,
443    will set Qt's busy cursor upon entry and pop it on exit.
444    """
445    def __enter__(self):
446        QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.BusyCursor)
447
448    def __exit__(self, *args):
449        QtWidgets.QApplication.restoreOverrideCursor()
450
451
452def InvisRootPrims(stage):
453    """Make all defined root prims of stage be invisible,
454    at Usd.TimeCode.Default()"""
455    for p in stage.GetPseudoRoot().GetChildren():
456        UsdGeom.Imageable(p).MakeInvisible()
457
458def _RemoveVisibilityRecursive(primSpec):
459    try:
460        primSpec.RemoveProperty(primSpec.attributes[UsdGeom.Tokens.visibility])
461    except IndexError:
462        pass
463    for child in primSpec.nameChildren:
464        _RemoveVisibilityRecursive(child)
465
466def ResetSessionVisibility(stage):
467    session = stage.GetSessionLayer()
468    with Sdf.ChangeBlock():
469        _RemoveVisibilityRecursive(session.pseudoRoot)
470
471# This is unfortunate.. but until UsdAttribute will return you a ResolveInfo,
472# we have little alternative, other than manually walking prim's PcpPrimIndex
473def HasSessionVis(prim):
474    """Is there a session-layer override for visibility for 'prim'?"""
475    session = prim.GetStage().GetSessionLayer()
476    primSpec = session.GetPrimAtPath(prim.GetPath())
477    return bool(primSpec and UsdGeom.Tokens.visibility in primSpec.attributes)
478
479# This should be codified on UsdModelAPI, but maybe after 117137 is addressed...
480def GetEnclosingModelPrim(prim):
481    """If 'prim' is inside/under a model of any kind, return the closest
482    such ancestor prim - If 'prim' has no model ancestor, return None"""
483    if prim:
484        prim = prim.GetParent()
485    while prim:
486        # We use Kind here instead of prim.IsModel because point instancer
487        # prototypes currently don't register as models in IsModel. See
488        # bug: http://bugzilla.pixar.com/show_bug.cgi?id=117137
489        if Kind.Registry.IsA(Usd.ModelAPI(prim).GetKind(), Kind.Tokens.model):
490            break
491        prim = prim.GetParent()
492
493    return prim
494
495def GetPrimLoadability(prim):
496    """Return a tuple of (isLoadable, isLoaded) for 'prim', according to
497    the following rules:
498    A prim is loadable if it is active, and either of the following are true:
499       * prim has a payload
500       * prim is a model group
501    The latter is useful because loading is recursive on a UsdStage, and it
502    is convenient to be able to (e.g.) load everything loadable in a set.
503
504    A prim 'isLoaded' only if there are no unloaded prims beneath it, i.e.
505    it is stating whether the prim is "fully loaded".  This
506    is a debatable definition, but seems useful for usdview's purposes."""
507    if not (prim.IsActive() and (prim.IsGroup() or prim.HasAuthoredPayloads())):
508        return (False, True)
509    # XXX Note that we are potentially traversing the entire stage here.
510    # If this becomes a performance issue, we can cast this query into C++,
511    # cache results, etc.
512    for p in Usd.PrimRange(prim, Usd.PrimIsActive):
513        if not p.IsLoaded():
514            return (True, False)
515    return (True, True)
516
517def GetPrimsLoadability(prims):
518    """Follow the logic of GetPrimLoadability for each prim, combining
519    results so that isLoadable is the disjunction of all prims, and
520    isLoaded is the conjunction."""
521    isLoadable = False
522    isLoaded = True
523    for prim in prims:
524        loadable, loaded = GetPrimLoadability(prim)
525        isLoadable = isLoadable or loadable
526        isLoaded = isLoaded and loaded
527    return (isLoadable, isLoaded)
528
529def GetFileOwner(path):
530    try:
531        if platform.system() == 'Windows':
532            # This only works if pywin32 is installed.
533            # Try "pip install pypiwin32".
534            import win32security as w32s
535            fs = w32s.GetFileSecurity(path, w32s.OWNER_SECURITY_INFORMATION)
536            sdo = fs.GetSecurityDescriptorOwner()
537            name, domain, use = w32.LookupAccountSid(None, sdo)
538            return "%s\\%s" % (domain, name)
539        else:
540            import pwd
541            return pwd.getpwuid(os.stat(path).st_uid).pw_name
542    except:
543        return "<unknown>"
544
545# In future when we have better introspection abilities in Usd core API,
546# we will change this function to accept a prim rather than a primStack.
547def GetAssetCreationTime(primStack, assetIdentifier):
548    """Finds the weakest layer in which assetInfo.identifier is set to
549    'assetIdentifier', and considers that an "asset-defining layer".
550    If assetInfo.identifier is not set in any layer, assumes the weakest
551    layer is the defining layer.  We then retrieve the creation time for
552    the asset by stat'ing the defining layer's real path.
553
554    Returns a triple of strings: (fileDisplayName, creationTime, owner)"""
555    definingLayer = None
556    for spec in reversed(primStack):
557        if spec.HasInfo('assetInfo'):
558            identifier = spec.GetInfo('assetInfo').get('identifier')
559            if identifier and identifier.path == assetIdentifier.path:
560                definingLayer = spec.layer
561                break
562    if definingLayer:
563        definingFile = definingLayer.realPath
564    else:
565        definingFile = primStack[-1].layer.realPath
566
567    if Ar.IsPackageRelativePath(definingFile):
568        definingFile = Ar.SplitPackageRelativePathOuter(definingFile)[0]
569
570    if not definingFile:
571        displayName = (definingLayer.GetDisplayName()
572                       if definingLayer and definingLayer.anonymous else
573                       "<in-memory layer>")
574        creationTime = "<unknown>"
575        owner = "<unknown>"
576    else:
577        displayName = definingFile.split('/')[-1]
578
579        try:
580            creationTime = time.ctime(os.stat(definingFile).st_ctime)
581        except:
582            creationTime = "<unknown>"
583
584        owner = GetFileOwner(definingFile)
585
586    return (displayName, creationTime, owner)
587
588
589def DumpMallocTags(stage, contextStr):
590    if Tf.MallocTag.IsInitialized():
591        callTree = Tf.MallocTag.GetCallTree()
592        memInMb = Tf.MallocTag.GetTotalBytes() / (1024.0 * 1024.0)
593
594        import os.path as path
595        import tempfile
596        layerName = path.basename(stage.GetRootLayer().identifier)
597        # CallTree.Report() gives us the most informative (and processable)
598        # form of output, but it only accepts a fileName argument.  So we
599        # use NamedTemporaryFile just to get a filename.
600        statsFile = tempfile.NamedTemporaryFile(prefix=layerName+'.',
601                                                suffix='.mallocTag',
602                                                delete=False)
603        statsFile.close()
604        reportName = statsFile.name
605        callTree.Report(reportName)
606        print("Memory consumption of %s for %s is %d Mb" % (contextStr,
607                                                            layerName,
608                                                            memInMb))
609        print("For detailed analysis, see " + reportName)
610    else:
611        print("Unable to accumulate memory usage since the Pxr MallocTag system was not initialized")
612
613def GetInstanceIdForIndex(prim, instanceIndex, time):
614    '''Attempt to find an authored Id value for the instance at index
615    'instanceIndex' at time 'time', on the given prim 'prim', which we access
616    as a UsdGeom.PointInstancer (whether it actually is or not, to provide
617    some dynamic duck-typing for custom instancer types that support Ids.
618    Returns 'None' if no ids attribute was found, or if instanceIndex is
619    outside the bounds of the ids array.'''
620    if not prim or instanceIndex < 0:
621        return None
622    ids = UsdGeom.PointInstancer(prim).GetIdsAttr().Get(time)
623    if not ids or instanceIndex >= len(ids):
624        return None
625    return ids[instanceIndex]
626
627def GetInstanceIndicesForIds(prim, instanceIds, time):
628    '''Attempt to find the instance indices of a list of authored instance IDs
629    for prim 'prim' at time 'time'. If the prim is not a PointInstancer or does
630    not have authored IDs, returns None. If any ID from 'instanceIds' does not
631    exist at the given time, its index is not added to the list (because it does
632    not have an index).'''
633    ids = UsdGeom.PointInstancer(prim).GetIdsAttr().Get(time)
634    if ids:
635        return [instanceIndex for instanceIndex, instanceId in enumerate(ids)
636            if instanceId in instanceIds]
637    else:
638        return None
639
640def Drange(start, stop, step):
641    '''Return a list whose first element is 'start' and the following elements
642    (if any) are 'start' plus increasing whole multiples of 'step', up to but
643    not greater than 'stop'.  For example:
644    Drange(1, 3, 0.3) -> [1, 1.3, 1.6, 1.9, 2.2, 2.5, 2.8]'''
645    lst = [start]
646    n = 1
647    while start + n * step <= stop:
648        lst.append(start + n * step)
649        n += 1
650    return lst
651
652class PrimNotFoundException(Exception):
653    """Raised when a prim does not exist at a valid path."""
654    def __init__(self, path):
655        super(PrimNotFoundException, self).__init__(
656            "Prim not found at path in stage: %s" % str(path))
657
658class PropertyNotFoundException(Exception):
659    """Raised when a property does not exist at a valid path."""
660    def __init__(self, path):
661        super(PropertyNotFoundException, self).__init__(
662            "Property not found at path in stage: %s" % str(path))
663
664class FixableDoubleValidator(QtGui.QDoubleValidator):
665    """This class implements a fixup() method for QDoubleValidator
666    (see method for specific behavior).  To work around the brokenness
667    of Pyside's fixup() wrapping, we allow the validator to directly
668    update its parent if it is a QLineEdit, from within fixup().  Thus
669    every QLineEdit must possess its own unique FixableDoubleValidator.
670
671    The fixup method we supply (which can be usefully called directly)
672    applies clamping and rounding to enforce the QDoubleValidator's
673    range and decimals settings."""
674
675    def __init__(self, parent):
676        super(FixableDoubleValidator, self).__init__(parent)
677
678        self._lineEdit = parent if isinstance(parent, QtWidgets.QLineEdit) else None
679
680    def fixup(self, valStr):
681        # We implement this to fulfill the virtual for internal QLineEdit
682        # behavior, hoping that PySide will do the right thing, but it is
683        # useless to call from Python directly due to string immutability
684        try:
685            val = float(valStr)
686            val = max(val, self.bottom())
687            val = min(val, self.top())
688            val = round(val)
689            valStr = str(val)
690            if self._lineEdit:
691                self._lineEdit.setText(valStr)
692        except ValueError:
693            pass
694