diff options
Diffstat (limited to 'appearance/AppearancePanel.qml')
| -rw-r--r-- | appearance/AppearancePanel.qml | 504 |
1 files changed, 504 insertions, 0 deletions
diff --git a/appearance/AppearancePanel.qml b/appearance/AppearancePanel.qml new file mode 100644 index 0000000..27a0196 --- /dev/null +++ b/appearance/AppearancePanel.qml @@ -0,0 +1,504 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import Quickshell +import Quickshell.Wayland +import QtQuick + +Scope { + id: root + + property string monitor: "DP-1" + property bool open: false + property string tab: "wallpaper" + property string notice: "" + + // What the mock screens show. Hovering a thumbnail previews it on the + // targeted screen; the other keeps whatever is currently set. + property string hovering: "" + + readonly property var screenObj: + Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0] + + function show(which) { + if (which) root.tab = which; + root.notice = ""; + root.open = true; + } + function close() { root.open = false; } + function toggle(which) { root.open ? close() : show(which); } + + Connections { + target: Udt + function onApplied(scheme, ok, message) { + // install.sh reloads nothing, so the panel has to say what is + // still showing the old scheme rather than pretend it is done. + root.notice = ok + ? `${scheme} applied. Reload: hyprctl reload, waybar, kitty, conky. Qt and GTK apps need restarting.` + : `failed: ${message}`; + } + } + + Connections { + target: Wallpapers + function onApplied(ok, message) { + root.notice = ok ? "" : `wallpaper failed: ${message}`; + if (ok) root.close(); + } + } + + // Quickshell exits once no window is visible, and this panel is closed + // most of the time. See AGENTS.md. + PanelWindow { + visible: true + implicitWidth: 1 + implicitHeight: 1 + color: "transparent" + exclusionMode: ExclusionMode.Ignore + mask: Region {} + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + } + + LazyLoader { + active: root.open + + PanelWindow { + id: win + screen: root.screenObj + + anchors { top: true; left: true; right: true; bottom: true } + color: "transparent" + exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.namespace: "quickshell-appearance" + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + + Rectangle { + anchors.fill: parent + color: "#000000" + opacity: 0.5 + MouseArea { anchors.fill: parent; onClicked: root.close() } + } + + // Keys reach a focused item, never the window itself. + Item { + anchors.fill: parent + focus: true + Keys.onEscapePressed: root.close() + Keys.onPressed: event => { + if (event.key === Qt.Key_Tab) { + root.tab = root.tab === "theme" ? "wallpaper" : "theme"; + event.accepted = true; + } + } + } + + Rectangle { + anchors.centerIn: parent + width: win.width - 120 + height: win.height - 100 + radius: 14 + color: Qt.alpha(Theme.base, 0.72) + border.width: 1 + border.color: Qt.alpha(Theme.text, 0.12) + + MouseArea { anchors.fill: parent } + + Column { + anchors.fill: parent + anchors.margins: 20 + spacing: 16 + + Row { + spacing: 8 + Tab { + text: "Wallpaper" + selected: root.tab === "wallpaper" + onClicked: root.tab = "wallpaper" + } + Tab { + text: "Theme" + selected: root.tab === "theme" + onClicked: root.tab = "theme" + } + Item { width: 12; height: 1 } + Text { + anchors.verticalCenter: parent.verticalCenter + text: "Tab switches · Esc closes" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.overlay + } + } + + Text { + visible: root.notice !== "" + width: parent.width + wrapMode: Text.Wrap + text: root.notice + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + color: root.notice.indexOf("failed") === 0 ? Theme.red : Theme.green + } + + Loader { + width: parent.width + height: parent.height - y + sourceComponent: root.tab === "theme" ? themeTab : wallpaperTab + } + } + } + } + } + + // --- theme ------------------------------------------------------------ + + Component { + id: themeTab + + Flickable { + contentHeight: col.implicitHeight + clip: true + + Column { + id: col + width: parent.width + spacing: 10 + + Repeater { + model: Udt.names + + Rectangle { + required property string modelData + readonly property var sw: Udt.swatches[modelData] ?? ({}) + readonly property bool isCurrent: Udt.current === modelData + + width: col.width + implicitHeight: 96 + radius: 10 + color: hover.hovered ? Qt.alpha(Theme.surface, 0.75) + : Qt.alpha(Theme.surface, 0.35) + border.width: 1 + border.color: isCurrent ? Qt.alpha(Theme.accent, 0.6) : "transparent" + + HoverHandler { id: hover } + MouseArea { + anchors.fill: parent + enabled: !Udt.applying && !isCurrent + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: Udt.apply(modelData) + } + + Row { + anchors.fill: parent + anchors.margins: 14 + spacing: 16 + + // A mock of the desktop in that scheme's colours: + // swatches alone say what the colours are, this + // says how they sit together. + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: 150; height: 68 + radius: 6 + color: sw.bg ?? Theme.base + border.width: 1 + border.color: Qt.alpha(sw.text ?? Theme.text, 0.15) + clip: true + + Column { + anchors.fill: parent + anchors.margins: 7 + spacing: 5 + + Row { + spacing: 4 + Rectangle { + width: 26; height: 8; radius: 2 + color: sw.accent ?? Theme.accent + } + Rectangle { + width: 16; height: 8; radius: 2 + color: sw.surface ?? Theme.surface + } + Rectangle { + width: 16; height: 8; radius: 2 + color: sw.surface ?? Theme.surface + } + } + + Rectangle { + width: parent.width; height: 20; radius: 3 + color: sw.surface ?? Theme.surface + + Rectangle { + anchors.verticalCenter: parent.verticalCenter + x: 5; width: 60; height: 5; radius: 2 + color: Qt.alpha(sw.text ?? Theme.text, 0.75) + } + Rectangle { + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: 5 + width: 22; height: 9; radius: 3 + color: sw.accent ?? Theme.accent + } + } + + Row { + spacing: 4 + Rectangle { width: 9; height: 9; radius: 5; color: sw.green ?? Theme.green } + Rectangle { width: 9; height: 9; radius: 5; color: sw.yellow ?? Theme.yellow } + Rectangle { width: 9; height: 9; radius: 5; color: sw.red ?? Theme.red } + } + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 8 + + Row { + spacing: 8 + Text { + text: modelData + font { family: Theme.fontFamily; pixelSize: Theme.fontSize; bold: true } + color: Theme.text + } + Text { + anchors.verticalCenter: parent.verticalCenter + visible: isCurrent + text: "current" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.accent + } + Text { + anchors.verticalCenter: parent.verticalCenter + visible: Udt.applying + text: "applying…" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.overlay + } + } + + Row { + spacing: 5 + Repeater { + model: [sw.bg, sw.surface, sw.text, sw.accent, + sw.green, sw.yellow, sw.red] + Rectangle { + required property var modelData + visible: modelData !== undefined && modelData !== "" + width: 22; height: 22; radius: 4 + color: modelData ?? "transparent" + border.width: 1 + border.color: Qt.alpha(Theme.text, 0.1) + } + } + } + } + } + } + } + } + } + } + + // --- wallpaper -------------------------------------------------------- + + Component { + id: wallpaperTab + + Row { + spacing: 20 + + Column { + width: parent.width - mockPane.width - parent.spacing + height: parent.height + spacing: 12 + + Row { + spacing: 8 + Text { + anchors.verticalCenter: parent.verticalCenter + text: "Set on" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + color: Theme.subtext + } + Tab { + text: "Horizontal" + selected: Wallpapers.target === "H" + onClicked: Wallpapers.target = "H" + } + Tab { + text: "Vertical" + selected: Wallpapers.target === "V" + onClicked: Wallpapers.target = "V" + } + Text { + anchors.verticalCenter: parent.verticalCenter + text: Wallpapers.scanning ? "scanning…" + : Wallpapers.applying ? "applying…" + : Wallpapers.files.length + " wallpapers" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.overlay + } + } + + GridView { + width: parent.width + height: parent.height - y + clip: true + cellWidth: Math.floor(width / Math.max(1, Math.floor(width / 300))) + cellHeight: cellWidth * 9 / 16 + 26 + // Several hundred files: only the visible thumbnails decode. + cacheBuffer: cellHeight * 2 + model: Wallpapers.files + + delegate: Item { + required property string modelData + width: GridView.view.cellWidth + height: GridView.view.cellHeight + + Rectangle { + readonly property bool staged: + Wallpapers.pendingH === modelData || Wallpapers.pendingV === modelData + + anchors.fill: parent + anchors.margins: 6 + radius: 8 + color: Qt.alpha(Theme.surface, thumbHover.hovered ? 0.8 : 0.3) + border.width: staged ? 2 : 1 + border.color: staged ? Theme.accent + : thumbHover.hovered ? Qt.alpha(Theme.accent, 0.6) + : "transparent" + clip: true + + HoverHandler { + id: thumbHover + // Hovering previews this image on the mock + // screen it would actually land on. + onHoveredChanged: + root.hovering = hovered ? modelData + : (root.hovering === modelData ? "" : root.hovering) + } + MouseArea { + anchors.fill: parent + enabled: !Wallpapers.applying + cursorShape: Qt.PointingHandCursor + onClicked: Wallpapers.pick(modelData) + } + + Column { + anchors.fill: parent + anchors.margins: 6 + spacing: 4 + + Image { + width: parent.width + height: parent.height - 18 + source: "file://" + modelData + asynchronous: true + cache: false + fillMode: Image.PreserveAspectCrop + // Decode at display size: full-resolution + // wallpapers would be megabytes each in memory. + sourceSize.width: 480 + } + + Text { + width: parent.width + elide: Text.ElideMiddle + text: Wallpapers.basename(modelData) + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 } + color: Theme.subtext + } + } + } + } + } + } + + // The two screens as they sit on the desk: DP-3 rotated upright on + // the left, DP-1 wide on the right, centred against each other the + // way Hyprland has them. Hovering a thumbnail fills the screen it + // would be set on; the other keeps what is on it now. + Item { + id: mockPane + // A third of the panel rather than a fixed width, so the + // preview keeps its share when the panel is resized. + width: Math.round(parent.width * 0.33) + height: parent.height + + // Divisor is the two panels side by side plus the gap; the + // bezels and stands are drawn outside that, hence the margin. + readonly property real scale: (width * 0.92) / (1080 + 2560 + 160) + + Column { + anchors.centerIn: parent + spacing: 14 + + Row { + spacing: Math.round(160 * mockPane.scale) + + MockScreen { + anchors.verticalCenter: parent.verticalCenter + label: "DP-3" + targeted: Wallpapers.target === "V" + source: (targeted && root.hovering !== "") + ? root.hovering : Wallpapers.shown("V") + // DP-3 has transform=1, so it stands upright. + panelWidth: Math.round(1080 * mockPane.scale) + panelHeight: Math.round(1920 * mockPane.scale) + } + + MockScreen { + anchors.verticalCenter: parent.verticalCenter + label: "DP-1" + targeted: Wallpapers.target === "H" + source: (targeted && root.hovering !== "") + ? root.hovering : Wallpapers.shown("H") + panelWidth: Math.round(2560 * mockPane.scale) + panelHeight: Math.round(1080 * mockPane.scale) + } + } + + Text { + anchors.horizontalCenter: parent.horizontalCenter + width: mockPane.width + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + text: root.hovering !== "" ? Wallpapers.basename(root.hovering) + : Wallpapers.dirty ? "click Apply to set" + : "click a wallpaper to stage it" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.overlay + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 8 + + Tab { + text: Wallpapers.applying ? "applying…" : "Apply" + selected: Wallpapers.dirty && !Wallpapers.applying + enabled: Wallpapers.dirty && !Wallpapers.applying + onClicked: Wallpapers.apply() + } + Tab { + text: "Reset" + enabled: Wallpapers.dirty && !Wallpapers.applying + onClicked: Wallpapers.clearPending() + } + } + } + } + } + } +} |
