Skip to content

Commit

Permalink
Merge pull request #5564 from johnhaddon/dragDrop
Browse files Browse the repository at this point in the history
GraphEditor : Create SceneReader, ImageReader or Reference nodes from file drag & drop
  • Loading branch information
johnhaddon authored Nov 28, 2023
2 parents faffd19 + 4700be0 commit ae89078
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 3 deletions.
3 changes: 3 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Features
Improvements
------------

- GraphEditor : Added drag & drop of files into the graph editor, automatically creating a SceneReader, ImageReader or Reference node as appropriate.
- ImageTransform, Resample : Improved performance for non-separable filters without scaling, with 2-6x speedups in some benchmark cases.
- Outputs : Included `renderPass` in the filename for newly created Arnold, Cycles and 3Delight outputs. Allowing rendered images to be written to a specific directory based on the name of the current render pass.
- GUI Config : Included `renderPass` in the default filename when writing ass files from an ArnoldRender node.
Expand All @@ -23,6 +24,7 @@ Fixes

- InteractiveRender : Fixed unnecessary updates to encapsulated locations when deforming an unrelated object.
- InteractiveArnoldRender : Fixed creation of new Catalogue image when editing output metadata or pixel filter.
- GraphEditor : Fixed error caused by additional connections to `dragEnterSignal()`.
- Windows `Scene/OpenGL/Shader` Menu : Removed `\` at the beginning of menu items.
- Arnold :
- Fixed translation of `UsdPreviewSurface` normal maps.
Expand All @@ -36,6 +38,7 @@ API
- SceneAlgo :
- Added `history()` overload for returning computation history independent of a scene location, this is useful when generating history from the globals.
- Added `optionHistory()` method which returns a computation history for one specific option.
- Widget : Added handling for drag & drop from an external application via the existing `dragEnterSignal()`, `dragMoveSignal()`, `dragLeaveSignal()` and `dropSignal()` signals.

Build
-----
Expand Down
1 change: 1 addition & 0 deletions include/GafferUI/View.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
#include "boost/regex.hpp"

#include <functional>
#include <unordered_map>

namespace Gaffer
{
Expand Down
10 changes: 8 additions & 2 deletions python/GafferUI/GraphEditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def __init__( self, scriptNode, **kw ) :
self.dragEnterSignal().connect( Gaffer.WeakMethod( self.__dragEnter ), scoped = False )
self.dragLeaveSignal().connect( Gaffer.WeakMethod( self.__dragLeave ), scoped = False )
self.dropSignal().connect( Gaffer.WeakMethod( self.__drop ), scoped = False )
self.__dragEnterPointer = None
self.__gadgetWidget.getViewportGadget().preRenderSignal().connect( Gaffer.WeakMethod( self.__preRender ), scoped = False )

with GafferUI.ListContainer( borderWidth = 8, spacing = 0 ) as overlay :
Expand Down Expand Up @@ -511,8 +512,12 @@ def __dragEnter( self, widget, event ) :

def __dragLeave( self, widget, event ) :

GafferUI.Pointer.setCurrent( self.__dragEnterPointer )
return True
if self.__dragEnterPointer is not None :
GafferUI.Pointer.setCurrent( self.__dragEnterPointer )
self.__dragEnterPointer = None
return True

return False

def __drop( self, widget, event ) :

Expand All @@ -523,6 +528,7 @@ def __drop( self, widget, event ) :
if dropNodes :
self.graphGadget().setRoot( dropNodes[0].parent() )
self.__frame( dropNodes, at = imath.V2f( event.line.p0.x, event.line.p0.y ) )
self.__dragEnterPointer = None
return True

return False
Expand Down
1 change: 1 addition & 0 deletions python/GafferUI/MultiLineTextWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ def __dragLeave( self, widget, event ) :
def __drop( self, widget, event ) :

self.insertText( self.__dropText( event.data ) )
return True

class _PlainTextEdit( QtWidgets.QPlainTextEdit ) :

Expand Down
4 changes: 3 additions & 1 deletion python/GafferUI/PlugValueWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -884,8 +884,10 @@ def __dragEnter( self, widget, event ) :

if isinstance( event.sourceWidget, GafferUI.PlugValueWidget ) :
sourcePlugValueWidget = event.sourceWidget
else :
elif event.sourceWidget is not None :
sourcePlugValueWidget = event.sourceWidget.ancestor( GafferUI.PlugValueWidget )
else :
sourcePlugValueWidget = None

if sourcePlugValueWidget is not None and sourcePlugValueWidget.getPlugs() & self.getPlugs() :
return False
Expand Down
127 changes: 127 additions & 0 deletions python/GafferUI/Widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ def dragEnterSignal( self ) :
self._dragEnterSignal = GafferUI.WidgetEventSignal()
self.__ensureEventFilter()
self.__ensureMouseTracking()
self._qtWidget().setAcceptDrops( True )
return self._dragEnterSignal

## This signal is emitted when a drag is moving within a Widget which
Expand Down Expand Up @@ -999,6 +1000,10 @@ def __init__( self ) :
QtCore.QEvent.Hide,
QtCore.QEvent.ContextMenu,
QtCore.QEvent.ParentChange,
QtCore.QEvent.DragEnter,
QtCore.QEvent.DragMove,
QtCore.QEvent.DragLeave,
QtCore.QEvent.Drop,
) )

def eventFilter( self, qObject, qEvent ) :
Expand Down Expand Up @@ -1066,6 +1071,22 @@ def eventFilter( self, qObject, qEvent ) :

return self.__parentChange( qObject, qEvent )

elif qEventType==qEvent.DragEnter :

return self.__foreignDragEnter( qObject, qEvent )

elif qEventType==qEvent.DragMove :

return self.__foreignDragMove( qObject, qEvent )

elif qEventType==qEvent.DragLeave :

return self.__foreignDragLeave( qObject, qEvent )

elif qEventType==qEvent.Drop :

return self.__foreignDrop( qObject, qEvent )

return False

def __toolTip( self, qObject, qEvent ) :
Expand Down Expand Up @@ -1519,6 +1540,112 @@ def __dragKeyPress( self, qObject, qKeyEvent ) :
self.__endDrag( self.__dragDropEvent.sourceWidget._qtWidget(), qEvent )
return True

# Although we have our own drag and drop system (see above), we also
# accept drops from Qt's system so that we can accept "foreign" drags
# from outside the application.
#
# > Note : We never _start_ a drag using Qt's system.

def __foreignDragEnter( self, qObject, qEvent ) :

widget = Widget._owner( qObject )
if widget is None or widget._dragEnterSignal is None :
return False

dragDropEvent = self.__foreignDragDropEvent( qEvent )
if dragDropEvent is None :
return False

if widget._dragEnterSignal( widget, dragDropEvent ) :
qEvent.acceptProposedAction()
return True

return False

def __foreignDragMove( self, qObject, qEvent ) :

widget = Widget._owner( qObject )
if widget is None or widget._dragMoveSignal is None :
return False

dragDropEvent = self.__foreignDragDropEvent( qEvent )
if dragDropEvent is None :
return False

widget._dragMoveSignal( widget, dragDropEvent )
qEvent.accept()

return True

def __foreignDragLeave( self, qObject, qEvent ) :

widget = Widget._owner( qObject )
if widget is None or widget._dragLeaveSignal is None :
return False

# Qt doesn't provide buttons, position or modifiers
# for a leave event.
dragDropEvent = GafferUI.DragDropEvent(
GafferUI.DragDropEvent.Buttons.None_,
GafferUI.DragDropEvent.Buttons.None_,
IECore.LineSegment3f(
imath.V3f( 0, 0, 1 ),
imath.V3f( 0, 0, 0 )
),
GafferUI.DragDropEvent.Modifiers.None_,
)
dragDropEvent.sourceWidget = None
dragDropEvent.destinationWidget = widget

widget._dragLeaveSignal( widget, dragDropEvent )
qEvent.accept()
return True

def __foreignDrop( self, qObject, qEvent ) :

widget = Widget._owner( qObject )
if widget is None or widget._dropSignal is None :
return False

dragDropEvent = self.__foreignDragDropEvent( qEvent )
if dragDropEvent is None :
return False

if widget._dropSignal( widget, dragDropEvent ) :
qEvent.acceptProposedAction()
return True

return False

def __foreignDragDropEvent( self, qEvent ) :

if qEvent.mimeData().hasUrls() :
data = IECore.StringVectorData( [
url.toString( url.PrettyDecoded | url.PreferLocalFile )
for url in qEvent.mimeData().urls()
] )
elif qEvent.mimeData().hasText() :
data = IECore.StringData( qEvent.mimeData().text() )
else :
return None

cursorPos = imath.V2i( qEvent.pos().x(), qEvent.pos().y() )

dragDropEvent = GafferUI.DragDropEvent(
Widget._buttons( qEvent.mouseButtons() ),
Widget._buttons( qEvent.mouseButtons() ),
IECore.LineSegment3f(
imath.V3f( cursorPos.x, cursorPos.y, 1 ),
imath.V3f( cursorPos.x, cursorPos.y, 0 )
),
Widget._modifiers( qEvent.keyboardModifiers() ),
)
dragDropEvent.data = data
dragDropEvent.sourceWidget = None
dragDropEvent.destinationWidget = None

return dragDropEvent

# Maps the position of the supplied Qt mouse event into the coordinate
# space of the target Gaffer widget. This is required as certain widget
# configurations (eg: QTableView with a visible header) result in qEvent
Expand Down
125 changes: 125 additions & 0 deletions startup/gui/graphEditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,16 @@
##########################################################################

import functools
import pathlib
import re

import imath

import IECore

import Gaffer
import GafferUI
import GafferImage
import GafferScene
import GafferSceneUI
import GafferDispatch
Expand Down Expand Up @@ -120,3 +124,124 @@ def __connectionContextMenu( graphEditor, destinationPlug, menuDefinition ) :
GafferUI.GraphEditor.appendConnectionNavigationMenuDefinitions( graphEditor, destinationPlug, menuDefinition )

GafferUI.GraphEditor.connectionContextMenuSignal().connect( __connectionContextMenu, scoped = False )

##########################################################################
# File drop handler
##########################################################################

def __sceneFileHandler( fileName ) :

result = GafferScene.SceneReader()
result["fileName"].setValue( fileName )
return result

def __imageFileHandler( fileName ) :

result = GafferImage.ImageReader()
result["fileName"].setValue( fileName )
return result

def __referenceFileHandler( fileName ) :

# We need a temp ScriptNode to be able to call `load()`,
# but we can safely discard it afterwards and reparent
# the Reference somewhere else.
script = Gaffer.ScriptNode()
script["Reference"] = Gaffer.Reference()
script["Reference"].load( fileName )
return script["Reference"]

## \todo Maybe we should move this to GraphGadget and add a
# public API to allow people to register their own handlers?

__fileHandlers = {
"grf" : __referenceFileHandler
}

__fileHandlers.update( {
ext : __sceneFileHandler
for ext in GafferScene.SceneReader.supportedExtensions()
} )

__fileHandlers.update( {
ext : __imageFileHandler
for ext in GafferImage.ImageReader.supportedExtensions()
} )

def __dropHandler( s ) :

path = pathlib.Path( s )
if not path.suffix or not path.is_file() :
return None

handler = __fileHandlers.get( path.suffix[1:].lower() )
return functools.partial( handler, path ) if handler is not None else None

def __canDropFiles( graphEditor, event ) :

return (
isinstance( event.data, IECore.StringVectorData ) and
all( __dropHandler( s ) is not None for s in event.data ) and
not Gaffer.MetadataAlgo.readOnly( graphEditor.graphGadget().getRoot() ) and
not Gaffer.MetadataAlgo.getChildNodesAreReadOnly( graphEditor.graphGadget().getRoot() )
)

def __dragEnter( graphEditor, event ) :

return __canDropFiles( graphEditor, event )

def __drop( graphEditor, event ) :

if not __canDropFiles( graphEditor, event ) :
return False

graphGadget = graphEditor.graphGadget()

position = graphEditor.graphGadgetWidget().getViewportGadget().rasterToGadgetSpace(
imath.V2f( event.line.p0.x, event.line.p0.y ),
gadget = graphEditor.graphGadget()
).p0
position = imath.V2f( position.x, position.y )

nodes = Gaffer.StandardSet()
with Gaffer.UndoScope( graphEditor.scriptNode() ) :

for s in event.data :

node = __dropHandler( s )()

## \todo GraphComponent should either provide a utility
# to sanitise a name, or `setName()` should just sanitise
# the name automatically.
name = re.sub(
"[^A-Za-z_:0-9]",
"_",
pathlib.Path( s ).stem
)
if name[0].isdigit() :
name = "_" + name

node.setName( name )
graphGadget.getRoot().addChild( node )

nodeGadget = graphGadget.nodeGadget( node )
width = nodeGadget.bound().size().x
if len( nodes ) :
position.x += width / 2

graphGadget.setNodePosition( node, position )
position.x += 2 + width / 2

nodes.add( node )

graphEditor.scriptNode().selection().clear()
graphEditor.scriptNode().selection().add( nodes )

return True

def __graphEditorCreated( graphEditor ) :

graphEditor.dragEnterSignal().connect( __dragEnter, scoped = False )
graphEditor.dropSignal().connect( __drop, scoped = False )

GafferUI.GraphEditor.instanceCreatedSignal().connect( __graphEditorCreated, scoped = False )

0 comments on commit ae89078

Please sign in to comment.