diff --git a/include/Gaffer/ScriptNode.h b/include/Gaffer/ScriptNode.h index 5cc7804167e..77a3e63376e 100644 --- a/include/Gaffer/ScriptNode.h +++ b/include/Gaffer/ScriptNode.h @@ -194,6 +194,10 @@ class ScriptNode : public Node /// operations. StringPlug *fileNamePlug(); const StringPlug *fileNamePlug() const; + /// Returns a plug which is used to flag when the script has had changes + /// made since the last call to save(). + BoolPlug *unsavedChangesPlug(); + const BoolPlug *unsavedChangesPlug() const; /// Loads the script specified in the filename plug. virtual void load(); /// Saves the script to the file specified by the filename plug. @@ -243,13 +247,13 @@ class ScriptNode : public Node ScriptExecutedSignal m_scriptExecutedSignal; ScriptEvaluatedSignal m_scriptEvaluatedSignal; - - StringPlugPtr m_fileNamePlug; - + ContextPtr m_context; void childRemoved( GraphComponent *parent, GraphComponent *child ); void plugSet( Plug *plug ); + + static size_t g_firstPlugIndex; }; diff --git a/python/GafferTest/ScriptNodeTest.py b/python/GafferTest/ScriptNodeTest.py index 3dc21fb1a27..f4287db94ca 100644 --- a/python/GafferTest/ScriptNodeTest.py +++ b/python/GafferTest/ScriptNodeTest.py @@ -720,6 +720,59 @@ def testLoadingMovedScriptDoesntKeepOldFileName( self ) : s.load() self.assertEqual( s["fileName"].getValue(), "/tmp/test2.gfr" ) + + def testUnsavedChanges( self ) : + + s = Gaffer.ScriptNode() + + self.assertEqual( s["unsavedChanges"].getValue(), False ) + + s["node"] = GafferTest.AddNode() + self.assertEqual( s["unsavedChanges"].getValue(), True ) + + s["fileName"].setValue( "/tmp/test.gfr" ) + s.save() + self.assertEqual( s["unsavedChanges"].getValue(), False ) + + s["node"]["op1"].setValue( 10 ) + self.assertEqual( s["unsavedChanges"].getValue(), True ) + + s.save() + self.assertEqual( s["unsavedChanges"].getValue(), False ) + + with Gaffer.UndoContext( s ) : + s["node"]["op1"].setValue( 20 ) + self.assertEqual( s["unsavedChanges"].getValue(), True ) + + s.save() + self.assertEqual( s["unsavedChanges"].getValue(), False ) + + s.undo() + self.assertEqual( s["unsavedChanges"].getValue(), True ) + + s.save() + self.assertEqual( s["unsavedChanges"].getValue(), False ) + + s.redo() + self.assertEqual( s["unsavedChanges"].getValue(), True ) + + s.save() + self.assertEqual( s["unsavedChanges"].getValue(), False ) + + s["node2"] = GafferTest.AddNode() + self.assertEqual( s["unsavedChanges"].getValue(), True ) + + s.save() + self.assertEqual( s["unsavedChanges"].getValue(), False ) + + s["node2"]["op1"].setInput( s["node"]["sum"] ) + self.assertEqual( s["unsavedChanges"].getValue(), True ) + + s.save() + self.assertEqual( s["unsavedChanges"].getValue(), False ) + + s.load() + self.assertEqual( s["unsavedChanges"].getValue(), False ) def tearDown( self ) : diff --git a/python/GafferUI/ApplicationMenu.py b/python/GafferUI/ApplicationMenu.py index 3378656de5d..231444cbbad 100644 --- a/python/GafferUI/ApplicationMenu.py +++ b/python/GafferUI/ApplicationMenu.py @@ -54,7 +54,26 @@ def quit( menu ) : scriptWindow = menu.ancestor( GafferUI.ScriptWindow ) application = scriptWindow.scriptNode().ancestor( Gaffer.ApplicationRoot.staticTypeId() ) - ## \todo Check scripts aren't modified + unsavedNames = [] + for script in application["scripts"].children() : + if script["unsavedChanges"].getValue() : + f = script["fileName"].getValue() + f = f.rpartition( "/" )[2] if f else "untitled" + unsavedNames.append( f ) + + if unsavedNames : + + dialogue = GafferUI.ConfirmationDialogue( + "Discard Unsaved Changes?", + "The following files have unsaved changes : \n\n" + + "\n".join( [ " - " + n for n in unsavedNames ] ) + + "\n\nDo you want to discard the changes and quit?", + confirmLabel = "Discard and Quit" + ) + + if not dialogue.waitForConfirmation() : + return + for script in application["scripts"].children() : application["scripts"].removeChild( script ) @@ -105,4 +124,4 @@ def __savePreferences( button ) : application = scriptWindow.scriptNode().ancestor( Gaffer.ApplicationRoot.staticTypeId() ) application.savePreferences() button.ancestor( type=GafferUI.Window ).setVisible( False ) - \ No newline at end of file + diff --git a/python/GafferUI/ScriptNodeUI.py b/python/GafferUI/ScriptNodeUI.py index 4ed63a20f17..765c7127232 100644 --- a/python/GafferUI/ScriptNodeUI.py +++ b/python/GafferUI/ScriptNodeUI.py @@ -1,6 +1,7 @@ ########################################################################## # # Copyright (c) 2012, John Haddon. All rights reserved. +# Copyright (c) 2013, 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 @@ -37,8 +38,14 @@ import Gaffer import GafferUI +GafferUI.PlugValueWidget.registerCreator( + Gaffer.ScriptNode.staticTypeId(), + "unsavedChanges", + None +) + GafferUI.PlugValueWidget.registerCreator( Gaffer.ScriptNode.staticTypeId(), "frameRange", GafferUI.CompoundNumericPlugValueWidget -) \ No newline at end of file +) diff --git a/python/GafferUI/ScriptWindow.py b/python/GafferUI/ScriptWindow.py index a87748b12fb..dcfa417bd2a 100644 --- a/python/GafferUI/ScriptWindow.py +++ b/python/GafferUI/ScriptWindow.py @@ -87,6 +87,21 @@ def getLayout( self ) : return self.__listContainer[1] + def _acceptsClose( self ) : + + if not self.__script["unsavedChanges"].getValue() : + return True + + f = self.__script["fileName"].getValue() + f = f.rpartition( "/" )[2] if f else "untitled" + + dialogue = GafferUI.ConfirmationDialogue( + "Discard Unsaved Changes?", + "The file %s has unsaved changes. Do you want to discard them?" % f, + confirmLabel = "Discard" + ) + return dialogue.waitForConfirmation() + def __closed( self, widget ) : scriptParent = self.__script.parent() @@ -95,7 +110,7 @@ def __closed( self, widget ) : def __scriptPlugChanged( self, plug ) : - if plug.isSame( self.__script["fileName"] ) : + if plug.isSame( self.__script["fileName"] ) or plug.isSame( self.__script["unsavedChanges"] ) : self.__updateTitle() def __updateTitle( self ) : @@ -107,8 +122,10 @@ def __updateTitle( self ) : else : d, n, f = f.rpartition( "/" ) d = " - " + d - - self.setTitle( "Gaffer : %s %s" % ( f, d ) ) + + u = " *" if self.__script["unsavedChanges"].getValue() else "" + + self.setTitle( "Gaffer : %s%s%s" % ( f, u, d ) ) __instances = [] # weak references to all instances - used by acquire() ## Returns the ScriptWindow for the specified script, creating one diff --git a/src/Gaffer/Action.cpp b/src/Gaffer/Action.cpp index c5acce128e5..3e131e757ae 100644 --- a/src/Gaffer/Action.cpp +++ b/src/Gaffer/Action.cpp @@ -65,11 +65,19 @@ void Action::enact( GraphComponentPtr subject, const Function &doFn, const Funct ActionPtr a = new Action( doFn, undoFn ); a->doAction(); s->m_actionAccumulator->push_back( a ); + { + UndoContext undoDisabled( s, UndoContext::Disabled ); + s->unsavedChangesPlug()->setValue( true ); + } s->actionSignal()( s, a.get(), Do ); } else { doFn(); + if( s && subject != s->unsavedChangesPlug() ) + { + s->unsavedChangesPlug()->setValue( true ); + } } } diff --git a/src/Gaffer/ScriptNode.cpp b/src/Gaffer/ScriptNode.cpp index e451e6db2d8..da55fc35eb8 100644 --- a/src/Gaffer/ScriptNode.cpp +++ b/src/Gaffer/ScriptNode.cpp @@ -60,11 +60,15 @@ GAFFER_DECLARECONTAINERSPECIALISATIONS( ScriptContainer, ScriptContainerTypeId ) IE_CORE_DEFINERUNTIMETYPED( ScriptNode ); +size_t ScriptNode::g_firstPlugIndex = 0; + ScriptNode::ScriptNode( const std::string &name ) : Node( name ), m_selection( new StandardSet ), m_undoIterator( m_undoList.end() ), m_context( new Context ) { - m_fileNamePlug = new StringPlug( "fileName", Plug::In, "", Plug::Default & ~Plug::Serialisable ); - addChild( m_fileNamePlug ); + storeIndexOfNextChild( g_firstPlugIndex ); + + addChild( new StringPlug( "fileName", Plug::In, "", Plug::Default & ~Plug::Serialisable ) ); + addChild( new BoolPlug( "unsavedChanges", Plug::In, false, Plug::Default & ~Plug::Serialisable ) ); CompoundPlugPtr frameRangePlug = new CompoundPlug( "frameRange", Plug::In ); IntPlugPtr frameStartPlug = new IntPlug( "start", Plug::In, 1 ); @@ -133,6 +137,10 @@ void ScriptNode::undo() for( ActionVector::reverse_iterator it=(*m_undoIterator)->rbegin(); it!=(*m_undoIterator)->rend(); it++ ) { (*it)->undoAction(); + { + UndoContext undoDisabled( this, UndoContext::Disabled ); + unsavedChangesPlug()->setValue( true ); + } actionSignal()( this, it->get(), Action::Undo ); } } @@ -151,6 +159,10 @@ void ScriptNode::redo() for( ActionVector::iterator it=(*m_undoIterator)->begin(); it!=(*m_undoIterator)->end(); it++ ) { (*it)->doAction(); + { + UndoContext undoDisabled( this, UndoContext::Disabled ); + unsavedChangesPlug()->setValue( true ); + } actionSignal()( this, it->get(), Action::Redo ); } m_undoIterator++; @@ -227,14 +239,24 @@ void ScriptNode::deleteNodes( Node *parent, const Set *filter ) StringPlug *ScriptNode::fileNamePlug() { - return m_fileNamePlug; + return getChild( g_firstPlugIndex ); } const StringPlug *ScriptNode::fileNamePlug() const { - return m_fileNamePlug; + return getChild( g_firstPlugIndex ); } +BoolPlug *ScriptNode::unsavedChangesPlug() +{ + return getChild( g_firstPlugIndex + 1 ); +} + +const BoolPlug *ScriptNode::unsavedChangesPlug() const +{ + return getChild( g_firstPlugIndex + 1 ); +} + void ScriptNode::execute( const std::string &pythonScript, Node *parent ) { throw IECore::Exception( "Cannot execute scripts on a ScriptNode not created in Python." ); @@ -282,22 +304,22 @@ const Context *ScriptNode::context() const IntPlug *ScriptNode::frameStartPlug() { - return getChild( "frameRange" )->getChild( "start" ); + return getChild( g_firstPlugIndex + 2 )->getChild( 0 ); } const IntPlug *ScriptNode::frameStartPlug() const { - return getChild( "frameRange" )->getChild( "start" ); + return getChild( g_firstPlugIndex + 2 )->getChild( 0 ); } IntPlug *ScriptNode::frameEndPlug() { - return getChild( "frameRange" )->getChild( "end" ); + return getChild( g_firstPlugIndex + 2 )->getChild( 1 ); } const IntPlug *ScriptNode::frameEndPlug() const { - return getChild( "frameRange" )->getChild( "end" ); + return getChild( g_firstPlugIndex + 2 )->getChild( 1 ); } void ScriptNode::childRemoved( GraphComponent *parent, GraphComponent *child ) diff --git a/src/GafferBindings/ScriptNodeBinding.cpp b/src/GafferBindings/ScriptNodeBinding.cpp index 41cbc24695f..22d4002dd33 100644 --- a/src/GafferBindings/ScriptNodeBinding.cpp +++ b/src/GafferBindings/ScriptNodeBinding.cpp @@ -127,6 +127,9 @@ class ScriptNodeWrapper : public NodeWrapper deleteNodes(); execute( s ); + + UndoContext undoDisabled( this, UndoContext::Disabled ); + unsavedChangesPlug()->setValue( false ); } virtual void save() const @@ -146,6 +149,9 @@ class ScriptNodeWrapper : public NodeWrapper { throw IECore::IOException( "Failed to write to \"" + fileName + "\"" ); } + + UndoContext undoDisabled( const_cast( this ), UndoContext::Disabled ); + const_cast( unsavedChangesPlug() )->setValue( false ); } private :