diff --git a/quickshell/Common/SessionData.qml b/quickshell/Common/SessionData.qml index 654ce3575..c40f77e2d 100644 --- a/quickshell/Common/SessionData.qml +++ b/quickshell/Common/SessionData.qml @@ -1512,6 +1512,15 @@ Singleton { saveSettings(); } + function removeRecentColor(color) { + const colorStr = color.toString(); + const recent = recentColors.filter(c => c !== colorStr); + if (recent.length === recentColors.length) + return; + recentColors = recent; + saveSettings(); + } + function setShowThirdPartyPlugins(enabled) { showThirdPartyPlugins = enabled; saveSettings(); diff --git a/quickshell/Modals/ColorPickerModal.qml b/quickshell/Modals/ColorPickerModal.qml index 8860b76d9..f145b9191 100644 --- a/quickshell/Modals/ColorPickerModal.qml +++ b/quickshell/Modals/ColorPickerModal.qml @@ -42,7 +42,12 @@ DModal { shouldBeVisible ? hideInstant() : show(); } - onSelectedColorChanged: contentLoader?.item?.setColor(selectedColor) + // show() applies selectedColor on open. While the picker is open, a change to the + // recent colors (which selectedColor follows) must not reset the color being edited. + onSelectedColorChanged: { + if (!shouldBeVisible) + contentLoader?.item?.setColor(selectedColor); + } onBackgroundClicked: hide() onOpened: Qt.callLater(() => contentLoader?.item?.focusInitial()) onDialogClosed: { diff --git a/quickshell/Modules/ColorPicker/ColorPickerContent.qml b/quickshell/Modules/ColorPicker/ColorPickerContent.qml index 1ac6a01a6..e0a388614 100644 --- a/quickshell/Modules/ColorPicker/ColorPickerContent.qml +++ b/quickshell/Modules/ColorPicker/ColorPickerContent.qml @@ -388,11 +388,46 @@ DDialog { Repeater { model: SessionData.recentColors.slice(0, 5) - CommonWidgets.DColorButton { + Item { + id: recentSwatch + required property var modelData - swatchColor: modelData - selected: Qt.colorEqual(root.currentColor, swatchColor) - onClicked: root.setColor(swatchColor) + + implicitWidth: recentButton.implicitWidth + implicitHeight: recentButton.implicitHeight + + HoverHandler { + id: recentHover + } + + CommonWidgets.DColorButton { + id: recentButton + anchors.fill: parent + swatchColor: recentSwatch.modelData + selected: Qt.colorEqual(root.currentColor, swatchColor) + onClicked: root.setColor(swatchColor) + Keys.onDeletePressed: SessionData.removeRecentColor(recentSwatch.modelData) + Keys.onPressed: event => { + if (event.key !== Qt.Key_Backspace) + return; + SessionData.removeRecentColor(recentSwatch.modelData); + event.accepted = true; + } + } + + CommonWidgets.DActionButton { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: -Theme.spacingXS + buttonSize: Theme.iconSizeSmall + Theme.spacingXS + iconSize: Theme.iconSizeSmall - Theme.spacingXS + iconName: "close" + backgroundColor: Theme.surfaceContainerHighest + focusPolicy: Qt.NoFocus + visible: recentHover.hovered + tooltipText: I18n.tr("Remove from recent colors", "button on a recently picked color swatch") + onClicked: SessionData.removeRecentColor(recentSwatch.modelData) + } } } } diff --git a/quickshell/tests/session-recent-colors.test.mjs b/quickshell/tests/session-recent-colors.test.mjs new file mode 100644 index 000000000..9efa88227 --- /dev/null +++ b/quickshell/tests/session-recent-colors.test.mjs @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import vm from "node:vm"; +import test from "node:test"; + +const source = readFileSync(new URL("../Common/SessionData.qml", import.meta.url), "utf8"); + +function session(recentColors) { + const context = vm.createContext({ recentColors, saves: 0 }); + vm.runInContext("function saveSettings() { saves++; }", context); + const methods = ["addRecentColor", "removeRecentColor"]; + const found = []; + for (const match of source.matchAll(/^ function (\w+)\([^\n]*\)(?:: \w+)? \{\n[\s\S]*?^ \}/gm)) { + if (!methods.includes(match[1])) + continue; + found.push(match[1]); + vm.runInContext(match[0], context); + } + assert.deepEqual(found.sort(), [...methods].sort()); + return context; +} + +test("removing a recent color drops only that color and saves", () => { + const context = session(["#ff0000", "#00ff00", "#0000ff"]); + context.removeRecentColor("#00ff00"); + assert.deepEqual(Array.from(context.recentColors), ["#ff0000", "#0000ff"]); + assert.equal(context.saves, 1); +}); + +test("removing a color that is not in the list changes nothing", () => { + const context = session(["#ff0000"]); + context.removeRecentColor("#123456"); + assert.deepEqual(Array.from(context.recentColors), ["#ff0000"]); + assert.equal(context.saves, 0); +}); + +test("a removed color can be picked again and comes back first", () => { + const context = session(["#ff0000", "#00ff00"]); + context.removeRecentColor("#ff0000"); + context.addRecentColor("#ff0000"); + assert.deepEqual(Array.from(context.recentColors), ["#ff0000", "#00ff00"]); +});