Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Widget Editor #6175

Open
wants to merge 7 commits into
base: 1.5_maintenance
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions python/GafferUI/PythonEditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ def __dropText( self, widget, dragData ) :
return repr( dragData )
elif isinstance( dragData, IECore.Data ) and hasattr( dragData, "value" ) :
return repr( dragData.value )
elif isinstance( dragData, GafferUI.WidgetPath ) :
return repr( dragData )

return None

Expand Down
340 changes: 340 additions & 0 deletions python/GafferUI/WidgetEditor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,340 @@
##########################################################################
#
# Copyright (c) 2024, Cinesite VFX Ltd. 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 imath

import IECore

import Gaffer
import GafferUI

from Qt import QtCore
from Qt import QtWidgets
from Qt import QtGui

# A `QtCore.Object` for capturing all mouse clicks before any UI elements
# get the click event so we can identify the widget clicked on.
class _ButtonPressFilter( QtCore.QObject ) :

def __init__( self ) :

QtCore.QObject.__init__( self )

self.__widgetPickedSignal = Gaffer.Signals.Signal1()

def eventFilter( self, obj, event ) :

if event.type() == QtCore.QEvent.MouseButtonPress :
widget = GafferUI.Widget.widgetAt( GafferUI.Widget.mousePosition() )

if widget is not None :
self.__widgetPickedSignal( widget )

return True

return False

# A signal emitted whenver a widget is picked. Slots should have the
# signature slot( widget ).
def widgetPickedSignal( self ) :

return self.__widgetPickedSignal


class WidgetPath( Gaffer.Path ) :
# A `Gaffer.Path` to a `GafferUI.Widget` rooted at `rootWidget`. Path
# entries are string representations of the integer index into the parent
# widget's children for the widget, or the name of the parent's member variable
# for the widget.

def __init__( self, scriptNode, path = None, root = "/", filter = None ) :

Gaffer.Path.__init__( self, path = path, root = root, filter = filter )

self.__scriptNode = scriptNode

def copy( self ) :

return self.__class__( self.__scriptNode, self[:], self.root(), self.getFilter() )

def isValid( self, canceller = None ) :

return self.widget() is not None

def isLeaf( self, canceller = None ) :

return self.isValid() and len( self.__childWidgets( self.widget() ) ) == 0

def propertyNames( self ) :

return Gaffer.Path.propertyNames() + [
"widgetEditor:name",
"widgetEditor:widget",
"widgetEditor:widgetType",
"widgetEditor:width",
"widgetEditor:height",
"widgetEditor:minimumWidth",
"widgetEditor:minimumHeight",
"widgetEditor:maximumWidth",
"widgetEditor:maximumHeight",
]

def property( self, name, canceller = None ) :

result = Gaffer.Path.property( self, name )

if result is not None :
return result

widget = self.widget()
if widget is None :
return None

if name == "widgetEditor:name" :
return self[-1]
elif name == "widgetEditor:widget" :
return widget
elif name == "widgetEditor:widgetType" :
return type( widget ).__name__
elif name == "widgetEditor:width" :
return widget.size().x
elif name == "widgetEditor:height" :
return widget.size().y
elif name == "widgetEditor:minimumWidth" :
return widget._qtWidget().minimumwidth()
elif name == "widgetEditor:minimumHeight" :
return widget._qtWidget().minimumheight()
elif name == "widgetEditor:maximumWidth" :
return widget._qtWidget().maximumWidth()
elif name == "widgetEditor:maximumHeight" :
return widget._qtWidget().maximumHeight()

def widget( self ) :
# Returns the `GafferUI.Widget` for this path.

if self.__scriptNode is None :
return None

widget = GafferUI.ScriptWindow.acquire( self.__scriptNode )
assert( widget is not None )
# A path with a single element is the top level `ScriptWindow`, start looking below that.
for i in self[1:] :
childWidgets = self.__childWidgets( widget )
if i.isnumeric():
widget = widget[ int( i ) ]
else :
widget = childWidgets[i]

return widget

def scriptNode( self ) :

return self.__scriptNode

def _children( self, canceller ) :

if not self.isValid() or self.isLeaf() :
return []

if len( self ) == 0 :
return [ WidgetPath( self.__scriptNode, self[:] + ["scriptWindow"], self.root(), self.getFilter() ) ]

childWidgets = self.__childWidgets( self.widget() )
return [
WidgetPath( self.__scriptNode, self[:] + [ k ], self.root(), self.getFilter() )
for k in childWidgets.keys()
]

def __repr__( self ) :

result = "GafferUI.ScriptWindow.acquire(root)"
for p in self[1:] :
if p.isnumeric() :
result += f"[{p}]"
else :
result += "." + p

return result

def __isAggregate( self, widget ) :

return hasattr( widget, "__getitem__" ) and hasattr( widget, "__len__" )

def __childWidgets( self, widget ) :

result = {}

visited = set()

if self.__isAggregate( widget ) :
for i in range( 0, len( widget ) ) :
if isinstance( widget[i], GafferUI.Widget ) and widget[i] not in visited :
result[str( i )] = widget[i]
visited.add( widget[i] )

for a in dir( widget ) :
if isinstance( getattr( widget, a ), GafferUI.Widget ) and getattr( widget, a ) not in visited :
result[a] = getattr( widget, a )
visited.add( getattr( widget, a ) )

return result

class WidgetEditor( GafferUI.Editor ) :

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

column = GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Vertical, borderWidth = 4, spacing = 4 )
GafferUI.Editor.__init__( self, column, scriptNode, **kw )

self.__scriptNode = scriptNode

with column :

with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) :

self.__pickButton = GafferUI.Button( "Pick Widget" )
self.__pickButton.buttonReleaseSignal().connect( Gaffer.WeakMethod( self.__pickButtonReleased ) )
self.__pickButton._qtWidget().setMaximumWidth( 150 )

