Skip to content

Commit

Permalink
Layouts/LayoutMenu : Improve management of custom layouts
Browse files Browse the repository at this point in the history
- `Layouts.add()` now takes a `persistent` argument just as `Bookmarks.add()` does, and automatically takes care of saving persistent layouts into the startup location.
- `LayoutMenu` provides improved management of custom layouts
	- New "Save As/Default" menu item allows the default layout to be replaced
	- New "Save As/*" options allow other layouts to be replaced

Fixes #51
  • Loading branch information
johnhaddon committed Jul 24, 2018
1 parent 8b25e6d commit 4ce6414
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 85 deletions.
67 changes: 23 additions & 44 deletions python/GafferUI/LayoutMenu.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@
#
##########################################################################

import os
import re
import functools

import IECore

Expand All @@ -57,13 +56,6 @@ def restore( menu, name ) :

scriptWindow.setLayout( layout )

## A function suitable as the command for a Layout/Delete/LayoutName menu item.
def delete( name, menu ) :

scriptWindow, layouts = __scriptWindowAndLayouts( menu )
layouts.remove( name )
__saveLayouts( scriptWindow.scriptNode().applicationRoot() )

## A function suitable as the command for a Layout/Save... menu item. It must be invoked from
# a menu which has a ScriptWindow in its ancestry.
def save( menu ) :
Expand All @@ -75,29 +67,18 @@ def save( menu ) :
while True :
layoutName = "Layout " + str( i )
i += 1
if "user:" + layoutName not in layoutNames :
if layoutName not in layoutNames :
break

d = GafferUI.TextInputDialogue( initialText=layoutName, title="Save Layout", confirmLabel="Save" )
t = d.waitForText( parentWindow = scriptWindow )
d.setVisible( False )

if t is None :
if not t :
return

layout = scriptWindow.getLayout()

layouts.add( "user:" + t, layout )

__saveLayouts( scriptWindow.scriptNode().applicationRoot() )

def __saveLayouts( applicationRoot ) :

f = open( os.path.join( applicationRoot.preferencesLocation(), "layouts.py" ), "w" )
f.write( "# This file was automatically generated by Gaffer.\n" )
f.write( "# Do not edit this file - it will be overwritten.\n\n" )

GafferUI.Layouts.acquire( applicationRoot ).save( f, re.compile( "user:.*" ) )
layouts.add( t, layout, persistent = True )

def fullScreen( menu, checkBox ) :

Expand All @@ -118,37 +99,35 @@ def layoutMenuCallable( menu ) :

menuDefinition = IECore.MenuDefinition()

layoutNames = sorted( layouts.names() )
# Menu items to set layout

layoutNames = sorted( layouts.names() )
for name in layoutNames :
menuDefinition.append( "/" + name, { "command" : functools.partial( restore, name = name ) } )
if layoutNames :

def restoreWrapper( name ) :

return lambda menu : restore( menu, name )

for name in layoutNames :

label = name
if label.startswith( "user:" ) :
label = label[5:]

menuDefinition.append( label, { "command" : restoreWrapper( name ) } )

menuDefinition.append( "/SetDivider", { "divider" : True } )

def deleteWrapper( name ) :
# Menu items to save layout

return lambda menu, : delete( name, menu )
menuDefinition.append( "/Save As/Default", { "command" : functools.partial( layouts.add, "Default", scriptWindow.getLayout(), persistent = True ) } )
menuDefinition.append( "/Save As/DefaultDivider", { "divider" : True } )

for name in layoutNames :
layoutSaveNames = [ x for x in layoutNames if x != "Default" ]
for name in layoutSaveNames :
menuDefinition.append( "/Save As/" + name, { "command" : functools.partial( layouts.add, name, scriptWindow.getLayout(), persistent = True ) } )
if layoutSaveNames :
menuDefinition.append( "/Save As/Divider", { "divider" : True } )

menuDefinition.append( "/Save As/New Layout...", { "command" : save } )

if name.startswith( "user:" ) :
# Manu items to delete layouts

