Files
noctalia-shell/Widgets/NColorPickerDialog.qml
T
2025-11-22 01:12:00 -05:00

771 lines
25 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services.UI
import "../Helpers/ColorList.js" as ColorList
Popup {
id: root
property color selectedColor: Color.black
property string editMode: "r" // Options: "r", "g", "b", "h", "s", "v"
// Code to deal with Hue when color is achromatic
property real stableHue: 0
onSelectedColorChanged: {
if (selectedColor.hsvHue >= 0) {
stableHue = selectedColor.hsvHue;
}
}
readonly property real displayHue: selectedColor.hsvHue < 0 ? stableHue : selectedColor.hsvHue
signal colorSelected(color color)
width: 580
height: {
const h = scrollView.implicitHeight + padding * 2;
Math.min(h, screen?.height - Style.barHeight - Style.marginL * 2);
}
padding: Style.marginXL
// Center popup in parent
x: (parent.width - width) * 0.5
y: (parent.height - height) * 0.5
modal: true
background: Rectangle {
color: Color.mSurface
radius: Style.radiusS
border.color: Color.mPrimary
border.width: Style.borderM
}
contentItem: NScrollView {
id: scrollView
width: parent.width
verticalPolicy: ScrollBar.AlwaysOff
horizontalPolicy: ScrollBar.AlwaysOff
ColumnLayout {
width: scrollView.availableWidth
spacing: Style.marginL
// Header
RowLayout {
Layout.fillWidth: true
RowLayout {
spacing: Style.marginS
NIcon {
icon: "color-picker"
pointSize: Style.fontSizeXXL
color: Color.mPrimary
}
NText {
text: I18n.tr("widgets.color-picker.title")
pointSize: Style.fontSizeXL
font.weight: Style.fontWeightBold
color: Color.mPrimary
}
}
Item {
Layout.fillWidth: true
}
NIconButton {
icon: "close"
onClicked: root.close()
}
}
// Color preview section
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 80
radius: Style.radiusS
color: root.selectedColor
border.color: Color.mOutline
border.width: Style.borderS
ColumnLayout {
spacing: 0
anchors.fill: parent
Item {
Layout.fillHeight: true
}
NText {
text: root.selectedColor.toString().toUpperCase()
family: Settings.data.ui.fontFixed
pointSize: Style.fontSizeL
font.weight: Font.Bold
color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? Color.black : Color.white
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "RGB(" + Math.round(root.selectedColor.r * 255) + ", " + Math.round(root.selectedColor.g * 255) + ", " + Math.round(root.selectedColor.b * 255) + ")"
family: Settings.data.ui.fontFixed
pointSize: Style.fontSizeM
color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? Color.black : Color.white
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
}
// Main Box
NBox {
Layout.fillWidth: true
Layout.preferredHeight: controlsOutterColumn.implicitHeight + Style.marginL * 2
ButtonGroup {
id: colorValues
}
ColumnLayout {
id: controlsOutterColumn
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
RowLayout {
spacing: Style.marginL // Ensure nice gap between Left and Right groups
// SpinBoxes Column
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignTop
Layout.preferredWidth: 3
// --- RED ---
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NRadioButton {
ButtonGroup.group: colorValues
text: "R"
font.weight: Font.Bold
checked: true
onClicked: root.editMode = "r"
Layout.fillWidth: false
}
NSpinBox {
id: redSpinBox
from: 0
to: 255
onValueChanged: {
if (!selectedSlider.pressed && !fieldMouse.pressed && !hexInput.activeFocus && value !== Math.round(root.selectedColor.r * 255)) {
root.selectedColor = Qt.rgba(value / 255, root.selectedColor.g, root.selectedColor.b, 1);
}
}
Binding {
target: redSpinBox
property: "value"
value: Math.round(root.selectedColor.r * 255)
}
}
}
// --- GREEN ---
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NRadioButton {
ButtonGroup.group: colorValues
text: "G"
font.weight: Font.Bold
onClicked: root.editMode = "g"
Layout.fillWidth: false
}
NSpinBox {
id: greenSpinBox
from: 0
to: 255
onValueChanged: {
if (!selectedSlider.pressed && !fieldMouse.pressed && !hexInput.activeFocus && value !== Math.round(root.selectedColor.g * 255)) {
root.selectedColor = Qt.rgba(root.selectedColor.r, value / 255, root.selectedColor.b, 1);
}
}
Binding {
target: greenSpinBox
property: "value"
value: Math.round(root.selectedColor.g * 255)
}
}
}
// --- BLUE ---
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NRadioButton {
ButtonGroup.group: colorValues
text: "B"
font.weight: Font.Bold
onClicked: root.editMode = "b"
Layout.fillWidth: false
}
NSpinBox {
id: blueSpinBox
from: 0
to: 255
onValueChanged: {
if (!selectedSlider.pressed && !fieldMouse.pressed && !hexInput.activeFocus && value !== Math.round(root.selectedColor.b * 255)) {
root.selectedColor = Qt.rgba(root.selectedColor.r, root.selectedColor.g, value / 255, 1);
}
}
Binding {
target: blueSpinBox
property: "value"
value: Math.round(root.selectedColor.b * 255)
}
}
}
// Spacer
Item {
Layout.fillHeight: true
Layout.fillWidth: true
}
// --- HUE ---
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NRadioButton {
ButtonGroup.group: colorValues
text: "H"
font.weight: Font.Bold
checked: true
onClicked: root.editMode = "h"
Layout.fillWidth: false
}
NSpinBox {
id: hueSpinBox
from: 0
to: 360
onValueChanged: {
if (!selectedSlider.pressed && !fieldMouse.pressed && !hexInput.activeFocus && value !== Math.round(root.displayHue * 360)) {
var newHue = value / 360;
root.selectedColor = Qt.hsva(newHue, root.selectedColor.hsvSaturation, root.selectedColor.hsvValue, 1);
root.stableHue = newHue;
}
}
Binding {
target: hueSpinBox
property: "value"
value: Math.round(root.displayHue * 360)
}
}
}
// --- SATURATION ---
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NRadioButton {
ButtonGroup.group: colorValues
text: "S"
font.weight: Font.Bold
onClicked: root.editMode = "s"
Layout.fillWidth: false
}
NSpinBox {
id: satSpinBox
from: 0
to: 100
onValueChanged: {
if (!selectedSlider.pressed && !fieldMouse.pressed && !hexInput.activeFocus && value !== Math.round(root.selectedColor.hsvSaturation * 100)) {
root.selectedColor = Qt.hsva(root.selectedColor.hsvHue, value / 100, root.selectedColor.hsvValue, 1);
}
}
Binding {
target: satSpinBox
property: "value"
value: Math.round(root.selectedColor.hsvSaturation * 100)
}
}
}
// --- VALUE ---
RowLayout {
spacing: Style.marginM
NRadioButton {
ButtonGroup.group: colorValues
text: "V"
font.weight: Font.Bold
onClicked: root.editMode = "v"
Layout.fillWidth: false
}
NSpinBox {
id: valSpinBox
from: 0
to: 100
onValueChanged: {
if (!selectedSlider.pressed && !fieldMouse.pressed && !hexInput.activeFocus && value !== Math.round(root.selectedColor.hsvValue * 100)) {
root.selectedColor = Qt.hsva(root.selectedColor.hsvHue, root.selectedColor.hsvSaturation, value / 100, 1);
}
}
Binding {
target: valSpinBox
property: "value"
value: Math.round(root.selectedColor.hsvValue * 100)
}
}
}
// Spacer
Item {
Layout.fillHeight: true
Layout.fillWidth: true
}
// Hex input
RowLayout {
id: hexInput
Layout.fillWidth: true
spacing: Style.marginM
NLabel {
label: "Hex:" // I18n.tr("widgets.color-picker.hex.label")
// description: I18n.tr("widgets.color-picker.hex.description")
Layout.alignment: Qt.AlignVCenter
}
NTextInput {
text: root.selectedColor.toString().toUpperCase()
fontFamily: Settings.data.ui.fontFixed
Layout.fillWidth: true
Layout.minimumWidth: implicitWidth
onEditingFinished: {
if (/^#[0-9A-F]{6}$/i.test(text)) {
root.selectedColor = text;
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredWidth: 5
Layout.alignment: Qt.AlignTop
spacing: Style.marginS
Layout.topMargin: Math.round(2 * Style.uiScaleRatio) //Shim to try and line up colorField with SpinBoxes
// --- SLIDER ---
NColorSlider {
id: selectedSlider
Layout.fillHeight: true
rainbowMode: root.editMode === "h"
topColor: {
if (rainbowMode)
return "transparent";
switch (root.editMode) {
case "r":
return "#FF0000";
case "g":
return "#00FF00";
case "b":
return "#0000FF";
default:
return "#FFFFFF";
}
}
Binding {
target: selectedSlider
property: "value"
when: !selectedSlider.pressed // Only update from model when NOT dragging
value: {
switch (root.editMode) {
case "r":
return root.selectedColor.r;
case "g":
return root.selectedColor.g;
case "b":
return root.selectedColor.b;
case "h":
return root.displayHue;
case "s":
return root.selectedColor.hsvSaturation;
case "v":
return root.selectedColor.hsvValue;
default:
return 0;
}
}
}
onMoved: {
var v = value;
switch (root.editMode) {
case "r":
root.selectedColor = Qt.rgba(v, root.selectedColor.g, root.selectedColor.b, 1);
break;
case "g":
root.selectedColor = Qt.rgba(root.selectedColor.r, v, root.selectedColor.b, 1);
break;
case "b":
root.selectedColor = Qt.rgba(root.selectedColor.r, root.selectedColor.g, v, 1);
break;
case "h":
root.selectedColor = Qt.hsva(v, root.selectedColor.hsvSaturation, root.selectedColor.hsvValue, 1);
root.stableHue = v;
break;
case "s":
root.selectedColor = Qt.hsva(root.selectedColor.hsvHue, v, root.selectedColor.hsvValue, 1);
break;
case "v":
root.selectedColor = Qt.hsva(root.selectedColor.hsvHue, root.selectedColor.hsvSaturation, v, 1);
break;
}
}
}
// Color Field
Rectangle {
id: colorField
Layout.fillWidth: true
Layout.preferredHeight: width
Layout.alignment: Qt.AlignTop
radius: 0
border.color: Color.mOutline
border.width: Style.borderS
clip: true
ShaderEffect {
anchors.fill: parent
anchors.margins: 1 // Avoid drawing over the border
fragmentShader: "../Shaders/qsb/color_picker.frag.qsb"
// Map strings to integers
readonly property int modeInt: {
switch (root.editMode) {
case "r":
return 0;
case "g":
return 1;
case "b":
return 2;
case "h":
return 3;
case "s":
return 4;
case "v":
return 5;
default:
return 0;
}
}
// Pass which radio is selected
readonly property real fixedVal: {
switch (root.editMode) {
case "r":
return root.selectedColor.r;
case "g":
return root.selectedColor.g;
case "b":
return root.selectedColor.b;
case "h":
return root.displayHue;
case "s":
return root.selectedColor.hsvSaturation;
case "v":
return root.selectedColor.hsvValue;
default:
return 0;
}
}
// Send as one vector because GPUs are so damn picky
property vector4d params: Qt.vector4d(1.0, fixedVal, modeInt, 0)
}
MouseArea {
id: fieldMouse
anchors.fill: parent
cursorShape: Qt.CrossCursor
// Update color when clicking or dragging
function updateColor(mouse) {
// Normalize X and Y (0.0 to 1.0)
var xVal = Math.max(0, Math.min(1, mouse.x / width));
var yVal = Math.max(0, Math.min(1, 1.0 - (mouse.y / height))); // Flip Y (0 at bottom)
// Get the current fixed value (from the slider)
var fixed = 0.0;
switch (root.editMode) {
case "r":
fixed = root.selectedColor.r;
root.selectedColor = Qt.rgba(fixed, xVal, yVal, 1);
break;
case "g":
fixed = root.selectedColor.g;
root.selectedColor = Qt.rgba(xVal, fixed, yVal, 1);
break;
case "b":
fixed = root.selectedColor.b;
root.selectedColor = Qt.rgba(xVal, yVal, fixed, 1);
break;
case "h":
// Use stableHue to prevent flipping to -1
fixed = root.displayHue;
root.selectedColor = Qt.hsva(fixed, xVal, yVal, 1);
root.stableHue = fixed;
break;
case "s":
fixed = root.selectedColor.hsvSaturation;
root.selectedColor = Qt.hsva(xVal, fixed, yVal, 1);
// If we dragged Hue (xVal), update stableHue
root.stableHue = yVal;
break;
case "v":
fixed = root.selectedColor.hsvValue;
root.selectedColor = Qt.hsva(xVal, yVal, fixed, 1);
// If we dragged Hue (xVal), update stableHue
root.stableHue = xVal;
break;
}
}
onPressed: mouse => updateColor(mouse)
onPositionChanged: mouse => updateColor(mouse)
}
// Color Indicator
Rectangle {
width: 10
height: 10
radius: 5
color: "transparent"
border.color: root.selectedColor.hsvValue < 0.5 ? "white" : "black"
border.width: 1
// Find position based on the current color
readonly property point selectedPos: {
switch (root.editMode) {
case "r":
return Qt.point(root.selectedColor.g, root.selectedColor.b);
case "g":
return Qt.point(root.selectedColor.r, root.selectedColor.b);
case "b":
return Qt.point(root.selectedColor.r, root.selectedColor.g);
case "h":
return Qt.point(root.selectedColor.hsvSaturation, root.selectedColor.hsvValue);
case "s":
return Qt.point(root.displayHue, root.selectedColor.hsvValue);
case "v":
return Qt.point(root.displayHue, root.selectedColor.hsvSaturation);
default:
return Qt.point(0, 0);
}
}
// Convert values to pixel position
x: (selectedPos.x * parent.width) - width / 2
y: ((1.0 - selectedPos.y) * parent.height) - height / 2
}
// Redraw the border in case Color Indicator is near the edge
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Color.mOutline
border.width: Style.borderS
antialiasing: false
}
}
}
}
NCollapsible {
id: paletteCollapsible
label: I18n.tr("widgets.color-picker.palette.label")
description: I18n.tr("widgets.color-picker.palette.description")
Layout.fillWidth: true
contentSpacing: Style.marginS
GridLayout {
Layout.alignment: Qt.AlignHCenter
columns: 15
columnSpacing: 6
rowSpacing: 6
Repeater {
model: ColorList.colors
Rectangle {
width: 24
height: 24
radius: Style.radiusXXS
color: modelData.color
border.color: root.selectedColor.toString() === modelData.color.toString() ? Color.mPrimary : Color.mOutline
border.width: Math.max(1, root.selectedColor.toString() === modelData.color.toString() ? Style.borderM : Style.borderS)
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: {
TooltipService.show(screen, parent, modelData.name + "\n" + parent.color.toString().toUpperCase(), "auto");
}
onExited: {
TooltipService.hide();
}
onClicked: {
root.selectedColor = modelData.color;
TooltipService.hide();
}
}
}
}
NDivider {
Layout.columnSpan: 15
Layout.fillWidth: true
Layout.topMargin: Style.marginXS
Layout.bottomMargin: 0
}
NLabel {
Layout.columnSpan: 15
Layout.fillWidth: true
description: I18n.tr("widgets.color-picker.theme-colors.description")
}
Repeater {
model: [
{
name: "mPrimary",
color: Color.mPrimary
},
{
name: "mSecondary",
color: Color.mSecondary
},
{
name: "mTertiary",
color: Color.mTertiary
},
{
name: "mError",
color: Color.mError
},
{
name: "mSurface",
color: Color.mSurface
},
{
name: "mSurfaceVariant",
color: Color.mSurfaceVariant
},
{
name: "mOutline",
color: Color.mOutline
}
]
Rectangle {
width: 24
height: 24
radius: 4
color: modelData.color
border.color: root.selectedColor.toString() === modelData.color.toString() ? Color.mPrimary : Color.mOutline
border.width: root.selectedColor.toString() === modelData.color.toString() ? 2 : 1
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: {
TooltipService.show(screen, parent, modelData.name + "\n" + parent.color.toString().toUpperCase(), "auto");
}
onExited: {
TooltipService.hide();
}
onClicked: {
root.selectedColor = modelData.color;
TooltipService.hide();
}
}
}
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 20
Layout.bottomMargin: 20
spacing: 10
Item {
Layout.fillWidth: true
}
NButton {
id: cancelButton
text: I18n.tr("widgets.color-picker.cancel")
outlined: cancelButton.hovered ? false : true
onClicked: {
root.close();
}
}
NButton {
text: I18n.tr("widgets.color-picker.apply")
icon: "check"
onClicked: {
root.colorSelected(root.selectedColor);
root.close();
}
}
}
}
}
}