diff --git a/Changes.md b/Changes.md index 3098013d82a..6b4ff41901d 100644 --- a/Changes.md +++ b/Changes.md @@ -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 ----- diff --git a/python/GafferUI/Layouts.py b/python/GafferUI/Layouts.py index 9bb3a048a47..3182eec8524 100644 --- a/python/GafferUI/Layouts.py +++ b/python/GafferUI/Layouts.py @@ -36,6 +36,7 @@ ########################################################################## import collections +import importlib import re import traceback import weakref @@ -125,7 +126,8 @@ 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[^(,]*\(" ) @@ -133,10 +135,10 @@ def create( self, name, scriptNode ) : 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 : @@ -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"{path} not available", 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() ) + ) diff --git a/python/GafferUITest/LayoutsTest.py b/python/GafferUITest/LayoutsTest.py index f1c7ec7a676..536b518fab9 100644 --- a/python/GafferUITest/LayoutsTest.py +++ b/python/GafferUITest/LayoutsTest.py @@ -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()