Skip to content

Commit

Permalink
Merge pull request #5619 from johnhaddon/layoutLoading
Browse files Browse the repository at this point in the history
Layouts : Improve loading of layouts with unavailable editors
  • Loading branch information
johnhaddon authored Jan 10, 2024
2 parents e044812 + b949e9e commit d9d4b79
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 5 deletions.
1 change: 1 addition & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Improvements
- Added bookmarks.
- USDLight : Added file browser for `shaping:ies:file` parameter.
- OpenColorIOContext : Added file browser for `config` plug.
- Layouts : Added the ability to load layouts containing editors that aren't currently available. This allows layouts containing new editors introduced in Gaffer 1.4 to be loaded in Gaffer 1.3.

Fixes
-----
Expand Down
93 changes: 88 additions & 5 deletions python/GafferUI/Layouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
##########################################################################

import collections
import importlib
import re
import traceback
import weakref
Expand Down Expand Up @@ -125,18 +126,19 @@ def create( self, name, scriptNode ) :

layout = self.__namedLayouts[name]

# first try to import the modules the layout needs
# First try to import the modules the layout needs. We wrap these with
# `_ModuleProxy` so that we can handle missing editor types gracefully.
contextDict = { "scriptNode" : scriptNode, "imath" : imath }
imported = set()
classNameRegex = re.compile( r"[a-zA-Z]*Gaffer[^(,]*\(" )
for className in classNameRegex.findall( layout.repr ) :
moduleName = className.partition( "." )[0]
if moduleName not in imported :
try :
exec( "import %s" % moduleName, contextDict, contextDict )
except( ImportError ) :
IECore.msg( IECore.MessageHandler.Level.Error, "GafferUI.Layouts", "Failed to load \"{layout}\" layout. {module} is not available.".format( layout=name, module=moduleName ) )
return GafferUI.CompoundEditor( scriptNode )
module = importlib.import_module( moduleName )
except ImportError :
module = None
contextDict[moduleName] = _EditorModuleProxy( module, moduleName )
imported.add( moduleName )

try :
Expand Down Expand Up @@ -205,3 +207,84 @@ def __save( self ) :

if self.__defaultPersistent and self.__default in self.__namedLayouts :
f.write( "layouts.setDefault( {0}, persistent = True )\n".format( repr( self.__default ) ) )

# Proxy used for modules needed by `Layouts.create()`, so we can gracefully
# handle missing modules and editors.
class _EditorModuleProxy :

def __init__( self, module, path ) :

self.__module = module
self.__path = path

def __getattr__( self, name ) :

if self.__module is not None :
a = getattr( self.__module, name, None )
if a is not None :
return a

return _EditorModuleProxy( None, f"{self.__path}.{name}" )

def __call__( self, *args, **kw ) :

return _MissingEditor( self.__path, *args, **kw )

# Proxy for Editor types that are currently unavailable.
class _MissingEditor( GafferUI.Editor ) :

def __init__( self, path, scriptNode, *args, **kw ) :

column = GafferUI.ListContainer( borderWidth = 8 )
GafferUI.Editor.__init__( self, column, scriptNode )

with column :

GafferUI.Spacer(
imath.V2i( 1 ),
parenting = { "expand" : True }
)

with GafferUI.ListContainer(
GafferUI.ListContainer.Orientation.Horizontal, spacing = 4,
parenting = { "horizontalAlignment" : GafferUI.Label.HorizontalAlignment.Center },
) :

GafferUI.Image( "warningNotification.png" )
GafferUI.Label(
f"<b>{path} not available</b>", horizontalAlignment = GafferUI.HorizontalAlignment.Center,
)

GafferUI.Spacer(
imath.V2i( 1 ),
parenting = { "expand" : True }
)

self.__path = path
self.__args = args
self.__kw = kw

# We don't actually derive from NodeSetEditor, because we don't know if
# we should or not. But we provide the same methods so that CompoundEditor
# doesn't get upset if it tries to restore a node set.
def setNodeSet( self, nodeSet ) :

self.__nodeSet = nodeSet

def getNodeSet( self ) :

return self.__nodeSet

def getTitle( self ) :

return IECore.CamelCase.toSpaced( self.__path.rpartition( "." )[2] )

def __repr__( self ) :

return "{path}( scriptNode{argsSeparator}{args}{kwSeparator}{kw} )".format(
path = self.__path,
argsSeparator = ", " if self.__args else "",
args = ", ".join( repr( a ) for a in self.__args ),
kwSeparator = ", " if self.__kw else "",
kw = ", ".join( f"{k} = {v}" for k, v in self.__kw.items() )
)
42 changes: 42 additions & 0 deletions python/GafferUITest/LayoutsTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,5 +191,47 @@ def testNodeSetRestore( self ) :
ns = editors[3].getNodeSet()
self.assertTrue( isinstance( ns, Gaffer.StandardSet ) )

def testMissingEditorType( self ) :

applicationRoot = Gaffer.ApplicationRoot( "testApp" )
layouts = GafferUI.Layouts.acquire( applicationRoot )

# Register a layout containing an editor that doesn't exist.
layouts.add( "MissingEditor", "GafferUI.NonexistentEditor( scriptNode )" )

# Check that we can still create such a layout, and that
# it has a proxy with the expected properties.
script = Gaffer.ScriptNode()
layout = layouts.create( "MissingEditor", script )
self.assertIsInstance( layout, GafferUI.Editor )
self.assertEqual( layout.getTitle(), "Nonexistent Editor" )

# Save the layout again and check that the same applies.
layouts.add( "MissingEditor2", layout )
layout2 = layouts.create( "MissingEditor2", script )
self.assertIsInstance( layout2, GafferUI.Editor )
self.assertEqual( layout2.getTitle(), "Nonexistent Editor" )

def testMissingEditorModule( self ) :

applicationRoot = Gaffer.ApplicationRoot( "testApp" )
layouts = GafferUI.Layouts.acquire( applicationRoot )

# Register a layout containing an editor from a module that doesn't exist.
layouts.add( "MissingEditor", "NonexistentGafferModule.NonexistentEditor( scriptNode )" )

# Check that we can still create such a layout, and that
# it has a proxy with the expected properties.
script = Gaffer.ScriptNode()
layout = layouts.create( "MissingEditor", script )
self.assertIsInstance( layout, GafferUI.Editor )
self.assertEqual( layout.getTitle(), "Nonexistent Editor" )

# Save the layout again and check that the same applies.
layouts.add( "MissingEditor2", layout )
layout2 = layouts.create( "MissingEditor2", script )
self.assertIsInstance( layout2, GafferUI.Editor )
self.assertEqual( layout2.getTitle(), "Nonexistent Editor" )

if __name__ == "__main__":
unittest.main()

0 comments on commit d9d4b79

Please sign in to comment.