mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
708 lines
30 KiB
QML
708 lines
30 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Hyprland
|
|
import qs.Common
|
|
import qs.Modals.Common
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
DankModal {
|
|
id: root
|
|
|
|
layerNamespace: "dms:color-picker"
|
|
|
|
HyprlandFocusGrab {
|
|
windows: [root.contentWindow]
|
|
active: CompositorService.isHyprland && root.shouldHaveFocus
|
|
}
|
|
|
|
property string pickerTitle: I18n.tr("Choose Color")
|
|
property color selectedColor: SessionData.recentColors.length > 0 ? SessionData.recentColors[0] : Theme.primary
|
|
property var onColorSelectedCallback: null
|
|
|
|
signal colorSelected(color selectedColor)
|
|
|
|
property color currentColor: Theme.primary
|
|
property real hue: 0
|
|
property real saturation: 1
|
|
property real value: 1
|
|
property real alpha: 1
|
|
property real gradientX: 0
|
|
property real gradientY: 0
|
|
|
|
readonly property var standardColors: ["#f44336", "#e91e63", "#9c27b0", "#673ab7", "#3f51b5", "#2196f3", "#03a9f4", "#00bcd4", "#009688", "#4caf50", "#8bc34a", "#cddc39", "#ffeb3b", "#ffc107", "#ff9800", "#ff5722", "#d32f2f", "#c2185b", "#7b1fa2", "#512da8", "#303f9f", "#1976d2", "#0288d1", "#0097a7", "#00796b", "#388e3c", "#689f38", "#afb42b", "#fbc02d", "#ffa000", "#f57c00", "#e64a19", "#c62828", "#ad1457", "#6a1b9a", "#4527a0", "#283593", "#1565c0", "#0277bd", "#00838f", "#00695c", "#2e7d32", "#558b2f", "#9e9d24", "#f9a825", "#ff8f00", "#ef6c00", "#d84315", "#ffffff", "#9e9e9e", "#212121"]
|
|
|
|
function show() {
|
|
currentColor = selectedColor;
|
|
updateFromColor(currentColor);
|
|
open();
|
|
}
|
|
|
|
function hide() {
|
|
onColorSelectedCallback = null;
|
|
close();
|
|
}
|
|
|
|
function hideInstant() {
|
|
instantClose();
|
|
}
|
|
|
|
onColorSelected: color => {
|
|
if (onColorSelectedCallback) {
|
|
onColorSelectedCallback(color);
|
|
}
|
|
}
|
|
|
|
function copyColorToClipboard(colorValue) {
|
|
Quickshell.execDetached(["sh", "-c", `echo -n "${colorValue}" | wl-copy`]);
|
|
ToastService.showInfo(`Color ${colorValue} copied`);
|
|
SessionData.addRecentColor(currentColor);
|
|
}
|
|
|
|
function updateFromColor(color) {
|
|
hue = color.hsvHue;
|
|
saturation = color.hsvSaturation;
|
|
value = color.hsvValue;
|
|
alpha = color.a;
|
|
gradientX = saturation;
|
|
gradientY = 1 - value;
|
|
}
|
|
|
|
function updateColor() {
|
|
currentColor = Qt.hsva(hue, saturation, value, alpha);
|
|
}
|
|
|
|
function updateColorFromGradient(x, y) {
|
|
saturation = Math.max(0, Math.min(1, x));
|
|
value = Math.max(0, Math.min(1, 1 - y));
|
|
updateColor();
|
|
selectedColor = currentColor;
|
|
}
|
|
|
|
function applyPickedColor(colorStr) {
|
|
if (colorStr.length < 7 || !colorStr.startsWith('#'))
|
|
return;
|
|
const pickedColor = Qt.color(colorStr);
|
|
root.selectedColor = pickedColor;
|
|
root.currentColor = pickedColor;
|
|
root.updateFromColor(pickedColor);
|
|
copyColorToClipboard(colorStr);
|
|
root.show();
|
|
}
|
|
|
|
function pickColorFromScreen() {
|
|
hideInstant();
|
|
Proc.runCommand("dms-color-pick", ["dms", "color", "pick", "--json"], (output, exitCode) => {
|
|
if (exitCode !== 0) {
|
|
console.warn("dms color pick exited with code:", exitCode);
|
|
root.show();
|
|
return;
|
|
}
|
|
try {
|
|
const result = JSON.parse(output);
|
|
if (result.hex) {
|
|
applyPickedColor(result.hex);
|
|
} else {
|
|
console.warn("Failed to parse dms color pick output: missing hex");
|
|
root.show();
|
|
}
|
|
} catch (e) {
|
|
console.warn("Failed to parse dms color pick JSON:", e);
|
|
root.show();
|
|
}
|
|
}, 0, Proc.noTimeout);
|
|
}
|
|
|
|
modalWidth: 680
|
|
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680
|
|
backgroundColor: Theme.surfaceContainer
|
|
cornerRadius: Theme.cornerRadius
|
|
borderColor: Theme.outlineMedium
|
|
borderWidth: 1
|
|
keepContentLoaded: true
|
|
allowStacking: true
|
|
|
|
onBackgroundClicked: hide()
|
|
|
|
content: Component {
|
|
FocusScope {
|
|
id: colorContent
|
|
|
|
property alias hexInput: hexInput
|
|
|
|
anchors.fill: parent
|
|
implicitHeight: mainColumn.implicitHeight
|
|
focus: true
|
|
|
|
Keys.onEscapePressed: event => {
|
|
root.hide();
|
|
event.accepted = true;
|
|
}
|
|
|
|
Column {
|
|
id: mainColumn
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: Theme.spacingM
|
|
spacing: Theme.spacingM
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
Column {
|
|
width: parent.width - 90
|
|
spacing: Theme.spacingXS
|
|
|
|
StyledText {
|
|
text: root.pickerTitle
|
|
font.pixelSize: Theme.fontSizeLarge
|
|
color: Theme.surfaceText
|
|
font.weight: Font.Medium
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Select a color from the palette or use custom sliders")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceTextMedium
|
|
}
|
|
}
|
|
|
|
DankActionButton {
|
|
iconName: "colorize"
|
|
iconSize: Theme.iconSize - 4
|
|
iconColor: Theme.surfaceText
|
|
onClicked: () => {
|
|
root.pickColorFromScreen();
|
|
}
|
|
}
|
|
|
|
DankActionButton {
|
|
iconName: "close"
|
|
iconSize: Theme.iconSize - 4
|
|
iconColor: Theme.surfaceText
|
|
onClicked: () => {
|
|
root.hide();
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingM
|
|
|
|
Rectangle {
|
|
id: gradientPicker
|
|
width: parent.width - 70
|
|
height: 280
|
|
radius: Theme.cornerRadius
|
|
border.color: Theme.outlineStrong
|
|
border.width: 1
|
|
clip: true
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: Qt.hsva(root.hue, 1, 1, 1)
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
gradient: Gradient {
|
|
orientation: Gradient.Horizontal
|
|
GradientStop {
|
|
position: 0.0
|
|
color: "#ffffff"
|
|
}
|
|
GradientStop {
|
|
position: 1.0
|
|
color: "transparent"
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
gradient: Gradient {
|
|
orientation: Gradient.Vertical
|
|
GradientStop {
|
|
position: 0.0
|
|
color: "transparent"
|
|
}
|
|
GradientStop {
|
|
position: 1.0
|
|
color: "#000000"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: pickerCircle
|
|
width: 16
|
|
height: 16
|
|
radius: 8
|
|
border.color: "white"
|
|
border.width: 2
|
|
color: "transparent"
|
|
x: root.gradientX * parent.width - width / 2
|
|
y: root.gradientY * parent.height - height / 2
|
|
|
|
Rectangle {
|
|
anchors.centerIn: parent
|
|
width: parent.width - 4
|
|
height: parent.height - 4
|
|
radius: width / 2
|
|
border.color: "black"
|
|
border.width: 1
|
|
color: "transparent"
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.CrossCursor
|
|
onPressed: mouse => {
|
|
const x = Math.max(0, Math.min(1, mouse.x / width));
|
|
const y = Math.max(0, Math.min(1, mouse.y / height));
|
|
root.gradientX = x;
|
|
root.gradientY = y;
|
|
root.updateColorFromGradient(x, y);
|
|
}
|
|
onPositionChanged: mouse => {
|
|
if (pressed) {
|
|
const x = Math.max(0, Math.min(1, mouse.x / width));
|
|
const y = Math.max(0, Math.min(1, mouse.y / height));
|
|
root.gradientX = x;
|
|
root.gradientY = y;
|
|
root.updateColorFromGradient(x, y);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: hueSlider
|
|
width: 50
|
|
height: 280
|
|
radius: Theme.cornerRadius
|
|
border.color: Theme.outlineStrong
|
|
border.width: 1
|
|
|
|
gradient: Gradient {
|
|
orientation: Gradient.Vertical
|
|
GradientStop {
|
|
position: 0.00
|
|
color: "#ff0000"
|
|
}
|
|
GradientStop {
|
|
position: 0.17
|
|
color: "#ffff00"
|
|
}
|
|
GradientStop {
|
|
position: 0.33
|
|
color: "#00ff00"
|
|
}
|
|
GradientStop {
|
|
position: 0.50
|
|
color: "#00ffff"
|
|
}
|
|
GradientStop {
|
|
position: 0.67
|
|
color: "#0000ff"
|
|
}
|
|
GradientStop {
|
|
position: 0.83
|
|
color: "#ff00ff"
|
|
}
|
|
GradientStop {
|
|
position: 1.00
|
|
color: "#ff0000"
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: hueIndicator
|
|
width: parent.width
|
|
height: 4
|
|
color: "white"
|
|
border.color: "black"
|
|
border.width: 1
|
|
y: root.hue * parent.height - height / 2
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.SizeVerCursor
|
|
onPressed: mouse => {
|
|
const h = Math.max(0, Math.min(1, mouse.y / height));
|
|
root.hue = h;
|
|
root.updateColor();
|
|
root.selectedColor = root.currentColor;
|
|
}
|
|
onPositionChanged: mouse => {
|
|
if (pressed) {
|
|
const h = Math.max(0, Math.min(1, mouse.y / height));
|
|
root.hue = h;
|
|
root.updateColor();
|
|
root.selectedColor = root.currentColor;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
StyledText {
|
|
text: I18n.tr("Material Colors")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceText
|
|
font.weight: Font.Medium
|
|
}
|
|
|
|
GridView {
|
|
width: parent.width
|
|
height: 140
|
|
cellWidth: 38
|
|
cellHeight: 38
|
|
clip: true
|
|
interactive: false
|
|
model: root.standardColors
|
|
|
|
delegate: Rectangle {
|
|
width: 36
|
|
height: 36
|
|
color: modelData
|
|
radius: 4
|
|
border.color: Theme.outlineStrong
|
|
border.width: 1
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: () => {
|
|
const pickedColor = Qt.color(modelData);
|
|
root.selectedColor = pickedColor;
|
|
root.currentColor = pickedColor;
|
|
root.updateFromColor(pickedColor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
Column {
|
|
width: 210
|
|
spacing: Theme.spacingXS
|
|
|
|
StyledText {
|
|
text: I18n.tr("Recent Colors")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceText
|
|
font.weight: Font.Medium
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingXS
|
|
|
|
Repeater {
|
|
model: 5
|
|
|
|
Rectangle {
|
|
width: 36
|
|
height: 36
|
|
radius: 4
|
|
border.color: Theme.outlineStrong
|
|
border.width: 1
|
|
|
|
color: {
|
|
if (index < SessionData.recentColors.length) {
|
|
return SessionData.recentColors[index];
|
|
}
|
|
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
|
|
}
|
|
|
|
opacity: index < SessionData.recentColors.length ? 1.0 : 0.3
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: index < SessionData.recentColors.length ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
enabled: index < SessionData.recentColors.length
|
|
onClicked: () => {
|
|
if (index < SessionData.recentColors.length) {
|
|
const pickedColor = SessionData.recentColors[index];
|
|
root.selectedColor = pickedColor;
|
|
root.currentColor = pickedColor;
|
|
root.updateFromColor(pickedColor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width - 330
|
|
spacing: Theme.spacingXS
|
|
|
|
StyledText {
|
|
text: I18n.tr("Opacity")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceText
|
|
font.weight: Font.Medium
|
|
}
|
|
|
|
DankSlider {
|
|
width: parent.width
|
|
value: Math.round(root.alpha * 100)
|
|
minimum: 0
|
|
maximum: 100
|
|
showValue: false
|
|
onSliderValueChanged: newValue => {
|
|
root.alpha = newValue / 100;
|
|
root.updateColor();
|
|
root.selectedColor = root.currentColor;
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: 100
|
|
height: 50
|
|
radius: Theme.cornerRadius
|
|
color: root.currentColor
|
|
border.color: Theme.outlineStrong
|
|
border.width: 2
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingM
|
|
|
|
Column {
|
|
width: (parent.width - Theme.spacingM * 2) / 3
|
|
spacing: Theme.spacingXS
|
|
|
|
StyledText {
|
|
text: I18n.tr("Hex")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceTextMedium
|
|
font.weight: Font.Medium
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingXS
|
|
|
|
DankTextField {
|
|
id: hexInput
|
|
width: parent.width - 36
|
|
height: 36
|
|
text: root.currentColor.toString()
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
textColor: {
|
|
if (text.length === 0)
|
|
return Theme.surfaceText;
|
|
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
|
|
return hexPattern.test(text) ? Theme.surfaceText : Theme.error;
|
|
}
|
|
placeholderText: "#000000"
|
|
backgroundColor: Theme.surfaceHover
|
|
borderWidth: 1
|
|
focusedBorderWidth: 2
|
|
topPadding: Theme.spacingS
|
|
bottomPadding: Theme.spacingS
|
|
onAccepted: () => {
|
|
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
|
|
if (!hexPattern.test(text))
|
|
return;
|
|
const color = Qt.color(text);
|
|
if (color) {
|
|
root.selectedColor = color;
|
|
root.currentColor = color;
|
|
root.updateFromColor(color);
|
|
}
|
|
}
|
|
}
|
|
|
|
DankActionButton {
|
|
iconName: "content_copy"
|
|
iconSize: Theme.iconSize - 6
|
|
iconColor: Theme.surfaceText
|
|
buttonSize: 36
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: () => {
|
|
root.copyColorToClipboard(hexInput.text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: (parent.width - Theme.spacingM * 2) / 3
|
|
spacing: Theme.spacingXS
|
|
|
|
StyledText {
|
|
text: I18n.tr("RGB")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceTextMedium
|
|
font.weight: Font.Medium
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingXS
|
|
|
|
Rectangle {
|
|
width: parent.width - 36
|
|
height: 36
|
|
radius: Theme.cornerRadius
|
|
color: Theme.surfaceHover
|
|
border.color: Theme.outline
|
|
border.width: 1
|
|
|
|
StyledText {
|
|
anchors.centerIn: parent
|
|
text: {
|
|
const r = Math.round(root.currentColor.r * 255);
|
|
const g = Math.round(root.currentColor.g * 255);
|
|
const b = Math.round(root.currentColor.b * 255);
|
|
if (root.alpha < 1) {
|
|
const a = Math.round(root.alpha * 255);
|
|
return `${r}, ${g}, ${b}, ${a}`;
|
|
}
|
|
return `${r}, ${g}, ${b}`;
|
|
}
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceText
|
|
}
|
|
}
|
|
|
|
DankActionButton {
|
|
iconName: "content_copy"
|
|
iconSize: Theme.iconSize - 6
|
|
iconColor: Theme.surfaceText
|
|
buttonSize: 36
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: () => {
|
|
const r = Math.round(root.currentColor.r * 255);
|
|
const g = Math.round(root.currentColor.g * 255);
|
|
const b = Math.round(root.currentColor.b * 255);
|
|
let rgbString;
|
|
if (root.alpha < 1) {
|
|
const a = Math.round(root.alpha * 255);
|
|
rgbString = `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
} else {
|
|
rgbString = `rgb(${r}, ${g}, ${b})`;
|
|
}
|
|
Quickshell.execDetached(["sh", "-c", `echo -n "${rgbString}" | wl-copy`]);
|
|
ToastService.showInfo(`${rgbString} copied`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: (parent.width - Theme.spacingM * 2) / 3
|
|
spacing: Theme.spacingXS
|
|
|
|
StyledText {
|
|
text: I18n.tr("HSV")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceTextMedium
|
|
font.weight: Font.Medium
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingXS
|
|
|
|
Rectangle {
|
|
width: parent.width - 36
|
|
height: 36
|
|
radius: Theme.cornerRadius
|
|
color: Theme.surfaceHover
|
|
border.color: Theme.outline
|
|
border.width: 1
|
|
|
|
StyledText {
|
|
anchors.centerIn: parent
|
|
text: {
|
|
const h = Math.round(root.hue * 360);
|
|
const s = Math.round(root.saturation * 100);
|
|
const v = Math.round(root.value * 100);
|
|
if (root.alpha < 1) {
|
|
const a = Math.round(root.alpha * 100);
|
|
return `${h}°, ${s}%, ${v}%, ${a}%`;
|
|
}
|
|
return `${h}°, ${s}%, ${v}%`;
|
|
}
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceText
|
|
}
|
|
}
|
|
|
|
DankActionButton {
|
|
iconName: "content_copy"
|
|
iconSize: Theme.iconSize - 6
|
|
iconColor: Theme.surfaceText
|
|
buttonSize: 36
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: () => {
|
|
const h = Math.round(root.hue * 360);
|
|
const s = Math.round(root.saturation * 100);
|
|
const v = Math.round(root.value * 100);
|
|
let hsvString;
|
|
if (root.alpha < 1) {
|
|
const a = Math.round(root.alpha * 100);
|
|
hsvString = `${h}, ${s}, ${v}, ${a}`;
|
|
} else {
|
|
hsvString = `${h}, ${s}, ${v}`;
|
|
}
|
|
Quickshell.execDetached(["sh", "-c", `echo -n "${hsvString}" | wl-copy`]);
|
|
ToastService.showInfo(`HSV ${hsvString} copied`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DankButton {
|
|
visible: root.onColorSelectedCallback !== null && root.onColorSelectedCallback !== undefined
|
|
width: 70
|
|
buttonHeight: 36
|
|
text: I18n.tr("Save")
|
|
backgroundColor: Theme.primary
|
|
textColor: Theme.background
|
|
anchors.right: parent.right
|
|
onClicked: {
|
|
SessionData.addRecentColor(root.currentColor);
|
|
root.colorSelected(root.currentColor);
|
|
root.hide();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|