Skip to content

Commit 9202b1b

Browse files
authored
Merge pull request #6797 from johnhaddon/autoParentImprovement
Widget : Refactor auto-parenting
2 parents 8a4cdbc + 334a89b commit 9202b1b

File tree

5 files changed

+78
-42
lines changed

5 files changed

+78
-42
lines changed

Changes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ API
2121

2222
- Metadata : `ValueFunctions` now receive a `target` parameter. This is particularly useful when registering a function against a wildcard pattern.
2323
- PlugAlgo : Added `RampffData` and `RampfColor3fData` support to `createPlugFromData()`.
24+
- Widget :
25+
- Improved automatic parenting via the `with parent` syntax. Children are now guaranteed to be fully constructed before they are parented.
26+
- Turned `toolTip`, `parenting` and `displayTransform` keyword-only constructor arguments.
2427

2528
Breaking Changes
2629
----------------
@@ -39,6 +42,7 @@ Breaking Changes
3942
- OSL Shaders : Replaced `Pattern/FloatSpline` and `Pattern/ColorSpline` with `Pattern/FloatRamp` and `Pattern/ColorRamp`. Old Gaffer scripts will be updated automatically on load, and when resaved will reference the new shaders. Note that the `.osl` files for the old shaders are still available, so that old USD files will continue to render.
4043
- GafferUI : Renamed SplineWidget to RampWidget. Renamed SplinePlugValueWidget to RampPlugValueWidget. The old RampPlugValueWidget is no longer exposed, since it was only used internally.
4144
- Metadata : Added `target` argument to `ValueFunction` signature.
45+
- Widget : The `toolTip`, `parenting` and `displayTransform` constructor arguments are no longer positional.
4246

4347
Build
4448
-----

python/GafferSceneUI/SceneInspector.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,12 @@ def __init__( self, scriptNode, **kw ) :
163163
rootSection = "TopRow",
164164
)
165165

