diff --git a/dank-qml-common b/dank-qml-common index 08f2850f4..bb889a069 160000 --- a/dank-qml-common +++ b/dank-qml-common @@ -1 +1 @@ -Subproject commit 08f2850f457c36585700fea489f3403389537faf +Subproject commit bb889a06932e20116f9d2d57d0f6c04f94fffcfe diff --git a/flake.lock b/flake.lock index 3b90d12da..5084f2c26 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "dank-qml-common": { "flake": false, "locked": { - "lastModified": 1789924572, - "narHash": "sha256-0zO+HFepbuUBedfE+FRoQ9XVFqsK9sXcqBU6NoAnlPQ=", + "lastModified": 1789949743, + "narHash": "sha256-nGF/lvqxo4GL/H+g+nVy/OcfkVmDIHpXs+HJqPiNvO4=", "owner": "AvengeMedia", "repo": "dank-qml-common", - "rev": "08f2850f457c36585700fea489f3403389537faf", + "rev": "bb889a06932e20116f9d2d57d0f6c04f94fffcfe", "type": "github" }, "original": { diff --git a/quickshell/Common/GridLayout.js b/quickshell/Common/GridLayout.js index aea1ea38f..46b09bad9 100644 --- a/quickshell/Common/GridLayout.js +++ b/quickshell/Common/GridLayout.js @@ -212,6 +212,21 @@ function crossGap(a, aSize, b, bSize) { return Math.max(0, b - (a + aSize), a - (b + bSize)); } +function nearestStep(edgeFor, from, minStep, maxStep, wanted) { + const distance = step => Math.abs(edgeFor(step) - wanted); + let best = Math.max(minStep, Math.min(maxStep, from)); + for (const direction of [1, -1]) { + for (let step = best + direction; step >= minStep && step <= maxStep; step += direction) { + const gap = distance(step); + if (gap > distance(best)) + break; + if (gap < distance(best)) + best = step; + } + } + return best; +} + function dimension(value, minimum, maximum, fallback, step = 1) { const min = minimum ?? 1; const max = Math.max(min, maximum ?? fallback ?? min); diff --git a/quickshell/Modals/FileBrowser/FileBrowserSurfaceModal.qml b/quickshell/Modals/FileBrowser/FileBrowserSurfaceModal.qml index 966abfb3e..13678d5e4 100644 --- a/quickshell/Modals/FileBrowser/FileBrowserSurfaceModal.qml +++ b/quickshell/Modals/FileBrowser/FileBrowserSurfaceModal.qml @@ -15,6 +15,7 @@ DankModal { property bool saveMode: false property bool folderMode: false property string defaultFileName: "" + property string revealPath: "" property var parentPopout: null signal fileSelected(string path) @@ -59,6 +60,7 @@ DankModal { saveMode: fileBrowserSurfaceModal.saveMode folderMode: fileBrowserSurfaceModal.folderMode defaultFileName: fileBrowserSurfaceModal.defaultFileName + revealPath: fileBrowserSurfaceModal.revealPath Component.onCompleted: initialize() diff --git a/quickshell/Modules/ControlCenter/Components/HeaderPane.qml b/quickshell/Modules/ControlCenter/Components/HeaderPane.qml index f5d654262..96f7be463 100644 --- a/quickshell/Modules/ControlCenter/Components/HeaderPane.qml +++ b/quickshell/Modules/ControlCenter/Components/HeaderPane.qml @@ -17,6 +17,7 @@ Item { signal powerButtonClicked signal lockRequested signal editModeToggled + signal editCancelled signal settingsButtonClicked signal headerTapped @@ -93,7 +94,6 @@ Item { Row { id: actionButtonsRow - width: CcMetrics.headerActionSize * 4 + spacing * 3 height: CcMetrics.headerActionSize anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -126,6 +126,16 @@ Item { onClicked: root.settingsButtonClicked() } + DankActionButton { + buttonSize: CcMetrics.headerActionSize + iconSize: CcMetrics.headerActionIconSize + iconName: "close" + iconColor: Theme.surfaceText + visible: root.editMode + Accessible.name: I18n.tr("Cancel") + onClicked: root.editCancelled() + } + DankActionButton { buttonSize: CcMetrics.headerActionSize iconSize: CcMetrics.headerActionIconSize diff --git a/quickshell/Modules/ControlCenter/ControlCenterContent.qml b/quickshell/Modules/ControlCenter/ControlCenterContent.qml index 6dac8a55b..eb455dbe4 100644 --- a/quickshell/Modules/ControlCenter/ControlCenterContent.qml +++ b/quickshell/Modules/ControlCenter/ControlCenterContent.qml @@ -26,6 +26,7 @@ FocusScope { return total; } property var pageHistory: [] + property var editSnapshot: null readonly property bool panelResizing: panelResizer.resizing readonly property real sheetContentWidth: host.sheetContentWidth ?? CcMetrics.sheetWidthFor(gridColumns) readonly property vector4d surfaceCornerRadii: host.surfaceCornerRadii ?? Qt.vector4d(Theme.windowRadius, Theme.windowRadius, Theme.windowRadius, Theme.windowRadius) @@ -125,12 +126,28 @@ FocusScope { configOverlayLoader.active = false; } + function cancelEdit() { + const snapshot = editSnapshot; + host.editMode = false; + if (!snapshot) + return; + if (JSON.stringify(SettingsData.controlCenterWidgets) !== snapshot.widgets) + SettingsData.set("controlCenterWidgets", JSON.parse(snapshot.widgets)); + if (SettingsData.controlCenterColumns !== snapshot.columns) + SettingsData.set("controlCenterColumns", snapshot.columns); + } + Keys.onEscapePressed: event => { if (configOverlayLoader.item?.visible) { configOverlayLoader.item.close(); event.accepted = true; return; } + if (host.editMode) { + host.editMode = false; + event.accepted = true; + return; + } if (pageOpen) { goBack(); event.accepted = true; @@ -151,10 +168,16 @@ FocusScope { } onEditModeChanged: { - if (editMode) + if (editMode) { host.collapseAll(); - else + editSnapshot = { + "widgets": JSON.stringify(SettingsData.controlCenterWidgets), + "columns": SettingsData.controlCenterColumns + }; + } else { panelResizer.cancel(); + editSnapshot = null; + } forceActiveFocus(); } @@ -165,7 +188,8 @@ FocusScope { anchors.margins: -(contentInset + Theme.spacingS) z: 1 visible: root.host.editMode - edgeResize: true + edgeResize: root.panelResizing || root.panelResizer.sideMovable(-1, root.gridColumns) + cornerResize: root.panelResizing || root.panelResizer.sideMovable(1, root.gridColumns) horizontalResize: true removable: false cornerRadius: Theme.windowRadius + Theme.spacingS @@ -233,6 +257,7 @@ FocusScope { tapToClose: root.host.headerTogglesClose ?? false onHeaderTapped: root.host.close() onEditModeToggled: root.host.editMode = !root.host.editMode + onEditCancelled: root.cancelEdit() onPowerButtonClicked: { const loader = root.host.powerMenuModalLoader; if (!loader) diff --git a/quickshell/Modules/ControlCenter/ControlCenterPopout.qml b/quickshell/Modules/ControlCenter/ControlCenterPopout.qml index 101c8be5b..251c6d9c1 100644 --- a/quickshell/Modules/ControlCenter/ControlCenterPopout.qml +++ b/quickshell/Modules/ControlCenter/ControlCenterPopout.qml @@ -15,6 +15,7 @@ DankPopout { resizing: contentLoader.item?.panelResizing ?? false surfacePadding: PopoutMetrics.editOverflow * 2 inputMargin: editMode ? PopoutMetrics.editOverflow * 2 : 0 + minimumSurfaceWidth: editMode ? CcMetrics.sheetWidthFor(gridColumnCap) : 0 property string expandedSection: "" property string pendingSection: "" diff --git a/quickshell/Modules/DankDash/DankDashPopout.qml b/quickshell/Modules/DankDash/DankDashPopout.qml index 0f299fce5..e5326ae5e 100644 --- a/quickshell/Modules/DankDash/DankDashPopout.qml +++ b/quickshell/Modules/DankDash/DankDashPopout.qml @@ -82,7 +82,7 @@ DankPopout { readonly property real editGutter: editMode ? PopoutMetrics.editOverflow : 0 popupWidth: panelWidthFor(DashMetrics.panelColumnsFor(activeTabId)) - minimumSurfaceWidth: panelWidthFor(DashRegistry.widestPanelColumns) + minimumSurfaceWidth: panelWidthFor(editMode ? columnCap : DashRegistry.widestPanelColumns) popupHeight: contentLoader.item?.implicitHeight ?? (DashMetrics.tabDefaultHeight + Theme.navigationHeight + DashMetrics.contentGap + DashMetrics.contentPadding * 2) triggerWidth: DashMetrics.triggerWidth screen: triggerScreen @@ -571,8 +571,8 @@ DankPopout { anchors.margins: -(contentInset + Theme.spacingS) z: 2 visible: root.editMode - edgeResize: mainContainer.panelResizer.sideMovable(-1, mainContainer.panelColumns) - cornerResize: mainContainer.panelResizer.sideMovable(1, mainContainer.panelColumns) + edgeResize: mainContainer.panelResizing || mainContainer.panelResizer.sideMovable(-1, mainContainer.panelColumns) + cornerResize: mainContainer.panelResizing || mainContainer.panelResizer.sideMovable(1, mainContainer.panelColumns) removable: false cornerRadius: Theme.windowRadius + Theme.spacingS handleOverhang: contentInset diff --git a/quickshell/Modules/DankDash/WallpaperTab.qml b/quickshell/Modules/DankDash/WallpaperTab.qml index bceb771ba..90f528430 100644 --- a/quickshell/Modules/DankDash/WallpaperTab.qml +++ b/quickshell/Modules/DankDash/WallpaperTab.qml @@ -515,6 +515,7 @@ Item { browserTitle: I18n.tr("Select Wallpaper Directory", "wallpaper directory file browser title") browserType: "wallpaper" showHiddenFiles: false + revealPath: root.getCurrentWallpaper() fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr", "*.svg"] parentPopout: root.parentPopout diff --git a/quickshell/Modules/Settings/LockScreenTab.qml b/quickshell/Modules/Settings/LockScreenTab.qml index 0aa82df93..eb82cfa88 100644 --- a/quickshell/Modules/Settings/LockScreenTab.qml +++ b/quickshell/Modules/Settings/LockScreenTab.qml @@ -150,6 +150,7 @@ Item { browserTitle: I18n.tr("Select lock screen background image") browserType: "wallpaper" showHiddenFiles: true + revealPath: SettingsData.lockScreenWallpaperPath || Theme.wallpaperPath fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.svg"] onFileSelected: path => { SettingsData.set("lockScreenWallpaperPath", path); diff --git a/quickshell/Modules/Settings/ThemeColorsTab.qml b/quickshell/Modules/Settings/ThemeColorsTab.qml index 1146bbb4e..9d7bcd9bb 100644 --- a/quickshell/Modules/Settings/ThemeColorsTab.qml +++ b/quickshell/Modules/Settings/ThemeColorsTab.qml @@ -21,7 +21,6 @@ Item { readonly property string matugenPreviewSource: SettingsData.matugenSeedColor || Theme.getMatugenColor("source_color", Theme.primary).toString() readonly property string matugenPreviewImage: (!SettingsData.matugenSeedColor && Theme.rawWallpaperPath && !Theme.rawWallpaperPath.startsWith("#")) ? Theme.rawWallpaperPath : "" readonly property string matugenPreviewKey: matugenPreviewSource + "|" + (SettingsData.matugenContrast ?? 0) + "|" + matugenPreviewImage + "|" + SettingsData.matugenSpec - property bool matugenGenerating: false readonly property var currentPalette: ThemePalette.pick({ "primary": Theme.primary, "secondary": Theme.secondary, @@ -32,7 +31,7 @@ Item { "warning": Theme.warning }) property bool matugenPreviewFailed: false - readonly property bool matugenPreviewsReady: !matugenGenerating && (matugenPreviewLoadedKey === matugenPreviewKey || matugenPreviewFailed || !Theme.matugenAvailable) + readonly property bool matugenPreviewsReady: matugenPreviewLoadedKey === matugenPreviewKey || matugenPreviewFailed || !Theme.matugenAvailable onMatugenPreviewKeyChanged: refreshMatugenSchemePreviews() property var installedRegistryThemes: [] readonly property var matugenSchemeOptions: { @@ -114,12 +113,6 @@ Item { function onMatugenAvailableChanged() { themeColorsTab.refreshMatugenSchemePreviews(); } - function onThemeGenerationStarting() { - themeColorsTab.matugenGenerating = true; - } - function onMatugenCompleted() { - themeColorsTab.matugenGenerating = false; - } } SettingsPage { diff --git a/quickshell/Modules/Settings/WallpaperColorsTab.qml b/quickshell/Modules/Settings/WallpaperColorsTab.qml index 40fb7b7e3..f7a5cfdcf 100644 --- a/quickshell/Modules/Settings/WallpaperColorsTab.qml +++ b/quickshell/Modules/Settings/WallpaperColorsTab.qml @@ -14,7 +14,6 @@ Column { id: root property var parentModal: null - property string browseTarget: "desktop" property var cachedIconThemes: SettingsData.availableIconThemes property var cachedCursorThemes: SettingsData.availableCursorThemes @@ -27,7 +26,6 @@ Column { } readonly property bool hasWallpaper: currentWallpaper !== "" readonly property bool wallpaperIsImage: hasWallpaper && !currentWallpaper.startsWith("#") - readonly property bool canCycleWallpaper: wallpaperIsImage && !currentWallpaper.startsWith("we") readonly property bool cyclingEnabled: { SessionData.monitorCyclingSettings; return perMonitor ? SessionData.getMonitorCyclingSettings(selectedScreen).enabled : SessionData.wallpaperCyclingEnabled; @@ -41,6 +39,7 @@ Column { "error": Theme.error, "warning": Theme.warning }) + readonly property string modeLabel: SessionData.isLightMode ? I18n.tr("Light", "adjective, wallpaper thumbnail label for light mode") : I18n.tr("Dark", "adjective, wallpaper thumbnail label for dark mode") readonly property string colorModeStatus: { if (SettingsData.matugenSmartMode) return I18n.tr("Wallpaper"); @@ -92,22 +91,13 @@ Column { return I18n.tr("No displays"); } - function openBrowser(target) { - browseTarget = target; + function openBrowser() { wallpaperBrowserLoader.active = true; if (wallpaperBrowserLoader.item) wallpaperBrowserLoader.item.open(); } - function applyBrowsedPath(path) { - switch (browseTarget) { - case "light": - SessionData.setWallpaperForMode(path, true); - return; - case "dark": - SessionData.setWallpaperForMode(path, false); - return; - } + function applyWallpaper(path) { if (perMonitor) { SessionData.setMonitorWallpaper(selectedScreen, path); SessionData.setMonitorCyclingFolderPath(selectedScreen, ""); @@ -118,25 +108,28 @@ Column { SessionData.saveSettings(); } - function pickColor(target, current, title) { + function pickColor() { const picker = PopoutService.colorPickerModal; if (!picker) return; - picker.selectedColor = current.startsWith("#") ? current : Theme.primary; - picker.pickerTitle = title; + picker.selectedColor = currentWallpaper.startsWith("#") ? currentWallpaper : Theme.primary; + picker.pickerTitle = I18n.tr("Choose Wallpaper Color", "wallpaper color picker title"); picker.onColorSelectedCallback = function (color) { - root.browseTarget = target; - root.applyBrowsedPath(color.toString()); + root.applyWallpaper(color.toString()); }; picker.show(); } - function clearDesktopWallpaper() { + function clearWallpaper() { if (perMonitor) { SessionData.setMonitorWallpaper(selectedScreen, ""); SessionData.setMonitorCyclingFolderPath(selectedScreen, ""); return; } + if (perMode) { + SessionData.setWallpaperForMode("", SessionData.isLightMode); + return; + } if (Theme.currentTheme === Theme.dynamic) Theme.switchTheme("blue"); SessionData.clearWallpaper(); @@ -144,24 +137,6 @@ Column { SessionData.saveSettings(); } - function cycleWallpaper(previous) { - if (!canCycleWallpaper) - return; - if (perMonitor) { - if (previous) { - WallpaperCyclingService.cyclePrevForMonitor(selectedScreen); - return; - } - WallpaperCyclingService.cycleNextForMonitor(selectedScreen); - return; - } - if (previous) { - WallpaperCyclingService.cyclePrevManually(); - return; - } - WallpaperCyclingService.cycleNextManually(); - } - ConfigInclude { id: cursorInclude includeKind: "cursor" @@ -189,10 +164,87 @@ Column { ToastService.showError(I18n.tr("Missing Environment Variables", "qt theme env error title"), I18n.tr("You need to set one of:\nQT_QPA_PLATFORMTHEME=gtk3 OR\nQT_QPA_PLATFORMTHEME=qt6ct OR\nQT_QPA_PLATFORMTHEME=qtengine\nas environment variables, and then restart the shell.\n\nOnly qt6ct requires qt6ct-kde to be installed.", "qt theme env error body")); } + component QuickTile: Rectangle { + id: tile + + property string title: "" + property string hint: "" + default property alias leading: leadingSlot.data + + signal clicked + + width: parent.width + height: Theme.listItemTwoLineHeight + radius: Theme.cornerRadiusM + color: Theme.foregroundColor(Theme.chipSurface, true) + + activeFocusOnTab: true + Accessible.role: Accessible.Button + Accessible.name: title + Accessible.description: hint + Accessible.onPressAction: tile.clicked() + Keys.onSpacePressed: tile.clicked() + Keys.onReturnPressed: tile.clicked() + + FocusRing {} + + Item { + id: leadingSlot + width: Theme.avatarSize + height: Theme.avatarSize + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.left: leadingSlot.right + anchors.leftMargin: Theme.spacingM + anchors.right: chevron.left + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXXS + + StyledText { + width: parent.width + text: tile.title + font.pixelSize: Theme.fontSizeMedium + font.weight: Theme.fontWeightMedium + color: Theme.surfaceText + elide: Text.ElideRight + } + + StyledText { + width: parent.width + text: tile.hint + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideRight + } + } + + DankIcon { + id: chevron + name: "chevron_right" + size: Theme.iconSize + color: Theme.surfaceVariantText + rotation: I18n.isRtl ? 180 : 0 + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + } + + StateLayer { + stateColor: Theme.surfaceText + cornerRadius: tile.radius + onClicked: tile.clicked() + } + } + SettingsCard { tab: "wallpaper" - tags: ["background", "image", "picture", "light", "dark", "mode", "monitor", "display"] - title: I18n.tr("Wallpaper") + tags: ["background", "image", "picture", "light", "dark", "mode", "theme", "color", "palette"] + title: I18n.tr("Appearance") settingKey: "wallpaper" SettingsDropdownRow { @@ -210,189 +262,89 @@ Column { settingKey: "colorMode" tags: ["light", "dark", "mode", "theme", "color", "palette"] body: Flow { - id: thumbs + id: hero - readonly property real thumbWidth: (width - spacing) / 2 + readonly property bool stacked: width < SettingsMetrics.wallpaperHeroStackWidth + readonly property real thumbWidth: stacked ? width : Math.round(width * SettingsMetrics.wallpaperHeroSplit) width: parent.width spacing: Theme.spacingL SettingsWallpaperThumb { - width: thumbs.thumbWidth - visible: !root.perMode + width: hero.thumbWidth path: root.currentWallpaper - canCycle: root.canCycleWallpaper - onBrowse: root.openBrowser("desktop") - onPickColor: root.pickColor("desktop", root.currentWallpaper, I18n.tr("Choose Wallpaper Color", "wallpaper color picker title")) - onClear: root.clearDesktopWallpaper() - onPrevious: root.cycleWallpaper(true) - onNext: root.cycleWallpaper(false) + badge: root.perMode ? root.modeLabel : "" + onBrowse: root.openBrowser() + onPickColor: root.pickColor() + onClear: root.clearWallpaper() } - SettingsWallpaperThumb { - width: thumbs.thumbWidth - visible: root.perMode - label: I18n.tr("Light", "adjective, wallpaper thumbnail label for light mode") - placeholderIcon: "light_mode" - path: SessionData.wallpaperPathLight - canCycle: SessionData.isLightMode && root.canCycleWallpaper - onBrowse: root.openBrowser("light") - onPickColor: root.pickColor("light", SessionData.wallpaperPathLight, I18n.tr("Choose Light Mode Color", "light mode wallpaper color picker title")) - onClear: { - root.browseTarget = "light"; - root.applyBrowsedPath(""); - } - onPrevious: root.cycleWallpaper(true) - onNext: root.cycleWallpaper(false) - } + Column { + width: hero.stacked ? hero.width : hero.width - hero.thumbWidth - hero.spacing + spacing: Theme.spacingM - SettingsWallpaperThumb { - width: thumbs.thumbWidth - visible: root.perMode - label: I18n.tr("Dark", "adjective, wallpaper thumbnail label for dark mode") - placeholderIcon: "dark_mode" - path: SessionData.wallpaperPathDark - canCycle: !SessionData.isLightMode && root.canCycleWallpaper - onBrowse: root.openBrowser("dark") - onPickColor: root.pickColor("dark", SessionData.wallpaperPathDark, I18n.tr("Choose Dark Mode Color", "dark mode wallpaper color picker title")) - onClear: { - root.browseTarget = "dark"; - root.applyBrowsedPath(""); - } - onPrevious: root.cycleWallpaper(true) - onNext: root.cycleWallpaper(false) - } - - Item { - id: quickPanel - width: root.perMode ? thumbs.width : thumbs.thumbWidth - height: root.perMode ? quickColumn.implicitHeight : thumbs.thumbWidth * SettingsMetrics.wallpaperThumbRatio - - Column { - id: quickColumn + DankButtonGroup { width: parent.width - spacing: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - - Row { - id: modeRow - width: parent.width - spacing: Theme.spacingS - - DankButtonGroup { - width: parent.width - scheduleButton.width - parent.spacing - fillWidth: true - checkEnabled: false - anchors.verticalCenter: parent.verticalCenter - model: [ - { - "text": I18n.tr("Light", "adjective, wallpaper thumbnail label for light mode"), - "icon": "light_mode" - }, - { - "text": I18n.tr("Dark", "adjective, wallpaper thumbnail label for dark mode"), - "icon": "dark_mode" - } - ] - currentIndex: SessionData.isLightMode ? 0 : 1 - selectionMode: "single" - onSelectionChanged: (index, selected) => { - if (!selected) - return; - const light = index === 0; - if (light === SessionData.isLightMode) - return; - Theme.screenTransition(); - Theme.setLightMode(light); - } - } - - DankActionButton { - id: scheduleButton - iconName: "schedule" - anchors.verticalCenter: parent.verticalCenter - Accessible.name: I18n.tr("Dark mode") - tooltipText: root.colorModeStatus - onClicked: root.parentModal?.navigateTo("theme_schedule") + fillWidth: true + checkEnabled: false + model: [ + { + "text": I18n.tr("Light", "adjective, wallpaper thumbnail label for light mode"), + "icon": "light_mode" + }, + { + "text": I18n.tr("Dark", "adjective, wallpaper thumbnail label for dark mode"), + "icon": "dark_mode" } + ] + currentIndex: SessionData.isLightMode ? 0 : 1 + selectionMode: "single" + onSelectionChanged: (index, selected) => { + if (!selected) + return; + const light = index === 0; + if (light === SessionData.isLightMode) + return; + Theme.screenTransition(); + Theme.setLightMode(light); } + } - Rectangle { - id: themeTile - width: parent.width - height: Theme.listItemTwoLineHeight - radius: Theme.cornerRadiusM - color: Theme.foregroundColor(Theme.chipSurface, true) + QuickTile { + title: I18n.tr("Dark mode") + hint: root.colorModeStatus + onClicked: root.parentModal?.navigateTo("theme_schedule") - activeFocusOnTab: true - Accessible.role: Accessible.Button - Accessible.name: I18n.tr("Theme & colors") - Accessible.description: Theme.currentThemeLabel - Accessible.onPressAction: root.parentModal?.navigateTo("theme") - Keys.onSpacePressed: root.parentModal?.navigateTo("theme") - Keys.onReturnPressed: root.parentModal?.navigateTo("theme") + DankIcon { + anchors.centerIn: parent + name: "schedule" + size: Theme.iconSize + color: Theme.primary + } + } - FocusRing {} + QuickTile { + title: I18n.tr("Theme & colors") + hint: Theme.currentThemeLabel + onClicked: root.parentModal?.navigateTo("theme") - DankPaletteSwatch { - id: themeSwatch - width: Theme.avatarSize - height: Theme.avatarSize - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - primaryColor: root.themePalette.primary - secondaryColor: root.themePalette.secondary - tertiaryColor: root.themePalette.tertiary - } - - Column { - anchors.left: themeSwatch.right - anchors.leftMargin: Theme.spacingM - anchors.right: themeChevron.left - anchors.rightMargin: Theme.spacingS - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingXXS - - StyledText { - width: parent.width - text: I18n.tr("Theme & colors") - font.pixelSize: Theme.fontSizeMedium - font.weight: Theme.fontWeightMedium - color: Theme.surfaceText - elide: Text.ElideRight - } - - StyledText { - width: parent.width - text: Theme.currentThemeLabel - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - elide: Text.ElideRight - } - } - - DankIcon { - id: themeChevron - name: "chevron_right" - size: Theme.iconSize - color: Theme.surfaceVariantText - rotation: I18n.isRtl ? 180 : 0 - anchors.right: parent.right - anchors.rightMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - } - - StateLayer { - stateColor: Theme.surfaceText - cornerRadius: themeTile.radius - onClicked: root.parentModal?.navigateTo("theme") - } + DankPaletteSwatch { + anchors.fill: parent + primaryColor: root.themePalette.primary + secondaryColor: root.themePalette.secondary + tertiaryColor: root.themePalette.tertiary } } } } } + } + + SettingsCard { + tab: "wallpaper" + tags: ["background", "image", "picture", "fill", "cycling", "transition", "monitor", "display"] + title: I18n.tr("Wallpaper") + settingKey: "wallpaperOptions" SettingsDropdownRow { id: fillModeRow @@ -860,9 +812,10 @@ Column { browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title") browserType: "wallpaper" showHiddenFiles: true + revealPath: root.currentWallpaper fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr", "*.svg"] onFileSelected: path => { - root.applyBrowsedPath(path); + root.applyWallpaper(path); close(); } } diff --git a/quickshell/Modules/Settings/Widgets/SettingsMetrics.qml b/quickshell/Modules/Settings/Widgets/SettingsMetrics.qml index 2b4d08e28..6a706f0e5 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsMetrics.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsMetrics.qml @@ -7,8 +7,8 @@ import qs.Common Singleton { readonly property real sidebarWidth: 320 readonly property real compactBreakpoint: 700 - readonly property real contentMaxWidth: 640 - readonly property real windowWidth: 1000 + readonly property real contentMaxWidth: 720 + readonly property real windowWidth: 1100 readonly property real windowHeight: 940 readonly property real windowMinWidth: 500 readonly property real windowMinHeight: 400 @@ -29,6 +29,8 @@ Singleton { readonly property real buttonGroupCompactThreshold: 200 readonly property real choiceCardPreviewRatio: 10 / 16 readonly property real wallpaperThumbRatio: 10 / 16 + readonly property real wallpaperHeroStackWidth: 440 + readonly property real wallpaperHeroSplit: 0.55 readonly property real disabledOpacity: 0.38 readonly property real highlightBlend: 0.2 readonly property real bannerTextMinWidth: 100 diff --git a/quickshell/Modules/Settings/Widgets/SettingsWallpaperThumb.qml b/quickshell/Modules/Settings/Widgets/SettingsWallpaperThumb.qml index bd29d8694..8ba471e17 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsWallpaperThumb.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsWallpaperThumb.qml @@ -1,171 +1,143 @@ import QtQuick -import QtQuick.Layouts import Quickshell.Widgets import qs.Common import qs.Widgets -Column { +Rectangle { id: root - property string label: "" + property string badge: "" property string path: "" property string placeholderIcon: "image" property string emptyText: I18n.tr("Not set", "wallpaper not set label") property bool allowColor: true - property bool canCycle: false readonly property bool isColor: path.startsWith("#") readonly property bool isImage: path !== "" && !isColor + readonly property string fileName: path !== "" ? path.split("/").pop() : emptyText signal browse signal pickColor signal clear - signal previous - signal next - spacing: Theme.spacingS + height: width * SettingsMetrics.wallpaperThumbRatio + radius: Theme.cornerRadiusM + color: isColor ? path : Theme.chipSurface - StyledText { - width: parent.width - text: root.label - font.pixelSize: Theme.fontSizeMedium - font.weight: Theme.fontWeightMedium - color: Theme.surfaceText - horizontalAlignment: Text.AlignHCenter - visible: root.label !== "" + ClippingRectangle { + anchors.fill: parent + radius: root.radius + color: "transparent" + + Loader { + id: imageLoader + anchors.fill: parent + active: root.visible && (root.Window.window?.visible ?? false) && root.isImage + asynchronous: true + + sourceComponent: CachingImage { + imagePath: root.path + animate: false + } + } + } + + DankIcon { + anchors.centerIn: parent + name: root.placeholderIcon + size: Theme.iconSizeLarge + color: Theme.surfaceVariantText + visible: !root.isColor && imageLoader.item?.status !== Image.Ready + } + + MouseArea { + anchors.fill: parent + onClicked: root.browse() } Rectangle { - id: frame - width: parent.width - height: width * SettingsMetrics.wallpaperThumbRatio - radius: Theme.cornerRadiusM - color: root.isColor ? root.path : Theme.chipSurface + anchors.fill: parent + radius: root.radius + color: Theme.withAlpha(Theme.scrimColor, Theme.scrimAlpha) + opacity: hoverArea.containsMouse ? 1 : 0 + visible: opacity > 0 - ClippingRectangle { - anchors.fill: parent - radius: frame.radius - color: "transparent" - - Loader { - id: imageLoader - anchors.fill: parent - active: root.visible && (root.Window.window?.visible ?? false) && root.isImage - asynchronous: true - - sourceComponent: CachingImage { - imagePath: root.path - animate: false - } + Behavior on opacity { + enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None + NumberAnimation { + duration: SettingsMetrics.fadeDuration + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.expressiveEffects } } - DankIcon { + Row { anchors.centerIn: parent - name: root.placeholderIcon - size: Theme.iconSizeLarge - color: Theme.surfaceVariantText - visible: !root.isColor && imageLoader.item?.status !== Image.Ready - } + spacing: Theme.spacingS - MouseArea { - anchors.fill: parent - onClicked: root.browse() - } - - Rectangle { - anchors.fill: parent - radius: frame.radius - color: Theme.withAlpha(Theme.scrimColor, Theme.scrimAlpha) - opacity: hoverArea.containsMouse ? 1 : 0 - visible: opacity > 0 - - Behavior on opacity { - enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None - NumberAnimation { - duration: SettingsMetrics.fadeDuration - easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.expressiveEffects - } + DankActionButton { + buttonSize: Theme.iconButtonSize + iconName: "folder_open" + iconSize: Theme.iconSizeMedium + backgroundColor: Theme.chipSurface + iconColor: Theme.surfaceText + Accessible.name: I18n.tr("Browse") + onClicked: root.browse() } - Row { - anchors.centerIn: parent - spacing: Theme.spacingS - - DankActionButton { - buttonSize: Theme.iconButtonSize - iconName: "folder_open" - iconSize: Theme.iconSizeMedium - backgroundColor: Theme.chipSurface - iconColor: Theme.surfaceText - Accessible.name: I18n.tr("Browse") - onClicked: root.browse() - } - - DankActionButton { - buttonSize: Theme.iconButtonSize - iconName: "palette" - iconSize: Theme.iconSizeMedium - backgroundColor: Theme.chipSurface - iconColor: Theme.surfaceText - visible: root.allowColor - tooltipText: I18n.tr("Custom") - onClicked: root.pickColor() - } - - DankActionButton { - buttonSize: Theme.iconButtonSize - iconName: "close" - iconSize: Theme.iconSizeMedium - backgroundColor: Theme.chipSurface - iconColor: Theme.error - visible: root.path !== "" - Accessible.name: I18n.tr("Clear") - onClicked: root.clear() - } + DankActionButton { + buttonSize: Theme.iconButtonSize + iconName: "palette" + iconSize: Theme.iconSizeMedium + backgroundColor: Theme.chipSurface + iconColor: Theme.surfaceText + visible: root.allowColor + tooltipText: I18n.tr("Custom") + onClicked: root.pickColor() } - } - MouseArea { - id: hoverArea - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.NoButton - cursorShape: Qt.PointingHandCursor + DankActionButton { + buttonSize: Theme.iconButtonSize + iconName: "close" + iconSize: Theme.iconSizeMedium + backgroundColor: Theme.chipSurface + iconColor: Theme.error + visible: root.path !== "" + Accessible.name: I18n.tr("Clear") + onClicked: root.clear() + } } } - RowLayout { - width: parent.width - spacing: Theme.spacingS - - DankActionButton { - buttonSize: Theme.iconButtonSize - iconName: "skip_previous" - iconSize: Theme.iconSize - visible: root.canCycle - Accessible.name: I18n.tr("Previous") - onClicked: root.previous() - } + Rectangle { + id: chip + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.margins: Theme.spacingS + width: Math.min(chipText.implicitWidth + Theme.spacingS * 2, parent.width - Theme.spacingS * 2) + height: chipText.implicitHeight + Theme.spacingXS * 2 + radius: Theme.cornerRadiusS + color: Theme.withAlpha(Theme.scrimColor, Theme.scrimAlpha) StyledText { - Layout.fillWidth: true - text: root.path !== "" ? root.path.split("/").pop() : root.emptyText + id: chipText + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + text: root.badge !== "" ? root.badge + " ยท " + root.fileName : root.fileName font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText + color: Theme.contrastLight elide: Text.ElideMiddle maximumLineCount: 1 - horizontalAlignment: Text.AlignHCenter - } - - DankActionButton { - buttonSize: Theme.iconButtonSize - iconName: "skip_next" - iconSize: Theme.iconSize - visible: root.canCycle - Accessible.name: I18n.tr("Next") - onClicked: root.next() + verticalAlignment: Text.AlignVCenter } } + + MouseArea { + id: hoverArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + cursorShape: Qt.PointingHandCursor + } } diff --git a/quickshell/Widgets/DankPanelResizer.qml b/quickshell/Widgets/DankPanelResizer.qml index 257ae6763..0b8681ae1 100644 --- a/quickshell/Widgets/DankPanelResizer.qml +++ b/quickshell/Widgets/DankPanelResizer.qml @@ -1,5 +1,6 @@ import QtQuick import qs.Common +import "../Common/GridLayout.js" as GridUtils QtObject { id: root @@ -56,10 +57,7 @@ QtObject { if (!drag) return; const wanted = edgeFor(drag.step, drag.side) + px + popout.renderedAlignedX - drag.x; - const edge = edgeFor(drag.previewStep, drag.side); - const direction = (wanted - edge) * drag.side >= 0 ? 1 : -1; - const gain = Math.max(Math.abs(edgeFor(drag.previewStep + direction, drag.side) - edge), stepWidth / 2); - const step = Math.max(minStep, Math.min(maxStep, drag.previewStep + direction * Math.round(Math.abs(wanted - edge) / gain))); + const step = GridUtils.nearestStep(step => edgeFor(step, drag.side), drag.step, minStep, maxStep, wanted); const rows = rowUnit > 0 ? Math.max(minRows, Math.min(maxRows, drag.rows + Math.round((py + popout.renderedAlignedY - drag.y) / rowUnit))) : drag.rows; if (step === drag.previewStep && rows === drag.previewRows) return; diff --git a/quickshell/tests/dash-grid.test.mjs b/quickshell/tests/dash-grid.test.mjs index 1c6cff648..46653e1c5 100644 --- a/quickshell/tests/dash-grid.test.mjs +++ b/quickshell/tests/dash-grid.test.mjs @@ -66,3 +66,9 @@ test("a dragged tile snaps to the cell under its corner", () => { assert.deepEqual(plain(grid.cellAt(grid.packCards(cards, order, 6, 600, 0, 100, true), 0, 0, 2, 1)), { col: 4, row: 0 }); assert.deepEqual(plain(grid.placedItems([{ id: "x" }, { id: "y" }], [null, { col: 1, row: 2 }])), [{ id: "x" }, { id: "y", col: 1, row: 2 }]); }); + +test("panel step follows the pointer and holds on a pinned edge", () => { + const rightEdge = step => Math.min(1000, 500 + step * 36); + assert.equal(grid.nearestStep(rightEdge, 8, 6, 30, rightEdge(8) + 50), 9); + assert.equal(grid.nearestStep(rightEdge, 20, 6, 30, 1400), 20); +}); diff --git a/quickshell/tests/qml/dash-panel-resize.qml b/quickshell/tests/qml/dash-panel-resize.qml index 1386c2af1..3af8c5048 100644 --- a/quickshell/tests/qml/dash-panel-resize.qml +++ b/quickshell/tests/qml/dash-panel-resize.qml @@ -1,6 +1,5 @@ import QtQuick import Quickshell -import Quickshell.Wayland import qs.Common import qs.Services import qs.Modules.DankDash @@ -80,11 +79,6 @@ ShellRoot { return host.resizing && host.renderedAlignedX === host.alignedX; } - function surfaceGeometry(host) { - const window = host.contentWindow; - return JSON.stringify([window.anchors.right, window.implicitWidth, window.WlrLayershell.margins.left]); - } - Component { id: dashComponent DankDashPopout {} @@ -127,11 +121,9 @@ ShellRoot { root.baseRows = c.panelRows; check(root.baseRows >= DashMetrics.minimumTabRows && DashMetrics.storedPanelRows("overview") === 0, "unset rows fit the cards: " + root.baseRows); check(!root.popout.contentWindow.anchors.right, "surface hugs the body outside edit mode"); - const dashSurface = surfaceGeometry(root.popout); root.popout.editMode = true; check(root.popout.popupWidth === widthFor(DashMetrics.defaultGridColumns), "edit mode keeps the panel width"); - check(surfaceGeometry(root.popout) === dashSurface, "entering edit mode preserves the bounded surface"); - check(root.popout.contentWindow.implicitWidth >= root.popout.popupWidth + PopoutMetrics.editOverflow * 4, "surface reserves the edit handle padding"); + check(!root.popout.contentWindow.anchors.right && root.popout.contentWindow.implicitWidth >= widthFor(root.popout.columnCap) + PopoutMetrics.editOverflow * 4, "edit mode holds a bounded surface wide enough for the column cap plus handle padding"); beginDrag(root.popout, c); dragTo(root.popout, c, 2 * columnGain(), 0); check(DashMetrics.panelPreview?.columns === 8, "preview snaps two columns wider: " + JSON.stringify(DashMetrics.panelPreview)); @@ -352,10 +344,9 @@ ShellRoot { const ccContent = root.cc.contentLoader?.item ?? null; check(root.cc.shouldBeVisible && ccContent, "control center open"); check(root.cc.popupWidth === CcMetrics.sheetWidthDefault, "control center default width"); - const ccSurface = surfaceGeometry(root.cc); root.cc.editMode = true; check(root.cc.popupWidth === CcMetrics.sheetWidthDefault, "control center edit mode keeps the sheet width"); - check(surfaceGeometry(root.cc) === ccSurface, "control center edit mode preserves the bounded surface"); + check(!root.cc.contentWindow.anchors.right && root.cc.contentWindow.implicitWidth >= CcMetrics.sheetWidthFor(root.cc.gridColumnCap) + PopoutMetrics.editOverflow * 4, "control center edit mode holds a bounded surface wide enough for the column cap"); beginDrag(root.cc, ccContent); dragTo(root.cc, ccContent, 1.4 * ccGain(), 0); check(CcMetrics.gridColumns === CcMetrics.defaultColumns + 1 && CcMetrics.sheetWidth === CcMetrics.sheetWidthFor(CcMetrics.defaultColumns + 1), "control center preview snaps to a column: " + CcMetrics.sheetWidth); diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 9bdb90db4..066071759 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -105,6 +105,33 @@ "page": "notifications", "icon": "palette" }, + { + "section": "wallpaper", + "label": "Appearance", + "tabIndex": -1, + "category": "Wallpaper & colors", + "keywords": [ + "appearance", + "background", + "bg", + "color", + "colors", + "colour", + "colours", + "dark", + "desktop", + "hue", + "image", + "light", + "mode", + "palette", + "picture", + "theme", + "tint", + "wallpaper" + ], + "page": "personalization" + }, { "section": "_hub_applications", "label": "Applications", @@ -685,6 +712,34 @@ ], "page": "personalization" }, + { + "section": "colorMode", + "label": "Dark mode", + "tabIndex": -1, + "category": "Wallpaper & colors", + "keywords": [ + "background", + "bg", + "color", + "colors", + "colour", + "colours", + "dark", + "dark mode", + "desktop", + "hue", + "image", + "light", + "mode", + "night", + "palette", + "picture", + "theme", + "tint", + "wallpaper" + ], + "page": "personalization" + }, { "section": "iconThemeDark", "label": "Dark mode theme", @@ -2076,36 +2131,6 @@ ], "icon": "memory" }, - { - "section": "colorMode", - "label": "Theme & colors", - "tabIndex": -1, - "category": "Wallpaper & colors", - "keywords": [ - "appearance", - "background", - "bg", - "color", - "colors", - "colour", - "colours", - "dark", - "desktop", - "hue", - "image", - "light", - "look", - "mode", - "palette", - "picture", - "style", - "theme", - "tint", - "wallpaper" - ], - "page": "personalization", - "icon": "schedule" - }, { "section": "focusRingWidth", "label": "Thickness", @@ -2243,7 +2268,7 @@ "page": "personalization" }, { - "section": "wallpaper", + "section": "wallpaperOptions", "label": "Wallpaper", "tabIndex": -1, "category": "Wallpaper & colors", @@ -2253,21 +2278,21 @@ "colors", "colour", "colours", - "dark", + "cycling", "desktop", "display", + "fill", "hue", "image", - "light", - "mode", "monitor", "palette", "picture", "tint", + "transition", "wallpaper" ], "page": "personalization", - "icon": "schedule" + "icon": "lock" }, { "section": "windowRadius",