self.__delayedPickButton = GafferUI.Button( "Pick Widget (3 sec delay)" )
self.__delayedPickButton.buttonReleaseSignal().connect( Gaffer.WeakMethod( self.__delayedPickButtonReleased ) )
self.__delayedPickButton._qtWidget().setMaximumWidth( 150 )

self.__timerWidget = GafferUI.BusyWidget( size = 25, busy = False )

self.__widgetNameColumn = GafferUI.PathListingWidget.StandardColumn( "Name", "widgetEditor:name" )

self.__widgetListingWidget = GafferUI.PathListingWidget(
WidgetPath( None ), # temp until we make a WidgetPath
columns = (
self.__widgetNameColumn,
GafferUI.PathListingWidget.StandardColumn( "Type", "widgetEditor:widgetType" ),
GafferUI.PathListingWidget.StandardColumn( "Width", "widgetEditor:width" ),
GafferUI.PathListingWidget.StandardColumn( "Height", "widgetEditor:height" ),
GafferUI.PathListingWidget.StandardColumn( "Minimum Width", "widgetEditor:minumumWidth" ),
GafferUI.PathListingWidget.StandardColumn( "Minimum Height", "widgetEditor:minumumHeight" ),
GafferUI.PathListingWidget.StandardColumn( "Maximum Width", "widgetEditor:maximumWidth" ),
GafferUI.PathListingWidget.StandardColumn( "Maximum Height", "widgetEditor:maximumHeight" ),
),
selectionMode = GafferUI.PathListingWidget.SelectionMode.Row,
displayMode = GafferUI.PathListingWidget.DisplayMode.Tree
)

self.__widgetListingWidget.dragBeginSignal().connectFront( Gaffer.WeakMethod( self.__dragBegin ) )
self.__widgetListingWidget.selectionChangedSignal().connect( Gaffer.WeakMethod( self.__selectionChanged ) )

self.visibilityChangedSignal().connect( Gaffer.WeakMethod( self.__visibilityChanged ) )

self.__buttonPressFilter = _ButtonPressFilter()
self.__buttonPressFilter.widgetPickedSignal().connect( Gaffer.WeakMethod( self.__widgetPicked ) )

self.__highlightEffects = {}

def __repr__( self ) :

return "GafferUI.WidgetEditor( scriptNode )"

def __dragBegin( self, widget, event ) :

path = self.__widgetListingWidget.pathAt( imath.V2f( event.line.p0.x, event.line.p0.y ) )

column = self.__widgetListingWidget.columnAt( imath.V2f( event.line.p0.x, event.line.p0.y ) )

if column == self.__widgetNameColumn :
GafferUI.Pointer.setCurrent( "nodes" )
return path

def __selectionChanged( self, pathListing ) :

for p, e in self.__highlightEffects.items() :
oldEffect, newEffect = e
p.property( "widgetEditor:widget" )._qtWidget().setGraphicsEffect( oldEffect )

self.__highlightEffects = {}

selection = pathListing.getSelectedPaths()

for p in selection :
w = p.property( "widgetEditor:widget" )
if w is not None :
oldEffect = w._qtWidget().graphicsEffect()

newEffect = QtWidgets.QGraphicsColorizeEffect()
newEffect.setColor( QtGui.QColor( 119, 156, 189, 255 ) )
newEffect.setStrength( 0.85 )

self.__highlightEffects[p] = ( oldEffect, newEffect )
w._qtWidget().setGraphicsEffect( newEffect )

def __installEventFilter( self ) :

self.__timerWidget.setBusy( False )
QtWidgets.QApplication.instance().installEventFilter( self.__buttonPressFilter )

def __pickButtonReleased( self, *unused ) :

self.__installEventFilter()

def __delayedPickButtonReleased( self, *unused ) :

self.__timerWidget.setBusy( True )
QtCore.QTimer.singleShot( 3000, self.__installEventFilter )

def __visibilityChanged( self, widget ) :

if widget.visible() and self.__widgetListingWidget.getPath().scriptNode() is None :
self.__widgetListingWidget.setPath( WidgetPath( self.__scriptNode ) )

def __widgetPathWalk( self, path, targetWidget ) :

for c in path.children() :
widget = c.property( "widgetEditor:widget" )
if widget == targetWidget :
return c
elif widget.isAncestorOf( targetWidget ) :
return self.__widgetPathWalk( c, targetWidget )

def __widgetPicked( self, widget ) :

path = self.__widgetPathWalk( self.__widgetListingWidget.getPath(), widget )
pm = IECore.PathMatcher()
pm.addPath( str( path ) )
self.__widgetListingWidget.setSelection( pm, True )
QtWidgets.QApplication.instance().removeEventFilter( self.__buttonPressFilter )


IECore.registerRunTimeTyped( WidgetPath, typeName = "GafferUI::WidgetPath" )
GafferUI.Editor.registerType( "WidgetEditor", WidgetEditor )
3 changes: 3 additions & 0 deletions python/GafferUI/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@ def __shiboken() :
from .TweakPlugValueWidget import TweakPlugValueWidget
from .PlugPopup import PlugPopup
from .OptionalValuePlugValueWidget import OptionalValuePlugValueWidget
from .WidgetEditor import WidgetEditor
from .WidgetEditor import WidgetPath


# and then specific node uis

Expand Down
1 change: 1 addition & 0 deletions startup/gui/layouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
layouts.registerEditor( "ImageInspector")
layouts.registerEditor( "RenderPassEditor" )
layouts.registerEditor( "AttributeEditor" )
layouts.registerEditor( "WidgetEditor" )

# Register some predefined layouts
#
Expand Down