166-
GafferUI.PlugLayout(
166+
compareRow = GafferUI.PlugLayout(
167167
self.settings(),
168168
orientation = GafferUI.ListContainer.Orientation.Horizontal,
169169
rootSection = "CompareRow",
170170
)
171+
compareRow.plugValueWidget( self.settings()["compare"] ).connect( self )
171172

172173
with GafferUI.TabbedContainer() :
173174

@@ -1060,7 +1061,14 @@ def createLabel( text ) :
10601061

10611062
GafferUI.WidgetAlgo.joinEdges( row )
10621063

1063-
self.parentChangedSignal().connect( Gaffer.WeakMethod( self.__parentChanged ) )
1064+
# We don't have access to the parent SceneInspector when we're constructed,
1065+
# so we have to rely on it calling this method to allow us to connect our
1066+
# input label to follow the node set.
1067+
## \todo Perhaps Editor should be a node with the settings plugs parented
1068+
# directly to it, in which case we'd have direct access.
1069+
def connect( self, sceneInspector ) :
1070+
1071+
self.__inputLabelWidget.connectToNodeSetWidget( sceneInspector )
10641072

10651073
def hasLabel( self ) :
10661074

@@ -1079,13 +1087,6 @@ def _updateFromValues( self, values, exception ) :
10791087
for x in range( 0, 2 ) :
10801088
grid[x,y].setVisible( enabled )
10811089

1082-
def __parentChanged( self, unused ) :
1083-
1084-
# We can't get access to the parent SceneInspector until we get parented,
1085-
# so we have to connect our input label to follow the node set in this
1086-
# awkward delayed fashion.
1087-
self.__inputLabelWidget.connectToNodeSetWidget( self.ancestor( GafferUI.NodeSetEditor ) )
1088-
10891090
SceneInspector._ComparePlugValueWidget = _ComparePlugValueWidget
10901091

10911092
def __contextMenu( column, pathListing, menuDefinition ) :

python/GafferUI/Widget.py

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,35 @@ class _WidgetMetaclass( Gaffer.Signals.Trackable.__class__ ) :
5555

5656
def __call__( cls, *args, **kw ) :
5757

58+
cls.pushParent( None ) # Disable auto-parenting of private widgets created in constructor.
5859
instance = type.__call__( cls, *args, **kw )
5960
instance._postConstructor()
61+
cls.popParent()
62+
63+
if len( cls.__parentStack ) and cls.__parentStack[-1] is not None :
64+
# Perform automatic parenting if necessary. We don't want to do this
65+
# for menus, because they don't have the same parenting semantics.
66+
# If other types end up with similar requirements then we should
67+
# probably just have a mechanism for them to say they don't want to
68+
# participate rather than hardcoding stuff here.
69+
if not isinstance( instance, GafferUI.Menu ) :
70+
parenting = kw.get( "parenting", {} )
71+
cls.__parentStack[-1].addChild( instance, **parenting )
6072

6173
return instance
6274

75+
__parentStack = []
76+
77+
@classmethod
78+
def pushParent( cls, parent ) :
79+
80+
cls.__parentStack.append( parent )
81+
82+
@classmethod
83+
def popParent( cls ) :
84+
85+
return cls.__parentStack.pop()
86+
6387
## The Widget class provides a base class for all widgets in GafferUI.
6488
#
6589
# When building UIs, you will typically derive from an existing Widget subclass such as Window,
@@ -123,7 +147,7 @@ class Widget( Gaffer.Signals.Trackable, metaclass = _WidgetMetaclass ) :
123147
# If a current parent has been defined using the `with` syntax described above,
124148
# the parenting argument may be passed as a dictionay of optional keywords for the
125149
# automatic `parent.addChild()` call.
126-
def __init__( self, topLevelWidget, toolTip="", parenting = None, displayTransform = None ) :
150+
def __init__( self, topLevelWidget, *, toolTip="", parenting = None, displayTransform = None ) :
127151

128152
Gaffer.Signals.Trackable.__init__( self )
129153

@@ -184,17 +208,6 @@ def __init__( self, topLevelWidget, toolTip="", parenting = None, displayTransfo
184208
self.__visible = not isinstance( self, GafferUI.Window )
185209
self.__displayTransform = displayTransform
186210

187-
# perform automatic parenting if necessary. we don't want to do this
188-
# for menus, because they don't have the same parenting semantics. if other
189-
# types end up with similar requirements then we should probably just have
190-
# a mechanism for them to say they don't want to participate rather than
191-
# hardcoding stuff here.
192-
if len( self.__parentStack ) and not isinstance( self, GafferUI.Menu ) :
193-
if self.__initNesting() == self.__parentStack[-1][1] + 1 :
194-
if self.__parentStack[-1][0] is not None :
195-
parenting = parenting or {}
196-
self.__parentStack[-1][0].addChild( self, **parenting )
197-
198211
self.__eventFilterInstalled = False
199212
# if a class has overridden getToolTip, then the tooltips
200213
# may be dynamic based on context, and we need to display
@@ -768,31 +781,12 @@ def _pushParent( cls, container ) :
768781

769782
assert( isinstance( container, ( GafferUI.ContainerWidget, type( None ) ) ) )
770783

771-
cls.__parentStack.append( ( container, cls.__initNesting() ) )
784+
cls.__class__.pushParent( container )
772785

773786
@classmethod
774787
def _popParent( cls ) :
775788

776-
return cls.__parentStack.pop()[0]
777-
778-
# Returns how many Widgets are currently in construction
779-
# on the call stack. We use this to avoid automatically
780-
# parenting Widgets that are being created inside a constructor.
781-
@staticmethod
782-
def __initNesting() :
783-
784-
widgetsInInit = set()
785-
frame = inspect.currentframe()
786-
while frame :
787-
if frame.f_code.co_name=="__init__" :
788-
frameSelf = frame.f_locals[frame.f_code.co_varnames[0]]
789-
if isinstance( frameSelf, Widget ) :
790-
widgetsInInit.add( frameSelf )
791-
frame = frame.f_back
792-
793-
return len( widgetsInInit )
794-
795-
__parentStack = []
789+
return cls.__class__.popParent()
796790

797791
## Converts a Qt key code into a string
798792
@classmethod

python/GafferUITest/ContainerWidgetTest.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,5 +137,24 @@ def __init__( self ) :
137137
self.assertIsInstance( w.getChild()[0], GafferUI.TextWidget )
138138
self.assertIsInstance( w.getChild()[1], GafferUI.Button )
139139

140+
def testChildFullyConstructedBeforeParenting( self ) :
141+
142+
class TestWidget( GafferUI.Label ) :
143+
144+
def __init__( self, **kw ) :
145+
146+
GafferUI.Label.__init__( self, **kw )
147+
148+
assert( self.parent() is None )
149+
150+
def _postConstructor( self ) :
151+
152+
assert( self.parent() is None )
153+
154+
with GafferUI.ListContainer() as container:
155+
widget = TestWidget()
156+
157+
self.assertIs( widget.parent(), container )
158+
140159
if __name__ == "__main__":
141160
unittest.main()

python/GafferUITest/TabbedContainerTest.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,14 @@
3535
#
3636
##########################################################################
3737

38+
import gc
3839
import unittest
3940

4041
import GafferUI
4142
import GafferUITest
4243

44+
from Qt import QtWidgets
45+
4346
class TabbedContainerTest( GafferUITest.TestCase ) :
4447

4548
def test( self ) :
@@ -319,6 +322,21 @@ def testTabVisibility( self ) :
319322
for tab in t2, t3 :
320323
self.assertTrue( container.getTabVisible( tab ) )
321324

325+
def testWithSplitContainerParent( self ) :
326+
327+
# This exposes a problem whereby a TabbedContainer parented to
328+
# a SplitContainer would create a zombie `QTabBar` instance in
329+
# Python.
330+
with GafferUI.SplitContainer() as splitContainer :
331+
with GafferUI.TabbedContainer() :
332+
GafferUI.Label( "Test" )
333+
334+
# There should be only one QTabBar in existence, and it should
335+
# be valid.
336+
qtTabBars = [ o for o in gc.get_objects() if isinstance( o, QtWidgets.QTabBar ) ]
337+
self.assertEqual( len( qtTabBars ), 1 )
338+
self.assertTrue( GafferUI._qtObjectIsValid( qtTabBars[0] ) )
339+
322340
def tearDown( self ) :
323341

324342
self.__current = None

0 commit comments

Comments
 (0)