diff --git a/Changes.md b/Changes.md index 1650bd6d098..b836392d358 100644 --- a/Changes.md +++ b/Changes.md @@ -10,6 +10,7 @@ Improvements - DeletePoints : Added modes for deleting points based on a list of ids. - Light Editor, Attribute Editor, Spreadsheet : Add original and current color swatches to color popups. - SceneView : Added fallback framing extents to create a reasonable view when `SceneGadget` is empty, for example if the grid is hidden. +- ColorChooser : Added an option to toggle the dynamic update of colors displayed in the slider and color field backgrounds. When enabled, the widget backgrounds update to show the color that will result from moving the indicator to a given position. When disabled, a static range of values is displayed instead. Fixes ----- diff --git a/python/GafferUI/ColorChooser.py b/python/GafferUI/ColorChooser.py index a4ff0e0f10a..d9f9f19d9bb 100644 --- a/python/GafferUI/ColorChooser.py +++ b/python/GafferUI/ColorChooser.py @@ -117,7 +117,7 @@ def _drawIndicator( painter, position ) : # A custom slider for drawing the backgrounds. class _ComponentSlider( GafferUI.Slider ) : - def __init__( self, color, component, **kw ) : + def __init__( self, color, component, dynamicBackground = True, **kw ) : GafferUI.Slider.__init__( self, 0.0, @@ -128,11 +128,19 @@ def __init__( self, color, component, **kw ) : self.color = color self.component = component + self.__dynamicBackground = dynamicBackground + self.__gradientToDraw = None + self.__size = self.size() # Sets the slider color in RGB space for RGBA channels, # HSV space for HSV channels and TMI space for TMI channels. def setColor( self, color ) : + if ( + ( self.__dynamicBackground and color != self.color ) or + ( not self.__dynamicBackground and self.component == "s" ) + ) : + self.__gradientToDraw = None self.color = color self._qtWidget().update() @@ -140,37 +148,59 @@ def getColor( self ) : return self.color - def _drawBackground( self, painter ) : + def setDynamicBackground( self, dynamicBackground ) : - size = self.size() - grad = QtGui.QLinearGradient( 0, 0, size.x, 0 ) + self.__dynamicBackground = dynamicBackground + self.__gradientToDraw = None + self._qtWidget().update() - displayTransform = self.displayTransform() + def getDynamicBackground( self ) : - if self.component == "a" : - c1 = imath.Color3f( 0 ) - c2 = imath.Color3f( 1 ) - else : - c1 = imath.Color3f( self.color[0], self.color[1], self.color[2] ) - c2 = imath.Color3f( self.color[0], self.color[1], self.color[2] ) - a = { "r" : 0, "g" : 1, "b" : 2, "h" : 0, "s" : 1, "v": 2, "t" : 0, "m" : 1, "i" : 2 }[self.component] - c1[a] = -1 if self.component in "tm" else 0 - c2[a] = 1 + return self.__dynamicBackground - numStops = max( 2, size.x // 2 ) - for i in range( 0, numStops ) : + def _drawBackground( self, painter ) : + + size = self.size() - t = float( i ) / (numStops-1) - c = c1 + (c2-c1) * t - if self.component in "hsv" : - c = c.hsv2rgb() - elif self.component in "tmi" : - c = _tmiToRGB( c ) + if self.__gradientToDraw is None or size != self.__size : + self.__gradientToDraw = QtGui.QLinearGradient( 0, 0, size.x, 0 ) - grad.setColorAt( t, self._qtColor( displayTransform( c ) ) ) + displayTransform = self.displayTransform() - brush = QtGui.QBrush( grad ) + if self.component == "a" : + c1 = imath.Color3f( 0 ) + c2 = imath.Color3f( 1 ) + else : + if self.__dynamicBackground : + c1 = imath.Color3f( self.color[0], self.color[1], self.color[2] ) + elif self.component in "rgbvi" : + c1 = imath.Color3f( 0 ) + elif self.component == "h" : + c1 = imath.Color3f( 0, 1, 1 ) + elif self.component == "s" : + c1 = imath.Color3f( self.color[0], 0, 1 ) + elif self.component in "tm" : + c1 = imath.Color3f( 0, 0, 0.5 ) + c2 = imath.Color3f( c1 ) + a = { "r" : 0, "g" : 1, "b" : 2, "h" : 0, "s" : 1, "v": 2, "t" : 0, "m" : 1, "i" : 2 }[self.component] + c1[a] = -1 if self.component in "tm" else 0 + c2[a] = 1 + + numStops = max( 2, size.x // 2 ) + for i in range( 0, numStops ) : + + t = float( i ) / (numStops-1) + c = c1 + (c2-c1) * t + if self.component in "hsv" : + c = c.hsv2rgb() + elif self.component in "tmi" : + c = _tmiToRGB( c ) + + self.__gradientToDraw.setColorAt( t, self._qtColor( displayTransform( c ) ) ) + + brush = QtGui.QBrush( self.__gradientToDraw ) painter.fillRect( 0, 0, size.x, size.y, brush ) + self.__size = size def _drawValue( self, painter, value, position, state ) : @@ -209,11 +239,12 @@ def _drawValue( self, painter, value, position, state ) : def _displayTransformChanged( self ) : GafferUI.Slider._displayTransformChanged( self ) + self.__gradientToDraw = None self._qtWidget().update() class _ColorField( GafferUI.Widget ) : - def __init__( self, color = imath.Color3f( 1.0 ), staticComponent = "v", **kw ) : + def __init__( self, color = imath.Color3f( 1.0 ), staticComponent = "v", dynamicBackground = True, **kw ) : GafferUI.Widget.__init__( self, QtWidgets.QWidget(), **kw ) @@ -232,6 +263,8 @@ def __init__( self, color = imath.Color3f( 1.0 ), staticComponent = "v", **kw ) self.__colorFieldToDraw = None self.setColor( color, staticComponent ) + self.setDynamicBackground( dynamicBackground ) + # Sets the color and the static component. `color` is in # RGB space for RGB static components, HSV space for # HSV static components and TMI space for TMI components. @@ -244,6 +277,16 @@ def getColor( self ) : return self.__color, self.__staticComponent + def setDynamicBackground( self, dynamicBackground ) : + + self.__dynamicBackground = dynamicBackground + self.__colorFieldToDraw = None + self._qtWidget().update() + + def getDynamicBackground( self ) : + + return self.__dynamicBackground + ## A signal emitted whenever a value has been changed. Slots should # have the signature slot( _ColorField, GafferUI.Slider.ValueChangedReason ) def valueChangedSignal( self ) : @@ -271,7 +314,11 @@ def __setColorInternal( self, color, staticComponent, reason ) : return zIndex = self.__zIndex() - if color[zIndex] != self.__color[zIndex] or staticComponent != self.__staticComponent : + if ( + staticComponent != self.__staticComponent or + ( self.__dynamicBackground and color[zIndex] != self.__color[zIndex] ) or + ( not self.__dynamicBackground and staticComponent == "h" ) + ) : self.__colorFieldToDraw = None self.__color = color @@ -430,10 +477,15 @@ def __drawBackground( self, painter ) : xIndex, yIndex = self.__xyIndices() zIndex = self.__zIndex() - staticValue = self.__color[zIndex] - c = imath.Color3f() - c[zIndex] = staticValue + if self.__dynamicBackground or self.__staticComponent == "h" : + c[zIndex] = self.__color[zIndex] + elif self.__staticComponent in "rgbtm" : + c[zIndex] = 0.0 + elif self.__staticComponent in "sv" : + c[zIndex] = 1.0 + elif self.__staticComponent == "i" : + c[zIndex] = 0.5 ColorSpace = enum.Enum( "ColorSpace", [ "RGB", "HSV", "TMI" ] ) if self.__staticComponent in "rgb" : @@ -835,6 +887,7 @@ def __init__( self, color=imath.Color3f( 1 ), **kw ) : self.__visibleComponentsChangedSignal = Gaffer.Signals.Signal1() self.__staticComponentChangedSignal = Gaffer.Signals.Signal1() self.__colorFieldVisibleChangedSignal = Gaffer.Signals.Signal1() + self.__dynamicSliderBackgroundsChangedSignal = Gaffer.Signals.Signal1() self.__optionsMenuSignal = Gaffer.Signals.Signal2() self.__colorFieldPrimaryIcon = GafferUI.Image( "colorFieldPrimaryIcon.png" ) @@ -920,6 +973,18 @@ def getColorFieldVisible( self ) : return self.__colorField.getVisible() + def setDynamicSliderBackgrounds( self, dynamic ) : + + for component, slider in self.__sliders.items() : + slider.setDynamicBackground( dynamic ) + self.__colorField.setDynamicBackground( dynamic ) + + self.__dynamicSliderBackgroundsChangedSignal( self ) + + def getDynamicSliderBackgrounds( self ) : + + return self.__sliders["r"].getDynamicBackground() + ## A signal emitted whenever the color is changed. Slots should # have the signature slot( ColorChooser, reason ). The reason # argument may be passed either a ColorChooser.ColorChangedReason, @@ -931,27 +996,28 @@ def colorChangedSignal( self ) : ## A signal emitted whenever the visible components are changed. Slots # should have the signature slot( ColorChooser ). - # `visibleComponents` is a string representing the components currently - # visible. def visibleComponentsChangedSignal( self ) : return self.__visibleComponentsChangedSignal ## A signal emitted whenever the static component is changed. Slots # should have the signature slot( ColorChooser ). - # `staticComponent` is a single character string representing the - # current static component. def staticComponentChangedSignal( self ) : return self.__staticComponentChangedSignal ## A signal emitted whenever the visibility of the color field changes. # Slots should have the signature slot( ColorChooser ). - # `visible` is a boolean representing the current visibility. def colorFieldVisibleChangedSignal( self ) : return self.__colorFieldVisibleChangedSignal + ## A signal emitted whenever the dynamic colors option is changed. + # Slots should have the signature slot( ColorChooser ). + def dynamicSliderBackgroundsChangedSignal( self ) : + + return self.__dynamicSliderBackgroundsChangedSignal + ## A signal emitted whenever the options menu is opened. # Slots should have the signature slot( ColorChooser, menuDefinition ) # and add menu items to `menuDefinition`. @@ -1018,6 +1084,19 @@ def __optionsMenuDefinition( self ) : } ) + result.append( "/__sliders__", { "divider": True, "label": "Sliders" } ) + + result.append( + "/Dynamic Backgrounds", + { + "command": Gaffer.WeakMethod( self.setDynamicSliderBackgrounds ), + "checkBox": self.getDynamicSliderBackgrounds(), + "description": """With Dynamic Backgrounds enabled, the backgrounds of the color sliders will +update to show the color that will result from moving the indicator to a given +position. When disabled, a static range of values is displayed.""" + } + ) + self.__optionsMenuSignal( self, result ) return result diff --git a/python/GafferUI/ColorChooserPlugValueWidget.py b/python/GafferUI/ColorChooserPlugValueWidget.py index a7e2a2fffc3..9640e4b3ecd 100644 --- a/python/GafferUI/ColorChooserPlugValueWidget.py +++ b/python/GafferUI/ColorChooserPlugValueWidget.py @@ -65,6 +65,10 @@ def __init__( self, plugs, **kw ) : if colorFieldVisible is not None : self.__colorChooser.setColorFieldVisible( colorFieldVisible ) + dynamicSliderBackgrounds = self.__colorChooserOption( "dynamicSliderBackgrounds" ) + if dynamicSliderBackgrounds is not None : + self.__colorChooser.setDynamicSliderBackgrounds( dynamicSliderBackgrounds ) + self.__colorChangedConnection = self.__colorChooser.colorChangedSignal().connect( Gaffer.WeakMethod( self.__colorChanged ) ) @@ -78,6 +82,9 @@ def __init__( self, plugs, **kw ) : self.__colorChooser.colorFieldVisibleChangedSignal().connect( functools.partial( Gaffer.WeakMethod( self.__colorChooserColorFieldVisibleChanged ) ) ) + self.__colorChooser.dynamicSliderBackgroundsChangedSignal().connect( + functools.partial( Gaffer.WeakMethod( self.__dynamicSliderBackgroundsChanged ) ) + ) self.__colorChooser.optionsMenuSignal().connect( functools.partial( Gaffer.WeakMethod( self.__colorChooserOptionsMenu ) ), scoped = False @@ -157,6 +164,10 @@ def __colorChooserColorFieldVisibleChanged( self, colorChooser ) : self.__colorChooserOptionChanged( "colorFieldVisible", colorChooser.getColorFieldVisible() ) + def __dynamicSliderBackgroundsChanged( self, colorChooser ) : + + self.__colorChooserOptionChanged( "dynamicSliderBackgrounds", colorChooser.getDynamicSliderBackgrounds() ) + def __colorChooserOptionsMenu( self, colorChooser, menuDefinition ) : menuDefinition.append( "/__saveDefaultOptions__", { "divider": True, "label": "Defaults" } ) @@ -193,14 +204,16 @@ def saveDefaultOptions( colorChooser, keyPrefix, scriptPath = None ) : visibleComponents = colorChooser.getVisibleComponents() staticComponent = colorChooser.getColorFieldStaticComponent() colorFieldVisible = colorChooser.getColorFieldVisible() + dynamicSliderBackgrounds = colorChooser.getDynamicSliderBackgrounds() for p in [ Gaffer.Color3fPlug, Gaffer.Color4fPlug ] : - for k in [ "visibleComponents", "staticComponent", "colorFieldVisible" ] : + for k in [ "visibleComponents", "staticComponent", "colorFieldVisible", "dynamicSliderBackgrounds" ] : Gaffer.Metadata.deregisterValue( p, keyPrefix + k ) Gaffer.Metadata.registerValue( p, keyPrefix + "visibleComponents", visibleComponents ) Gaffer.Metadata.registerValue( p, keyPrefix + "staticComponent", staticComponent ) Gaffer.Metadata.registerValue( p, keyPrefix + "colorFieldVisible", colorFieldVisible ) + Gaffer.Metadata.registerValue( p, keyPrefix + "dynamicSliderBackgrounds", dynamicSliderBackgrounds ) if scriptPath is None : return @@ -226,6 +239,7 @@ def saveDefaultOptions( colorChooser, keyPrefix, scriptPath = None ) : newScript.append( f"Gaffer.Metadata.registerValue( Gaffer.Color{c}fPlug, \"{keyPrefix}visibleComponents\", \"{visibleComponents}\" )\n" ) newScript.append( f"Gaffer.Metadata.registerValue( Gaffer.Color{c}fPlug, \"{keyPrefix}staticComponent\", \"{staticComponent}\" )\n" ) newScript.append( f"Gaffer.Metadata.registerValue( Gaffer.Color{c}fPlug, \"{keyPrefix}colorFieldVisible\", {colorFieldVisible} )\n" ) + newScript.append( f"Gaffer.Metadata.registerValue( Gaffer.Color{c}fPlug, \"{keyPrefix}dynamicSliderBackgrounds\", {dynamicSliderBackgrounds} )\n" ) with open( scriptPath, "w" ) as outFile : outFile.writelines( newScript ) \ No newline at end of file diff --git a/python/GafferUI/ColorSwatchPlugValueWidget.py b/python/GafferUI/ColorSwatchPlugValueWidget.py index 9c2fdec1fd4..0ed0604bf55 100644 --- a/python/GafferUI/ColorSwatchPlugValueWidget.py +++ b/python/GafferUI/ColorSwatchPlugValueWidget.py @@ -147,6 +147,9 @@ def __init__( self, plugs, parentWindow ) : self.colorChooser().colorFieldVisibleChangedSignal().connect( functools.partial( Gaffer.WeakMethod( self.__colorChooserColorFieldVisibleChanged ) ) ) + self.colorChooser().dynamicSliderBackgroundsChangedSignal().connect( + functools.partial( Gaffer.WeakMethod( self.__dynamicSliderBackgroundsChanged ) ) + ) self.colorChooser().optionsMenuSignal().connect( functools.partial( Gaffer.WeakMethod( self.__colorChooserOptionsMenu ) ), scoped = False @@ -183,6 +186,10 @@ def __init__( self, plugs, parentWindow ) : if colorFieldVisible is not None : self.colorChooser().setColorFieldVisible( colorFieldVisible ) + dynamicSliderBackgrounds = self.__colorChooserOption( "dynamicSliderBackgrounds" ) + if dynamicSliderBackgrounds is not None : + self.colorChooser().setDynamicSliderBackgrounds( dynamicSliderBackgrounds ) + parentWindow.addChildWindow( self, removeOnClose = True ) @classmethod @@ -287,3 +294,7 @@ def __colorChooserStaticComponentChanged( self, colorChooser ) : def __colorChooserColorFieldVisibleChanged( self, colorChooser ) : self.__colorChooserOptionChanged( "colorFieldVisible", colorChooser.getColorFieldVisible() ) + + def __dynamicSliderBackgroundsChanged( self, colorChooser ) : + + self.__colorChooserOptionChanged( "dynamicSliderBackgrounds", colorChooser.getDynamicSliderBackgrounds() ) diff --git a/python/GafferUITest/ColorChooserTest.py b/python/GafferUITest/ColorChooserTest.py index f7a50f0ce85..6c28a62dd4a 100644 --- a/python/GafferUITest/ColorChooserTest.py +++ b/python/GafferUITest/ColorChooserTest.py @@ -123,6 +123,16 @@ def __getColorFieldVisibility( self, widget ) : c = self.__colorChooserFromWidget( widget ) return c.getColorFieldVisible() + def __setDynamicSliderBackgrounds( self, widget, dynamic ) : + + c = self.__colorChooserFromWidget( widget ) + c.setDynamicSliderBackgrounds( dynamic ) + + def __getDynamicSliderBackgrounds( self, widget ) : + + c = self.__colorChooserFromWidget( widget ) + return c.getDynamicSliderBackgrounds() + def testMetadata( self ) : script = Gaffer.ScriptNode() @@ -145,12 +155,14 @@ def testMetadata( self ) : self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:visibleComponents" ) ) self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:staticComponent" ) ) self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:colorFieldVisible" ) ) + self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:dynamicSliderBackgrounds" ) ) # Modify widget self.__setVisibleComponents( widget, "rgbtmi" ) self.__setStaticComponent( widget, "t" ) self.__setColorFieldVisibility( widget, False ) + self.__setDynamicSliderBackgrounds( widget, False ) for c in "rgbtmi" : self.assertTrue( self.__sliderFromWidget( widget, c ).getVisible() ) @@ -158,15 +170,18 @@ def testMetadata( self ) : self.assertFalse( self.__sliderFromWidget( widget, c ).getVisible() ) self.assertEqual( self.__getStaticComponent( widget ), "t" ) self.assertFalse( self.__getColorFieldVisibility( widget ) ) + self.assertFalse( self.__getDynamicSliderBackgrounds( widget ) ) for p in [ "rgbPlug2" ] : self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:visibleComponents" ) ) self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:staticComponent" ) ) self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:colorFieldVisible" ) ) + self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:dynamicSliderBackgrounds" ) ) self.assertEqual( set( Gaffer.Metadata.value( script["node"]["rgbPlug1"], "colorChooser:inline:visibleComponents" ) ), set( "rgbtmi" ) ) self.assertEqual( Gaffer.Metadata.value( script["node"]["rgbPlug1"], "colorChooser:inline:staticComponent" ), "t" ) self.assertFalse( Gaffer.Metadata.value( script["node"]["rgbPlug1"], "colorChooser:inline:colorFieldVisible" ) ) + self.assertFalse( Gaffer.Metadata.value( script["node"]["rgbPlug1"], "colorChooser:inline:dynamicSliderBackgrounds" ) ) # Recreate widget and should have the same state @@ -180,6 +195,7 @@ def testMetadata( self ) : self.assertFalse( self.__sliderFromWidget( widget, c ).getVisible() ) self.assertEqual( self.__getStaticComponent( widget ), "t" ) self.assertFalse( self.__getColorFieldVisibility( widget ) ) + self.assertFalse( self.__getDynamicSliderBackgrounds( widget ) ) # We haven't saved the defaults, so a widget for a second plug # gets the original defaults. @@ -191,11 +207,13 @@ def testMetadata( self ) : self.assertTrue( self.__sliderFromWidget( widget2, c ).getVisible() ) self.assertEqual( self.__getStaticComponent( widget2 ), "v" ) self.assertTrue( self.__getColorFieldVisibility( widget2 ) ) + self.assertTrue( self.__getDynamicSliderBackgrounds( widget2 ) ) for p in [ "rgbPlug2" ] : self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:visibleComponents" ) ) self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:staticComponent" ) ) self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:colorFieldVisible" ) ) + self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:dynamicSliderBackgrounds" ) ) # Don't serialize state @@ -211,11 +229,13 @@ def testMetadata( self ) : self.assertTrue( self.__sliderFromWidget( widget, c ).getVisible() ) self.assertEqual( self.__getStaticComponent( widget ), "v" ) self.assertTrue( self.__getColorFieldVisibility( widget ) ) + self.assertTrue( self.__getDynamicSliderBackgrounds( widget ) ) for p in [ "rgbPlug1", "rgbPlug2" ] : self.assertIsNone( Gaffer.Metadata.value( script2["node"][p], "colorChooser:inline:visibleComponents" ) ) self.assertIsNone( Gaffer.Metadata.value( script2["node"][p], "colorChooser:inline:staticComponent" ) ) self.assertIsNone( Gaffer.Metadata.value( script2["node"][p], "colorChooser:inline:colorFieldVisible" ) ) + self.assertIsNone( Gaffer.Metadata.value( script2["node"][p], "colorChooser:inline:dynamicSliderBackgrounds" ) ) def testSaveDefaultOptions( self ) : @@ -243,12 +263,15 @@ def testSaveDefaultOptions( self ) : self.assertEqual( self.__getStaticComponent( rgbaWidget ), "v" ) self.assertTrue( self.__getColorFieldVisibility( rgbWidget ) ) self.assertTrue( self.__getColorFieldVisibility( rgbaWidget ) ) + self.assertTrue( self.__getDynamicSliderBackgrounds( rgbWidget ) ) + self.assertTrue( self.__getDynamicSliderBackgrounds( rgbaWidget ) ) # Modify `rgbWidget` self.__setVisibleComponents( rgbWidget, "rgbhsv" ) self.__setStaticComponent( rgbWidget, "g" ) self.__setColorFieldVisibility( rgbWidget, False ) + self.__setDynamicSliderBackgrounds( rgbWidget, False ) # Save defaults colorChooser = self.__colorChooserFromWidget( rgbWidget ) @@ -277,6 +300,8 @@ def testSaveDefaultOptions( self ) : self.assertEqual( self.__getStaticComponent( rgbaWidget ), "g" ) self.assertFalse( self.__getColorFieldVisibility( rgbWidget ) ) self.assertFalse( self.__getColorFieldVisibility( rgbaWidget ) ) + self.assertFalse( self.__getDynamicSliderBackgrounds( rgbWidget ) ) + self.assertFalse( self.__getDynamicSliderBackgrounds( rgbaWidget ) ) if __name__ == "__main__" : unittest.main()