Files
noctalia-shell/Widgets/NColorPickerDialog.qml
T
2026-01-02 02:38:05 -05:00

768 lines
26 KiB
QML

import QtQml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "../Helpers/ColorList.js" as ColorList
import qs.Commons
import qs.Services.UI
import qs.Widgets
Popup {
id: root
property var screen
property color selectedColor: Color.black
enum EditMode {
R,
G,
B,
H,
S,
V
}
property int editMode: NColorPickerDialog.EditMode.R
// 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
padding: Style.marginXL
modal: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onOpened: {
// Center on screen after popup is opened and parent position is known
if (screen && parent) {
var parentPos = parent.mapToItem(null, 0, 0);
x = Math.round((screen.width - width) / 2 - parentPos.x);
y = Math.round((screen.height - height) / 2 - parentPos.y);
}
}
background: Rectangle {
color: Color.mSurface
radius: Style.iRadiusS
border.color: Color.mPrimary
border.width: Style.borderM
}
contentItem: ColumnLayout {
id: mainContent
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.iRadiusS
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: Style.fontWeightMedium
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: Style.fontWeightMedium
checked: true
onClicked: root.editMode = NColorPickerDialog.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: Style.fontWeightMedium
onClicked: root.editMode = NColorPickerDialog.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: Style.fontWeightMedium
onClicked: root.editMode = NColorPickerDialog.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: Style.fontWeightMedium
checked: true
onClicked: root.editMode = NColorPickerDialog.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: Style.fontWeightMedium
onClicked: root.editMode = NColorPickerDialog.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: Style.fontWeightMedium
onClicked: root.editMode = NColorPickerDialog.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:"
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 NColorPickerDialog.EditMode.R:
return "#FF0000";
case NColorPickerDialog.EditMode.G:
return "#00FF00";
case NColorPickerDialog.EditMode.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 NColorPickerDialog.EditMode.R:
return root.selectedColor.r;
case NColorPickerDialog.EditMode.G:
return root.selectedColor.g;
case NColorPickerDialog.EditMode.B:
return root.selectedColor.b;
case NColorPickerDialog.EditMode.H:
return root.displayHue;
case NColorPickerDialog.EditMode.S:
return root.selectedColor.hsvSaturation;
case NColorPickerDialog.EditMode.V:
return root.selectedColor.hsvValue;
default:
return 0;
}
}
}
onMoved: {
var v = value;
switch (root.editMode) {
case NColorPickerDialog.EditMode.R:
root.selectedColor = Qt.rgba(v, root.selectedColor.g, root.selectedColor.b, 1);
break;
case NColorPickerDialog.EditMode.G:
root.selectedColor = Qt.rgba(root.selectedColor.r, v, root.selectedColor.b, 1);
break;
case NColorPickerDialog.EditMode.B:
root.selectedColor = Qt.rgba(root.selectedColor.r, root.selectedColor.g, v, 1);
break;
case NColorPickerDialog.EditMode.H:
root.selectedColor = Qt.hsva(v, root.selectedColor.hsvSaturation, root.selectedColor.hsvValue, 1);
root.stableHue = v;
break;
case NColorPickerDialog.EditMode.S:
root.selectedColor = Qt.hsva(root.selectedColor.hsvHue, v, root.selectedColor.hsvValue, 1);
break;
case NColorPickerDialog.EditMode.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"
// Pass which radio is selected
readonly property real fixedVal: {
switch (root.editMode) {
case NColorPickerDialog.EditMode.R:
return root.selectedColor.r;
case NColorPickerDialog.EditMode.G:
return root.selectedColor.g;
case NColorPickerDialog.EditMode.B:
return root.selectedColor.b;
case NColorPickerDialog.EditMode.H:
return root.displayHue;
case NColorPickerDialog.EditMode.S:
return root.selectedColor.hsvSaturation;
case NColorPickerDialog.EditMode.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, root.editMode, 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 NColorPickerDialog.EditMode.R:
fixed = root.selectedColor.r;
root.selectedColor = Qt.rgba(fixed, xVal, yVal, 1);
break;
case NColorPickerDialog.EditMode.G:
fixed = root.selectedColor.g;
root.selectedColor = Qt.rgba(xVal, fixed, yVal, 1);
break;
case NColorPickerDialog.EditMode.B:
fixed = root.selectedColor.b;
root.selectedColor = Qt.rgba(xVal, yVal, fixed, 1);
break;
case NColorPickerDialog.EditMode.H:
// Use stableHue to prevent flipping to -1
fixed = root.displayHue;
root.selectedColor = Qt.hsva(fixed, xVal, yVal, 1);
root.stableHue = fixed;
break;
case NColorPickerDialog.EditMode.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 NColorPickerDialog.EditMode.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: Math.min(Style.iRadiusXS, width / 2)
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 NColorPickerDialog.EditMode.R:
return Qt.point(root.selectedColor.g, root.selectedColor.b);
case NColorPickerDialog.EditMode.G:
return Qt.point(root.selectedColor.r, root.selectedColor.b);
case NColorPickerDialog.EditMode.B:
return Qt.point(root.selectedColor.r, root.selectedColor.g);
case NColorPickerDialog.EditMode.H:
return Qt.point(root.selectedColor.hsvSaturation, root.selectedColor.hsvValue);
case NColorPickerDialog.EditMode.S:
return Qt.point(root.displayHue, root.selectedColor.hsvValue);
case NColorPickerDialog.EditMode.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
}
}
}
}
NLabel {
label: I18n.tr("widgets.color-picker.palette.label")
Layout.fillWidth: true
}
NScrollView {
Layout.fillWidth: true
Layout.preferredHeight: Math.min(paletteGrid.implicitHeight, 200)
verticalPolicy: paletteGrid.implicitHeight > 200 ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
horizontalPolicy: ScrollBar.AlwaysOff
GridLayout {
id: paletteGrid
width: parent.availableWidth
columns: 17
columnSpacing: 6
rowSpacing: 6
NLabel {
Layout.columnSpan: 17
Layout.fillWidth: true
description: I18n.tr("widgets.color-picker.palette.theme-colors")
}
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: Style.iRadiusXXS
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(parent, modelData.name + "\n" + parent.color.toString().toUpperCase());
}
onExited: {
TooltipService.hide();
}
onClicked: {
root.selectedColor = modelData.color;
TooltipService.hide();
}
}
}
}
NDivider {
Layout.columnSpan: 17
Layout.fillWidth: true
Layout.topMargin: Style.marginXS
Layout.bottomMargin: 0
}
NLabel {
Layout.columnSpan: 17
Layout.fillWidth: true
description: I18n.tr("widgets.color-picker.palette.description")
}
Repeater {
model: ColorList.colors
Rectangle {
width: 24
height: 24
radius: Math.min(Style.iRadiusXS, width / 2)
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(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: 1
Layout.bottomMargin: 1
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);
// Delay close to prevent click propagation to elements behind the dialog
Qt.callLater(() => {
root.close();
});
}
}
}
}
}