QmlDesigner: Add RGB und HSV picker

* Add modes to color picker
* Add color picker for RGBA mode
* Add color picker for HSVA mode
* Add luminance slider for RGBA mode

Change-Id: I0bb1dbb67b7c18d156eee0d4e07cfa942162f832
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
This commit is contained in:
Henning Gruendl
2021-07-16 15:55:28 +02:00
committed by Henning Gründl
parent 4895eb346a
commit 6fef74a8cb
4 changed files with 426 additions and 121 deletions

View File

@@ -413,7 +413,7 @@ SecondColumnLayout {
id: transparentIndicator
icon: StudioTheme.Constants.transparent
pixelSize: StudioTheme.Values.myIconFontSize * 1.4
tooltip: qsTr("Transparent TODO")
tooltip: qsTr("Transparent")
onClicked: {
colorPicker.alpha = 0
colorPicker.updateColor()
@@ -575,22 +575,29 @@ SecondColumnLayout {
onRightMouseButtonClicked: contextMenu.popup(colorPicker)
onColorInvalidated: {
if (colorPicker.saturation > 0.0 && colorPicker.lightness > 0.0) {
hueSpinBox.value = colorPicker.hue
switch (colorPicker.mode) {
case ColorPicker.Mode.HSLA:
hslHueSpinBox.value = colorPicker.hue
hslSaturationSpinBox.value = colorPicker.saturationHSL
hslLightnessSpinBox.value = colorPicker.lightness
hslAlphaSpinBox.value = colorPicker.alpha
break
case ColorPicker.Mode.RGBA:
redSpinBox.value = (colorPicker.color.r * 255)
greenSpinBox.value = (colorPicker.color.g * 255)
blueSpinBox.value = (colorPicker.color.b * 255)
rgbAlphaSpinBox.value = (colorPicker.alpha * 255)
break
case ColorPicker.Mode.HSVA:
default:
hsvHueSpinBox.value = colorPicker.hue
hsvSaturationSpinBox.value = colorPicker.saturationHSV
hsvValueSpinBox.value = colorPicker.value
hsvAlphaSpinBox.value = colorPicker.alpha
break
}
if (colorPicker.lightness > 0.0)
saturationSpinBox.value = colorPicker.saturation
else
colorPicker.saturation = saturationSpinBox.value
lightnessSpinBox.value = colorPicker.lightness
hslaAlphaSpinBox.value = colorPicker.alpha
redSpinBox.value = (colorPicker.color.r * 255)
greenSpinBox.value = (colorPicker.color.g * 255)
blueSpinBox.value = (colorPicker.color.b * 255)
rgbaAlphaSpinBox.value = (colorPicker.alpha * 255)
}
}
@@ -766,29 +773,22 @@ SecondColumnLayout {
+ 4 * StudioTheme.Values.colorEditorPopupSpinBoxWidth
width: implicitWidth
actionIndicatorVisible: false
model: ["RGBA", "HSLA"]
onActivated: {
switch (colorMode.currentText) {
case "RGBA":
rgbaRow.visible = true
hslaRow.visible = false
break
case "HSLA":
rgbaRow.visible = false
hslaRow.visible = true
break
default:
console.log("Unknown color mode selected.")
rgbaRow.visible = true
hslaRow.visible = false
}
}
textRole: "text"
valueRole: "value"
model: [
{ value: ColorPicker.Mode.HSVA, text: "HSVA" },
{ value: ColorPicker.Mode.RGBA, text: "RGBA" },
{ value: ColorPicker.Mode.HSLA, text: "HSLA" }
]
onActivated: colorPicker.mode = colorMode.currentValue
}
}
RowLayout {
id: rgbaRow
visible: colorPicker.mode === ColorPicker.Mode.RGBA
Layout.fillWidth: true
spacing: StudioTheme.Values.controlGap
@@ -847,7 +847,7 @@ SecondColumnLayout {
}
DoubleSpinBox {
id: rgbaAlphaSpinBox
id: rgbAlphaSpinBox
width: StudioTheme.Values.colorEditorPopupSpinBoxWidth
stepSize: 1
@@ -856,7 +856,7 @@ SecondColumnLayout {
decimals: 0
onValueModified: {
var tmp = rgbaAlphaSpinBox.value / 255.0
var tmp = rgbAlphaSpinBox.value / 255.0
if (colorPicker.alpha !== tmp && !colorPicker.block) {
colorPicker.alpha = tmp
colorPicker.updateColor()
@@ -868,49 +868,109 @@ SecondColumnLayout {
RowLayout {
id: hslaRow
visible: false
visible: colorPicker.mode === ColorPicker.Mode.HSLA
Layout.fillWidth: true
spacing: StudioTheme.Values.controlGap
DoubleSpinBox {
id: hueSpinBox
id: hslHueSpinBox
width: StudioTheme.Values.colorEditorPopupSpinBoxWidth
onValueModified: {
if (colorPicker.hue !== hueSpinBox.value && !colorPicker.block) {
colorPicker.hue = hueSpinBox.value
if (colorPicker.hue !== hslHueSpinBox.value
&& !colorPicker.block) {
colorPicker.hue = hslHueSpinBox.value
colorPicker.updateColor()
}
}
}
DoubleSpinBox {
id: saturationSpinBox
id: hslSaturationSpinBox
width: StudioTheme.Values.colorEditorPopupSpinBoxWidth
onValueModified: {
if (colorPicker.saturation !== saturationSpinBox.value && !colorPicker.block) {
colorPicker.saturation = saturationSpinBox.value
if (colorPicker.saturationHSL !== hslSaturationSpinBox.value
&& !colorPicker.block) {
colorPicker.saturationHSL = hslSaturationSpinBox.value
colorPicker.updateColor()
}
}
}
DoubleSpinBox {
id: lightnessSpinBox
id: hslLightnessSpinBox
width: StudioTheme.Values.colorEditorPopupSpinBoxWidth
onValueModified: {
if (colorPicker.lightness !== lightnessSpinBox.value && !colorPicker.block) {
colorPicker.lightness = lightnessSpinBox.value
if (colorPicker.lightness !== hslLightnessSpinBox.value
&& !colorPicker.block) {
colorPicker.lightness = hslLightnessSpinBox.value
colorPicker.updateColor()
}
}
}
DoubleSpinBox {
id: hslaAlphaSpinBox
id: hslAlphaSpinBox
width: StudioTheme.Values.colorEditorPopupSpinBoxWidth
onValueModified: {
if (colorPicker.alpha !== hslaAlphaSpinBox.value && !colorPicker.block) {
colorPicker.alpha = hslaAlphaSpinBox.value
if (colorPicker.alpha !== hslAlphaSpinBox.value
&& !colorPicker.block) {
colorPicker.alpha = hslAlphaSpinBox.value
colorPicker.updateColor()
}
}
}
}
RowLayout {
id: hsvaRow
visible: colorPicker.mode === ColorPicker.Mode.HSVA
Layout.fillWidth: true
spacing: StudioTheme.Values.controlGap
DoubleSpinBox {
id: hsvHueSpinBox
width: StudioTheme.Values.colorEditorPopupSpinBoxWidth
onValueModified: {
if (colorPicker.hue !== hsvHueSpinBox.value
&& !colorPicker.block) {
colorPicker.hue = hsvHueSpinBox.value
colorPicker.updateColor()
}
}
}
DoubleSpinBox {
id: hsvSaturationSpinBox
width: StudioTheme.Values.colorEditorPopupSpinBoxWidth
onValueModified: {
if (colorPicker.saturationHSV !== hsvSaturationSpinBox.value
&& !colorPicker.block) {
colorPicker.saturationHSV = hsvSaturationSpinBox.value
colorPicker.updateColor()
}
}
}
DoubleSpinBox {
id: hsvValueSpinBox
width: StudioTheme.Values.colorEditorPopupSpinBoxWidth
onValueModified: {
if (colorPicker.value !== hsvValueSpinBox.value
&& !colorPicker.block) {
colorPicker.value = hsvValueSpinBox.value
colorPicker.updateColor()
}
}
}
DoubleSpinBox {
id: hsvAlphaSpinBox
width: StudioTheme.Values.colorEditorPopupSpinBoxWidth
onValueModified: {
if (colorPicker.alpha !== hsvAlphaSpinBox.value
&& !colorPicker.block) {
colorPicker.alpha = hsvAlphaSpinBox.value
colorPicker.updateColor()
}
}

View File

@@ -29,15 +29,26 @@ import StudioTheme 1.0 as StudioTheme
Column {
id: root
enum Mode {
HSVA,
RGBA,
HSLA
}
property int mode: ColorPicker.Mode.HSVA
property color color
property real alpha: 1
property real hue: 0
property real saturation: 0
property real saturationHSL: 0
property real saturationHSV: 0
property real lightness: 0
property real value: 0
property real alpha: 1
property bool achromatic: false
property int sliderMargins: 6
property bool block: false
signal updateColor
@@ -46,30 +57,84 @@ Column {
spacing: 10
onAlphaChanged: invalidateColor()
onSaturationChanged: invalidateColor()
onLightnessChanged: invalidateColor()
onHueChanged: invalidateColor()
onColorChanged: {
var myAlpha = root.color.a
rgbToHsl(root.color)
root.alpha = myAlpha
onModeChanged: {
switch (root.mode) {
case ColorPicker.Mode.RGBA:
root.color = Qt.rgba(root.color.r, root.color.g, root.color.b, root.alpha)
break
case ColorPicker.Mode.HSLA:
root.color = Qt.hsla(root.hue, root.saturationHSL, root.lightness, root.alpha)
break
case ColorPicker.Mode.HSVA:
default:
root.color = Qt.hsva(root.hue, root.saturationHSV, root.value, root.alpha)
break
}
gradientOverlay.requestPaint()
}
onHueChanged: {
if (root.mode === ColorPicker.Mode.HSLA)
root.color.hslHue = root.hue
else
root.color.hsvHue = root.hue
}
onSaturationHSLChanged: {
root.color.hslSaturation = root.saturationHSL
invalidateColor()
}
onSaturationHSVChanged: {
root.color.hsvSaturation = root.saturationHSV
}
onLightnessChanged: {
root.color.hslLightness = root.lightness
}
onValueChanged: {
root.color.hsvValue = root.value
}
onAlphaChanged: invalidateColor()
onColorChanged: invalidateColor()
function invalidateColor() {
if (root.block)
return
root.block = true
root.color = Qt.hsla(root.hue,
root.saturation,
root.lightness,
root.alpha)
if (root.color.hsvSaturation > 0.0
&& root.color.hsvValue > 0.0
&& root.color.hsvHue !== -1.0)
root.hue = root.color.hsvHue
if (root.saturation > 0.0 && root.lightness > 0.0)
hueSlider.value = root.hue
if (root.color.hslSaturation > 0.0
&& root.color.hslLightness > 0.0
&& root.color.hslHue !== -1.0)
root.hue = root.color.hslHue
if (root.color.hslLightness !== 0.0 && root.color.hslLightness !== 1.0 && !root.achromatic)
root.saturationHSL = root.color.hslSaturation
if (root.color.hsvValue !== 0.0 && root.color.hsvValue !== 1.0 && !root.achromatic)
root.saturationHSV = root.color.hsvSaturation
root.lightness = root.color.hslLightness
root.value = root.color.hsvValue
if (root.color.hslLightness === 0.0 || root.color.hslLightness === 1.0
|| root.color.hsvValue === 0.0 || root.color.hsvValue === 1.0
|| root.color.hsvHue === -1.0 || root.color.hslHue === -1.0)
root.achromatic = true
else
root.achromatic = false
if (root.mode === ColorPicker.Mode.HSLA)
root.color = Qt.hsla(root.hue, root.saturationHSL, root.lightness, root.alpha)
else
root.color = Qt.hsva(root.hue, root.saturationHSV, root.value, root.alpha)
luminanceSlider.value = (1.0 - root.value)
hueSlider.value = root.hue
opacitySlider.value = (1.0 - root.alpha)
root.colorInvalidated()
@@ -77,39 +142,51 @@ Column {
root.block = false
}
function rgbToHsl(color) {
var r = color.r
var g = color.g
var b = color.b
function drawHSVA(ctx) {
for (var row = 0; row < gradientOverlay.height; row++) {
var gradient = ctx.createLinearGradient(0, 0, gradientOverlay.width, 0)
var v = Math.abs(row - gradientOverlay.height) / gradientOverlay.height
var max = Math.max(r, g, b), min = Math.min(r, g, b)
var h, s, l = (max + min) / 2
gradient.addColorStop(0, Qt.hsva(root.hue, 0, v, 1))
gradient.addColorStop(1, Qt.hsva(root.hue, 1, v, 1))
if (max === min) {
h = 0
s = 0
} else {
var d = max - min
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6
ctx.fillStyle = gradient
ctx.fillRect(0, row, gradientOverlay.width, 1)
}
}
root.block = true
function drawRGBA(ctx) {
var gradient = ctx.createLinearGradient(0, 0, gradientOverlay.width, 0)
gradient.addColorStop(0.000, Qt.rgba(1, 0, 0, 1))
gradient.addColorStop(0.167, Qt.rgba(1, 1, 0, 1))
gradient.addColorStop(0.333, Qt.rgba(0, 1, 0, 1))
gradient.addColorStop(0.500, Qt.rgba(0, 1, 1, 1))
gradient.addColorStop(0.667, Qt.rgba(0, 0, 1, 1))
gradient.addColorStop(0.833, Qt.rgba(1, 0, 1, 1))
gradient.addColorStop(1.000, Qt.rgba(1, 0, 0, 1))
if (s > 0)
root.hue = h
ctx.fillStyle = gradient
ctx.fillRect(0, 0, gradientOverlay.width, gradientOverlay.height)
root.saturation = s
root.lightness = l
gradient = ctx.createLinearGradient(0, 0, 0, gradientOverlay.height)
gradient.addColorStop(0.000, Qt.rgba(0, 0, 0, 0))
gradient.addColorStop(1.000, Qt.rgba(1, 1, 1, 1))
root.block = false
invalidateColor()
ctx.fillStyle = gradient
ctx.fillRect(0, 0, gradientOverlay.width, gradientOverlay.height)
}
function drawHSLA(ctx) {
for (var row = 0; row < gradientOverlay.height; row++) {
var gradient = ctx.createLinearGradient(0, 0, gradientOverlay.width, 0)
var l = Math.abs(row - gradientOverlay.height) / gradientOverlay.height
gradient.addColorStop(0, Qt.hsla(root.hue, 0, l, 1))
gradient.addColorStop(1, Qt.hsla(root.hue, 1, l, 1))
ctx.fillStyle = gradient
ctx.fillRect(0, row, gradientOverlay.width, 1)
}
}
Rectangle {
@@ -133,26 +210,30 @@ Column {
Canvas {
id: gradientOverlay
property real hue: root.hue
anchors.fill: parent
opacity: root.alpha
opacity: root.color.a
Connections {
target: root
function onHueChanged() { gradientOverlay.requestPaint() }
}
onHueChanged: requestPaint()
onPaint: {
var ctx = gradientOverlay.getContext('2d')
ctx.save()
ctx.clearRect(0, 0, gradientOverlay.width, gradientOverlay.height)
for (var row = 0; row < gradientOverlay.height; row++) {
var gradient = ctx.createLinearGradient(0, 0, gradientOverlay.width,0)
var l = Math.abs(row - gradientOverlay.height) / gradientOverlay.height
gradient.addColorStop(0, Qt.hsla(gradientOverlay.hue, 0, l, 1))
gradient.addColorStop(1, Qt.hsla(gradientOverlay.hue, 1, l, 1))
ctx.fillStyle = gradient
ctx.fillRect(0, row, gradientOverlay.width, 1)
switch (root.mode) {
case ColorPicker.Mode.RGBA:
root.drawRGBA(ctx)
break
case ColorPicker.Mode.HSLA:
root.drawHSLA(ctx)
break
case ColorPicker.Mode.HSVA:
default:
root.drawHSVA(ctx)
break
}
ctx.restore()
@@ -162,25 +243,41 @@ Column {
Canvas {
id: pickerCross
property real cavnasSaturation: root.saturation
property real canvasLightness: root.lightness
property color strokeStyle: "lightGray"
opacity: 0.8
anchors.fill: parent
antialiasing: true
onCavnasSaturationChanged: requestPaint();
onCanvasLightnessChanged: requestPaint();
Connections {
target: root
function onColorInvalidated() { pickerCross.requestPaint() }
function onColorChanged() { pickerCross.requestPaint() }
function onModeChanged() { pickerCross.requestPaint() }
}
onPaint: {
var ctx = pickerCross.getContext('2d')
ctx.save()
ctx.clearRect(0, 0, pickerCross.width, pickerCross.height)
var yy = pickerCross.height -root.lightness * pickerCross.height
var xx = root.saturation * pickerCross.width
var yy, xx = 0
switch (root.mode) {
case ColorPicker.Mode.RGBA:
yy = pickerCross.height - root.saturationHSV * pickerCross.height
xx = root.hue * pickerCross.width
break
case ColorPicker.Mode.HSLA:
yy = pickerCross.height - root.lightness * pickerCross.height
xx = root.saturationHSL * pickerCross.width
break
case ColorPicker.Mode.HSVA:
default:
yy = pickerCross.height - root.value * pickerCross.height
xx = root.saturationHSV * pickerCross.width
break
}
ctx.strokeStyle = pickerCross.strokeStyle
ctx.lineWidth = 1
@@ -200,24 +297,37 @@ Column {
}
MouseArea {
id: mapMouseArea
id: mouseArea
anchors.fill: parent
preventStealing: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPositionChanged: function(mouse) {
if (pressed && mouse.buttons === Qt.LeftButton) {
if (mouseArea.pressed && mouse.buttons === Qt.LeftButton) {
var xx = Math.max(0, Math.min(mouse.x, parent.width))
var yy = Math.max(0, Math.min(mouse.y, parent.height))
root.lightness = 1.0 - yy / parent.height
root.saturation = xx / parent.width
switch (root.mode) {
case ColorPicker.Mode.RGBA:
root.saturationHSV = 1.0 - yy / parent.height
root.hue = xx / parent.width
break
case ColorPicker.Mode.HSLA:
root.saturationHSL = xx / parent.width
root.lightness = 1.0 - yy / parent.height
break
case ColorPicker.Mode.HSVA:
default:
root.saturationHSV = xx / parent.width
root.value = 1.0 - yy / parent.height
break
}
}
}
onPressed: function(mouse) {
if (mouse.button === Qt.LeftButton)
positionChanged(mouse)
mouseArea.positionChanged(mouse)
}
onReleased: function(mouse) {
if (mouse.button === Qt.LeftButton)
@@ -233,10 +343,23 @@ Column {
HueSlider {
id: hueSlider
visible: root.mode !== ColorPicker.Mode.RGBA
width: parent.width
onValueChanged: {
if (root.hue !== value)
root.hue = value
if (root.hue !== hueSlider.value)
root.hue = hueSlider.value
}
onClicked: root.updateColor()
}
LuminanceSlider {
id: luminanceSlider
visible: root.mode === ColorPicker.Mode.RGBA
width: parent.width
color: Qt.hsva(root.hue, root.color.hsvSaturation, 1, 1)
onValueChanged: {
if (root.value !== luminanceSlider.value)
root.value = (1.0 - luminanceSlider.value)
}
onClicked: root.updateColor()
}
@@ -246,8 +369,8 @@ Column {
width: parent.width
color: Qt.rgba(root.color.r, root.color.g, root.color.b, 1)
onValueChanged: {
if (root.alpha !== value)
root.alpha = (1.0 - value)
if (root.alpha !== opacitySlider.value)
root.alpha = (1.0 - opacitySlider.value)
}
onClicked: root.updateColor()
}

View File

@@ -0,0 +1,121 @@
/****************************************************************************
**
** Copyright (C) 2021 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
import QtQuick 2.15
import StudioTheme 1.0 as StudioTheme
Item {
id: root
property real value: 1
property real minimum: 0
property real maximum: 1
property bool pressed: mouseArea.pressed
property bool integer: false
property color color
signal clicked
height: StudioTheme.Values.hueSliderHeight
function updatePos() {
if (root.maximum > root.minimum) {
var pos = (track.width - handle.width) * (root.value - root.minimum) / (root.maximum - root.minimum)
return Math.min(Math.max(pos, 0), track.width - handle.width)
} else {
return 0
}
}
Item {
id: track
width: parent.width
height: parent.height
Image {
id: checkerboard
anchors.fill: parent
source: "images/checkers.png"
fillMode: Image.Tile
}
Rectangle {
anchors.fill: parent
border.color: StudioTheme.Values.themeControlOutline
border.width: StudioTheme.Values.border
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.000; color: root.color }
GradientStop { position: 1.000; color: "black" }
}
}
Rectangle {
id: handle
width: StudioTheme.Values.hueSliderHandleWidth
height: track.height - 4
anchors.verticalCenter: parent.verticalCenter
smooth: true
color: "transparent"
radius: 2
border.color: "black"
border.width: 1
x: root.updatePos()
y: 2
z: 1
Rectangle {
anchors.fill: parent
anchors.margins: 1
color: "transparent"
radius: 1
border.color: "white"
border.width: 1
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
preventStealing: true
function calculateValue() {
var handleX = Math.max(0, Math.min(mouseArea.mouseX, mouseArea.width))
var realValue = (root.maximum - root.minimum) * handleX / mouseArea.width + root.minimum
root.value = root.integer ? Math.round(realValue) : realValue
}
onPressed: calculateValue()
onReleased: root.clicked()
onPositionChanged: {
if (pressed)
calculateValue()
}
}
}
}

View File

@@ -45,6 +45,7 @@ Label 2.0 Label.qml
LineEdit 2.0 LineEdit.qml
LinkIndicator2D 2.0 LinkIndicator2D.qml
ListViewComboBox 2.0 ListViewComboBox.qml
LuminanceSlider 2.0 LuminanceSlider.qml
MarginSection 2.0 MarginSection.qml
MultiIconLabel 2.0 MultiIconLabel.qml
OpacitySlider 2.0 OpacitySlider.qml