diff --git a/python/GafferUI/ColorChooser.py b/python/GafferUI/ColorChooser.py
index 66b92f56f7..8206e340f4 100644
--- a/python/GafferUI/ColorChooser.py
+++ b/python/GafferUI/ColorChooser.py
@@ -37,13 +37,19 @@
import collections
import enum
+import functools
+import math
import sys
import imath
+import IECore
+
import Gaffer
import GafferUI
+from Qt import QtCore
from Qt import QtGui
+from Qt import QtWidgets
__tmiToRGBMatrix = imath.M33f(
-1.0 / 2.0, 0.0, 1.0 / 2.0,
@@ -157,6 +163,297 @@ def _displayTransformChanged( self ) :
GafferUI.Slider._displayTransformChanged( self )
self._qtWidget().update()
+class _ColorField( GafferUI.Widget ) :
+
+ def __init__( self, color = imath.Color3f( 1.0 ), staticComponent = "h", **kw ) :
+
+ GafferUI.Widget.__init__( self, QtWidgets.QWidget(), **kw )
+
+ # \todo Allow the widget to grow if the containing window is resized. It should also
+ # be constrained to be square.
+ self._qtWidget().setMinimumSize( 216, 216 )
+ self._qtWidget().setSizePolicy( QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed )
+
+ self._qtWidget().paintEvent = Gaffer.WeakMethod( self.__paintEvent )
+
+ self.buttonPressSignal().connect( Gaffer.WeakMethod( self.__buttonPress ), scoped = False )
+ self.dragBeginSignal().connect( Gaffer.WeakMethod( self.__dragBegin ), scoped = False )
+ self.dragEnterSignal().connect( Gaffer.WeakMethod( self.__dragEnter ), scoped = False )
+ self.dragMoveSignal().connect( Gaffer.WeakMethod( self.__dragMove ), scoped = False )
+ self.dragEndSignal().connect( Gaffer.WeakMethod( self.__dragEnd ), scoped = False )
+
+ self.__valueChangedSignal = Gaffer.Signals.Signal2()
+
+ self.__color = color
+ self.__staticComponent = staticComponent
+ self.__colorFieldToDraw = None
+ self.setColor( color, staticComponent )
+
+ # 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.
+ def setColor( self, color, staticComponent ) :
+
+ self.__setColorInternal( color, staticComponent, GafferUI.Slider.ValueChangedReason.SetValues )
+
+ # Returns a tuple of the color and static component.
+ def getColor( self ) :
+
+ return self.__color, self.__staticComponent
+
+ ## A signal emitted whenever a value has been changed. Slots should
+ # have the signature slot( _ColorField, GafferUI.Slider.ValueChangedReason )
+ def valueChangedSignal( self ) :
+
+ return self.__valueChangedSignal
+
+ def __setColorInternal( self, color, staticComponent, reason ) :
+
+ dragBeginOrEnd = reason in ( GafferUI.Slider.ValueChangedReason.DragBegin, GafferUI.Slider.ValueChangedReason.DragEnd )
+ if self.__color == color and self.__staticComponent == staticComponent and not dragBeginOrEnd :
+ return
+
+ zIndex = self.__zIndex()
+ if color[zIndex] != self.__color[zIndex] or staticComponent != self.__staticComponent :
+ self.__colorFieldToDraw = None
+
+ self.__color = color
+ self.__staticComponent = staticComponent
+
+ self._qtWidget().update()
+
+ self.valueChangedSignal()( self, reason )
+
+ def __xyIndices( self ) :
+
+ xIndex = { "r": 1, "g": 0, "b": 0, "h": 1, "s": 0, "v": 0, "t": 1, "m": 0, "i": 0 }[self.__staticComponent]
+ yIndex = { "r": 2, "g": 2, "b": 1, "h": 2, "s": 2, "v": 1, "t": 2, "m": 2, "i": 1 }[self.__staticComponent]
+
+ return xIndex, yIndex
+
+ def __zIndex( self ) :
+ zIndex = { "r": 0, "g": 1, "b": 2, "h": 0, "s": 1, "v": 2, "t": 0, "m": 1, "i": 2 }[self.__staticComponent]
+
+ return zIndex
+
+ def __xyAxes( self ) :
+ xAxis = { "r": "g", "g": "r", "b": "r", "h": "s", "s": "h", "v": "h", "t": "m", "m": "t", "i": "t" }[self.__staticComponent]
+ yAxis = { "r": "b", "g": "b", "b": "g", "h": "v", "s": "v", "v": "s", "t": "i", "m": "i", "i": "m" }[self.__staticComponent]
+
+ return xAxis, yAxis
+
+ def __colorToPosition( self, color ) :
+
+ xIndex, yIndex = self.__xyIndices()
+ color = imath.V2f( color[xIndex], color[yIndex] )
+
+ xComponent, yComponent = self.__xyAxes()
+ minC = imath.V2f( _ranges[xComponent].min, _ranges[yComponent].min )
+ maxC = imath.V2f( _ranges[xComponent].max, _ranges[yComponent].max )
+
+ p = ( ( color - minC ) / ( maxC - minC ) ) * self.bound().size()
+ p.y = self.bound().size().y - p.y
+
+ return p
+
+ def __positionToColor( self, position ) :
+
+ xIndex, yIndex = self.__xyIndices()
+
+ c, staticComponent = self.getColor()
+ c = c.__class__( c )
+
+ size = self.bound().size()
+
+ xComponent, yComponent = self.__xyAxes()
+
+ c[xIndex] = ( position.x / float( size.x ) ) * ( _ranges[xComponent].max - _ranges[xComponent].min ) + _ranges[xComponent].min
+ c[yIndex] = ( 1.0 - ( position.y / float( size.y ) ) ) * ( _ranges[yComponent].max - _ranges[yComponent].min ) + _ranges[yComponent].min
+
+ return c
+
+ def __buttonPress( self, widget, event ) :
+
+ if event.buttons != GafferUI.ButtonEvent.Buttons.Left :
+ return False
+
+ c, staticComponent = self.getColor()
+ self.__setColorInternal( self.__positionToColor( event.line.p0 ), staticComponent, GafferUI.Slider.ValueChangedReason.Click )
+
+ return True
+
+ def __clampPosition( self, position ) :
+
+ size = self.bound().size()
+ return imath.V2f( min( size.x, max( 0.0, position.x ) ), min( size.y, max( 0.0, position.y ) ) )
+
+ def __dragBegin( self, widget, event ) :
+
+ if event.buttons == GafferUI.ButtonEvent.Buttons.Left :
+ c, staticComponent = self.getColor()
+ self.__setColorInternal(
+ self.__positionToColor( self.__clampPosition( event.line.p0 ) ),
+ staticComponent,
+ GafferUI.Slider.ValueChangedReason.DragBegin
+ )
+ return IECore.NullObject.defaultNullObject()
+
+ return None
+
+ def __dragEnter( self, widget, event ) :
+
+ if event.sourceWidget is self :
+ return True
+
+ return False
+
+ def __dragMove( self, widget, event ) :
+
+ c, staticComponent = self.getColor()
+ self.__setColorInternal(
+ self.__positionToColor( self.__clampPosition( event.line.p0 ) ),
+ staticComponent,
+ GafferUI.Slider.ValueChangedReason.DragMove
+ )
+ return True
+
+ def __dragEnd( self, widget, event ) :
+
+ c, staticComponent = self.getColor()
+ self.__setColorInternal(
+ self.__positionToColor( self.__clampPosition( event.line.p0 ) ),
+ staticComponent,
+ GafferUI.Slider.ValueChangedReason.DragEnd
+ )
+ return True
+
+ def __drawBackground( self, painter ) :
+
+ numStops = 50
+ if self.__colorFieldToDraw is None :
+ self.__colorFieldToDraw = QtGui.QImage( QtCore.QSize( numStops, numStops ), QtGui.QImage.Format.Format_RGB32 )
+
+ displayTransform = self.displayTransform()
+
+ xIndex, yIndex = self.__xyIndices()
+ zIndex = self.__zIndex()
+
+ staticValue = self.__color[zIndex]
+
+ c = imath.Color3f()
+ c[zIndex] = staticValue
+
+ ColorSpace = enum.Enum( "ColorSpace", [ "RGB", "HSV", "TMI" ] )
+ if self.__staticComponent in "rgb" :
+ colorSpace = ColorSpace.RGB
+ elif self.__staticComponent in "hsv" :
+ colorSpace = ColorSpace.HSV
+ else :
+ colorSpace = ColorSpace.TMI
+
+ xComponent, yComponent = self.__xyAxes()
+
+ for x in range( 0, numStops ) :
+ tx = float( x ) / ( numStops - 1 )
+ c[xIndex] = _ranges[xComponent].min + ( _ranges[xComponent].max - _ranges[xComponent].min ) * tx
+
+ for y in range( 0, numStops ) :
+ ty = float( y ) / ( numStops - 1 )
+
+ c[yIndex] = _ranges[yComponent].min + ( _ranges[yComponent].max - _ranges[yComponent].min ) * ty
+
+ if colorSpace == ColorSpace.RGB :
+ cRGB = c
+ elif colorSpace == ColorSpace.HSV :
+ cRGB = c.hsv2rgb()
+ else :
+ cRGB = _tmiToRGB( c )
+
+ cRGB = displayTransform( cRGB )
+ self.__colorFieldToDraw.setPixel( x, numStops - 1 - y, self._qtColor( cRGB ).rgb() )
+
+ painter.drawImage( self._qtWidget().contentsRect(), self.__colorFieldToDraw )
+
+ def __drawValue( self, painter ) :
+
+ position = self.__colorToPosition( self.__color )
+
+ pen = QtGui.QPen( QtGui.QColor( 0, 0, 0, 255 ) )
+ pen.setWidth( 1 )
+ painter.setPen( pen )
+
+ color = QtGui.QColor( 119, 156, 255, 255 )
+
+ painter.setBrush( QtGui.QBrush( color ) )
+
+ size = self.size()
+
+ # Use a dot when both axes are a valid value.
+ if position.x >= 0 and position.y >= 0 and position.x <= size.x and position.y <= size.y :
+ painter.drawEllipse( QtCore.QPoint( position.x, position.y ), 4.5, 4.5 )
+ return
+
+ triangleWidth = 5.0
+ triangleSpacing = 2.0
+ positionClamped = imath.V2f(
+ min( max( 0.0, position.x ), size.x ),
+ min( max( 0.0, position.y ), size.y )
+ )
+ offset = imath.V2f( 0 )
+ # Use a corner triangle if both axes are invalid values.
+ if position.x > size.x and position.y < 0 :
+ rotation = -45.0 # Triangle pointing to the top-right
+ offset = imath.V2f( -triangleSpacing, triangleSpacing )
+ elif position.x < 0 and position.y < 0 :
+ rotation = -135.0 # Top-left
+ offset = imath.V2f( triangleSpacing, triangleSpacing )
+ elif position.x < 0 and position.y > size.y :
+ rotation = -225.0 # Bottom-left
+ offset = imath.V2f( triangleSpacing, -triangleSpacing )
+ elif position.x > size.x and position.y > size.y :
+ rotation = -315.0 # Bottom-right
+ offset = imath.V2f( -triangleSpacing, -triangleSpacing )
+
+ # Finally, use a top / left / bottom / right triangle if one axis is an invalid value.
+ elif position.y < 0 :
+ rotation = -90.0 # Top
+ offset = imath.V2f( 0, triangleSpacing )
+ # Clamp it in more to account for the triangle size
+ positionClamped.x = min( max( triangleWidth + triangleSpacing, positionClamped.x ), size.x - triangleWidth - triangleSpacing )
+ elif position.x < 0 :
+ rotation = -180.0 # Left
+ offset = imath.V2f( triangleSpacing, 0 )
+ positionClamped.y = min( max( triangleWidth + triangleSpacing, positionClamped.y ), size.y - triangleWidth - triangleSpacing )
+ elif position.y > size.y :
+ rotation = -270.0 # Bottom
+ offset = imath.V2f( 0, -triangleSpacing )
+ positionClamped.x = min( max( triangleWidth + triangleSpacing, positionClamped.x ), size.x - triangleWidth - triangleSpacing )
+ else :
+ rotation = 0.0 # Right
+ offset = imath.V2f( -triangleSpacing, 0 )
+ positionClamped.y = min( max( triangleWidth + triangleSpacing, positionClamped.y ), size.y - triangleWidth - triangleSpacing )
+
+ rightPoints = [ imath.V2f( 0, 0 ), imath.V2f( -6, triangleWidth ), imath.V2f( -6, -triangleWidth ) ]
+ xform = imath.M33f().rotate( math.radians( rotation ) ) * imath.M33f().translate(
+ positionClamped + offset
+ )
+ points = [ p * xform for p in rightPoints ]
+ # Transforming the points introduces slight precision errors which can be noticeable
+ # when drawing polygons. Round the values to compensate.
+ points = [ QtCore.QPoint( round( p.x ), round( p.y ) ) for p in points ]
+
+ painter.drawConvexPolygon( points )
+
+ def __paintEvent( self, event ) :
+
+ painter = QtGui.QPainter( self._qtWidget() )
+ painter.setRenderHint( QtGui.QPainter.Antialiasing )
+ painter.setRenderHint( QtGui.QPainter.SmoothPixmapTransform)
+
+ self.__drawBackground( painter )
+
+ self.__drawValue( painter )
+
class ColorChooser( GafferUI.Widget ) :
ColorChangedReason = enum.Enum( "ColorChangedReason", [ "Invalid", "SetColor", "Reset" ] )
@@ -180,28 +477,61 @@ def __init__( self, color=imath.Color3f( 1 ), **kw ) :
self.__componentValueChangedConnections = []
with self.__column :
-
- with GafferUI.GridContainer( spacing = 4 ) :
-
- # sliders and numeric widgets
- for row, component in enumerate( "rgbahsv" ) :
- self.__channelLabels[component] = GafferUI.Label( component.capitalize(), parenting = { "index" : ( 0, row ), "alignment" : ( GafferUI.HorizontalAlignment.Center, GafferUI.VerticalAlignment.Center ) } )
- numericWidget = GafferUI.NumericWidget( 0.0, parenting = { "index" : ( 1, row ) } )
-
- numericWidget.setFixedCharacterWidth( 6 )
- numericWidget.component = component
- self.__numericWidgets[component] = numericWidget
-
- slider = _ComponentSlider( color, component, parenting = { "index" : ( 2, row ) } )
- self.__sliders[component] = slider
-
- self.__componentValueChangedConnections.append(
- numericWidget.valueChangedSignal().connect( Gaffer.WeakMethod( self.__componentValueChanged ), scoped = False )
- )
-
- self.__componentValueChangedConnections.append(
- slider.valueChangedSignal().connect( Gaffer.WeakMethod( self.__componentValueChanged ), scoped = False )
- )
+ with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) :
+
+ self.__colorField = _ColorField( color, "h" )
+ self.__colorValueChangedConnection = self.__colorField.valueChangedSignal().connect( Gaffer.WeakMethod( self.__colorValueChanged ), scoped = False )
+ # \todo Don't hide color field when we're ready to expose the UI
+ self.__colorField.setVisible( False )
+
+ with GafferUI.GridContainer( spacing = 4 ) :
+
+ # sliders and numeric widgets
+ c, staticComponent = self.__colorField.getColor()
+ for row, component in enumerate( "rgbahsvtmi" ) :
+ self.__channelLabels[component] = GafferUI.Label( component.capitalize(), parenting = { "index" : ( 0, row ), "alignment" : ( GafferUI.HorizontalAlignment.Center, GafferUI.VerticalAlignment.Center ) } )
+
+ if component != "a" :
+ if component == staticComponent :
+ self.__channelLabels[component]._qtWidget().setProperty( "gafferColorStaticComponent", True)
+ self.__channelLabels[component].buttonPressSignal().connect(
+ functools.partial(
+ Gaffer.WeakMethod( self.__setStaticComponent ),
+ component = component
+ ),
+ scoped = False
+ )
+ self.__channelLabels[component].enterSignal().connect(
+ functools.partial(
+ Gaffer.WeakMethod( self.__labelEnter ),
+ component = component
+ ),
+ scoped = False
+ )
+ self.__channelLabels[component].leaveSignal().connect(
+ functools.partial(
+ Gaffer.WeakMethod( self.__labelLeave ),
+ component = component
+ ),
+ scoped = False
+ )
+
+ numericWidget = GafferUI.NumericWidget( 0.0, parenting = { "index" : ( 1, row ) } )
+
+ numericWidget.setFixedCharacterWidth( 6 )
+ numericWidget.component = component
+ self.__numericWidgets[component] = numericWidget
+
+ slider = _ComponentSlider( color, component, parenting = { "index" : ( 2, row ) } )
+ self.__sliders[component] = slider
+
+ self.__componentValueChangedConnections.append(
+ numericWidget.valueChangedSignal().connect( Gaffer.WeakMethod( self.__componentValueChanged ), scoped = False )
+ )
+
+ self.__componentValueChangedConnections.append(
+ slider.valueChangedSignal().connect( Gaffer.WeakMethod( self.__componentValueChanged ), scoped = False )
+ )
# initial and current colour swatches
with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) as self.__swatchRow :
@@ -215,6 +545,12 @@ def __init__( self, color=imath.Color3f( 1 ), **kw ) :
self.__colorChangedSignal = Gaffer.Signals.Signal2()
+ # \todo Don't hide TMI sliders when we're ready to expose the new UI
+ for c in "tmi" :
+ self.__channelLabels[c].setVisible( False )
+ self.__numericWidgets[c].setVisible( False )
+ self.__sliders[c].setVisible( False )
+
self.__updateUIFromColor()
## The default color starts as the value passed when creating the widget.
@@ -308,6 +644,18 @@ def __componentValueChanged( self, componentWidget, reason ) :
self.__setColorInternal( newColor, reason, self.__ColorSpace.TMI )
+ def __colorValueChanged( self, colorWidget, reason ) :
+
+ c, staticComponent = colorWidget.getColor()
+ if staticComponent in "rgb" :
+ colorSpace = self.__ColorSpace.RGB
+ elif staticComponent in "hsv" :
+ colorSpace = self.__ColorSpace.HSV
+ else :
+ colorSpace = self.__ColorSpace.TMI
+
+ self.__setColorInternal( c, reason, colorSpace )
+
def __setColorInternal( self, color, reason, colorSpace = __ColorSpace.RGB ) :
dragBeginOrEnd = reason in (
@@ -337,8 +685,9 @@ def __setColorInternal( self, color, reason, colorSpace = __ColorSpace.RGB ) :
colorHSV = colorRGB.rgb2hsv()
colorTMI = color
- colorHSV[0] = colorHSV[0] if colorHSV[1] > 1e-7 and colorHSV[2] > 1e-7 else self.__colorHSV[0]
- colorHSV[1] = colorHSV[1] if colorHSV[2] > 1e-7 else self.__colorHSV[1]
+ if colorSpace != self.__ColorSpace.HSV :
+ colorHSV[0] = colorHSV[0] if colorHSV[1] > 1e-7 and colorHSV[2] > 1e-7 else self.__colorHSV[0]
+ colorHSV[1] = colorHSV[1] if colorHSV[2] > 1e-7 else self.__colorHSV[1]
self.__color = colorRGB
self.__colorHSV = colorHSV
@@ -360,7 +709,9 @@ def __setColorInternal( self, color, reason, colorSpace = __ColorSpace.RGB ) :
def __updateUIFromColor( self ) :
- with Gaffer.Signals.BlockedConnection( self.__componentValueChangedConnections ) :
+ with Gaffer.Signals.BlockedConnection(
+ self.__componentValueChangedConnections + [ self.__colorValueChangedConnection ]
+ ) :
c = self.getColor()
@@ -389,3 +740,51 @@ def __updateUIFromColor( self ) :
for component, index in ( ( "h", 0 ), ( "s", 1 ), ( "v", 2 ) ) :
self.__sliders[component].setValue( self.__colorHSV[index] )
self.__numericWidgets[component].setValue( self.__colorHSV[index] )
+
+ for slider in [ v for k, v in self.__sliders.items() if k in "tmi" ] :
+ slider.setColor( self.__colorTMI )
+
+ for component, index in ( ( "t", 0 ), ( "m", 1 ), ( "i", 2 ) ) :
+ self.__sliders[component].setValue( self.__colorTMI[index] )
+ self.__numericWidgets[component].setValue( self.__colorTMI[index] )
+
+ c, staticComponent = self.__colorField.getColor()
+ assert( staticComponent in "rgbhsvtmi" )
+ if staticComponent in "rgb" :
+ self.__colorField.setColor( self.__color, staticComponent )
+ elif staticComponent in "hsv" :
+ self.__colorField.setColor( self.__colorHSV, staticComponent )
+ else :
+ self.__colorField.setColor( self.__colorTMI, staticComponent )
+
+ def __setStaticComponent( self, widget, event, component ) :
+
+ if event.buttons != GafferUI.ButtonEvent.Buttons.Left :
+ return False
+
+ c, staticComponent = self.__colorField.getColor()
+ self.__channelLabels[staticComponent]._qtWidget().setProperty( "gafferColorStaticComponent", False )
+ self.__channelLabels[staticComponent]._repolish()
+
+ assert( component in "rgbhsvtmi" )
+ if component in "rgb" :
+ self.__colorField.setColor( self.__color, component )
+ elif component in "hsv" :
+ self.__colorField.setColor( self.__colorHSV, component )
+ else :
+ self.__colorField.setColor( self.__colorTMI, component )
+
+ self.__channelLabels[component]._qtWidget().setProperty( "gafferColorStaticComponent", True )
+ self.__channelLabels[component]._repolish()
+
+ return True
+
+ def __labelEnter( self, widget, component ) :
+
+ self.__channelLabels[component]._qtWidget().setProperty( "gafferColorStaticComponentHover", True )
+ self.__channelLabels[component]._repolish()
+
+ def __labelLeave( self, widget, component ) :
+
+ self.__channelLabels[component]._qtWidget().setProperty( "gafferColorStaticComponentHover", False)
+ self.__channelLabels[component]._repolish()
diff --git a/python/GafferUI/_StyleSheet.py b/python/GafferUI/_StyleSheet.py
index 2e016adf93..0241808093 100644
--- a/python/GafferUI/_StyleSheet.py
+++ b/python/GafferUI/_StyleSheet.py
@@ -271,6 +271,22 @@ def styleColor( key ) :
margin-bottom: 6px;
}
+ QLabel#gafferColorComponentLabel {
+ padding-left: 12px;
+ }
+
+ QLabel#gafferColorComponentLabel[gafferColorStaticComponent="true"] {
+ background-image: url(:/colorChooserStaticChannelIcon.png);
+ background-repeat: no-repeat;
+ background-position: left;
+ }
+
+ QLabel#gafferColorComponentLabel[gafferColorStaticComponentHover="true"] {
+ background-image: url(:/colorChooserStaticChannelHighlightedIcon.png);
+ background-repeat: no-repeat;
+ background-position: left;
+ }
+
QMenuBar {
background-color: $backgroundDarkest;
font-weight: bold;
diff --git a/resources/graphics.py b/resources/graphics.py
index 3578d0c160..0c703cb446 100644
--- a/resources/graphics.py
+++ b/resources/graphics.py
@@ -471,6 +471,20 @@
"activeRenderPass",
"activeRenderPassFadedHighlighted",
]
+ },
+
+ "colorChooser" : {
+
+ "options" : {
+ "requiredWidth" : 10,
+ "requiredHeight" : 10,
+ "validatePixelAlignment" : True
+ },
+
+ "ids" : [
+ "colorChooserStaticChannelIcon",
+ "colorChooserStaticChannelHighlightedIcon",
+ ]
}
},
diff --git a/resources/graphics.svg b/resources/graphics.svg
index 9eb16c4a52..16669f77c6 100644
--- a/resources/graphics.svg
+++ b/resources/graphics.svg
@@ -1674,6 +1674,29 @@
id="rect7" />RenderPassEditor
+
+ Color Chooser
+
+
+
+
+
+
+
+
+