mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
581 lines
22 KiB
QML
581 lines
22 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Wayland
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
PanelWindow {
|
|
id: root
|
|
|
|
property string pickerTitle: "Choose Color"
|
|
property color selectedColor: Theme.primary
|
|
property bool shouldBeVisible: false
|
|
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
|
|
|
|
function open() {
|
|
currentColor = selectedColor
|
|
updateFromColor(currentColor)
|
|
shouldBeVisible = true
|
|
Qt.callLater(() => colorContent.forceActiveFocus())
|
|
}
|
|
|
|
function close() {
|
|
shouldBeVisible = false
|
|
onColorSelectedCallback = null
|
|
}
|
|
|
|
function show() {
|
|
open()
|
|
}
|
|
|
|
function hide() {
|
|
close()
|
|
}
|
|
|
|
onColorSelected: (color) => {
|
|
if (onColorSelectedCallback) {
|
|
onColorSelectedCallback(color)
|
|
}
|
|
}
|
|
|
|
function copyColorToClipboard(colorValue) {
|
|
Quickshell.execDetached(["sh", "-c", `echo "${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()
|
|
}
|
|
|
|
function pickColorFromScreen() {
|
|
close()
|
|
hyprpickerProcess.running = true
|
|
}
|
|
|
|
Process {
|
|
id: hyprpickerProcess
|
|
running: false
|
|
command: ["hyprpicker", "--format=hex"]
|
|
|
|
stdout: SplitParser {
|
|
onRead: data => {
|
|
const colorStr = data.trim()
|
|
if (colorStr.length >= 7 && colorStr.startsWith('#')) {
|
|
root.currentColor = colorStr
|
|
root.updateFromColor(root.currentColor)
|
|
hexInput.text = root.currentColor.toString()
|
|
copyColorToClipboard(colorStr)
|
|
root.open()
|
|
}
|
|
}
|
|
}
|
|
|
|
onExited: (exitCode, exitStatus) => {
|
|
if (exitCode !== 0) {
|
|
console.warn("hyprpicker exited with code:", exitCode)
|
|
}
|
|
root.open()
|
|
}
|
|
}
|
|
|
|
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"
|
|
]
|
|
|
|
visible: shouldBeVisible
|
|
|
|
WlrLayershell.namespace: "quickshell:color-picker"
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
|
|
|
color: "transparent"
|
|
|
|
anchors {
|
|
top: true
|
|
left: true
|
|
right: true
|
|
bottom: true
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: root.close()
|
|
|
|
Rectangle {
|
|
color: "#80000000"
|
|
anchors.fill: parent
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.centerIn: parent
|
|
width: 680
|
|
height: 680
|
|
radius: Theme.cornerRadius
|
|
color: Theme.surfaceContainer
|
|
border.color: Theme.outlineMedium
|
|
border.width: 1
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: {} // Prevent clicks from propagating to background
|
|
}
|
|
|
|
FocusScope {
|
|
id: colorContent
|
|
|
|
anchors.fill: parent
|
|
focus: root.shouldBeVisible
|
|
|
|
Keys.onEscapePressed: event => {
|
|
root.close()
|
|
event.accepted = true
|
|
}
|
|
|
|
Column {
|
|
anchors.fill: parent
|
|
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: qsTr("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: () => {
|
|
pickColorFromScreen()
|
|
}
|
|
}
|
|
|
|
DankActionButton {
|
|
iconName: "close"
|
|
iconSize: Theme.iconSize - 4
|
|
iconColor: Theme.surfaceText
|
|
onClicked: () => {
|
|
root.close()
|
|
}
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
onPositionChanged: mouse => {
|
|
if (pressed) {
|
|
const h = Math.max(0, Math.min(1, mouse.y / height))
|
|
root.hue = h
|
|
root.updateColor()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
StyledText {
|
|
text: qsTr("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: () => {
|
|
root.currentColor = modelData
|
|
root.updateFromColor(root.currentColor)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
Column {
|
|
width: 210
|
|
spacing: Theme.spacingXS
|
|
|
|
StyledText {
|
|
text: qsTr("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.surfaceContainerHigh
|
|
}
|
|
|
|
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) {
|
|
root.currentColor = SessionData.recentColors[index]
|
|
root.updateFromColor(root.currentColor)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width - 330
|
|
spacing: Theme.spacingXS
|
|
|
|
StyledText {
|
|
text: qsTr("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()
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: 100
|
|
height: 50
|
|
radius: Theme.cornerRadius
|
|
color: root.currentColor
|
|
border.color: Theme.outlineStrong
|
|
border.width: 2
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
StyledText {
|
|
text: qsTr("Hex:")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceTextMedium
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
DankTextField {
|
|
id: hexInput
|
|
width: 120
|
|
height: 38
|
|
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
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
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.currentColor = color
|
|
root.updateFromColor(color)
|
|
}
|
|
}
|
|
}
|
|
|
|
DankButton {
|
|
width: 80
|
|
buttonHeight: 36
|
|
text: qsTr("Apply")
|
|
backgroundColor: Theme.primary
|
|
textColor: Theme.background
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: {
|
|
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/
|
|
if (!hexPattern.test(hexInput.text)) return
|
|
const color = Qt.color(hexInput.text)
|
|
if (color) {
|
|
root.currentColor = color
|
|
root.updateFromColor(color)
|
|
root.selectedColor = root.currentColor
|
|
colorSelected(root.currentColor)
|
|
SessionData.addRecentColor(root.currentColor)
|
|
root.close()
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
width: parent.width - 460
|
|
height: 1
|
|
}
|
|
|
|
DankButton {
|
|
width: 70
|
|
buttonHeight: 36
|
|
text: qsTr("Cancel")
|
|
backgroundColor: "transparent"
|
|
textColor: Theme.surfaceText
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: root.close()
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: Theme.cornerRadius
|
|
color: "transparent"
|
|
border.color: Theme.surfaceVariantAlpha
|
|
border.width: 1
|
|
z: -1
|
|
}
|
|
}
|
|
|
|
DankButton {
|
|
width: 70
|
|
buttonHeight: 36
|
|
text: qsTr("Copy")
|
|
backgroundColor: Theme.primary
|
|
textColor: Theme.background
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: {
|
|
const colorString = root.currentColor.toString()
|
|
copyColorToClipboard(colorString)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|