menuDefinition.append( "/Delete/%s" % name[5:], { "command" : deleteWrapper( name ) } )
for name in sorted( layouts.names( persistent = True ) ) :
menuDefinition.append( "/Delete/%s" % name, { "command" : functools.partial( layouts.remove, name = name ) } )

menuDefinition.append( "/Save...", { "command" : save } )
menuDefinition.append( "/DeleteDivider", { "divider" : True } )

menuDefinition.append( "/SaveDivider", { "divider" : True } )
# Other menu items

menuDefinition.append( "/Full Screen", { "command" : fullScreen, "checkBox" : fullScreenCheckBox, "shortCut" : "`" } )

Expand Down
88 changes: 47 additions & 41 deletions python/GafferUI/Layouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
#
##########################################################################

import collections
import os
import re
import weakref

import Gaffer
import GafferUI
Expand All @@ -49,11 +52,13 @@
# Layouts.acquire() method.
class Layouts( object ) :

## Typically acquire() should be used in preference
# to this constructor.
def __init__( self ) :
__Layout = collections.namedtuple( "__Layout", [ "repr", "persistent"] )

self.__namedLayouts = {}
## Use acquire() in preference to this constructor.
def __init__( self, applicationRoot ) :

self.__applicationRoot = weakref.ref( applicationRoot )
self.__namedLayouts = collections.OrderedDict()
self.__registeredEditors = []

## Acquires the set of layouts for the specified application.
Expand All @@ -69,30 +74,44 @@ def acquire( cls, applicationOrApplicationRoot ) :
try :
return applicationRoot.__layouts
except AttributeError :
pass

applicationRoot.__layouts = Layouts()
applicationRoot.__layouts = Layouts( applicationRoot )

return applicationRoot.__layouts

## Serialises the passed Editor and stores it using the given name. This
# layout can then be recreated using the create() method below.
def add( self, name, editor ) :
# layout can then be recreated using the create() method below. If
# `persistent` is `True`, then the layout will be saved in the application
# preferences and restored when the application next runs.
def add( self, name, editor, persistent = False ) :

if not isinstance( editor, basestring ) :
editor = repr( editor )

self.__namedLayouts[name] = editor
if name.startswith( "user:" ) :
# Backwards compatibility with old persistent layouts, which
# were differentiated by being prefixed with "user:".
persistent = True
name = name[5:]

self.__namedLayouts[name] = self.__Layout( editor, persistent )

if persistent :
self.__save()

## Removes a layout previously stored with add().
def remove( self, name ) :

del self.__namedLayouts[name]
l = self.__namedLayouts.pop( name )
if l.persistent :
self.__save()

## Returns a list of the names of currently defined layouts
def names( self ) :
def names( self, persistent = None ) :

return self.__namedLayouts.keys()
return [
item[0] for item in self.__namedLayouts.items()
if persistent is None or item[1].persistent == persistent
]

## Recreates a previously stored layout for the specified script,
# returning it in the form of a CompoundEditor.
Expand All @@ -104,39 +123,13 @@ def create( self, name, scriptNode ) :
contextDict = { "scriptNode" : scriptNode }
imported = set()
classNameRegex = re.compile( "[a-zA-Z]*Gaffer[^(,]*\(" )
for className in classNameRegex.findall( layout ) :
for className in classNameRegex.findall( layout.repr ) :
moduleName = className.partition( "." )[0]
if moduleName not in imported :
exec( "import %s" % moduleName, contextDict, contextDict )
imported.add( moduleName )

return eval( layout, contextDict, contextDict )

## Saves all layouts whose name matches the optional regular expression into the file object
# specified. If the file is later evaluated during application startup, it will reregister
# the layouts with the application.
## \todo Remove this method and follow the model in Bookmarks.py, where user bookmarks
# are saved automatically. This wasn't possible when Layouts.py was first introduced,
# because at that point in time, the Layouts class didn't have access to an application.
def save( self, fileObject, nameRegex = None ) :

# decide what to write
namesToWrite = []
for name in self.names() :
if nameRegex.match( name ) or nameRegex is None :
namesToWrite.append( name )

# write the necessary import statement and acquire the layouts
fileObject.write( "import GafferUI\n\n" )
fileObject.write( "layouts = GafferUI.Layouts.acquire( application )\n\n" )

