From 290d5cbebc696fe1d64b883717fa8ea9c2fff4c9 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Tue, 24 May 2022 10:32:40 +0300 Subject: [PATCH] feat(StatusColorSpace): impl color space component (#679) --- ui/StatusQ/sandbox/controls/Buttons.qml | 16 +- ui/StatusQ/sandbox/main.qml | 41 ++-- .../sandbox/pages/StatusColorSpacePage.qml | 110 ++++++++++ ui/StatusQ/sandbox/qml.qrc | 1 + .../StatusQ/Components/StatusColorSpace.qml | 201 ++++++++++++++++++ ui/StatusQ/src/StatusQ/Components/qmldir | 1 + .../Controls/StatusColorSelectorGrid.qml | 35 +-- .../src/StatusQ/Popups/StatusColorDialog.qml | 145 +++++++++++++ ui/StatusQ/src/StatusQ/Popups/qmldir | 1 + ui/StatusQ/statusq.qrc | 2 + 10 files changed, 513 insertions(+), 40 deletions(-) create mode 100644 ui/StatusQ/sandbox/pages/StatusColorSpacePage.qml create mode 100644 ui/StatusQ/src/StatusQ/Components/StatusColorSpace.qml create mode 100644 ui/StatusQ/src/StatusQ/Popups/StatusColorDialog.qml diff --git a/ui/StatusQ/sandbox/controls/Buttons.qml b/ui/StatusQ/sandbox/controls/Buttons.qml index a9518c7dea..db743d6ef3 100644 --- a/ui/StatusQ/sandbox/controls/Buttons.qml +++ b/ui/StatusQ/sandbox/controls/Buttons.qml @@ -3,9 +3,9 @@ import QtQuick.Layouts 1.14 import QtQuick.Dialogs 1.3 import StatusQ.Controls 0.1 +import StatusQ.Popups 0.1 import StatusQ.Core.Theme 0.1 - Column { spacing: 10 Grid { @@ -307,12 +307,14 @@ Column { onClicked: { colorDialog.open(); } - ColorDialog { - id: colorDialog - property bool colorSelected: false - onAccepted: { - colorSelected = true; - } + } + + StatusColorDialog { + id: colorDialog + anchors.centerIn: parent + property bool colorSelected: false + onAccepted: { + colorSelected = true; } } } diff --git a/ui/StatusQ/sandbox/main.qml b/ui/StatusQ/sandbox/main.qml index fd7e84158f..1284d67a46 100644 --- a/ui/StatusQ/sandbox/main.qml +++ b/ui/StatusQ/sandbox/main.qml @@ -149,36 +149,36 @@ StatusWindow { spacing: 0 StatusListSectionHeadline { text: "StatusQ.Core" } - StatusNavigationListItem { + StatusNavigationListItem { title: "Icons" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.control(title); } StatusListSectionHeadline { text: "StatusQ.Layout" } - StatusNavigationListItem { + StatusNavigationListItem { title: "Layouts" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.control(title.substring(0, title.length - 1)); } StatusListSectionHeadline { text: "StatusQ.Controls" } - StatusNavigationListItem { + StatusNavigationListItem { title: "Buttons" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.control(title); } - StatusNavigationListItem { + StatusNavigationListItem { title: "StatusSwitchTab" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.page("StatusTabSwitch"); } - StatusNavigationListItem { + StatusNavigationListItem { title: "StatusChatCommandButton" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.page(title); } - StatusNavigationListItem { + StatusNavigationListItem { title: "Controls" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.control(title); @@ -193,37 +193,37 @@ StatusWindow { selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.page(title); } - StatusNavigationListItem { + StatusNavigationListItem { title: "StatusInput" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.page(title); } - StatusNavigationListItem { + StatusNavigationListItem { title: "StatusSelect" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.page(title); } - StatusNavigationListItem { + StatusNavigationListItem { title: "StatusAccountSelector" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.page(title); } - StatusNavigationListItem { + StatusNavigationListItem { title: "StatusAssetSelector" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.page(title); } - StatusNavigationListItem { + StatusNavigationListItem { title: "StatusColorSelector" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.page(title); } - StatusNavigationListItem { + StatusNavigationListItem { title: "StatusWalletColorButton" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.page(title); } - StatusNavigationListItem { + StatusNavigationListItem { title: "StatusWalletColorSelect" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.page(title); @@ -239,22 +239,22 @@ StatusWindow { onClicked: mainPageView.page(title); } StatusListSectionHeadline { text: "StatusQ.Components" } - StatusNavigationListItem { + StatusNavigationListItem { title: "StatusAddress" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.page(title); } - StatusNavigationListItem { + StatusNavigationListItem { title: "List Items" selected: viewLoader.source.toString().includes(title.replace(/\s+/g, '')) onClicked: mainPageView.control(title.replace(/\s+/g, '')); } - StatusNavigationListItem { + StatusNavigationListItem { title: "StatusChatInfoToolBar" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.page(title); } - StatusNavigationListItem { + StatusNavigationListItem { title: "Others" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.control(title); @@ -285,7 +285,7 @@ StatusWindow { onClicked: mainPageView.page(title); } StatusListSectionHeadline { text: "StatusQ.Popup" } - StatusNavigationListItem { + StatusNavigationListItem { title: "StatusPopupMenu" selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.page(title); @@ -312,6 +312,11 @@ StatusWindow { selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.page(title, true); } + StatusNavigationListItem { + title: "StatusColorSpace" + selected: viewLoader.source.toString().includes(title) + onClicked: mainPageView.page(title, true); + } } } } diff --git a/ui/StatusQ/sandbox/pages/StatusColorSpacePage.qml b/ui/StatusQ/sandbox/pages/StatusColorSpacePage.qml new file mode 100644 index 0000000000..af5434869a --- /dev/null +++ b/ui/StatusQ/sandbox/pages/StatusColorSpacePage.qml @@ -0,0 +1,110 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 + +import Sandbox 0.1 + +Item { + + ColumnLayout { + anchors.centerIn: parent + + StatusBaseText { + text: qsTr("Thickness") + } + + StatusSlider { + id: thicknessSlider + from: colorSpace.width / 8 + to: colorSpace.width / 16 + value: colorSpace.width / 10 + Layout.fillWidth: true + } + + StatusBaseText { + text: qsTr("Min saturate: ") + colorSpace.minSaturate.toFixed(2) + } + + StatusSlider { + id: minSatSlider + from: 0 + to: 0.5 + value: 0 + Layout.fillWidth: true + } + + StatusBaseText { + text: qsTr("Max saturate: ") + colorSpace.maxSaturate.toFixed(2) + } + + StatusSlider { + id: maxSatSlider + from: 0.51 + to: 1.0 + value: 1.0 + Layout.fillWidth: true + } + + StatusBaseText { + text: qsTr("Min value: ") + colorSpace.minValue.toFixed(2) + } + + StatusSlider { + id: minValSlider + from: 0 + to: 0.5 + value: 0 + Layout.fillWidth: true + } + + StatusBaseText { + text: qsTr("Max value: ") + colorSpace.maxValue.toFixed(2) + } + + StatusSlider { + id: maxValSlider + from: 0.51 + to: 1.0 + value: 1.0 + Layout.fillWidth: true + } + + StatusColorSpace { + id: colorSpace + color: "#ed77eb" + thickness: thicknessSlider.value + minSaturate: minSatSlider.value + maxSaturate: maxSatSlider.value + minValue: minValSlider.value + maxValue: maxValSlider.value + } + + StatusBaseText { + text: qsTr("Color") + ": " + colorSpace.color + } + + Rectangle { + color: colorSpace.color + implicitHeight: 48 + radius: 10 + Layout.fillWidth: true + + StatusBaseText { + anchors.centerIn: parent + color: Theme.palette.white + font.pixelSize: 15 + text: "Quick brown fox jumps over the lazy dog" + } + } + + StatusButton { + text: "Randomize" + onPressed: colorSpace.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1) + } + } +} diff --git a/ui/StatusQ/sandbox/qml.qrc b/ui/StatusQ/sandbox/qml.qrc index 502c28a250..ec480fe4a9 100644 --- a/ui/StatusQ/sandbox/qml.qrc +++ b/ui/StatusQ/sandbox/qml.qrc @@ -43,5 +43,6 @@ pages/StatusToastMessagePage.qml pages/StatusWizardStepperPage.qml pages/StatusTabBarButtonPage.qml + pages/StatusColorSpacePage.qml diff --git a/ui/StatusQ/src/StatusQ/Components/StatusColorSpace.qml b/ui/StatusQ/src/StatusQ/Components/StatusColorSpace.qml new file mode 100644 index 0000000000..fc2df5253e --- /dev/null +++ b/ui/StatusQ/src/StatusQ/Components/StatusColorSpace.qml @@ -0,0 +1,201 @@ +import QtQuick 2.14 +import QtGraphicalEffects 1.0 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 + +Item { + id: root + + property int thickness: width * 0.1 + property real minSaturate: 0.0 + property real minValue: 0.0 + property real maxSaturate: 1.0 + property real maxValue: 1.0 + property color color: "red" + property color rootColor: Qt.hsva(color.hsvHue, 1, 1, 1) + + function pickColorFromHueGauge(x, y) { + // Get angle of picked color + let theta = Math.atan2(y - hueGauge.height / 2, x - hueGauge.width / 2) * 0.5 / Math.PI; + if (theta < 0.0) + theta += 1.0; + + // Convert angle value to color + return Qt.hsva(1 - theta, 1, 1, 1) + } + + function pickColorFromSatValRect(x, y) { + // x for saturation, reversed y for value + let sat = mapFromRange(Math.min(Math.max(x, 0), satValRect.width), + minSaturate, maxSaturate, satValRect.width); + let val = 1 - mapFromRange(Math.min(Math.max(y, 0), satValRect.height), + 1 - maxValue, 1 - minValue, satValRect.height); + return Qt.hsva(rootColor.hsvHue, sat, val, 1); + } + + function angleOnHueGauge(pickingColor) { + // color hue to angle + return (1 - pickingColor.hsvHue) * 2 * Math.PI; + } + + // TODO: mapToRange & mapFromRange to helper js + function mapToRange(value, minValue, maxValue, range) { + return (value - minValue) / (maxValue - minValue) * range; + } + + function mapFromRange(pos, minValue, maxValue, range) { + return pos / range * (maxValue - minValue) + minValue; + } + + onColorChanged: { + // update root color if only we are not picking on satValRect + if (!d.pickingSatVal) + rootColor = Qt.hsva(color.hsvHue, 1, 1, 1) + } + + implicitWidth: 340 + implicitHeight: 340 + + QtObject { + id: d + + property bool pickingSatVal: false + } + + ConicalGradient { + id: hueGauge + anchors.fill: parent + angle: 90.0 + gradient: Gradient { + GradientStop { position: 0.000; color: Qt.hsva(1.000, 1, 1, 1) } + GradientStop { position: 0.167; color: Qt.hsva(0.833, 1, 1, 1) } + GradientStop { position: 0.333; color: Qt.hsva(0.666, 1, 1, 1) } + GradientStop { position: 0.500; color: Qt.hsva(0.500, 1, 1, 1) } + GradientStop { position: 0.667; color: Qt.hsva(0.333, 1, 1, 1) } + GradientStop { position: 0.833; color: Qt.hsva(0.166, 1, 1, 1) } + GradientStop { position: 1.000; color: Qt.hsva(0.000, 1, 1, 1) } + } + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Item { + width: root.width + height: root.height + + Rectangle { + anchors.fill: parent + radius: Math.min(width, height) / 2 + } + } + } + + MouseArea { + id: hueArea + + property bool pressedOnGauge: false + + anchors.fill: parent + preventStealing: true + onPressed: { + // Check we clicked on gauge + let dist = Math.sqrt(Math.pow(width / 2 - mouseX, 2) + + Math.pow(height / 2 - mouseY, 2)); + let radius = Math.min(width, height) / 2; + if (dist < radius - thickness || dist > radius) + return; + + pressedOnGauge = true; + pickRootColor(); + } + onReleased: pressedOnGauge = false + onPositionChanged: if (pressedOnGauge) pickRootColor() + + function pickRootColor() { + // Update both colors + rootColor = pickColorFromHueGauge(mouseX, mouseY); + color.hsvHue = rootColor.hsvHue; + color.hsvSaturation = Math.min(maxSaturate, Math.max(minSaturate, color.hsvSaturation)); + color.hsvValue = Math.min(maxValue, Math.max(minValue, color.hsvValue)); + } + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: root.thickness + radius: Math.min(width, height) / 2 + + color: Theme.palette.baseColor5 + + Rectangle { + id: satValRect + anchors.centerIn: parent + width: parent.width * 0.55 + height: width + border.color: Theme.palette.baseColor3 + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { position: -minSaturate; color: "white" } + GradientStop { position: 2.0 - maxSaturate; color: rootColor } + } + + Rectangle { + anchors.fill: parent + gradient: Gradient { + orientation: Gradient.Vertical + GradientStop { position: maxValue - 1; color: "transparent" } + GradientStop { position: 1.0 + minValue; color: "black" } + } + } + + MouseArea { + id: satValArea + anchors.fill: parent + preventStealing: true + onPressed: { + d.pickingSatVal = true; + pickColor(); + } + onReleased: d.pickingSatVal = false + onPositionChanged: pickColor() + + function pickColor() { + root.color = pickColorFromSatValRect(mouseX, mouseY); + } + } + + Rectangle { + id: satValLens + x: mapToRange(root.color.hsvSaturation, minSaturate, maxSaturate, + satValRect.width) - radius + y: mapToRange(1 - root.color.hsvValue, 1 - maxValue, 1 - minValue, + satValRect.height) - radius + width: thickness * 1.3 + height: width + radius: height / 2 + border.width: 2 + border.color: Theme.palette.baseColor3 + color: root.color + visible: x + 1 >= -radius && x - 1 <= satValRect.width - radius && + y + 1 >= -radius && y - 1 <= satValRect.height - radius + } + } + } + + Rectangle { + id: hueLens + + property real theta: angleOnHueGauge(rootColor); + property real dist: (hueGauge.width - width) / 2 + thickness / 6; + + anchors.centerIn: parent + anchors.horizontalCenterOffset: dist * Math.cos(theta); + anchors.verticalCenterOffset: dist * Math.sin(theta); + width: thickness * 1.3 + height: width + radius: height / 2 + border.width: 2 + border.color: Theme.palette.baseColor3 + color: rootColor + } +} diff --git a/ui/StatusQ/src/StatusQ/Components/qmldir b/ui/StatusQ/src/StatusQ/Components/qmldir index d70e7aee3d..09bc245c1a 100644 --- a/ui/StatusQ/src/StatusQ/Components/qmldir +++ b/ui/StatusQ/src/StatusQ/Components/qmldir @@ -34,3 +34,4 @@ StatusTagSelector 0.1 StatusTagSelector.qml StatusToastMessage 0.1 StatusToastMessage.qml StatusWizardStepper 0.1 StatusWizardStepper.qml StatusImageCropPanel 0.1 StatusImageCropPanel.qml +StatusColorSpace 0.0 StatusColorSpace.qml diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusColorSelectorGrid.qml b/ui/StatusQ/src/StatusQ/Controls/StatusColorSelectorGrid.qml index fcdd599cce..ebc6e67677 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusColorSelectorGrid.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusColorSelectorGrid.qml @@ -10,21 +10,24 @@ Column { property alias titleText: title.text property alias title: title + property alias columns: grid.columns property int selectedColorIndex: 0 property string selectedColor: "" - property var model: [StatusColors.colors['black'], - StatusColors.colors['grey'], - StatusColors.colors['blue2'], - StatusColors.colors['purple'], - StatusColors.colors['cyan'], - StatusColors.colors['violet'], - StatusColors.colors['red2'], - StatusColors.colors['yellow'], - StatusColors.colors['green2'], - StatusColors.colors['moss'], - StatusColors.colors['brown'], - StatusColors.colors['brown2']] + property var model:[ StatusColors.colors['black'], + StatusColors.colors['grey'], + StatusColors.colors['blue2'], + StatusColors.colors['purple'], + StatusColors.colors['cyan'], + StatusColors.colors['violet'], + StatusColors.colors['red2'], + StatusColors.colors['yellow'], + StatusColors.colors['green2'], + StatusColors.colors['moss'], + StatusColors.colors['brown'], + StatusColors.colors['brown2'] ] + + signal colorSelected(color color) spacing: 16 @@ -36,6 +39,7 @@ Column { } Grid { + id: grid columns: 6 rowSpacing: 16 columnSpacing: 32 @@ -45,9 +49,10 @@ Column { checked: index === selectedColorIndex radioButtonColor: root.model[index] || "transparent" onCheckedChanged: { - if(checked) { - selectedColorIndex = index - selectedColor = root.model[index] + if (checked) { + selectedColorIndex = index; + selectedColor = root.model[index]; + root.colorSelected(selectedColor); } } } diff --git a/ui/StatusQ/src/StatusQ/Popups/StatusColorDialog.qml b/ui/StatusQ/src/StatusQ/Popups/StatusColorDialog.qml new file mode 100644 index 0000000000..ce61436381 --- /dev/null +++ b/ui/StatusQ/src/StatusQ/Popups/StatusColorDialog.qml @@ -0,0 +1,145 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.12 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Controls.Validators 0.1 +import StatusQ.Components 0.1 +import StatusQ.Popups 0.1 + +StatusModal { + id: root + + property alias color: colorSpace.color + property alias standartColors: colorSelectionGrid.model + property alias acceptText: acceptButton.text + property alias previewText: preview.text + + signal accepted() + + onColorChanged: { + if (!hexInput.locked) + hexInput.text = color.toString(); + + if (colorSelectionGrid.selectedColor != color) + colorSelectionGrid.selectedColorIndex = -1; + } + Component.onCompleted: { + hexInput.text = color.toString(); + } + + width: 680 + implicitHeight: 820 + + contentItem: ScrollView { + id: scroll + width: parent.width + topPadding: 30 + leftPadding: 20 + rightPadding: 20 + bottomPadding: 20 + contentHeight: column.height + + ScrollBar.vertical.policy: ScrollBar.AsNeeded + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + clip: true + + ColumnLayout { + id: column + width: scroll.width - scroll.leftPadding - scroll.rightPadding + spacing: 12 + + StatusColorSpace { + id: colorSpace + + property real hueFactor: Math.max(rootColor.g + rootColor.b * 0.4, + rootColor.g + rootColor.r * 0.6) + + minSaturate: Math.max(0.4, hueFactor * 0.55) + maxSaturate: 1.0 + minValue: 0.4 + // Curve to pick colors readable with white text + maxValue: Math.min(1.0, 1.65 - hueFactor * 0.5) + Layout.alignment: Qt.AlignHCenter + } + + StatusInput { + id: hexInput + + property color newColor: text + // TODO: editingFinished() signal instead of this crutch + property bool locked: false + + implicitWidth: 256 + validators: [ + StatusRegularExpressionValidator { + regularExpression: /^#(?:[0-9a-fA-F]{3}){1,2}$/ + errorMessage: qsTr("This is not a valid color") + } + ] + validationMode: StatusInput.ValidationMode.Always + + onNewColorChanged: { + if (!valid) + return; + + locked = true; + root.color = newColor; + locked = false; + } + Layout.alignment: Qt.AlignHCenter + } + + StatusBaseText { + text: qsTr("Preview") + font.pixelSize: 15 + } + + Rectangle { + implicitHeight: 48 + radius: 10 + color: root.color + Layout.fillWidth: true + + StatusBaseText { + id: preview + x: 16 + y: 16 + text: root.color.toString() + color: Theme.palette.white + font.pixelSize: 15 + } + } + + StatusBaseText { + text: qsTr("Standart colours") + font.pixelSize: 15 + } + + StatusColorSelectorGrid { + id: colorSelectionGrid + columns: 8 + model: ["#4360df", "#887af9", "#d37ef4", "#51d0f0", "#26a69a", "#7cda00", "#eab700", "#fa6565"] + selectedColorIndex: -1 + onColorSelected: { + root.color = selectedColor; + } + Layout.alignment: Qt.AlignHCenter + } + } + } + + rightButtons: [ + StatusButton { + id: acceptButton + text: qsTr("Select Colour") + onClicked: { + root.accepted(); + root.close(); + } + } + ] +} diff --git a/ui/StatusQ/src/StatusQ/Popups/qmldir b/ui/StatusQ/src/StatusQ/Popups/qmldir index 1e9ef78b30..39f7a142b6 100644 --- a/ui/StatusQ/src/StatusQ/Popups/qmldir +++ b/ui/StatusQ/src/StatusQ/Popups/qmldir @@ -11,3 +11,4 @@ StatusModalDivider 0.1 StatusModalDivider.qml StatusSearchPopupMenuItem 0.1 StatusSearchPopupMenuItem.qml StatusSearchLocationMenu 0.1 StatusSearchLocationMenu.qml StatusSpellcheckingMenuItems 0.1 StatusSpellcheckingMenuItems.qml +StatusColorDialog 0.1 StatusColorDialog.qml diff --git a/ui/StatusQ/statusq.qrc b/ui/StatusQ/statusq.qrc index 1dd850bf3f..f5f2850a19 100644 --- a/ui/StatusQ/statusq.qrc +++ b/ui/StatusQ/statusq.qrc @@ -34,6 +34,7 @@ src/StatusQ/Components/StatusTagSelector.qml src/StatusQ/Components/StatusToastMessage.qml src/StatusQ/Components/StatusWizardStepper.qml + src/StatusQ/Components/StatusColorSpace.qml src/StatusQ/Components/private/statusMessage/StatusAudioMessage.qml src/StatusQ/Components/private/statusMessage/StatusEditMessage.qml src/StatusQ/Components/private/statusMessage/StatusImageMessage.qml @@ -138,6 +139,7 @@ src/StatusQ/Popups/StatusSearchPopup.qml src/StatusQ/Popups/StatusSearchPopupMenuItem.qml src/StatusQ/Popups/StatusSpellcheckingMenuItems.qml + src/StatusQ/Popups/StatusColorDialog.qml src/StatusQ/Popups/statusModal/StatusImageWithTitle.qml src/StatusQ/Popups/statusModal/StatusModalFooter.qml src/StatusQ/Popups/statusModal/StatusModalHeader.qml