# finally write out the layouts
for name in namesToWrite :
fileObject.write( "layouts.add( {0}, {1} )\n\n".format( repr( name ), repr( self.__namedLayouts[name] ) ) )

# tidy up by deleting the temporary variable, keeping the namespace clean for
# subsequently executed config files.
fileObject.write( "del layouts\n" )
return eval( layout.repr, contextDict, contextDict )

## The Editor factory provides access to every single registered subclass of
# editor, but specific applications may wish to only provide a subset of those
Expand All @@ -157,3 +150,16 @@ def deregisterEditor( self, editorName ) :
def registeredEditors( self ) :

return self.__registeredEditors

def __save( self ) :

f = open( os.path.join( self.__applicationRoot().preferencesLocation(), "layouts.py" ), "w" )
f.write( "# This file was automatically generated by Gaffer.\n" )
f.write( "# Do not edit this file - it will be overwritten.\n\n" )

f.write( "import GafferUI\n\n" )
f.write( "layouts = GafferUI.Layouts.acquire( application )\n" )

for name, layout in self.__namedLayouts.items() :
if layout.persistent :
f.write( "layouts.add( {0}, {1}, persistent = True )\n".format( repr( name ), repr( layout.repr ) ) )
84 changes: 84 additions & 0 deletions python/GafferUITest/LayoutsTest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
##########################################################################
#
# Copyright (c) 2018, Image Engine Design Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided with
# the distribution.
#
# * Neither the name of John Haddon nor the names of
# any other contributors to this software may be used to endorse or
# promote products derived from this software without specific prior
# written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
##########################################################################

import Gaffer
import GafferUI
import GafferUITest

class LayoutsTest( GafferUITest.TestCase ) :

def testAcquire( self ) :

a = Gaffer.ApplicationRoot( "testApp" )
self.assertIsInstance( GafferUI.Layouts.acquire( a ), GafferUI.Layouts )
self.assertIs( GafferUI.Layouts.acquire( a ), GafferUI.Layouts.acquire( a ) )

def testAddAndRemove( self ) :

a = Gaffer.ApplicationRoot( "testApp" )
l = GafferUI.Layouts.acquire( a )
self.assertEqual( l.names(), [] )

l.add( "JustTheGraphEditor", "GafferUI.GraphEditor( script )" )
self.assertEqual( l.names(), [ "JustTheGraphEditor" ] )

l.add( "JustTheNodeEditor", "GafferUI.NodeEditor( script )" )
self.assertEqual( l.names(), [ "JustTheGraphEditor", "JustTheNodeEditor" ] )

l.remove( "JustTheGraphEditor" )
self.assertEqual( l.names(), [ "JustTheNodeEditor" ] )

l.remove( "JustTheNodeEditor" )
self.assertEqual( l.names(), [] )

def testPersistence( self ) :

a = Gaffer.ApplicationRoot( "testApp" )
l = GafferUI.Layouts.acquire( a )
self.assertEqual( l.names(), [] )

l.add( "JustTheGraphEditor", "GafferUI.GraphEditor( script )" )
self.assertEqual( l.names(), [ "JustTheGraphEditor" ] )
self.assertEqual( l.names( persistent = False ), [ "JustTheGraphEditor" ] )
self.assertEqual( l.names( persistent = True ), [] )

l.add( "JustTheNodeEditor", "GafferUI.NodeEditor( script )", persistent = True )
self.assertEqual( l.names(), [ "JustTheGraphEditor", "JustTheNodeEditor" ] )
self.assertEqual( l.names( persistent = False ), [ "JustTheGraphEditor" ] )
self.assertEqual( l.names( persistent = True ), [ "JustTheNodeEditor" ] )

if __name__ == "__main__":
unittest.main()
1 change: 1 addition & 0 deletions python/GafferUITest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
from ErrorDialogueTest import ErrorDialogueTest
from WidgetAlgoTest import WidgetAlgoTest
from BackupsTest import BackupsTest
from LayoutsTest import LayoutsTest

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

0 comments on commit 4ce6414

Please sign in to comment.