From de1c5c99faccee9a49766a632c882cf222eca866 Mon Sep 17 00:00:00 2001 From: purian23 Date: Thu, 1 Oct 2026 19:34:28 -0400 Subject: [PATCH] feat(settings + keyboard): core clean up & keyboard driven overhaul in settings --- dank-qml-common | 2 +- flake.lock | 6 +- quickshell/Common/BarWidgetCatalog.qml | 4 + quickshell/Modals/Settings/ProfileSection.qml | 4 + .../Modals/Settings/SettingsContent.qml | 189 ++++++- quickshell/Modals/Settings/SettingsModal.qml | 45 +- .../Settings/SettingsPaneNavigation.qml | 141 +++++ .../Modals/Settings/SettingsSidebar.qml | 117 +++- .../Details/VpnProfileDelegate.qml | 29 +- .../Modules/Plugins/SelectionSetting.qml | 1 + quickshell/Modules/Plugins/SliderSetting.qml | 1 + quickshell/Modules/Settings/AudioTab.qml | 132 ++--- quickshell/Modules/Settings/AutoStartTab.qml | 265 ++------- quickshell/Modules/Settings/BarHubHeader.qml | 49 +- .../BarWidgetOptions/ClockOptions.qml | 2 +- .../BarWidgetOptions/MusicOptions.qml | 2 +- .../BarWidgetOptions/SystemUpdateOptions.qml | 2 +- quickshell/Modules/Settings/BarWidgetTab.qml | 2 +- .../Modules/Settings/ColorDropdownRow.qml | 18 +- quickshell/Modules/Settings/DankDashTab.qml | 4 +- .../Settings/DesktopWidgetInstanceCard.qml | 134 +---- .../Modules/Settings/DesktopWidgetsTab.qml | 194 +++---- .../DisplayConfig/MonitorIdentifyOverlay.qml | 5 +- .../Settings/DisplayConfig/OutputCard.qml | 1 + .../Modules/Settings/DisplayConfigTab.qml | 1 + quickshell/Modules/Settings/DockHubHeader.qml | 13 +- .../Modules/Settings/DockWidgetsTab.qml | 21 +- .../Modules/Settings/GammaControlTab.qml | 6 +- quickshell/Modules/Settings/GreeterTab.qml | 38 +- .../Modules/Settings/KeybindEditorDialog.qml | 1 + quickshell/Modules/Settings/KeybindsTab.qml | 1 + quickshell/Modules/Settings/LockScreenTab.qml | 70 +-- .../Modules/Settings/MediaPlayerTab.qml | 4 +- .../Modules/Settings/NetworkCellularTab.qml | 3 +- .../Modules/Settings/NetworkEthernetTab.qml | 32 +- .../Modules/Settings/NetworkStatusTab.qml | 1 + .../Modules/Settings/NetworkWifiTab.qml | 346 +++--------- .../Modules/Settings/NotificationRulesTab.qml | 59 +- .../Modules/Settings/NotificationsTab.qml | 64 +-- .../Modules/Settings/PaletteInjectTab.qml | 2 - .../Modules/Settings/PluginSettingsPage.qml | 2 +- .../Modules/Settings/PluginUpdatesDialog.qml | 237 +++----- .../Modules/Settings/PluginsHubHeader.qml | 8 +- .../Modules/Settings/PluginsManageTab.qml | 2 +- quickshell/Modules/Settings/PowerSleepTab.qml | 1 + quickshell/Modules/Settings/PrinterTab.qml | 32 +- .../Modules/Settings/SettingsHubPage.qml | 5 +- .../Modules/Settings/SoftwareUpdatesTab.qml | 2 +- .../Modules/Settings/ThemeColorsTab.qml | 320 +++-------- .../Modules/Settings/ThemeSurfacesTab.qml | 4 +- .../Modules/Settings/TimeWeatherTab.qml | 154 +++--- .../Modules/Settings/TypographyMotionTab.qml | 5 +- quickshell/Modules/Settings/UsersTab.qml | 2 +- .../Modules/Settings/WallpaperColorsTab.qml | 11 +- .../Modules/Settings/WeatherSettingsTab.qml | 4 +- .../Settings/Widgets/AppBrowserPopup.qml | 5 +- .../Settings/Widgets/PluginSettingsHost.qml | 10 - .../Widgets/SettingsButtonGroupRow.qml | 1 + .../Modules/Settings/Widgets/SettingsCard.qml | 1 + .../Settings/Widgets/SettingsDeleteButton.qml | 28 + .../Settings/Widgets/SettingsDivider.qml | 7 +- .../Settings/Widgets/SettingsDropdownRow.qml | 1 + .../Settings/Widgets/SettingsInstanceRow.qml | 13 +- .../Settings/Widgets/SettingsLayoutPicker.qml | 16 +- .../Settings/Widgets/SettingsMetrics.qml | 2 + .../Settings/Widgets/SettingsNoteRow.qml | 48 +- .../Settings/Widgets/SettingsRenameDialog.qml | 114 ++++ .../Settings/Widgets/SettingsReorderRow.qml | 1 + .../Modules/Settings/Widgets/SettingsRow.qml | 34 +- .../Settings/Widgets/SettingsSectionLabel.qml | 10 +- .../Settings/Widgets/SettingsSidebarItem.qml | 25 +- .../Settings/Widgets/SettingsSliderRow.qml | 1 + .../Settings/Widgets/SettingsSplitRow.qml | 18 +- .../Settings/Widgets/SettingsSwatchGrid.qml | 115 +++- .../Settings/Widgets/SettingsToggleRow.qml | 2 +- quickshell/Modules/Settings/WidgetsTab.qml | 44 +- .../Modules/Settings/WidgetsTabSection.qml | 25 +- .../Modules/Settings/WindowRulesTab.qml | 71 +-- quickshell/Services/PluginService.qml | 9 + quickshell/Widgets/DankColorButton.qml | 3 + quickshell/Widgets/DankDetailChip.qml | 3 + quickshell/Widgets/FocusRing.qml | 13 +- quickshell/tests/plugin-reload.test.mjs | 16 +- quickshell/tests/qml/settings-keyboard.qml | 521 ++++++++++++++++++ quickshell/tests/qml/settings-panes.qml | 198 +++++++ 85 files changed, 2264 insertions(+), 1891 deletions(-) create mode 100644 quickshell/Modals/Settings/SettingsPaneNavigation.qml create mode 100644 quickshell/Modules/Settings/Widgets/SettingsDeleteButton.qml create mode 100644 quickshell/Modules/Settings/Widgets/SettingsRenameDialog.qml create mode 100644 quickshell/Widgets/DankColorButton.qml create mode 100644 quickshell/Widgets/DankDetailChip.qml create mode 100644 quickshell/tests/qml/settings-keyboard.qml create mode 100644 quickshell/tests/qml/settings-panes.qml diff --git a/dank-qml-common b/dank-qml-common index fdd2dfe3f..fd2f92dc7 160000 --- a/dank-qml-common +++ b/dank-qml-common @@ -1 +1 @@ -Subproject commit fdd2dfe3f3444585bc1f44180837724abbdfe7f1 +Subproject commit fd2f92dc755aebc3ecda4b0ce3e27ce6d6ca9f92 diff --git a/flake.lock b/flake.lock index 6ea0223f5..14ef96695 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "dank-qml-common": { "flake": false, "locked": { - "lastModified": 1790818052, - "narHash": "sha256-B4QALG/6pkJuxKOOSs7wU+qJdOXHWYUt2YK6E3Y5M84=", + "lastModified": 1790897239, + "narHash": "sha256-JgDpkgnLeNuLFLcxpo3oLJC7NC7uiM7+w+Jv1e4xlIM=", "owner": "AvengeMedia", "repo": "dank-qml-common", - "rev": "fdd2dfe3f3444585bc1f44180837724abbdfe7f1", + "rev": "fd2f92dc755aebc3ecda4b0ce3e27ce6d6ca9f92", "type": "github" }, "original": { diff --git a/quickshell/Common/BarWidgetCatalog.qml b/quickshell/Common/BarWidgetCatalog.qml index f6ffb2bca..b16e26ee0 100644 --- a/quickshell/Common/BarWidgetCatalog.qml +++ b/quickshell/Common/BarWidgetCatalog.qml @@ -276,4 +276,8 @@ Singleton { function hasOptions(id) { return optionFiles[id] !== undefined; } + + function configurable(item) { + return hasOptions(item?.id) || !!item?.pluginId; + } } diff --git a/quickshell/Modals/Settings/ProfileSection.qml b/quickshell/Modals/Settings/ProfileSection.qml index b7cf7ad59..b5ce3b764 100644 --- a/quickshell/Modals/Settings/ProfileSection.qml +++ b/quickshell/Modals/Settings/ProfileSection.qml @@ -7,6 +7,10 @@ import qs.Modules.Settings.Widgets SettingsNavRow { id: root + signal navigationRequested(bool keyboard) + + onClicked: keyboard => navigationRequested(keyboard) + title: UserInfoService.fullName || I18n.tr("User") hint: DgopService.hostname || "DMS" singleLineTitle: true diff --git a/quickshell/Modals/Settings/SettingsContent.qml b/quickshell/Modals/Settings/SettingsContent.qml index f7916651c..7a1fc64b0 100644 --- a/quickshell/Modals/Settings/SettingsContent.qml +++ b/quickshell/Modals/Settings/SettingsContent.qml @@ -27,17 +27,158 @@ FocusScope { readonly property string pageTitle: (pageInfo?.titleFrom ? SettingsUiState[pageInfo.titleFrom] : "") || (pageInfo?.text ?? "") readonly property real pageContentMaxWidth: currentPageItem?.contentMaxWidth ?? SettingsMetrics.contentMaxWidth readonly property bool animationsEnabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None + // Mouse navigation lands parked; only keyboard navigation highlights a control on the page + property bool keyboardNavigation: true focus: true + function rememberFocus() { + const item = Window.activeFocusItem; + if (pageStack.currentItem && item !== currentPageItem && _contains(currentPageItem, item)) + pageStack.currentItem.rememberedFocus = item; + } + + function _validRemembered() { + const remembered = pageStack.currentItem?.rememberedFocus; + return remembered?.visible && remembered.enabled && _contains(currentPageItem, remembered) ? remembered : null; + } + function _focusPage() { + const revision = parentModal?.modalFocusScope?.focusRevision; Qt.callLater(() => { - if (!sessionVisible || !currentPageItem || parentModal?.searchFocused) + if (revision !== parentModal?.modalFocusScope?.focusRevision) return; - currentPageItem.forceActiveFocus(); + if (parentModal?.modalFocusScope?.transientOwnsFocus()) + return; + if (!sessionVisible || !currentPageItem || !pageStack.currentItem.presented) + return; + if (parentModal?.focusPane === "sidebar") + return; + if (!keyboardNavigation) { + if (!_contains(currentPageItem, Window.activeFocusItem)) + parkFocus(); + return; + } + const remembered = _validRemembered(); + if (remembered) { + remembered.forceActiveFocus(Qt.TabFocusReason); + _reveal(remembered); + return; + } + let item = currentPageItem; + do { + item = item.nextItemInFocusChain(true); + if (!item || item === currentPageItem || !_contains(currentPageItem, item)) { + currentPageItem.forceActiveFocus(Qt.TabFocusReason); + return; + } + } while (!item.visible || !item.enabled || !_inViewport(item)); + item.forceActiveFocus(Qt.TabFocusReason); }); } + function goBack(keyboard) { + if (!showBack) + return false; + if (menuHidden && !(parentModal?.canGoBack ?? false)) { + parentModal.toggleMenu(); + return true; + } + parentModal?.goBack(keyboard); + return true; + } + + function _contains(ancestor, item) { + for (let p = item; p; p = p.parent) { + if (p === ancestor) + return true; + } + return false; + } + + // Parked focus highlights nothing until a key resumes from rememberedFocus + function parkFocus() { + focusAnchor.forceActiveFocus(Qt.MouseFocusReason); + } + + Item { + id: focusAnchor + } + + function _moveFocus(forward) { + const page = currentPageItem; + let start = Window.activeFocusItem; + if (!page || (start !== focusAnchor && !_contains(page, start))) + return false; + if (start === focusAnchor) { + const remembered = _validRemembered(); + if (remembered) { + remembered.forceActiveFocus(Qt.TabFocusReason); + _reveal(remembered); + return true; + } + start = page; + } + let item = start; + do { + item = item.nextItemInFocusChain(forward); + if (!item || item === start || !_contains(page, item)) + return false; + } while (!item.visible || !item.enabled); + item.forceActiveFocus(forward ? Qt.TabFocusReason : Qt.BacktabFocusReason); + _reveal(item); + return true; + } + + function _inViewport(item) { + for (let f = item.parent; f; f = f.parent) { + if (typeof f.flick !== "function") + continue; + const y = item.mapToItem(f.contentItem, 0, 0).y; + const top = Math.max(0, y - Theme.spacingL); + const bottom = Math.min(f.contentHeight, y + item.height + Theme.spacingL); + if (top < f.contentY || bottom > f.contentY + f.height) + return false; + } + return true; + } + + function _reveal(item) { + for (let f = item.parent; f; f = f.parent) { + if (typeof f.flick !== "function" || f.contentHeight <= f.height) + continue; + const top = item.mapToItem(f.contentItem, 0, 0).y - Theme.spacingL; + const bottom = top + item.height + Theme.spacingL * 2; + if (top < f.contentY) + f.contentY = Math.max(0, top); + else if (bottom > f.contentY + f.height) + f.contentY = Math.min(f.contentHeight - f.height, bottom - f.height); + return; + } + } + + // Keys bubble up from the focused control, so a control that uses an arrow itself (text, lists, open menus) keeps it + Keys.onPressed: event => { + const mods = event.modifiers & ~Qt.KeypadModifier; + const backKey = I18n.isRtl ? Qt.Key_Right : Qt.Key_Left; + if ((event.key === Qt.Key_Up || event.key === Qt.Key_Down) && mods === Qt.NoModifier) { + event.accepted = _moveFocus(event.key === Qt.Key_Down); + return; + } + if ((event.key === backKey && mods === Qt.AltModifier) || (event.key === Qt.Key_Backspace && mods === Qt.NoModifier)) { + event.accepted = goBack(true); + return; + } + if (event.key !== Qt.Key_Escape || mods !== Qt.NoModifier) + return; + const focused = Window.activeFocusItem; + if (focused && focused !== currentPageItem && focused.cursorPosition !== undefined) + currentPageItem?.forceActiveFocus(); + else if (!goBack(true)) + parentModal?.focusSidebar(); + event.accepted = true; + } + function _fileFor(page) { if (pageFiles[page]) return pageFiles[page]; @@ -114,6 +255,30 @@ FocusScope { } } + function _scrollerOf(item) { + if (typeof item.flick === "function") + return item; + for (const child of item.children) { + const found = _scrollerOf(child); + if (found) + return found; + } + return null; + } + + // A page's Flickable accepts every press inside it before the pane handler sees it, so the scroller carries its own. + // Handlers die with their parent, so one per page. + Component { + id: scrollerTap + + TapHandler { + onPressedChanged: { + if (pressed) + root.parentModal?.modalFocusScope?.pointerFocus("content"); + } + } + } + Component { id: pageComponent @@ -127,6 +292,7 @@ FocusScope { readonly property int status: loader.status readonly property bool settled: !pending && loader.status !== Loader.Loading && (presented || loader.status !== Loader.Ready) + property Item rememberedFocus: null property bool pending: true property bool presented: false @@ -147,10 +313,16 @@ FocusScope { anchors.fill: parent opacity: host.presented ? 1 : 0 + enabled: host.presented onLoaded: { if (item.pageActive !== undefined) item.pageActive = Qt.binding(() => host.pageActive); + const scroller = root._scrollerOf(item); + if (scroller) + scrollerTap.createObject(scroller, { + "parent": scroller + }); root._focusPage(); } } @@ -180,6 +352,11 @@ FocusScope { } } + onPresentedChanged: { + if (presented && pageActive) + root._focusPage(); + } + onPageActiveChanged: { if (pageActive) root._focusPage(); @@ -281,13 +458,7 @@ FocusScope { iconSize: Theme.iconSize iconColor: Theme.surfaceText visible: root.showBack - onClicked: { - if (root.menuHidden && !(root.parentModal?.canGoBack ?? false)) { - root.parentModal.toggleMenu(); - return; - } - root.parentModal?.goBack(); - } + onClicked: root.goBack() } } diff --git a/quickshell/Modals/Settings/SettingsModal.qml b/quickshell/Modals/Settings/SettingsModal.qml index 5f0594b55..4a9154872 100644 --- a/quickshell/Modals/Settings/SettingsModal.qml +++ b/quickshell/Modals/Settings/SettingsModal.qml @@ -31,6 +31,7 @@ DankFloatingWindow { } property alias sidebar: sidebar readonly property alias modalFocusScope: contentFocusScope + readonly property alias focusPane: contentFocusScope.activePane property string currentPage: "personalization" property var pageHistory: [] readonly property int currentTabIndex: SettingsTabs.tabIndexForPage(currentPage) @@ -54,6 +55,8 @@ DankFloatingWindow { visible = false; } CompositorService.closeNiriOverviewOnWindowFocus(); + if (!shouldBeVisible && isCompactMode) + menuVisible = true; shouldBeVisible = true; if (readyToMap) visible = true; @@ -82,12 +85,15 @@ DankFloatingWindow { return true; } - function navigateTo(pageId: string): bool { + // Rows report how they were activated; buttons and cards fall back to the focused control's reason + function navigateTo(pageId, keyboard = contentFocusScope.keyboardDriven()): bool { const resolved = SettingsTabs.resolvePage(pageId); if (!resolved || resolved === currentPage) return false; + content.rememberFocus(); pageHistory = pageHistory.concat([currentPage]); currentPage = resolved; + focusCurrentPage(keyboard); return true; } @@ -95,17 +101,19 @@ DankFloatingWindow { return setPage(name); } - function goBack() { + function goBack(keyboard = contentFocusScope.keyboardDriven()) { if (pageHistory.length > 0) { const history = pageHistory.slice(); const target = history.pop(); pageHistory = history; currentPage = target; + focusCurrentPage(keyboard); return; } if (!currentParentId) return; currentPage = currentParentId; + focusCurrentPage(keyboard); } function setTabIndex(tabIndex: int) { @@ -140,20 +148,22 @@ DankFloatingWindow { show(); } - function focusCurrentPage() { - content._focusPage(); + function focusCurrentPage(keyboard = true) { + contentFocusScope.focusContent(keyboard); + } + + function focusSidebar() { + contentFocusScope.focusSidebar(); } function focusSearch() { - if (isCompactMode) - menuVisible = true; - Qt.callLater(sidebar.focusSearch); + contentFocusScope.focusSearch(); } function toggleMenu() { menuVisible = !menuVisible; if (menuVisible) - Qt.callLater(sidebar.focusSearch); + focusSearch(); } objectName: "settingsModal" @@ -184,9 +194,11 @@ DankFloatingWindow { if (!visible) { pageHistory = []; closingModal(); - } else if (!isCompactMode || menuVisible) { + } else { + const revision = contentFocusScope.focusRevision; Qt.callLater(() => { - sidebar.focusSearch(); + if (settingsModal.visible && revision === contentFocusScope.focusRevision) + settingsModal.focusSearch(); }); } } @@ -257,22 +269,19 @@ DankFloatingWindow { } } - FocusScope { + SettingsPaneNavigation { id: contentFocusScope + sidebar: sidebar + content: content + parentModal: settingsModal + LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.childrenInherit: true anchors.fill: parent focus: true - Keys.onPressed: event => { - if (event.key !== Qt.Key_F || !(event.modifiers & Qt.ControlModifier)) - return; - settingsModal.focusSearch(); - event.accepted = true; - } - Keys.onBackPressed: event => { settingsModal.goBack(); event.accepted = true; diff --git a/quickshell/Modals/Settings/SettingsPaneNavigation.qml b/quickshell/Modals/Settings/SettingsPaneNavigation.qml new file mode 100644 index 000000000..a670c99cd --- /dev/null +++ b/quickshell/Modals/Settings/SettingsPaneNavigation.qml @@ -0,0 +1,141 @@ +import QtQuick + +FocusScope { + id: root + + required property Item sidebar + required property Item content + required property var parentModal + property string activePane: "sidebar" + property Item sidebarFocus: null + readonly property Item focusedItem: Window.activeFocusItem + property int focusRevision: 0 + + focus: true + + // Controls take focus on press with the mouse reason, so visualFocus tells how the activating control was reached + function keyboardDriven() { + return focusedItem?.visualFocus ?? false; + } + + function contains(ancestor, item) { + for (let p = item; p; p = p.parent) { + if (p === ancestor) + return true; + } + return false; + } + + function transientOwnsFocus() { + for (let p = focusedItem; p && p !== root; p = p.parent) { + if (p.capturing === true || p.Accessible.role === Accessible.Dialog || p.Accessible.role === Accessible.PopupMenu) + return true; + } + return false; + } + + onFocusedItemChanged: { + if (transientOwnsFocus()) + return; + if (contains(sidebar, focusedItem)) { + if (focusedItem !== sidebar) + sidebarFocus = focusedItem; + activePane = "sidebar"; + } else if (contains(content, focusedItem)) { + activePane = "content"; + content.rememberFocus(); + content._reveal(focusedItem); + } + } + + function focusSidebar() { + focusRevision++; + activePane = "sidebar"; + if (parentModal.isCompactMode) + parentModal.menuVisible = true; + if (sidebarFocus?.visible && sidebarFocus.enabled && contains(sidebar, sidebarFocus)) { + sidebarFocus.forceActiveFocus(Qt.TabFocusReason); + sidebar.ensureRowVisible(sidebarFocus); + } else { + sidebar.focusNavigation(); + } + } + + function focusContent(keyboard = true) { + focusRevision++; + activePane = "content"; + if (parentModal.isCompactMode) + parentModal.menuVisible = false; + content.keyboardNavigation = keyboard; + content._focusPage(); + } + + function focusSearch() { + focusRevision++; + activePane = "sidebar"; + if (parentModal.isCompactMode) + parentModal.menuVisible = true; + sidebar.focusSearch(); + } + + function pointerFocus(pane) { + const revision = ++focusRevision; + activePane = pane; + // Child controls get the press first. A background click only moves the keyboard region, highlighting nothing. + Qt.callLater(() => { + if (revision !== focusRevision || transientOwnsFocus()) + return; + const region = pane === "sidebar" ? sidebar : content; + if (contains(region, focusedItem)) + return; + if (pane === "sidebar") + sidebar.forceActiveFocus(Qt.MouseFocusReason); + else + content.parkFocus(); + }); + } + + TapHandler { + parent: root.sidebar + onPressedChanged: { + if (pressed) + root.pointerFocus("sidebar"); + } + } + + // Page scrollers accept presses before this handler sees them; SettingsContent gives each one its own + TapHandler { + parent: root.content + onPressedChanged: { + if (pressed) + root.pointerFocus("content"); + } + } + + TapHandler { + acceptedButtons: Qt.BackButton + onTapped: { + if (!transientOwnsFocus()) + root.content.goBack(false); + } + } + + Keys.onPressed: event => { + const mods = event.modifiers & ~Qt.KeypadModifier; + if (transientOwnsFocus()) { + if ((event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab || event.key === Qt.Key_F) && (mods & Qt.ControlModifier)) + event.accepted = true; + return; + } + if ((event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab) && (mods === Qt.ControlModifier || mods === (Qt.ControlModifier | Qt.ShiftModifier))) { + if (activePane === "sidebar") + focusContent(); + else + focusSidebar(); + event.accepted = true; + } else if (event.key === Qt.Key_F && mods === Qt.ControlModifier) { + focusSearch(); + event.accepted = true; + } + } +} diff --git a/quickshell/Modals/Settings/SettingsSidebar.qml b/quickshell/Modals/Settings/SettingsSidebar.qml index af864c4b0..488955819 100644 --- a/quickshell/Modals/Settings/SettingsSidebar.qml +++ b/quickshell/Modals/Settings/SettingsSidebar.qml @@ -46,12 +46,62 @@ Item { searchField.forceActiveFocus(); } - function focusAfterNavigation() { - if (parentModal?.isCompactMode && !parentModal.menuVisible) { - parentModal.focusCurrentPage(); + function focusAfterNavigation(keyboard) { + if (keyboard || (parentModal?.isCompactMode && !parentModal.menuVisible)) { + parentModal?.focusCurrentPage(keyboard); return; } - focusSearch(); + parentModal?.focusSidebar(); + } + + function focusNavigation() { + function findRow(item) { + if (item.modelData?.id === root.activeCategoryId && item.visible && item.enabled) + return item; + for (const child of item.children ?? []) { + const row = findRow(child); + if (row) + return row; + } + return null; + } + const row = findRow(sidebarColumn); + if (row && !searchActive) { + row.forceActiveFocus(Qt.TabFocusReason); + ensureRowVisible(row); + } else { + focusSearch(); + } + } + + function moveRowFocus(forward) { + const start = Window.activeFocusItem; + if (start === root) { + parentModal?.focusSidebar(); + return true; + } + let item = start; + do { + item = item?.nextItemInFocusChain(forward); + if (!item || item === start) + return false; + let ancestor = item; + while (ancestor && ancestor !== root) + ancestor = ancestor.parent; + if (!ancestor) + return false; + } while (!item.visible || !item.enabled); + keyboardHighlightId = ""; + item.forceActiveFocus(forward ? Qt.TabFocusReason : Qt.BacktabFocusReason); + ensureRowVisible(item); + return true; + } + + Keys.onPressed: event => { + if ((event.modifiers & ~Qt.KeypadModifier) !== Qt.NoModifier) + return; + if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) + event.accepted = moveRowFocus(event.key === Qt.Key_Down); } function navigableIds() { @@ -79,7 +129,7 @@ Item { return; pageRequested(keyboardHighlightId); keyboardHighlightId = ""; - Qt.callLater(root.focusAfterNavigation); + root.focusAfterNavigation(true); } function ensureRowVisible(item) { @@ -120,13 +170,13 @@ Item { parentModal?.navigateTo("bar_widget"); } - function selectSearchResult(result) { + function selectSearchResult(result, keyboard = true) { if (!result) return; if (result.runtimeType === "barWidget" || result.runtimeType === "barWidgetAdd") { openBarWidget(result.runtimeId, result.runtimeType === "barWidgetAdd"); keyboardHighlightId = ""; - Qt.callLater(root.focusAfterNavigation); + root.focusAfterNavigation(keyboard); return; } if (result.section) @@ -135,7 +185,7 @@ Item { if (page) pageRequested(page); keyboardHighlightId = ""; - Qt.callLater(root.focusAfterNavigation); + root.focusAfterNavigation(keyboard); } function navigateSearchResults(delta) { @@ -200,7 +250,11 @@ Item { anchors.topMargin: SettingsMetrics.searchBarGap height: SettingsMetrics.searchBarHeight placeholderText: I18n.tr("Search settings", "settings search field placeholder") - onFocusStateChanged: hasFocus => root.searchFocused = hasFocus + onFocusStateChanged: hasFocus => { + root.searchFocused = hasFocus; + if (!hasFocus) + root.keyboardHighlightId = ""; + } onTextChanged: { SettingsSearchService.search(text); root.searchSelectedIndex = 0; @@ -230,6 +284,10 @@ Item { root.selectSearchResult(SettingsSearchService.results[root.searchSelectedIndex]); return; } + if (!root.searchActive && root.keyboardHighlightId === "") { + root.parentModal?.focusCurrentPage(); + return; + } root.selectHighlighted(); } Keys.onDownPressed: event => { @@ -240,16 +298,19 @@ Item { navPrev(); event.accepted = true; } + // Specific key handlers accept by default; with nothing to cycle, Tab must reach the focus chain Keys.onTabPressed: event => { - if (!root.searchActive && root.keyboardHighlightId === "") - return; - navNext(); - event.accepted = true; + event.accepted = !(event.modifiers & Qt.ControlModifier) && (root.searchActive || root.keyboardHighlightId !== ""); + if (event.accepted) + navNext(); } Keys.onBacktabPressed: event => { - if (!root.searchActive && root.keyboardHighlightId === "") - return; - navPrev(); + event.accepted = !(event.modifiers & Qt.ControlModifier) && (root.searchActive || root.keyboardHighlightId !== ""); + if (event.accepted) + navPrev(); + } + Keys.onEnterPressed: event => { + navSelect(); event.accepted = true; } Keys.onReturnPressed: event => { @@ -287,11 +348,17 @@ Item { spacing: SettingsMetrics.sidebarGroupGap ProfileSection { + id: profileRow width: parent.width - parent.leftPadding - parent.rightPadding visible: !root.searchActive - onClicked: { + highlighted: activeFocus + onActiveFocusChanged: { + if (activeFocus) + root.ensureRowVisible(profileRow); + } + onNavigationRequested: keyboard => { root.pageRequested("user_accounts"); - Qt.callLater(root.focusAfterNavigation); + root.focusAfterNavigation(keyboard); } } @@ -312,6 +379,10 @@ Item { required property int index required property var modelData + onActiveFocusChanged: { + if (activeFocus) + root.ensureRowVisible(resultDelegate); + } isFirstInGroup: index === 0 isLastInGroup: index === SettingsSearchService.results.length - 1 iconName: modelData.icon || "settings" @@ -319,7 +390,7 @@ Item { hint: modelData.category accent: SettingsTabs.accentFor(modelData.page || SettingsTabs.pageForTabIndex(modelData.tabIndex)) active: root.searchSelectedIndex === index - onClicked: root.selectSearchResult(modelData) + onClicked: keyboard => root.selectSearchResult(modelData, keyboard) } } @@ -365,6 +436,10 @@ Item { id: categoryRow required property var modelData + onActiveFocusChanged: { + if (activeFocus) + root.ensureRowVisible(categoryRow); + } readonly property bool isHighlighted: root.keyboardHighlightId === modelData.id onIsHighlightedChanged: { if (isHighlighted) @@ -381,10 +456,10 @@ Item { accent: SettingsTabs.accentFor(modelData.id) active: root.activeCategoryId === modelData.id && !SettingsTabs.isPluginPage(root.currentPage) highlighted: isHighlighted - onClicked: { + onClicked: keyboard => { root.keyboardHighlightId = ""; root.pageRequested(modelData.id); - Qt.callLater(root.focusAfterNavigation); + root.focusAfterNavigation(keyboard); } } } diff --git a/quickshell/Modules/ControlCenter/Details/VpnProfileDelegate.qml b/quickshell/Modules/ControlCenter/Details/VpnProfileDelegate.qml index a38cc6f9b..c36fd7334 100644 --- a/quickshell/Modules/ControlCenter/Details/VpnProfileDelegate.qml +++ b/quickshell/Modules/ControlCenter/Details/VpnProfileDelegate.qml @@ -139,34 +139,13 @@ CcListRow { Repeater { model: root.configFields - Rectangle { + DankDetailChip { required property var modelData - width: fieldContent.width + Theme.spacingM * 2 - height: Theme.buttonHeightXS - radius: Theme.cornerRadiusS + label: modelData.label + value: modelData.value color: Theme.chipSurface - - Row { - id: fieldContent - anchors.centerIn: parent - spacing: Theme.spacingXS - - StyledText { - text: modelData.label + ":" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: modelData.value - font.pixelSize: Theme.fontSizeSmall - font.weight: Theme.fontWeightMedium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - } + border.width: 0 } } } diff --git a/quickshell/Modules/Plugins/SelectionSetting.qml b/quickshell/Modules/Plugins/SelectionSetting.qml index 7f9d6b8d6..a04d388e9 100644 --- a/quickshell/Modules/Plugins/SelectionSetting.qml +++ b/quickshell/Modules/Plugins/SelectionSetting.qml @@ -69,6 +69,7 @@ Column { } DankDropdown { + downKeyOpens: false width: parent.width text: root.label description: root.description diff --git a/quickshell/Modules/Plugins/SliderSetting.qml b/quickshell/Modules/Plugins/SliderSetting.qml index deffd10ce..15fd586c3 100644 --- a/quickshell/Modules/Plugins/SliderSetting.qml +++ b/quickshell/Modules/Plugins/SliderSetting.qml @@ -58,6 +58,7 @@ Column { } DankSlider { + upDownKeysStep: false width: parent.width value: root.value minimum: root.minimum diff --git a/quickshell/Modules/Settings/AudioTab.qml b/quickshell/Modules/Settings/AudioTab.qml index f6e7a1ce2..65dddd739 100644 --- a/quickshell/Modules/Settings/AudioTab.qml +++ b/quickshell/Modules/Settings/AudioTab.qml @@ -16,11 +16,8 @@ Item { property var outputDevices: [] property var inputDevices: [] property var parentModal: null - property bool renameOpen: false - property bool renameMounted: false property var editingDevice: null property string editingDeviceType: "" - property string newDeviceName: "" property bool isReloadingAudio: false property var hiddenOutputDeviceNames: SessionData.hiddenOutputDeviceNames ?? [] property var hiddenInputDeviceNames: SessionData.hiddenInputDeviceNames ?? [] @@ -28,30 +25,16 @@ Item { property bool showHiddenInputDevices: false function openRenameDialog(device, type) { - if (renameOpen) - return; editingDevice = device; editingDeviceType = type; - newDeviceName = AudioService.displayName(device); - renameOpen = true; - renameMounted = true; - if (renameLoader.item) - renameLoader.item.show(); + renameDialog.show(AudioService.displayName(device)); } - function closeRenameDialog() { - if (!renameOpen) + function saveDeviceName(name) { + if (!editingDevice) return; - renameOpen = false; - if (renameLoader.item) - renameLoader.item.opened = false; - } - - function saveDeviceName() { - if (!editingDevice || newDeviceName.trim() === "") - return; - AudioService.setDeviceAlias(editingDevice.name, newDeviceName); - closeRenameDialog(); + AudioService.setDeviceAlias(editingDevice.name, name); + renameDialog.hide(); } function persistHiddenOutputDeviceNames(deviceNames) { @@ -216,9 +199,7 @@ Item { deviceType: "output" showHideButton: true - onEditRequested: device => { - root.openRenameDialog(device, "output"); - } + onEditRequested: device => root.openRenameDialog(device, "output") onHideRequested: device => { root.persistHiddenOutputDeviceNames([...root.hiddenOutputDeviceNames, device.name]); @@ -242,6 +223,7 @@ Item { DankSlider { id: maxVolSlider + upDownKeysStep: false anchors.left: maxVolLabel.right anchors.leftMargin: Theme.spacingS anchors.right: parent.right @@ -381,9 +363,7 @@ Item { deviceType: "input" showHideButton: true - onEditRequested: device => { - root.openRenameDialog(device, "input"); - } + onEditRequested: device => root.openRenameDialog(device, "input") onHideRequested: device => { root.persistHiddenInputDeviceNames([...root.hiddenInputDeviceNames, device.name]); @@ -513,82 +493,32 @@ Item { } - Loader { - id: renameLoader + SettingsRenameDialog { + id: renameDialog parent: root.parentModal?.modalFocusScope ?? root - anchors.fill: parent - z: 1000 - active: root.renameMounted - onLoaded: item.show() + title: I18n.tr("Set custom device name") + supportingText: root.editingDevice?.name ?? "" + labelText: I18n.tr("Custom name") + leftIconName: root.editingDeviceType === "input" ? "mic" : "speaker" + onAccepted: name => root.saveDeviceName(name) - sourceComponent: DankDialog { - id: renameDialog + aboveField: StyledText { + visible: AudioService.hasDeviceAlias(root.editingDevice?.name ?? "") + text: I18n.tr("Original: %1", "Shows the original device name before renaming").arg(AudioService.originalName(root.editingDevice)) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + elide: Text.ElideRight + horizontalAlignment: Text.AlignLeft + } - embedded: nativeWindow - opened: nativeWindow - maximumWidth: SettingsMetrics.formDialogWidth - surfaceColor: Theme.hostSurface - title: I18n.tr("Set custom device name") - supportingText: root.editingDevice?.name ?? "" - acceptEnabled: root.newDeviceName.trim() !== "" - onAccepted: root.saveDeviceName() - onRejected: root.closeRenameDialog() - onActiveChanged: { - if (!active && !root.renameOpen) - root.renameMounted = false; - } - - function show() { - nameInput.text = root.newDeviceName; - opened = true; - forceActiveFocus(); - nameInput.forceActiveFocus(); - nameInput.selectAll(); - } - - actions: [ - DankButton { - text: I18n.tr("Cancel") - backgroundColor: "transparent" - textColor: Theme.primary - onClicked: renameDialog.rejected() - }, - DankButton { - text: I18n.tr("Save") - iconName: "check" - enabled: renameDialog.acceptEnabled - onClicked: root.saveDeviceName() - } - ] - - StyledText { - visible: AudioService.hasDeviceAlias(root.editingDevice?.name ?? "") - text: I18n.tr("Original: %1", "Shows the original device name before renaming").arg(AudioService.originalName(root.editingDevice)) - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - width: parent.width - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - } - - DankTextField { - id: nameInput - outlined: true - leftIconName: root.editingDeviceType === "input" ? "mic" : "speaker" - labelText: I18n.tr("Custom name") - width: parent.width - showClearButton: true - onTextChanged: root.newDeviceName = text - } - - StyledText { - width: parent.width - text: I18n.tr("Press Enter and the audio system will restart to apply the change", "Audio device rename dialog hint") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignLeft - } + belowField: StyledText { + width: parent.width + text: I18n.tr("Press Enter and the audio system will restart to apply the change", "Audio device rename dialog hint") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignLeft } } } diff --git a/quickshell/Modules/Settings/AutoStartTab.qml b/quickshell/Modules/Settings/AutoStartTab.qml index 8a0eeebe8..a9ce33ed8 100644 --- a/quickshell/Modules/Settings/AutoStartTab.qml +++ b/quickshell/Modules/Settings/AutoStartTab.qml @@ -392,110 +392,36 @@ Item { } SettingsRow { + id: appPickerRow + + readonly property var selectedApp: root.desktopApps.find(a => (a.id || a.execString) === root.newEntryDesktopId) ?? null + visible: root.newEntryType === "desktop" - body: Column { + title: I18n.tr("App", "noun, application picker label in autostart add entry") + subtitle: root.newEntryDesktopId ? (selectedApp?.name || selectedApp?.id || root.newEntryDesktopId) : I18n.tr("No application selected") + clickable: true + showChevron: true + onClicked: appBrowserPopup.show() + leading: AppIconRenderer { + width: Theme.iconSize + height: Theme.iconSize + iconValue: appPickerRow.selectedApp?.icon || "application-x-executable" + iconSize: Theme.iconSize + fallbackText: (appPickerRow.selectedApp?.name || "?").charAt(0).toUpperCase() + } + } + + SettingsRow { + visible: root.newEntryType === "desktop" + body: DankTextField { + outlined: true + leftIconName: "terminal" + labelText: I18n.tr("Command") + supportingText: I18n.tr("Wrap the app command. %command% is replaced with the actual executable", "autostart command field hint, keep %command% verbatim") width: parent.width - spacing: Theme.spacingM - - Item { - width: parent.width - height: appLabelColumn.height - - Column { - id: appLabelColumn - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingXS - - StyledText { - text: I18n.tr("App", "noun, application picker label in autostart add entry") - font.pixelSize: Theme.fontSizeMedium - font.weight: Theme.fontWeightMedium - color: Theme.surfaceText - } - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - - StyledRect { - height: 40 - radius: Theme.cornerRadius - color: root.newEntryDesktopId ? Theme.floatingWindowFieldColor : Theme.withAlpha(Theme.chipSurface, Theme.floatingWindowForegroundLayers ? Theme.floatingWindowForegroundTransparency * 0.5 : 0) - LayoutMirroring.enabled: I18n.isRtl - LayoutMirroring.childrenInherit: true - - readonly property string selectedName: { - if (!root.newEntryDesktopId) - return ""; - const app = root.desktopApps.find(a => (a.id || a.execString) === root.newEntryDesktopId); - return app ? (app.name || app.id || "") : root.newEntryDesktopId; - } - - width: parent.width - browseButton.width - Theme.spacingM - - Row { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingM - visible: root.newEntryDesktopId !== "" - - Image { - width: 24 - height: 24 - source: { - const app = root.desktopApps.find(a => (a.id || a.execString) === root.newEntryDesktopId); - return Paths.resolveIconUrl(app?.icon || "application-x-executable"); - } - sourceSize.width: 24 - sourceSize.height: 24 - fillMode: Image.PreserveAspectFit - anchors.verticalCenter: parent.verticalCenter - onStatusChanged: { - if (status === Image.Error) - source = "image://icon/application-x-executable"; - } - } - - StyledText { - text: parent.parent.selectedName - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - } - - StyledText { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - text: I18n.tr("No application selected") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - visible: root.newEntryDesktopId === "" - } - } - - DankButton { - id: browseButton - text: I18n.tr("Browse") - iconName: "search" - onClicked: appBrowserPopup.show() - } - } - - DankTextField { - outlined: true - leftIconName: "terminal" - labelText: I18n.tr("Command") - supportingText: I18n.tr("Wrap the app command. %command% is replaced with the actual executable", "autostart command field hint, keep %command% verbatim") - width: parent.width - placeholderText: "%command%" - text: root.newEntryCommandWrapper - onTextChanged: root.newEntryCommandWrapper = text - } + placeholderText: "%command%" + text: root.newEntryCommandWrapper + onTextChanged: root.newEntryCommandWrapper = text } } @@ -590,117 +516,46 @@ Item { } } - SettingsRow { - body: Column { - id: entriesList - width: parent.width - spacing: Theme.spacingS + Repeater { + model: root.entries - Repeater { - model: root.entries + delegate: SettingsRow { + id: entryRow - delegate: Rectangle { - width: entriesList.width - height: 48 - radius: Theme.cornerRadius - color: Theme.floatingWindowFieldColor - border.width: 0 + required property var modelData - Row { - anchors.left: parent.left - anchors.right: entryToggle.left - anchors.leftMargin: Theme.spacingM - anchors.rightMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingM - - StyledText { - text: (index + 1).toString() - font.pixelSize: Theme.fontSizeSmall - font.weight: Theme.fontWeightMedium - color: Theme.primary - width: 20 - anchors.verticalCenter: parent.verticalCenter - } - - Image { - width: 24 - height: 24 - source: Paths.resolveIconUrl(modelData.icon || "application-x-executable") - sourceSize.width: 24 - sourceSize.height: 24 - fillMode: Image.PreserveAspectFit - anchors.verticalCenter: parent.verticalCenter - onStatusChanged: { - if (status === Image.Error) - source = "image://icon/application-x-executable"; - } - } - - Column { - width: parent.width - 20 - 24 - Theme.spacingM * 2 - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingXXS - - StyledText { - width: parent.width - text: modelData.name - font.pixelSize: Theme.fontSizeMedium - font.weight: Theme.fontWeightMedium - color: modelData.hidden ? Theme.surfaceVariantText : Theme.surfaceText - maximumLineCount: 1 - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - opacity: modelData.hidden ? 0.6 : 1.0 - } - - StyledText { - width: parent.width - text: modelData.hidden ? I18n.tr("Disabled") : modelData.exec - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - maximumLineCount: 1 - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - } - } - } - - DankToggle { - id: entryToggle - anchors.right: entryRemoveButton.left - anchors.rightMargin: Theme.spacingS - anchors.verticalCenter: parent.verticalCenter - checked: !modelData.hidden - onToggled: checked => root.setHidden(modelData, !checked) - } - - DankActionButton { - id: entryRemoveButton - anchors.right: parent.right - anchors.rightMargin: Theme.spacingS - anchors.verticalCenter: parent.verticalCenter - iconName: "close" - Accessible.name: I18n.tr("Remove") - iconSize: 16 - buttonSize: 32 - circular: true - iconColor: Theme.error - onClicked: root.removeEntry(modelData.filePath) - } - } + title: modelData.name + titleColor: modelData.hidden ? Theme.surfaceVariantText : Theme.surfaceText + subtitle: modelData.hidden ? I18n.tr("Disabled") : modelData.exec + singleLineTitle: true + leading: AppIconRenderer { + width: Theme.iconSize + height: Theme.iconSize + iconValue: entryRow.modelData.icon || "application-x-executable" + iconSize: Theme.iconSize + fallbackText: (entryRow.modelData.name || "?").charAt(0).toUpperCase() } - StyledText { - width: parent.width - text: I18n.tr("No autostart entries") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - horizontalAlignment: Text.AlignHCenter - visible: root.entries.length === 0 + DankToggle { + hideText: true + text: entryRow.title + checked: !entryRow.modelData.hidden + onToggled: checked => root.setHidden(entryRow.modelData, !checked) + } + + DankActionButton { + iconName: "delete" + iconColor: Theme.error + tooltipText: I18n.tr("Remove") + onClicked: root.removeEntry(entryRow.modelData.filePath) } } } + + SettingsRow { + visible: root.entries.length === 0 + subtitle: I18n.tr("No autostart entries") + } } SettingsCard { diff --git a/quickshell/Modules/Settings/BarHubHeader.qml b/quickshell/Modules/Settings/BarHubHeader.qml index 8b7657b64..bd9be32ed 100644 --- a/quickshell/Modules/Settings/BarHubHeader.qml +++ b/quickshell/Modules/Settings/BarHubHeader.qml @@ -10,7 +10,6 @@ Column { id: root property var parentModal: null - property string confirmingRemoveId: "" property string editingBarId: "" property string renameDraft: "" readonly property bool dotEnabled: SettingsData.dotBarConfig?.enabled ?? false @@ -19,10 +18,7 @@ Column { BarSelectionState { id: bar - onSelectedBarIdChanged: { - root.editingBarId = ""; - root.confirmingRemoveId = ""; - } + onSelectedBarIdChanged: root.editingBarId = "" } function finishRename(value) { @@ -65,11 +61,6 @@ Column { } function deleteBar(barId) { - if (confirmingRemoveId !== barId) { - confirmingRemoveId = barId; - return; - } - confirmingRemoveId = ""; SettingsData.deleteBarConfig(barId); bar.select("default"); } @@ -128,7 +119,6 @@ Column { checked: modelData.enabled ?? false toggleVisible: root.canToggleBar(modelData) deletable: root.canDeleteBar(modelData) - confirmingDelete: root.confirmingRemoveId === modelData.id onClicked: bar.select(modelData.id) onToggled: checked => { bar.select(modelData.id); @@ -205,46 +195,21 @@ Column { iconName: modelData.icon title: modelData.text hint: modelData.hint ?? "" - onClicked: root.parentModal?.navigateTo(modelData.id) + onClicked: keyboard => root.parentModal?.navigateTo(modelData.id, keyboard) } } } - SettingsRow { - id: dotRow - + SettingsSplitRow { settingKey: "dotEnabled" tags: ["dot", "dankdot", "companion", "floating", "island", "enable"] iconName: "blur_on" iconColor: root.dotEnabled ? Theme.primary : Theme.onSurfaceVariant title: I18n.tr("Dot", "bar layout: free-floating dot that opens island activities") subtitle: I18n.tr("A floating companion that works alongside any bar layout", "bar settings: what the dot is") - clickable: root.dotEnabled - onClicked: root.parentModal?.navigateTo("dankbar_dot") - - DankIcon { - name: "chevron_right" - size: Theme.iconSize - color: Theme.onSurfaceVariant - rotation: I18n.isRtl ? 180 : 0 - visible: root.dotEnabled - anchors.verticalCenter: parent.verticalCenter - } - - Rectangle { - width: Theme.dividerWidth - height: SettingsMetrics.splitDividerHeight - color: Theme.outlineVariant - visible: root.dotEnabled - anchors.verticalCenter: parent.verticalCenter - } - - DankToggle { - hideText: true - text: dotRow.title - checked: root.dotEnabled - anchors.verticalCenter: parent.verticalCenter - onToggled: value => SettingsData.setDotEnabled(value, bar.selectedBarId) - } + navigable: root.dotEnabled + checked: root.dotEnabled + onNavigated: keyboard => root.parentModal?.navigateTo("dankbar_dot", keyboard) + onToggled: value => SettingsData.setDotEnabled(value, bar.selectedBarId) } } diff --git a/quickshell/Modules/Settings/BarWidgetOptions/ClockOptions.qml b/quickshell/Modules/Settings/BarWidgetOptions/ClockOptions.qml index 91105c1ee..bfacc6961 100644 --- a/quickshell/Modules/Settings/BarWidgetOptions/ClockOptions.qml +++ b/quickshell/Modules/Settings/BarWidgetOptions/ClockOptions.qml @@ -47,7 +47,7 @@ Column { iconName: "schedule" title: I18n.tr("Time & weather") hint: I18n.tr("Clock format, calendar, weather location") - onClicked: root.page.parentModal?.navigateTo("time_weather") + onClicked: keyboard => root.page.parentModal?.navigateTo("time_weather", keyboard) } } } diff --git a/quickshell/Modules/Settings/BarWidgetOptions/MusicOptions.qml b/quickshell/Modules/Settings/BarWidgetOptions/MusicOptions.qml index 4778497fe..c48527fe7 100644 --- a/quickshell/Modules/Settings/BarWidgetOptions/MusicOptions.qml +++ b/quickshell/Modules/Settings/BarWidgetOptions/MusicOptions.qml @@ -57,7 +57,7 @@ Column { iconName: "music_note" title: I18n.tr("Media player") hint: I18n.tr("Visualizer, album art, excluded players") - onClicked: root.page.parentModal?.navigateTo("media_player") + onClicked: keyboard => root.page.parentModal?.navigateTo("media_player", keyboard) } } } diff --git a/quickshell/Modules/Settings/BarWidgetOptions/SystemUpdateOptions.qml b/quickshell/Modules/Settings/BarWidgetOptions/SystemUpdateOptions.qml index bff38ba34..50d0e2678 100644 --- a/quickshell/Modules/Settings/BarWidgetOptions/SystemUpdateOptions.qml +++ b/quickshell/Modules/Settings/BarWidgetOptions/SystemUpdateOptions.qml @@ -25,7 +25,7 @@ Column { iconName: "system_update_alt" title: I18n.tr("Software updates") hint: I18n.tr("DMS and system updates") - onClicked: root.page.parentModal?.navigateTo("updater") + onClicked: keyboard => root.page.parentModal?.navigateTo("updater", keyboard) } } } diff --git a/quickshell/Modules/Settings/BarWidgetTab.qml b/quickshell/Modules/Settings/BarWidgetTab.qml index e3fa97edd..640208d09 100644 --- a/quickshell/Modules/Settings/BarWidgetTab.qml +++ b/quickshell/Modules/Settings/BarWidgetTab.qml @@ -132,7 +132,7 @@ Item { SettingsNavRow { title: I18n.tr("Weather") iconName: "partly_cloudy_day" - onClicked: root.parentModal?.navigateTo("weather") + onClicked: keyboard => root.parentModal?.navigateTo("weather", keyboard) } } diff --git a/quickshell/Modules/Settings/ColorDropdownRow.qml b/quickshell/Modules/Settings/ColorDropdownRow.qml index 287689bb1..62723139f 100644 --- a/quickshell/Modules/Settings/ColorDropdownRow.qml +++ b/quickshell/Modules/Settings/ColorDropdownRow.qml @@ -130,6 +130,17 @@ Column { height: Theme.listItemHeight radius: Theme.cornerRadiusM color: Theme.chipSurface + activeFocusOnTab: root.currentMode === "custom" + Accessible.role: Accessible.Button + Accessible.name: I18n.tr("Custom color") + Accessible.onPressAction: root.openCustomColorPicker() + Keys.onSpacePressed: root.openCustomColorPicker() + Keys.onReturnPressed: root.openCustomColorPicker() + Keys.onEnterPressed: root.openCustomColorPicker() + + FocusRing { + id: chipRing + } Row { anchors.fill: parent @@ -187,7 +198,12 @@ Column { StateLayer { stateColor: Theme.surfaceText cornerRadius: customChip.radius - onClicked: root.openCustomColorPicker() + focused: chipRing.visible + onClicked: { + chipRing.pointerFocused = true; + customChip.forceActiveFocus(Qt.MouseFocusReason); + root.openCustomColorPicker(); + } } } } diff --git a/quickshell/Modules/Settings/DankDashTab.qml b/quickshell/Modules/Settings/DankDashTab.qml index 5fdbeb015..a8ed6d4b3 100644 --- a/quickshell/Modules/Settings/DankDashTab.qml +++ b/quickshell/Modules/Settings/DankDashTab.qml @@ -27,7 +27,7 @@ FocusScope { settingKey: "dashWeatherSettings" title: I18n.tr("Weather") iconName: "partly_cloudy_day" - onClicked: root.parentModal?.navigateTo("weather") + onClicked: keyboard => root.parentModal?.navigateTo("weather", keyboard) } } @@ -40,7 +40,7 @@ FocusScope { title: I18n.tr("Media player") hint: I18n.tr("Lyrics providers", "Lyrics source priority settings") iconName: "music_note" - onClicked: root.parentModal?.navigateTo("media_player") + onClicked: keyboard => root.parentModal?.navigateTo("media_player", keyboard) } } diff --git a/quickshell/Modules/Settings/DesktopWidgetInstanceCard.qml b/quickshell/Modules/Settings/DesktopWidgetInstanceCard.qml index 3eb3f51a4..6e5223365 100644 --- a/quickshell/Modules/Settings/DesktopWidgetInstanceCard.qml +++ b/quickshell/Modules/Settings/DesktopWidgetInstanceCard.qml @@ -1,7 +1,6 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Controls import qs.Common import qs.Services import qs.Widgets @@ -11,7 +10,6 @@ SettingsReorderRow { id: root required property var instanceData - property bool confirmingDelete: false readonly property string instanceId: instanceData?.id ?? "" readonly property string widgetType: instanceData?.widgetType ?? "" @@ -24,6 +22,8 @@ SettingsReorderRow { iconName: widgetDef?.icon ?? "widgets" title: widgetName + subtitle: deleteButton.confirming ? I18n.tr("Confirm Delete") : "" + subtitleColor: Theme.error clickable: true onClicked: configureRequested() @@ -35,16 +35,14 @@ SettingsReorderRow { color: Theme.onSurfaceVariant rotation: I18n.isRtl ? 180 : 0 }, - Rectangle { - anchors.verticalCenter: parent.verticalCenter - width: Theme.dividerWidth - height: SettingsMetrics.splitDividerHeight - color: Theme.outlineVariant + SettingsDivider { + vertical: true }, DankToggle { anchors.verticalCenter: parent.verticalCenter hideText: true - checked: instanceData?.enabled ?? true + text: root.title + checked: root.instanceData?.enabled ?? true onToggled: isChecked => { SettingsData.updateDesktopWidgetInstance(root.instanceId, { enabled: isChecked @@ -52,119 +50,15 @@ SettingsReorderRow { } }, DankActionButton { - id: menuButton anchors.verticalCenter: parent.verticalCenter - iconName: "more_vert" - Accessible.name: I18n.tr("Options") - onClicked: { - if (actionsMenu.visible) { - actionsMenu.close(); - return; - } - actionsMenu.open(); - } - - Popup { - id: actionsMenu - x: -width + parent.width - y: parent.height + Theme.spacingXS - width: 160 - padding: Theme.spacingXS - modal: false - focus: true - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent - - onClosed: root.confirmingDelete = false - - background: Rectangle { - color: Theme.floatingWindowSurface - radius: Theme.windowRadius - border.color: Theme.outlineMedium - border.width: Theme.layerOutlineWidth - } - - contentItem: Column { - spacing: Theme.spacingXXS - - Rectangle { - width: parent.width - height: Theme.iconSizeLarge - radius: Theme.cornerRadius - color: duplicateArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0) - - Row { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingS - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingS - - DankIcon { - name: "content_copy" - size: Theme.iconSizeSmall - color: Theme.surfaceText - } - - StyledText { - text: I18n.tr("Duplicate", "verb, desktop widget menu action") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - } - } - - MouseArea { - id: duplicateArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - actionsMenu.close(); - root.duplicateRequested(); - } - } - } - - Rectangle { - width: parent.width - height: Theme.iconSizeLarge - radius: Theme.cornerRadius - color: deleteArea.containsMouse ? Theme.errorHover : Theme.withAlpha(Theme.errorHover, 0) - - Row { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingS - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingS - - DankIcon { - name: root.confirmingDelete ? "warning" : "delete" - size: Theme.iconSizeSmall - color: Theme.error - } - - StyledText { - text: root.confirmingDelete ? I18n.tr("Confirm Delete") : I18n.tr("Delete") - font.pixelSize: Theme.fontSizeSmall - color: Theme.error - } - } - - MouseArea { - id: deleteArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (root.confirmingDelete) { - actionsMenu.close(); - root.deleteRequested(); - return; - } - root.confirmingDelete = true; - } - } - } - } - } + iconName: "content_copy" + tooltipText: I18n.tr("Duplicate", "verb, desktop widget menu action") + onClicked: root.duplicateRequested() + }, + SettingsDeleteButton { + id: deleteButton + anchors.verticalCenter: parent.verticalCenter + onDeleteRequested: root.deleteRequested() } ] } diff --git a/quickshell/Modules/Settings/DesktopWidgetsTab.qml b/quickshell/Modules/Settings/DesktopWidgetsTab.qml index a25377bf9..3585ae549 100644 --- a/quickshell/Modules/Settings/DesktopWidgetsTab.qml +++ b/quickshell/Modules/Settings/DesktopWidgetsTab.qml @@ -15,8 +15,8 @@ Item { property var groupCollapsedStates: ({}) property var parentModal: null - property string editingGroupId: "" property string newGroupName: "" + property string renamingGroupId: "" readonly property var allInstances: SettingsData.desktopWidgetInstances || [] readonly property var allGroups: SettingsData.desktopWidgetGroups || [] @@ -48,6 +48,18 @@ Item { parentModal?.navigateTo("desktop_widget"); } + function openRenameDialog(group) { + renamingGroupId = group.id; + renameDialog.show(group.name); + } + + function saveGroupName(name) { + SettingsData.updateDesktopWidgetGroup(renamingGroupId, { + name: name + }); + renameDialog.hide(); + } + function showWidgetBrowser() { widgetBrowserLoader.active = true; if (widgetBrowserLoader.item) @@ -95,140 +107,66 @@ Item { expanded: root.allGroups.length > 0 SettingsRow { - body: Column { + body: Row { + spacing: Theme.spacingS width: parent.width - spacing: Theme.spacingM - Row { - spacing: Theme.spacingS - width: parent.width - - DankTextField { - id: newGroupField - outlined: true - leftIconName: "folder" - labelText: I18n.tr("Name") - width: parent.width - addGroupBtn.width - Theme.spacingS - text: root.newGroupName - onTextChanged: root.newGroupName = text - onAccepted: { - if (!text.trim()) - return; - SettingsData.createDesktopWidgetGroup(text.trim()); - root.newGroupName = ""; - text = ""; - } - } - - DankButton { - id: addGroupBtn - iconName: "add" - text: I18n.tr("Add") - enabled: root.newGroupName.trim().length > 0 - onClicked: { - SettingsData.createDesktopWidgetGroup(root.newGroupName.trim()); - root.newGroupName = ""; - newGroupField.text = ""; - } + DankTextField { + id: newGroupField + outlined: true + leftIconName: "folder" + labelText: I18n.tr("Name") + width: parent.width - addGroupBtn.width - Theme.spacingS + text: root.newGroupName + onTextChanged: root.newGroupName = text + onAccepted: { + if (!text.trim()) + return; + SettingsData.createDesktopWidgetGroup(text.trim()); + root.newGroupName = ""; + text = ""; } } - Column { - width: parent.width - spacing: Theme.spacingXS - visible: root.allGroups.length > 0 + DankButton { + id: addGroupBtn + iconName: "add" + text: I18n.tr("Add") + enabled: root.newGroupName.trim().length > 0 + onClicked: { + SettingsData.createDesktopWidgetGroup(root.newGroupName.trim()); + root.newGroupName = ""; + newGroupField.text = ""; + } + } + } + } - Repeater { - model: root.allGroups + Repeater { + model: root.allGroups - Rectangle { - id: groupItem - required property var modelData - required property int index + delegate: SettingsRow { + id: groupRow - width: parent.width - height: Math.max(Theme.iconButtonSize, groupNameLoader.height + Theme.spacingS) - radius: Theme.cornerRadius - color: groupMouseArea.containsMouse ? Theme.surfaceHover : Theme.floatingWindowFieldColor + required property var modelData - Row { - anchors.fill: parent - anchors.leftMargin: Theme.spacingS - anchors.rightMargin: Theme.spacingS - spacing: Theme.spacingS + iconName: "folder" + title: modelData.name + singleLineTitle: true - DankIcon { - name: "folder" - visible: !groupNameLoader.active - size: Theme.iconSizeSmall - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } + DankActionButton { + iconName: "edit" + tooltipText: I18n.tr("Rename") + onClicked: root.openRenameDialog(groupRow.modelData) + } - Loader { - id: groupNameLoader - active: root.editingGroupId === groupItem.modelData.id - width: active ? parent.width - deleteGroupBtn.width - Theme.spacingS : 0 - height: active && item ? item.implicitHeight : 0 - anchors.verticalCenter: parent.verticalCenter - - sourceComponent: DankTextField { - outlined: true - leftIconName: "folder" - labelText: I18n.tr("Name") - text: groupItem.modelData.name - onAccepted: { - if (!text.trim()) - return; - SettingsData.updateDesktopWidgetGroup(groupItem.modelData.id, { - name: text.trim() - }); - root.editingGroupId = ""; - } - onEditingFinished: { - if (!text.trim()) - return; - SettingsData.updateDesktopWidgetGroup(groupItem.modelData.id, { - name: text.trim() - }); - root.editingGroupId = ""; - } - Component.onCompleted: forceActiveFocus() - } - } - - StyledText { - visible: root.editingGroupId !== groupItem.modelData.id - text: groupItem.modelData.name - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - elide: Text.ElideRight - width: parent.width - Theme.iconSizeSmall - deleteGroupBtn.width - Theme.spacingS * 3 - } - - DankActionButton { - id: deleteGroupBtn - iconName: "delete" - Accessible.name: I18n.tr("Delete") - backgroundColor: Theme.withAlpha(Theme.error, 0.15) - iconColor: Theme.error - anchors.verticalCenter: parent.verticalCenter - onClicked: { - SettingsData.removeDesktopWidgetGroup(groupItem.modelData.id); - ToastService.showInfo(I18n.tr("Group removed")); - } - } - } - - MouseArea { - id: groupMouseArea - anchors.fill: parent - z: -1 - hoverEnabled: true - onDoubleClicked: root.editingGroupId = groupItem.modelData.id - } - } + DankActionButton { + iconName: "delete" + iconColor: Theme.error + tooltipText: I18n.tr("Delete") + onClicked: { + SettingsData.removeDesktopWidgetGroup(groupRow.modelData.id); + ToastService.showInfo(I18n.tr("Group removed")); } } } @@ -347,4 +285,12 @@ Item { SettingsReorderPreview { group: dragGroup } + + SettingsRenameDialog { + id: renameDialog + parent: root.parentModal?.modalFocusScope ?? root + supportingText: root.allGroups.find(g => g.id === root.renamingGroupId)?.name ?? "" + leftIconName: "folder" + onAccepted: name => root.saveGroupName(name) + } } diff --git a/quickshell/Modules/Settings/DisplayConfig/MonitorIdentifyOverlay.qml b/quickshell/Modules/Settings/DisplayConfig/MonitorIdentifyOverlay.qml index fb4ac3192..eb33a1a91 100644 --- a/quickshell/Modules/Settings/DisplayConfig/MonitorIdentifyOverlay.qml +++ b/quickshell/Modules/Settings/DisplayConfig/MonitorIdentifyOverlay.qml @@ -40,8 +40,9 @@ Variants { Behavior on opacity { NumberAnimation { - duration: Theme.mediumDuration - easing.type: Theme.emphasizedEasing + duration: Theme.expressiveDurations.expressiveEffects + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.expressiveEffects } } diff --git a/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml b/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml index 3ee2d03d7..31c575dba 100644 --- a/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml +++ b/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml @@ -156,6 +156,7 @@ Column { DankDropdown { id: scaleDropdown + downKeyOpens: false visible: !scaleRow.customMode width: Math.min(dropdownWidth, scaleRow.width - SettingsMetrics.rowPaddingH * 2) Accessible.name: scaleRow.title diff --git a/quickshell/Modules/Settings/DisplayConfigTab.qml b/quickshell/Modules/Settings/DisplayConfigTab.qml index d34ffff4f..76c98c8df 100644 --- a/quickshell/Modules/Settings/DisplayConfigTab.qml +++ b/quickshell/Modules/Settings/DisplayConfigTab.qml @@ -140,6 +140,7 @@ Item { DankDropdown { id: profileDropdown + downKeyOpens: false width: parent.width - newButton.width - editMonitorsButton.width - deleteButton.width - Theme.spacingS * 3 compactMode: true dropdownWidth: width diff --git a/quickshell/Modules/Settings/DockHubHeader.qml b/quickshell/Modules/Settings/DockHubHeader.qml index 0c6cd97ab..85f9bd6f4 100644 --- a/quickshell/Modules/Settings/DockHubHeader.qml +++ b/quickshell/Modules/Settings/DockHubHeader.qml @@ -10,7 +10,6 @@ Column { id: root property var parentModal: null - property string confirmingRemoveId: "" property string editingDockId: "" property string renameDraft: "" @@ -26,18 +25,12 @@ Column { } function addDock() { - confirmingRemoveId = ""; dock.selectedDockId = SettingsData.createDockConfig(); if (!dock.config?.enabled) ToastService.showWarning(I18n.tr("Every edge of this display is already taken")); } function removeDock(id) { - if (confirmingRemoveId !== id) { - confirmingRemoveId = id; - return; - } - confirmingRemoveId = ""; SettingsData.removeDockConfig(id); } @@ -47,10 +40,7 @@ Column { DockSelectionState { id: dock - onSelectedDockIdChanged: { - root.editingDockId = ""; - root.confirmingRemoveId = ""; - } + onSelectedDockIdChanged: root.editingDockId = "" } SettingsCard { @@ -77,7 +67,6 @@ Column { selected: dock.selectedDockId === modelData.id checked: modelData.enabled deletable: SettingsData.dockConfigs.length > 1 - confirmingDelete: root.confirmingRemoveId === modelData.id onClicked: dock.selectedDockId = modelData.id onToggled: checked => { dock.selectedDockId = modelData.id; diff --git a/quickshell/Modules/Settings/DockWidgetsTab.qml b/quickshell/Modules/Settings/DockWidgetsTab.qml index 34e93ef90..3576ec732 100644 --- a/quickshell/Modules/Settings/DockWidgetsTab.qml +++ b/quickshell/Modules/Settings/DockWidgetsTab.qml @@ -11,7 +11,6 @@ Item { property var parentModal: null property int catalogRevision: 0 - property string confirmingRemoveId: "" readonly property bool dockHosted: true readonly property var appsEntry: dock.config?.widgets.find(item => item.widgetId === "appsDock") ?? null @@ -61,8 +60,6 @@ Item { DockSelectionState { id: dock - - onSelectedDockIdChanged: root.confirmingRemoveId = "" } Connections { @@ -126,7 +123,6 @@ Item { } function updateWidgets(widgets) { - root.confirmingRemoveId = ""; if (!dock.hasConfig) return; SettingsData.updateDockConfig(dock.selectedDockId, { @@ -173,13 +169,12 @@ Item { id: widgetRow required property var modelData - readonly property bool confirmingRemove: root.confirmingRemoveId === modelData.id readonly property string slotOption: root.slotOptions[modelData.widgetId] ?? "" readonly property bool shown: slotOption ? dock.config[slotOption] === true : modelData.enabled !== false title: root.widgetName(modelData.widgetId) iconName: root.widgetIcon(modelData.widgetId) - subtitle: confirmingRemove ? I18n.tr("Confirm Delete") : "" + subtitle: removeButton.confirming ? I18n.tr("Confirm Delete") : "" subtitleColor: Theme.error body: SettingsSliderRow { width: parent.width @@ -218,18 +213,10 @@ Item { } } - DankActionButton { + SettingsDeleteButton { + id: removeButton visible: !widgetRow.slotOption - iconName: widgetRow.confirmingRemove ? "warning" : "delete" - iconColor: widgetRow.confirmingRemove ? Theme.error : Theme.surfaceVariantText - Accessible.name: widgetRow.confirmingRemove ? I18n.tr("Confirm Delete") : I18n.tr("Remove") - onClicked: { - if (!widgetRow.confirmingRemove) { - root.confirmingRemoveId = widgetRow.modelData.id; - return; - } - root.updateWidgets(dock.config.widgets.filter(item => item.id !== widgetRow.modelData.id)); - } + onDeleteRequested: root.updateWidgets(dock.config.widgets.filter(item => item.id !== widgetRow.modelData.id)) } } } diff --git a/quickshell/Modules/Settings/GammaControlTab.qml b/quickshell/Modules/Settings/GammaControlTab.qml index af43230ce..92def50d4 100644 --- a/quickshell/Modules/Settings/GammaControlTab.qml +++ b/quickshell/Modules/Settings/GammaControlTab.qml @@ -19,9 +19,9 @@ Item { width: (parent.width - Theme.spacingM) / 2 height: tileColumn.implicitHeight + Theme.spacingM * 2 radius: Theme.cornerRadius - color: Theme.floatingWindowNestedSurface - border.color: Theme.outlineMedium - border.width: Theme.layerOutlineWidth + color: Theme.floatingWindowFieldColor + border.color: Theme.floatingWindowFieldBorderColor + border.width: Theme.outlineWidth Column { id: tileColumn diff --git a/quickshell/Modules/Settings/GreeterTab.qml b/quickshell/Modules/Settings/GreeterTab.qml index 9f78a2d0e..9e2598a9c 100644 --- a/quickshell/Modules/Settings/GreeterTab.qml +++ b/quickshell/Modules/Settings/GreeterTab.qml @@ -216,31 +216,13 @@ Item { } } - SettingsRow { + SettingsNoteRow { visible: root.greeterStatusOutput !== "" - - body: Rectangle { - width: parent.width - height: statusTextArea.implicitHeight + Theme.spacingM * 2 - radius: Theme.cornerRadius - color: Theme.floatingWindowFieldColor - border.color: Theme.outlineMedium - border.width: Theme.layerOutlineWidth - - StyledText { - id: statusTextArea - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Theme.spacingM - text: root.greeterStatusOutput - font.pixelSize: Theme.fontSizeSmall - isMonospace: true - color: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignLeft - } - } + noteIconName: "" + monospace: true + text: root.greeterStatusOutput + tint: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText + tintBackground: Theme.floatingWindowFieldColor } } @@ -250,7 +232,7 @@ Item { tags: ["greeter", "login", "authentication", "pam", "fingerprint", "security", "key"] title: I18n.tr("Authentication") iconName: "fingerprint" - onClicked: root.parentModal?.navigateTo("greeter_auth") + onClicked: keyboard => root.parentModal?.navigateTo("greeter_auth", keyboard) } } @@ -266,19 +248,19 @@ Item { SettingsNavRow { title: I18n.tr("Wallpaper & colors") iconName: "wallpaper" - onClicked: root.parentModal?.navigateTo("personalization") + onClicked: keyboard => root.parentModal?.navigateTo("personalization", keyboard) } SettingsNavRow { title: I18n.tr("Fonts & motion") iconName: "text_fields" - onClicked: root.parentModal?.navigateTo("typography") + onClicked: keyboard => root.parentModal?.navigateTo("typography", keyboard) } SettingsNavRow { title: I18n.tr("Lock screen") iconName: "lock" - onClicked: root.parentModal?.navigateTo("lock_screen") + onClicked: keyboard => root.parentModal?.navigateTo("lock_screen", keyboard) } } diff --git a/quickshell/Modules/Settings/KeybindEditorDialog.qml b/quickshell/Modules/Settings/KeybindEditorDialog.qml index ba269e28d..c1f99d973 100644 --- a/quickshell/Modules/Settings/KeybindEditorDialog.qml +++ b/quickshell/Modules/Settings/KeybindEditorDialog.qml @@ -901,6 +901,7 @@ DankDialog { SettingsRow { body: DankButtonGroup { + arrowKeysSelect: false width: parent.width fillWidth: true maximumWidth: width diff --git a/quickshell/Modules/Settings/KeybindsTab.qml b/quickshell/Modules/Settings/KeybindsTab.qml index 013e77a97..1fc178c3d 100644 --- a/quickshell/Modules/Settings/KeybindsTab.qml +++ b/quickshell/Modules/Settings/KeybindsTab.qml @@ -587,6 +587,7 @@ Item { DankListView { id: flickable + keyNavigationEnabled: false readonly property real columnWidth: Math.min(SettingsMetrics.contentMaxWidth, width - Theme.spacingL * 2) property Item fabBar: null diff --git a/quickshell/Modules/Settings/LockScreenTab.qml b/quickshell/Modules/Settings/LockScreenTab.qml index 1dbafd0be..54a44fdb7 100644 --- a/quickshell/Modules/Settings/LockScreenTab.qml +++ b/quickshell/Modules/Settings/LockScreenTab.qml @@ -355,7 +355,7 @@ Item { title: I18n.tr("Weather") checked: SettingsData.lockScreenShowWeather onToggled: checked => SettingsData.set("lockScreenShowWeather", checked) - onNavigated: root.parentModal?.navigateTo("weather") + onNavigated: keyboard => root.parentModal?.navigateTo("weather", keyboard) } SettingsDropdownRow { @@ -605,7 +605,6 @@ Item { id: securityKeyCapture width: 200 anchors.verticalCenter: parent.verticalCenter - focus: capturing text: capturing ? I18n.tr("Press key...", "lock screen security key shortcut key combination capture prompt") : SettingsData.lockScreenSecurityKeyShortcut backgroundColor: capturing ? Theme.selectedContainer : Theme.chipSurface textColor: Theme.surfaceText @@ -631,8 +630,14 @@ Item { startCapture(); } - Keys.onPressed: event => { - if (!securityKeyCapture.capturing) + Keys.onPressed: event => captureKey(event) + // Specific Tab handlers stop Qt focus traversal while capturing. + Keys.onTabPressed: event => captureKey(event) + Keys.onBacktabPressed: event => captureKey(event) + + function captureKey(event) { + event.accepted = capturing; + if (!capturing) return; if (KeyUtils.isModifierKey(event.key)) @@ -640,7 +645,6 @@ Item { if (event.key === Qt.Key_Escape) { securityKeyCapture.stopCapture(); - event.accepted = true; return; } @@ -798,28 +802,14 @@ Item { } } - SettingsRow { + SettingsNoteRow { visible: root.authValidateMessage !== "" - body: Rectangle { - width: parent.width - height: Math.min(160, authStatusText.implicitHeight + Theme.spacingM * 2) - radius: Theme.cornerRadius - color: Theme.floatingWindowFieldColor - border.color: Theme.outlineMedium - border.width: Theme.layerOutlineWidth - - StyledText { - id: authStatusText - anchors.fill: parent - anchors.margins: Theme.spacingM - text: root.authValidateMessage - font.pixelSize: Theme.fontSizeSmall - font.family: "monospace" - color: !root.authValidateOk ? Theme.error : (root.authValidateWarn ? Theme.warning : Theme.surfaceVariantText) - wrapMode: Text.Wrap - verticalAlignment: Text.AlignTop - } - } + noteIconName: "" + monospace: true + maxHeight: SettingsMetrics.noteMaxHeight + text: root.authValidateMessage + tint: !root.authValidateOk ? Theme.error : (root.authValidateWarn ? Theme.warning : Theme.surfaceVariantText) + tintBackground: Theme.floatingWindowFieldColor } SettingsDropdownRow { @@ -866,28 +856,14 @@ Item { } } - SettingsRow { + SettingsNoteRow { visible: !root.lockU2fControlledByPrimary && root.u2fValidateMessage !== "" - body: Rectangle { - width: parent.width - height: Math.min(160, u2fStatusText.implicitHeight + Theme.spacingM * 2) - radius: Theme.cornerRadius - color: Theme.floatingWindowFieldColor - border.color: Theme.outlineMedium - border.width: Theme.layerOutlineWidth - - StyledText { - id: u2fStatusText - anchors.fill: parent - anchors.margins: Theme.spacingM - text: root.u2fValidateMessage - font.pixelSize: Theme.fontSizeSmall - font.family: "monospace" - color: !root.u2fValidateOk ? Theme.error : (root.u2fValidateWarn ? Theme.warning : Theme.surfaceVariantText) - wrapMode: Text.Wrap - verticalAlignment: Text.AlignTop - } - } + noteIconName: "" + monospace: true + maxHeight: SettingsMetrics.noteMaxHeight + text: root.u2fValidateMessage + tint: !root.u2fValidateOk ? Theme.error : (root.u2fValidateWarn ? Theme.warning : Theme.surfaceVariantText) + tintBackground: Theme.floatingWindowFieldColor } SettingsRow { diff --git a/quickshell/Modules/Settings/MediaPlayerTab.qml b/quickshell/Modules/Settings/MediaPlayerTab.qml index 059f05af5..3c0be3e15 100644 --- a/quickshell/Modules/Settings/MediaPlayerTab.qml +++ b/quickshell/Modules/Settings/MediaPlayerTab.qml @@ -88,8 +88,8 @@ Item { title: I18n.tr("Dashboard") hint: I18n.tr("Media") iconName: "dashboard" - onClicked: { - root.parentModal?.navigateTo("dank_dash"); + onClicked: keyboard => { + root.parentModal?.navigateTo("dank_dash", keyboard); SettingsSearchService.navigateToSection("dashOptions:media"); } } diff --git a/quickshell/Modules/Settings/NetworkCellularTab.qml b/quickshell/Modules/Settings/NetworkCellularTab.qml index a6520aa76..06c83f52b 100644 --- a/quickshell/Modules/Settings/NetworkCellularTab.qml +++ b/quickshell/Modules/Settings/NetworkCellularTab.qml @@ -263,8 +263,7 @@ Item { anchors.verticalCenter: parent.verticalCenter iconName: profileDelegate.isActive ? "link_off" : "link" tooltipText: profileDelegate.isActive ? I18n.tr("Disconnect") : I18n.tr("Connect") - buttonSize: 28 - iconSize: 18 + iconSize: Theme.iconSizeSmall iconColor: profileDelegate.isActive ? Theme.error : Theme.primary onClicked: { if (profileDelegate.isActive) diff --git a/quickshell/Modules/Settings/NetworkEthernetTab.qml b/quickshell/Modules/Settings/NetworkEthernetTab.qml index 3481889a3..df2a54dd2 100644 --- a/quickshell/Modules/Settings/NetworkEthernetTab.qml +++ b/quickshell/Modules/Settings/NetworkEthernetTab.qml @@ -261,37 +261,11 @@ Item { return fields; } - delegate: Rectangle { + delegate: DankDetailChip { required property var modelData - required property int index - width: ethFieldContent.width + Theme.spacingM * 2 - height: 32 - radius: Theme.cornerRadius - Theme.outlineWidthFocused - color: Theme.floatingWindowFieldColor - border.width: Theme.outlineWidth - border.color: Theme.floatingWindowFieldBorderColor - - Row { - id: ethFieldContent - anchors.centerIn: parent - spacing: Theme.spacingXS - - StyledText { - text: modelData.label + ":" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: modelData.value - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Theme.fontWeightMedium - anchors.verticalCenter: parent.verticalCenter - } - } + label: modelData.label + value: modelData.value } } } diff --git a/quickshell/Modules/Settings/NetworkStatusTab.qml b/quickshell/Modules/Settings/NetworkStatusTab.qml index d9ec5c612..48cc3b4df 100644 --- a/quickshell/Modules/Settings/NetworkStatusTab.qml +++ b/quickshell/Modules/Settings/NetworkStatusTab.qml @@ -137,6 +137,7 @@ Item { } DankButtonGroup { + arrowKeysSelect: false id: preferenceButtons readonly property var preferenceValues: { diff --git a/quickshell/Modules/Settings/NetworkWifiTab.qml b/quickshell/Modules/Settings/NetworkWifiTab.qml index c7abe9083..9196d9474 100644 --- a/quickshell/Modules/Settings/NetworkWifiTab.qml +++ b/quickshell/Modules/Settings/NetworkWifiTab.qml @@ -1,7 +1,6 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Controls import qs.Common import qs.Modules.Network import qs.Modules.Settings.Widgets @@ -25,6 +24,48 @@ Item { NetworkService.removeRef(); } + function wifiDetailFields(net) { + const fields = []; + if (!net || net.outOfRange) + return fields; + + fields.push({ + label: I18n.tr("Signal"), + value: (net.signal || 0) + "%" + }); + if (net.frequency) + fields.push({ + label: I18n.tr("Frequency", "wifi network detail label, radio frequency in GHz"), + value: (net.frequency / 1000).toFixed(1) + " GHz" + }); + if (net.channel) + fields.push({ + label: I18n.tr("Channel", "wifi network detail label, radio channel number"), + value: String(net.channel) + }); + if (net.rate) + fields.push({ + label: I18n.tr("Rate", "noun, wifi network detail label, link speed in Mbps"), + value: net.rate + " Mbps" + }); + if (net.mode) + fields.push({ + label: I18n.tr("Mode"), + value: net.mode + }); + if (net.bssid) + fields.push({ + label: "BSSID", + value: net.bssid + }); + fields.push({ + label: I18n.tr("Security", "noun, settings page name and wifi security type label"), + value: net.secured ? (net.enterprise ? I18n.tr("Enterprise", "wifi security type value, 802.1x enterprise network") : "WPA/WPA2") : I18n.tr("Open", "network security type", true) + }); + + return fields; + } + SettingsPage { id: mainColumn @@ -278,31 +319,8 @@ Item { width: parent.width spacing: Theme.spacingS - DankIcon { - id: scanningIcon - name: "wifi_find" - size: Theme.iconSizeLarge - color: Theme.surfaceVariantText + DankLoadingIndicator { anchors.horizontalCenter: parent.horizontalCenter - - SequentialAnimation { - running: networkWifiTab.visible && NetworkService.isScanning - loops: Animation.Infinite - OpacityAnimator { - target: scanningIcon - to: 0.3 - duration: 400 - easing.type: Easing.InOutQuad - } - OpacityAnimator { - target: scanningIcon - to: 1.0 - duration: 400 - easing.type: Easing.InOutQuad - } - onRunningChanged: if (!running) - scanningIcon.opacity = 1.0 - } } StyledText { @@ -380,7 +398,6 @@ Item { iconName: wifiNetworkDelegate.isExpanded ? "expand_less" : "expand_more" Accessible.name: wifiNetworkDelegate.isExpanded ? I18n.tr("Collapse") : I18n.tr("Expand") iconSize: Theme.iconSizeSmall - buttonSize: 28 visible: wifiNetworkDelegate.isConnected || wifiNetworkDelegate.modelData.saved onClicked: { if (wifiNetworkDelegate.isExpanded) { @@ -395,7 +412,6 @@ Item { DankActionButton { iconName: "qr_code" tooltipText: I18n.tr("Show QR Code") - buttonSize: 28 visible: wifiNetworkDelegate.modelData.secured && wifiNetworkDelegate.modelData.saved && !(wifiNetworkDelegate.modelData.enterprise || false) onClicked: { PopoutService.showWifiQRCodeModal(wifiNetworkDelegate.modelData.ssid); @@ -405,7 +421,6 @@ Item { DankActionButton { iconName: "push_pin" Accessible.name: wifiNetworkDelegate.isPinned ? I18n.tr("Unpin") : I18n.tr("Pin", "verb, keep an item pinned in place") - buttonSize: 28 iconColor: wifiNetworkDelegate.isPinned ? Theme.primary : Theme.surfaceVariantText onClicked: { root.toggleWifiPin(wifiNetworkDelegate.modelData.ssid); @@ -414,8 +429,7 @@ Item { DankActionButton { iconName: "delete" - tooltipText: I18n.tr("Forget") - buttonSize: 28 + tooltipText: I18n.tr("Forget", "verb, remove a saved wifi network, button") iconColor: Theme.error visible: wifiNetworkDelegate.modelData.saved || wifiNetworkDelegate.isConnected onClicked: { @@ -447,80 +461,13 @@ Item { visible: !NetworkService.networkInfoLoading Repeater { - model: { - const fields = []; - const net = wifiNetworkDelegate.modelData; - if (!net) - return fields; + model: wifiNetworkDelegate.isExpanded ? networkWifiTab.wifiDetailFields(wifiNetworkDelegate.modelData) : [] - fields.push({ - label: I18n.tr("Signal"), - value: net.signal + "%" - }); - if (net.frequency) - fields.push({ - label: I18n.tr("Frequency", "wifi network detail label, radio frequency in GHz"), - value: (net.frequency / 1000).toFixed(1) + " GHz" - }); - if (net.channel) - fields.push({ - label: I18n.tr("Channel", "wifi network detail label, radio channel number"), - value: String(net.channel) - }); - if (net.rate) - fields.push({ - label: I18n.tr("Rate", "noun, wifi network detail label, link speed in Mbps"), - value: net.rate + " Mbps" - }); - if (net.mode) - fields.push({ - label: I18n.tr("Mode"), - value: net.mode - }); - if (net.bssid) - fields.push({ - label: "BSSID", - value: net.bssid - }); - fields.push({ - label: I18n.tr("Security", "noun, settings page name and wifi security type label"), - value: net.secured ? (net.enterprise ? I18n.tr("Enterprise", "wifi security type value, 802.1x enterprise network") : "WPA/WPA2") : I18n.tr("Open", "network security type", true) - }); - - return fields; - } - - delegate: Rectangle { + delegate: DankDetailChip { required property var modelData - required property int index - width: wifiFieldContent.width + Theme.spacingM * 2 - height: Theme.buttonHeightXS - radius: Theme.cornerRadiusS - color: Theme.floatingWindowFieldColor - border.width: Theme.outlineWidth - border.color: Theme.floatingWindowFieldBorderColor - - Row { - id: wifiFieldContent - anchors.centerIn: parent - spacing: Theme.spacingXS - - StyledText { - text: modelData.label + ":" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: modelData.value - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Theme.fontWeightMedium - anchors.verticalCenter: parent.verticalCenter - } - } + label: modelData.label + value: modelData.value } } } @@ -574,7 +521,8 @@ Item { readonly property bool isConnecting: NetworkService.isWifiConnecting && NetworkService.connectingSSID === modelData.ssid readonly property bool isPinned: root.getPinnedWifiNetworks().includes(modelData.ssid) readonly property bool isOutOfRange: modelData.outOfRange || false - readonly property bool isExpanded: !isOutOfRange && root.expandedSavedWifiSsid === modelData.ssid + readonly property bool isExpanded: root.expandedSavedWifiSsid === modelData.ssid + readonly property var detailFields: networkWifiTab.wifiDetailFields(modelData) width: parent?.width ?? 0 spacing: Theme.groupedListGap @@ -595,13 +543,10 @@ Item { subtitleColor: savedWifiDelegate.isConnecting ? Theme.warning : supportingContentColor clickable: !savedWifiDelegate.isOutOfRange && (!NetworkService.isWifiConnecting || savedWifiDelegate.isConnected) onClicked: { - if (savedWifiDelegate.isOutOfRange) - return; - if (savedWifiDelegate.isExpanded) { - root.expandedSavedWifiSsid = ""; - } else { - root.expandedSavedWifiSsid = savedWifiDelegate.modelData.ssid; - } + WifiConnectionActions.connectToNetwork(savedWifiDelegate.modelData, { + connected: savedWifiDelegate.isConnected, + disconnectWhenConnected: true + }); } leading: [ @@ -635,8 +580,6 @@ Item { iconName: savedWifiDelegate.isExpanded ? "expand_less" : "expand_more" Accessible.name: savedWifiDelegate.isExpanded ? I18n.tr("Collapse") : I18n.tr("Expand") iconSize: Theme.iconSizeSmall - buttonSize: 28 - visible: !savedWifiDelegate.isOutOfRange onClicked: { if (savedWifiDelegate.isExpanded) { root.expandedSavedWifiSsid = ""; @@ -649,7 +592,6 @@ Item { DankActionButton { iconName: "qr_code" tooltipText: I18n.tr("Show QR Code") - buttonSize: 28 visible: savedWifiDelegate.modelData.secured && !(savedWifiDelegate.modelData.enterprise || false) onClicked: { PopoutService.showWifiQRCodeModal(savedWifiDelegate.modelData.ssid); @@ -659,7 +601,6 @@ Item { DankActionButton { iconName: "push_pin" Accessible.name: savedWifiDelegate.isPinned ? I18n.tr("Unpin") : I18n.tr("Pin", "verb, keep an item pinned in place") - buttonSize: 28 iconColor: savedWifiDelegate.isPinned ? Theme.primary : Theme.surfaceVariantText onClicked: { root.toggleWifiPin(savedWifiDelegate.modelData.ssid); @@ -667,188 +608,37 @@ Item { } DankActionButton { - id: savedWifiMoreButton - iconName: "more_horiz" - Accessible.name: I18n.tr("Options") - buttonSize: 28 - onClicked: { - if (savedWifiMenu.visible) { - savedWifiMenu.close(); - return; - } - savedWifiMenu.popup(savedWifiMoreButton, -savedWifiMenu.width + savedWifiMoreButton.width, savedWifiMoreButton.height + Theme.spacingXS); - } + iconName: "delete" + tooltipText: I18n.tr("Forget", "verb, remove a saved wifi network, button") + iconColor: Theme.error + onClicked: root.showForgetNetworkConfirm(savedWifiDelegate.modelData.ssid) } } SettingsRow { - visible: savedWifiDelegate.isExpanded + visible: savedWifiDelegate.isExpanded && savedWifiDelegate.detailFields.length > 0 body: Flow { width: parent.width spacing: Theme.spacingXS Repeater { - model: { - const fields = []; - const net = savedWifiDelegate.modelData; - if (!net) - return fields; + model: savedWifiDelegate.isExpanded ? savedWifiDelegate.detailFields : [] - fields.push({ - label: I18n.tr("Signal"), - value: (net.signal || 0) + "%" - }); - if (net.frequency) - fields.push({ - label: I18n.tr("Frequency"), - value: (net.frequency / 1000).toFixed(1) + " GHz" - }); - if (net.channel) - fields.push({ - label: I18n.tr("Channel"), - value: String(net.channel) - }); - if (net.rate) - fields.push({ - label: I18n.tr("Rate"), - value: net.rate + " Mbps" - }); - if (net.mode) - fields.push({ - label: I18n.tr("Mode"), - value: net.mode - }); - if (net.bssid) - fields.push({ - label: "BSSID", - value: net.bssid - }); - fields.push({ - label: I18n.tr("Security"), - value: net.secured ? (net.enterprise ? I18n.tr("Enterprise") : "WPA/WPA2") : I18n.tr("Open", "network security type", true) - }); - - return fields; - } - - delegate: Rectangle { + delegate: DankDetailChip { required property var modelData - required property int index - width: savedWifiFieldContent.width + Theme.spacingM * 2 - height: Theme.buttonHeightXS - radius: Theme.cornerRadiusS - color: Theme.floatingWindowFieldColor - border.width: Theme.outlineWidth - border.color: Theme.floatingWindowFieldBorderColor - - Row { - id: savedWifiFieldContent - anchors.centerIn: parent - spacing: Theme.spacingXS - - StyledText { - text: modelData.label + ":" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: modelData.value - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Theme.fontWeightMedium - anchors.verticalCenter: parent.verticalCenter - } - } + label: modelData.label + value: modelData.value } } } } - Menu { - id: savedWifiMenu - width: 170 - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent - - background: Rectangle { - color: Theme.floatingWindowSurface - radius: Theme.windowRadius - border.width: 0 - } - - MenuItem { - text: isConnecting ? I18n.tr("Connecting...") : (isConnected ? I18n.tr("Disconnect") : I18n.tr("Connect")) - height: isOutOfRange ? 0 : 32 - visible: !isOutOfRange - enabled: !isConnecting - - contentItem: StyledText { - text: parent.text - font.pixelSize: Theme.fontSizeSmall - color: parent.enabled ? Theme.surfaceText : Theme.surfaceVariantText - leftPadding: Theme.spacingS - verticalAlignment: Text.AlignVCenter - } - - background: Rectangle { - color: parent.hovered ? Theme.primaryHoverLight : Theme.withAlpha(Theme.primaryHoverLight, 0) - radius: Theme.cornerRadiusS - } - - onTriggered: { - WifiConnectionActions.connectToNetwork(modelData, { - connected: isConnected, - disconnectWhenConnected: true - }); - } - } - - MenuItem { - text: modelData.autoconnect ? I18n.tr("Disable autoconnect") : I18n.tr("Enable autoconnect") - height: DMSService.apiVersion > 13 ? 32 : 0 - visible: DMSService.apiVersion > 13 - - contentItem: StyledText { - text: parent.text - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - leftPadding: Theme.spacingS - verticalAlignment: Text.AlignVCenter - } - - background: Rectangle { - color: parent.hovered ? Theme.primaryHoverLight : Theme.withAlpha(Theme.primaryHoverLight, 0) - radius: Theme.cornerRadiusS - } - - onTriggered: { - NetworkService.setWifiAutoconnect(modelData.ssid, !(modelData.autoconnect || false)); - } - } - - MenuItem { - text: I18n.tr("Forget network") - height: Theme.buttonHeightXS - - contentItem: StyledText { - text: parent.text - font.pixelSize: Theme.fontSizeSmall - color: Theme.error - leftPadding: Theme.spacingS - verticalAlignment: Text.AlignVCenter - } - - background: Rectangle { - color: parent.hovered ? Theme.errorHover : Theme.withAlpha(Theme.errorHover, 0) - radius: Theme.cornerRadiusS - } - - onTriggered: { - root.showForgetNetworkConfirm(modelData.ssid); - } - } + SettingsToggleRow { + visible: savedWifiDelegate.isExpanded && DMSService.apiVersion > 13 + text: I18n.tr("Autoconnect", "toggle, connect to this wifi or vpn automatically") + checked: savedWifiDelegate.modelData.autoconnect || false + onToggled: checked => NetworkService.setWifiAutoconnect(savedWifiDelegate.modelData.ssid, checked) } } } diff --git a/quickshell/Modules/Settings/NotificationRulesTab.qml b/quickshell/Modules/Settings/NotificationRulesTab.qml index a712b103c..a26c95d2a 100644 --- a/quickshell/Modules/Settings/NotificationRulesTab.qml +++ b/quickshell/Modules/Settings/NotificationRulesTab.qml @@ -252,56 +252,23 @@ Item { tags: ["notification", "mute", "unmute", "popup"] SettingsRow { - body: Column { - width: parent.width - spacing: Theme.spacingS + subtitle: root.mutedRules.length > 0 ? I18n.tr("Apps with notification popups muted. Unmute or delete to remove.") : I18n.tr("No apps muted. Right-click a notification and choose \"Mute popups\" to add one here.") + } - StyledText { - text: mutedRules.length > 0 ? I18n.tr("Apps with notification popups muted. Unmute or delete to remove.") : I18n.tr("No apps muted. Right-click a notification and choose \"Mute popups\" to add one here.") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - wrapMode: Text.WordWrap - width: parent.width - bottomPadding: Theme.spacingS - } + Repeater { + model: root.mutedRules - Repeater { - model: mutedRules + delegate: SettingsRow { + required property var modelData - delegate: Rectangle { - width: parent.width - height: mutedRow.implicitHeight + Theme.spacingS * 2 - radius: Theme.cornerRadius - color: Theme.floatingWindowFieldColor + title: modelData.rule?.pattern || I18n.tr("Unknown") + singleLineTitle: true - Row { - id: mutedRow - anchors.fill: parent - anchors.margins: Theme.spacingS - spacing: Theme.spacingM - - StyledText { - id: mutedAppLabel - text: (modelData.rule && modelData.rule.pattern) ? modelData.rule.pattern : I18n.tr("Unknown") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Item { - width: Math.max(0, parent.width - mutedAppLabel.width - unmuteBtn.width - parent.spacing * 2) - height: 1 - } - - DankButton { - id: unmuteBtn - text: I18n.tr("Unmute") - backgroundColor: Theme.chipSurface - textColor: Theme.primary - onClicked: SettingsData.removeNotificationRule(modelData.index) - } - } - } + DankButton { + text: I18n.tr("Unmute") + backgroundColor: "transparent" + textColor: Theme.primary + onClicked: SettingsData.removeNotificationRule(modelData.index) } } } diff --git a/quickshell/Modules/Settings/NotificationsTab.qml b/quickshell/Modules/Settings/NotificationsTab.qml index 4b6eb6b6f..998fe1630 100644 --- a/quickshell/Modules/Settings/NotificationsTab.qml +++ b/quickshell/Modules/Settings/NotificationsTab.qml @@ -270,59 +270,23 @@ Column { } SettingsRow { - body: Column { - width: parent.width - spacing: Theme.spacingS + subtitle: I18n.tr("Right-click a notification and choose \"Allow in Do Not Disturb\" to add an app") + } - StyledText { - text: I18n.tr("Right-click a notification and choose \"Allow in Do Not Disturb\" to add an app") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - wrapMode: Text.WordWrap - width: parent.width - bottomPadding: Theme.spacingS - } + Repeater { + model: root.dndBypassRules - Repeater { - model: root.dndBypassRules + delegate: SettingsRow { + required property var modelData - delegate: Rectangle { - width: parent.width - height: dndBypassRow.implicitHeight + Theme.spacingS * 2 - radius: Theme.cornerRadius - color: Theme.floatingWindowFieldColor + title: modelData.rule?.pattern || I18n.tr("Unknown") + singleLineTitle: true - Row { - id: dndBypassRow - anchors.fill: parent - anchors.margins: Theme.spacingS - spacing: Theme.spacingM - - StyledText { - id: dndBypassLabel - text: modelData.rule.pattern || I18n.tr("Unknown") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Item { - width: Math.max(0, parent.width - parent.spacing * 2 - dndBypassLabel.width - dndBypassRemoveBtn.width) - height: 1 - } - - DankActionButton { - id: dndBypassRemoveBtn - buttonSize: 28 - iconName: "delete" - Accessible.name: I18n.tr("Remove") - iconSize: 18 - iconColor: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - onClicked: SettingsData.removeNotificationRule(modelData.index) - } - } - } + DankActionButton { + iconName: "delete" + iconColor: Theme.error + tooltipText: I18n.tr("Remove") + onClicked: SettingsData.removeNotificationRule(modelData.index) } } } @@ -335,7 +299,7 @@ Column { iconName: "rule_settings" title: I18n.tr("Rules") hint: SettingsTabs.page("notification_rules")?.hint ?? "" - onClicked: root.parentModal?.navigateTo("notification_rules") + onClicked: keyboard => root.parentModal?.navigateTo("notification_rules", keyboard) } } diff --git a/quickshell/Modules/Settings/PaletteInjectTab.qml b/quickshell/Modules/Settings/PaletteInjectTab.qml index 729cbab54..32f01be36 100644 --- a/quickshell/Modules/Settings/PaletteInjectTab.qml +++ b/quickshell/Modules/Settings/PaletteInjectTab.qml @@ -84,8 +84,6 @@ Item { DankToggle { id: enableToggle - width: 40 - height: 24 hideText: true checked: paletteItem.modelData.enabled !== false onToggled: checked => PaletteInjectService.updatePalette(paletteItem.index, "enabled", checked) diff --git a/quickshell/Modules/Settings/PluginSettingsPage.qml b/quickshell/Modules/Settings/PluginSettingsPage.qml index dab3ae825..e4a35b7e3 100644 --- a/quickshell/Modules/Settings/PluginSettingsPage.qml +++ b/quickshell/Modules/Settings/PluginSettingsPage.qml @@ -200,7 +200,7 @@ FocusScope { iconName: "widgets" title: I18n.tr("Desktop widgets") hint: I18n.tr("Desktop widget plugins are configured per widget instance") - onClicked: root.parentModal?.navigateTo("desktop_widgets") + onClicked: keyboard => root.parentModal?.navigateTo("desktop_widgets", keyboard) } } diff --git a/quickshell/Modules/Settings/PluginUpdatesDialog.qml b/quickshell/Modules/Settings/PluginUpdatesDialog.qml index 88735419b..c1aae0274 100644 --- a/quickshell/Modules/Settings/PluginUpdatesDialog.qml +++ b/quickshell/Modules/Settings/PluginUpdatesDialog.qml @@ -1,11 +1,10 @@ import QtQuick -import QtQuick.Layouts import qs.Common import qs.Widgets import qs.Modules.Settings.Widgets import qs.Services -StyledRect { +SettingsCard { id: root property var updatesList: [] @@ -17,16 +16,18 @@ StyledRect { signal pluginUpdated(string pluginId) signal updatesRequested(var plugins) - width: parent.width - height: visible ? innerColumn.implicitHeight + Theme.spacingL * 2 : 0 - radius: Theme.cornerRadiusM - color: Theme.floatingWindowNestedSurface - border.color: Theme.outlineMedium - border.width: Theme.layerOutlineWidth - clip: true - + iconName: "download" + title: I18n.tr("Available Updates (%1)", "plugin updates dialog title, %1 is a count").arg(updatesList.length) visible: false + headerActions: DankActionButton { + iconName: "close" + tooltipText: I18n.tr("Close") + iconColor: Theme.surfaceVariantText + enabled: !root.isUpdating && !root.operationsBlocked + onClicked: root.hide() + } + function show(list) { if (isUpdating || operationsBlocked) return; @@ -47,7 +48,7 @@ StyledRect { } function updateAll() { - updatesRequested(updatesList.slice()); + updatesRequested(updatesList.filter(plugin => PluginService.checkPluginCompatibility(plugin.requires_dms))); } function updatePlugins(list) { @@ -81,166 +82,76 @@ StyledRect { updateNext(); } - Column { - id: innerColumn - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Theme.spacingL - spacing: Theme.spacingM + SettingsNoteRow { + visible: !root.isUpdating && root.updatesList.length > 0 + text: I18n.tr("Plugin updates can change the code running in your session. Review the changes before updating.", "plugin update audit reminder") + } - RowLayout { - width: parent.width - spacing: Theme.spacingM + SettingsRow { + visible: root.isUpdating + title: root.currentUpdatingPlugin ? I18n.tr("Updating %1...", "plugin updates dialog progress, %1 is the plugin name").arg(root.currentUpdatingPlugin) : I18n.tr("Updating plugins...") + leading: DankSpinner { + size: Theme.iconSize + running: root.isUpdating + } + } - DankIcon { - name: "download" - size: Theme.iconSize - color: Theme.primary - Layout.alignment: Qt.AlignVCenter - } + SettingsRow { + visible: Object.keys(root.updateErrors).length > 0 + subtitle: Object.values(root.updateErrors).join("\n") + subtitleColor: Theme.error + } - StyledText { - Layout.fillWidth: true - text: I18n.tr("Available Updates (%1)", "plugin updates dialog title, %1 is a count").arg(root.updatesList.length) - font.pixelSize: Theme.fontSizeLarge - font.weight: Theme.fontWeightMedium - color: Theme.surfaceText - wrapMode: Text.Wrap + Repeater { + model: root.isUpdating ? [] : root.updatesList + + delegate: SettingsRow { + required property var modelData + + readonly property bool compatible: PluginService.checkPluginCompatibility(modelData.requires_dms) + + iconName: modelData.icon || "extension" + title: modelData.name || "" + subtitle: !compatible ? I18n.tr("Requires DMS %1", "plugin incompatibility notice, %1 is the required DMS version").arg(modelData.requires_dms) : modelData.author ? I18n.tr("by %1", "author attribution").arg(modelData.author) : "" + subtitleColor: compatible ? supportingContentColor : Theme.error + + DankActionButton { + iconName: "open_in_new" + tooltipText: I18n.tr("View Changes", "open plugin changes before updating") + visible: !!modelData.diffUrl || !!modelData.repo + onClicked: Qt.openUrlExternally(modelData.diffUrl || modelData.repo) } DankActionButton { - id: collapseBtn - iconName: "close" - Accessible.name: I18n.tr("Close") - iconSize: Theme.iconSizeMedium - iconColor: Theme.outline - Layout.alignment: Qt.AlignVCenter - enabled: !root.isUpdating && !root.operationsBlocked - onClicked: root.hide() - } - } - - RowLayout { - width: parent.width - spacing: Theme.spacingS - visible: !root.isUpdating && root.updatesList.length > 0 - - DankIcon { - name: "warning" - size: Theme.iconSizeMedium - color: Theme.warning - Layout.alignment: Qt.AlignTop - } - - StyledText { - Layout.fillWidth: true - text: I18n.tr("Plugin updates can change the code running in your session. Review the changes before updating.", "plugin update audit reminder") - font.pixelSize: Theme.fontSizeSmall - color: Theme.onSurfaceVariant - wrapMode: Text.Wrap - } - } - - Item { - width: parent.width - height: isUpdating ? Theme.buttonHeightS : 0 - visible: isUpdating - clip: true - - Row { - anchors.centerIn: parent - spacing: Theme.spacingM - - DankSpinner { - size: Theme.iconSize - running: root.isUpdating - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: root.currentUpdatingPlugin ? I18n.tr("Updating %1...", "plugin updates dialog progress, %1 is the plugin name").arg(root.currentUpdatingPlugin) : I18n.tr("Updating plugins...") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - } - } - - StyledText { - width: parent.width - visible: Object.keys(root.updateErrors).length > 0 - text: Object.values(root.updateErrors).join("\n") - color: Theme.error - wrapMode: Text.Wrap - } - - DankFlickable { - width: parent.width - height: Math.min(listCol.implicitHeight, Theme.smallBreakpoint) - clip: true - contentHeight: listCol.implicitHeight - visible: !isUpdating - - Column { - id: listCol - width: parent.width - spacing: Theme.spacingM - - Repeater { - model: root.updatesList - - delegate: SettingsRow { - required property var modelData - width: listCol.width - iconName: modelData.icon || "extension" - title: modelData.name || "" - subtitle: modelData.author ? I18n.tr("by %1", "author attribution").arg(modelData.author) : "" - DankActionButton { - iconName: "open_in_new" - tooltipText: I18n.tr("View Changes", "open plugin changes before updating") - visible: !!modelData.diffUrl || !!modelData.repo - onClicked: Qt.openUrlExternally(modelData.diffUrl || modelData.repo) - } - DankActionButton { - iconName: "download" - tooltipText: I18n.tr("Update", "verb, button installing a newer plugin version") - enabled: !root.isUpdating && !root.operationsBlocked - onClicked: root.updateSingle(modelData) - } - } - } - - StyledText { - width: parent.width - text: I18n.tr("No updates available.") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - horizontalAlignment: Text.AlignHCenter - visible: root.updatesList.length === 0 - } - } - } - - Row { - anchors.right: parent.right - spacing: Theme.spacingM - visible: !isUpdating - - DankButton { - text: I18n.tr("Cancel") - iconName: "close" - backgroundColor: Theme.chipSurface - textColor: Theme.surfaceText - onClicked: root.hide() - } - - DankButton { - text: I18n.tr("Update All") iconName: "download" - enabled: !root.operationsBlocked && root.updatesList.length > 0 - onClicked: root.updateAll() + tooltipText: I18n.tr("Update", "verb, button installing a newer plugin version") + enabled: !root.isUpdating && !root.operationsBlocked && compatible + onClicked: root.updateSingle(modelData) } } } + + SettingsRow { + visible: !root.isUpdating && root.updatesList.length === 0 + subtitle: I18n.tr("No updates available.") + } + + SettingsRow { + visible: !root.isUpdating + + DankButton { + text: I18n.tr("Cancel") + iconName: "close" + backgroundColor: "transparent" + textColor: Theme.primary + onClicked: root.hide() + } + + DankButton { + text: I18n.tr("Update All") + iconName: "download" + enabled: !root.operationsBlocked && root.updatesList.some(plugin => PluginService.checkPluginCompatibility(plugin.requires_dms)) + onClicked: root.updateAll() + } + } } diff --git a/quickshell/Modules/Settings/PluginsHubHeader.qml b/quickshell/Modules/Settings/PluginsHubHeader.qml index 1bd160d71..5f1f1bb8b 100644 --- a/quickshell/Modules/Settings/PluginsHubHeader.qml +++ b/quickshell/Modules/Settings/PluginsHubHeader.qml @@ -418,7 +418,7 @@ Column { wrapText: true Layout.alignment: Qt.AlignTop Layout.topMargin: (Theme.buttonHeightS - Theme.buttonHeightXS) / 2 - onClicked: root.parentModal?.navigateTo("plugins_manage") + onClicked: keyboard => root.parentModal?.navigateTo("plugins_manage", keyboard) } } @@ -482,7 +482,7 @@ Column { textColor: storeCard.accentColor maximumWidth: parent.width wrapText: true - onClicked: root.parentModal?.navigateTo("plugins_manage") + onClicked: keyboard => root.parentModal?.navigateTo("plugins_manage", keyboard) } } } @@ -611,7 +611,7 @@ Column { color: SettingsSearchService.highlightSection === highlightKey ? Theme.blend(surfaceColor, Theme.primary, SettingsMetrics.highlightBlend) : surfaceColor clickable: true Accessible.name: modelData.text - onClicked: root.parentModal?.navigateTo(modelData.id) + onClicked: keyboard => root.parentModal?.navigateTo(modelData.id, keyboard) Timer { interval: 0 @@ -750,7 +750,7 @@ Column { iconName: "settings" variant: "tonal" Accessible.name: I18n.tr("Settings") - onClicked: root.parentModal?.navigateTo(installedCard.modelData.id) + onClicked: keyboard => root.parentModal?.navigateTo(installedCard.modelData.id, keyboard) } Row { id: cardActions diff --git a/quickshell/Modules/Settings/PluginsManageTab.qml b/quickshell/Modules/Settings/PluginsManageTab.qml index cee160f48..baf0da986 100644 --- a/quickshell/Modules/Settings/PluginsManageTab.qml +++ b/quickshell/Modules/Settings/PluginsManageTab.qml @@ -52,7 +52,7 @@ FocusScope { text: PluginService.pluginDirectory font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText - font.family: "monospace" + isMonospace: true width: parent.width elide: Text.ElideMiddle horizontalAlignment: Text.AlignLeft diff --git a/quickshell/Modules/Settings/PowerSleepTab.qml b/quickshell/Modules/Settings/PowerSleepTab.qml index b22a4bc7c..1d7344cff 100644 --- a/quickshell/Modules/Settings/PowerSleepTab.qml +++ b/quickshell/Modules/Settings/PowerSleepTab.qml @@ -301,6 +301,7 @@ Item { DankDropdown { id: bootEntryDropdown + downKeyOpens: false enabled: bootEntryPicker.options.length > 0 Accessible.name: bootEntryPicker.title width: Math.min(dropdownWidth, bootEntryPicker.width - SettingsMetrics.rowPaddingH * 2) diff --git a/quickshell/Modules/Settings/PrinterTab.qml b/quickshell/Modules/Settings/PrinterTab.qml index 3bd098a39..a9f82b193 100644 --- a/quickshell/Modules/Settings/PrinterTab.qml +++ b/quickshell/Modules/Settings/PrinterTab.qml @@ -573,37 +573,11 @@ Item { return fields; } - delegate: Rectangle { + delegate: DankDetailChip { required property var modelData - required property int index - width: fieldContent.width + Theme.spacingM * 2 - height: 32 - radius: Theme.cornerRadius - Theme.outlineWidthFocused - color: Theme.floatingWindowFieldColor - border.width: Theme.outlineWidth - border.color: Theme.floatingWindowFieldBorderColor - - Row { - id: fieldContent - anchors.centerIn: parent - spacing: Theme.spacingXS - - StyledText { - text: modelData.label + ":" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: modelData.value - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Theme.fontWeightMedium - anchors.verticalCenter: parent.verticalCenter - } - } + label: modelData.label + value: modelData.value } } } diff --git a/quickshell/Modules/Settings/SettingsHubPage.qml b/quickshell/Modules/Settings/SettingsHubPage.qml index 36f4a490e..52b8396ad 100644 --- a/quickshell/Modules/Settings/SettingsHubPage.qml +++ b/quickshell/Modules/Settings/SettingsHubPage.qml @@ -27,6 +27,7 @@ FocusScope { id: headerLoader width: parent.width + focus: status === Loader.Ready source: root.headerFile ? Qt.resolvedUrl(root.headerFile + ".qml") : "" onLoaded: item.parentModal = Qt.binding(() => root.parentModal) } @@ -45,7 +46,7 @@ FocusScope { title: modelData.text hint: modelData.hint ?? "" trailingBadge: modelData.kind === "plugin" && PluginService.loadedPlugins[modelData.pluginId] === undefined ? I18n.tr("Disabled") : "" - onClicked: root.parentModal?.navigateTo(modelData.id) + onClicked: keyboard => root.parentModal?.navigateTo(modelData.id, keyboard) } } } @@ -63,7 +64,7 @@ FocusScope { iconName: modelData.icon title: modelData.text hint: modelData.hint ?? "" - onClicked: root.parentModal?.navigateTo(modelData.id) + onClicked: keyboard => root.parentModal?.navigateTo(modelData.id, keyboard) } } } diff --git a/quickshell/Modules/Settings/SoftwareUpdatesTab.qml b/quickshell/Modules/Settings/SoftwareUpdatesTab.qml index ff645afc7..90170e528 100644 --- a/quickshell/Modules/Settings/SoftwareUpdatesTab.qml +++ b/quickshell/Modules/Settings/SoftwareUpdatesTab.qml @@ -436,7 +436,7 @@ Item { title: I18n.tr("Release notes") iconName: "auto_awesome" hint: root.notesRelease?.codename ? "v" + root.notesRelease.version + " · " + root.notesRelease.codename : "v" + (root.notesRelease?.version ?? "") - onClicked: root.parentModal?.navigateTo("updater_changelog") + onClicked: keyboard => root.parentModal?.navigateTo("updater_changelog", keyboard) } SettingsRow { diff --git a/quickshell/Modules/Settings/ThemeColorsTab.qml b/quickshell/Modules/Settings/ThemeColorsTab.qml index 8ac587c93..3f465dcd9 100644 --- a/quickshell/Modules/Settings/ThemeColorsTab.qml +++ b/quickshell/Modules/Settings/ThemeColorsTab.qml @@ -3,6 +3,7 @@ import Quickshell import Quickshell.Io import Quickshell.Widgets import qs.Common +import qs.Modals.Common import qs.Modals.FileBrowser import qs.Services import qs.Widgets @@ -29,7 +30,18 @@ Item { }) onMatugenPreviewKeyChanged: MatugenPreviewService.refresh() onMatugenAvailableChanged: MatugenPreviewService.refresh() + readonly property var log: Log.scoped("ThemeColorsTab") property var installedRegistryThemes: [] + readonly property var activeRegistryTheme: { + const file = SettingsData.customThemeFile; + if (Theme.currentThemeCategory !== "registry" || Theme.currentTheme !== Theme.custom || !file) + return null; + return installedRegistryThemes.find(t => file.endsWith((t.sourceDir || t.id) + "/theme.json")) ?? null; + } + + function registryThemeDir(theme) { + return Quickshell.env("HOME") + "/.config/DankMaterialShell/themes/" + (theme.sourceDir || theme.id); + } Component.onCompleted: { if (DMSService.dmsAvailable) DMSService.listInstalledThemes(); @@ -69,16 +81,7 @@ Item { spacing: Theme.spacingS StyledText { - property string registryThemeName: { - if (Theme.currentThemeCategory !== "registry") - return ""; - for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) { - var t = themeColorsTab.installedRegistryThemes[i]; - if (SettingsData.customThemeFile && SettingsData.customThemeFile.endsWith((t.sourceDir || t.id) + "/theme.json")) - return t.name; - } - return ""; - } + readonly property string registryThemeName: themeColorsTab.activeRegistryTheme?.name ?? "" text: { if (Theme.currentThemeCategory === "registry" && registryThemeName) return I18n.tr("Current Theme: %1", "current theme label").arg(registryThemeName); @@ -123,6 +126,7 @@ Item { clip: true DankButtonGroup { + arrowKeysSelect: false id: themeCategoryGroup anchors.horizontalCenter: parent.horizontalCenter buttonPadding: parent.width < 420 ? Theme.spacingS : Theme.spacingL @@ -338,7 +342,9 @@ Item { Behavior on opacity { NumberAnimation { - duration: Theme.shortDuration + duration: Theme.expressiveDurations.expressiveFastEffects + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.expressiveEffects } } } @@ -503,181 +509,52 @@ Item { SettingsRow { visible: Theme.currentThemeCategory === "registry" body: Column { - id: registrySection width: parent.width spacing: Theme.spacingM - Grid { - id: themeGrid - property int cardWidth: registrySection.width < 350 ? 100 : 140 - property int cardHeight: registrySection.width < 350 ? 72 : 100 - columns: Math.max(1, Math.floor((registrySection.width + spacing) / (cardWidth + spacing))) - spacing: Theme.spacingS - anchors.horizontalCenter: parent.horizontalCenter + SettingsSwatchGrid { + minTileWidth: SettingsMetrics.previewTileMinWidth visible: themeColorsTab.installedRegistryThemes.length > 0 - - Repeater { - model: themeColorsTab.installedRegistryThemes - - Rectangle { - id: themeCard - property bool isActive: Theme.currentThemeCategory === "registry" && Theme.currentThemeName === "custom" && SettingsData.customThemeFile && SettingsData.customThemeFile.endsWith((modelData.sourceDir || modelData.id) + "/theme.json") - property bool hasVariants: modelData.hasVariants || false - property var variants: modelData.variants || null - property string selectedVariant: hasVariants ? SettingsData.getRegistryThemeVariant(modelData.id, variants?.default || "") : "" - property string previewPath: { - const baseDir = Quickshell.env("HOME") + "/.config/DankMaterialShell/themes/" + (modelData.sourceDir || modelData.id); - const mode = Theme.isLightMode ? "light" : "dark"; - if (hasVariants && selectedVariant) - return baseDir + "/preview-" + selectedVariant + "-" + mode + ".svg"; - return baseDir + "/preview-" + mode + ".svg"; - } - width: themeGrid.cardWidth - height: themeGrid.cardHeight - radius: Theme.cornerRadius - color: Theme.floatingWindowFieldColor - border.color: isActive ? Theme.primary : Theme.outlineMedium - border.width: isActive ? Theme.outlineWidthFocused : Theme.layerOutlineWidth - scale: isActive ? 1.03 : 1 - - Behavior on scale { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.emphasizedEasing - } - } - - Image { - id: previewImage - anchors.fill: parent - anchors.margins: Theme.spacingXXS - source: "file://" + themeCard.previewPath - fillMode: Image.PreserveAspectFit - smooth: true - mipmap: true - } - - DankIcon { - anchors.centerIn: parent - name: "palette" - size: themeGrid.cardWidth < 120 ? 24 : 32 - color: Theme.primary - visible: previewImage.status === Image.Error || previewImage.status === Image.Null - } - - DankPaletteSwatch { - readonly property var palette: ThemePalette.pick((Theme.isLightMode ? themeCard.modelData.light : themeCard.modelData.dark) ?? themeCard.modelData.dark) - anchors.top: parent.top - anchors.left: parent.left - anchors.margins: Theme.spacingXS - width: Theme.iconSizeMedium - height: Theme.iconSizeMedium - visible: !!palette - primaryColor: palette?.primary ?? Theme.primary - secondaryColor: palette?.secondary ?? primaryColor - tertiaryColor: palette?.tertiary ?? secondaryColor - } - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: themeGrid.cardWidth < 120 ? 18 : 22 - bottomLeftRadius: parent.radius - bottomRightRadius: parent.radius - color: Theme.withAlpha(Theme.scrimColor, Theme.scrimAlpha) - - StyledText { - anchors.centerIn: parent - text: modelData.name - font.pixelSize: themeGrid.cardWidth < 120 ? Theme.fontSizeSmall - 2 : Theme.fontSizeSmall - color: Theme.contrastLight - font.weight: Theme.fontWeightMedium - elide: Text.ElideRight - width: parent.width - Theme.spacingXS * 2 - horizontalAlignment: Text.AlignHCenter - } - } - - Rectangle { - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: themeGrid.cardWidth < 120 ? 2 : 4 - width: themeGrid.cardWidth < 120 ? 16 : 20 - height: width - radius: Theme.fullRadius(width, height) - color: Theme.primary - visible: themeCard.isActive - - DankIcon { - anchors.centerIn: parent - name: "check" - size: themeGrid.cardWidth < 120 ? 10 : 14 - color: Theme.onPrimary - } - } - - DankBadge { - anchors.top: parent.top - anchors.left: parent.left - anchors.margins: themeGrid.cardWidth < 120 ? 2 : 4 - color: Theme.secondaryContainer - textColor: Theme.onSecondaryContainer - visible: themeCard.hasVariants && !deleteButton.visible - text: { - if (themeCard.variants?.type === "multi") - return themeCard.variants?.accents?.length || 0; - return themeCard.variants?.options?.length || 0; - } - } - - MouseArea { - id: cardMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - const themesDir = Quickshell.env("HOME") + "/.config/DankMaterialShell/themes"; - const themePath = themesDir + "/" + (modelData.sourceDir || modelData.id) + "/theme.json"; - SettingsData.set("customThemeFile", themePath); - Theme.switchTheme("custom", true, true); - } - } - - DankActionButton { - id: deleteButton - anchors.top: parent.top - anchors.left: parent.left - anchors.margins: themeGrid.cardWidth < 120 ? 2 : 4 - buttonSize: themeGrid.cardWidth < 120 ? 18 : 24 - iconName: "close" - iconSize: themeGrid.cardWidth < 120 ? 10 : 14 - iconColor: Theme.contrastLight - backgroundColor: Theme.withAlpha(Theme.scrimColor, Theme.scrimAlpha) - stateColor: Theme.error - tooltipText: I18n.tr("Delete") - opacity: cardMouseArea.containsMouse || hovered ? 1 : 0 - visible: opacity > 0 - - Behavior on opacity { - NumberAnimation { - duration: Theme.shortDuration - } - } - - onClicked: { - ToastService.showInfo(I18n.tr("Uninstalling: %1", "uninstallation progress").arg(modelData.name)); - DMSService.uninstallTheme(modelData.id, response => { - if (response.error) { - ToastService.showError(I18n.tr("Uninstall failed: %1", "uninstallation error").arg(response.error)); - return; - } - ToastService.showInfo(I18n.tr("Uninstalled: %1", "uninstallation success").arg(modelData.name)); - DMSService.listInstalledThemes(); - }); - } - } - } + currentValue: themeColorsTab.activeRegistryTheme?.id ?? "" + options: themeColorsTab.installedRegistryThemes.map(theme => ({ + "value": theme.id, + "label": theme.name, + "deletable": true + })) + resolveOption: option => { + const theme = themeColorsTab.installedRegistryThemes.find(t => t.id === option.value); + if (!theme) + return {}; + const mode = Theme.isLightMode ? "light" : "dark"; + const variant = theme.hasVariants ? SettingsData.getRegistryThemeVariant(theme.id, theme.variants?.default || "") : ""; + const palette = ThemePalette.pick((Theme.isLightMode ? theme.light : theme.dark) ?? theme.dark) ?? {}; + const variantCount = theme.variants?.type === "multi" ? theme.variants?.accents?.length : theme.variants?.options?.length; + return { + "preview": themeColorsTab.registryThemeDir(theme) + "/preview-" + (variant ? variant + "-" : "") + mode + ".svg", + "primary": palette.primary ?? Theme.primary.toString(), + "secondary": palette.secondary, + "tertiary": palette.tertiary, + "badge": theme.hasVariants && variantCount ? String(variantCount) : "" + }; + } + onSelected: value => { + const theme = themeColorsTab.installedRegistryThemes.find(t => t.id === value); + if (!theme) + return; + SettingsData.set("customThemeFile", themeColorsTab.registryThemeDir(theme) + "/theme.json"); + Theme.switchTheme("custom", true, true); + } + onDeleteRequested: value => { + const theme = themeColorsTab.installedRegistryThemes.find(t => t.id === value); + if (!theme) + return; + uninstallThemeConfirm.showWithOptions({ + "title": I18n.tr("Uninstall"), + "message": I18n.tr("Uninstall %1?", "plugin removal confirmation").arg(theme.name), + "confirmText": I18n.tr("Uninstall"), + "confirmColor": Theme.error, + "onConfirm": () => themeColorsTab.uninstallRegistryTheme(theme) + }); } } @@ -711,14 +588,7 @@ Item { property string activeThemeId: { switch (Theme.currentThemeCategory) { case "registry": - if (Theme.currentTheme !== "custom") - return ""; - for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) { - var t = themeColorsTab.installedRegistryThemes[i]; - if (SettingsData.customThemeFile && SettingsData.customThemeFile.endsWith((t.sourceDir || t.id) + "/theme.json")) - return t.id; - } - return ""; + return themeColorsTab.activeRegistryTheme?.id ?? ""; case "custom": return Theme.currentThemeId || ""; default: @@ -730,12 +600,7 @@ Item { return null; switch (Theme.currentThemeCategory) { case "registry": - for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) { - var t = themeColorsTab.installedRegistryThemes[i]; - if (t.id === activeThemeId && t.hasVariants) - return t.variants; - } - return null; + return themeColorsTab.activeRegistryTheme?.hasVariants ? themeColorsTab.activeRegistryTheme.variants : null; case "custom": return Theme.currentThemeVariants || null; default: @@ -801,6 +666,7 @@ Item { visible: variantSelector.isMultiVariant && variantSelector.flavorOptions.length > 1 DankButtonGroup { + arrowKeysSelect: false id: flavorButtonGroup anchors.horizontalCenter: parent.horizontalCenter property int _count: variantSelector.flavorNames.length @@ -849,43 +715,21 @@ Item { Repeater { model: variantSelector.activeThemeVariants?.accents || [] - Rectangle { + DankColorButton { required property var modelData required property int index - property string accentId: modelData.id - property bool isSelected: accentId === variantSelector.selectedAccent + readonly property string accentId: modelData.id + width: accentColorsGrid.dotSize height: accentColorsGrid.dotSize - radius: Theme.fullRadius(width, height) - color: modelData.color || modelData[variantSelector.selectedFlavor]?.primary || Theme.primary - border.color: Theme.outline - border.width: isSelected ? Theme.outlineWidthFocused : Theme.outlineWidth - scale: isSelected ? 1.1 : 1 - - DankTooltipHost { - text: modelData.name - target: parent - hoverArea: accentMouseArea - } - - MouseArea { - id: accentMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (parent.isSelected) - return; - Theme.screenTransition(); - SettingsData.setRegistryThemeMultiVariant(variantSelector.activeThemeId, variantSelector.selectedFlavor, parent.accentId, variantSelector.colorMode); - } - } - - Behavior on scale { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.emphasizedEasing - } + swatchColor: modelData.color || modelData[variantSelector.selectedFlavor]?.primary || Theme.primary + selected: accentId === variantSelector.selectedAccent + tooltipText: modelData.name + onClicked: { + if (selected) + return; + Theme.screenTransition(); + SettingsData.setRegistryThemeMultiVariant(variantSelector.activeThemeId, variantSelector.selectedFlavor, accentId, variantSelector.colorMode); } } } @@ -899,6 +743,7 @@ Item { visible: !variantSelector.isMultiVariant && variantSelector.variantNames.length > 0 DankButtonGroup { + arrowKeysSelect: false id: variantButtonGroup anchors.horizontalCenter: parent.horizontalCenter property int _count: variantSelector.variantNames.length @@ -1035,4 +880,21 @@ Item { extractSaveFileView.path = outputPath; extractSaveFileView.setText(json); } + + function uninstallRegistryTheme(theme) { + ToastService.showInfo(I18n.tr("Uninstalling: %1", "uninstallation progress").arg(theme.name)); + DMSService.uninstallTheme(theme.id, response => { + if (response.error) { + ToastService.showError(I18n.tr("Uninstall failed: %1", "uninstallation error").arg(response.error)); + return; + } + ToastService.showInfo(I18n.tr("Uninstalled: %1", "uninstallation success").arg(theme.name)); + DMSService.listInstalledThemes(); + }); + } + + ConfirmDialogOverlay { + id: uninstallThemeConfirm + parent: themeColorsTab.parentModal?.modalFocusScope ?? themeColorsTab + } } diff --git a/quickshell/Modules/Settings/ThemeSurfacesTab.qml b/quickshell/Modules/Settings/ThemeSurfacesTab.qml index f27272588..8febad1b8 100644 --- a/quickshell/Modules/Settings/ThemeSurfacesTab.qml +++ b/quickshell/Modules/Settings/ThemeSurfacesTab.qml @@ -212,7 +212,7 @@ Column { settingKey: "blurXrayLink" visible: CompositorService.isNiri || CompositorService.isHyprland title: I18n.tr("Xray options are in Compositor → Layout") - onClicked: root.parentModal?.navigateTo("compositor_layout") + onClicked: keyboard => root.parentModal?.navigateTo("compositor_layout", keyboard) } SettingsButtonGroupRow { @@ -291,7 +291,7 @@ Column { subtitle: SettingsTabs.page("surface_shadows")?.hint ?? "" resetKeys: ["m3ElevationEnabled"] checked: SettingsData.m3ElevationEnabled ?? true - onNavigated: root.parentModal?.navigateTo("surface_shadows") + onNavigated: keyboard => root.parentModal?.navigateTo("surface_shadows", keyboard) onToggled: checked => SettingsData.set("m3ElevationEnabled", checked) } } diff --git a/quickshell/Modules/Settings/TimeWeatherTab.qml b/quickshell/Modules/Settings/TimeWeatherTab.qml index dcda86108..bcae65c6a 100644 --- a/quickshell/Modules/Settings/TimeWeatherTab.qml +++ b/quickshell/Modules/Settings/TimeWeatherTab.qml @@ -200,8 +200,6 @@ Item { outlined: true leftIconName: "calendar_today" labelText: I18n.tr("Format") - width: parent.width - Theme.spacingM * 2 - x: Theme.spacingM visible: root.editingBarFormat || root.isCustomFormat(SettingsData.clockDateFormat) placeholderText: I18n.tr("Enter custom top bar format (e.g., ddd MMM d)") text: SettingsData.clockDateFormat @@ -281,8 +279,6 @@ Item { outlined: true leftIconName: "calendar_today" labelText: I18n.tr("Format") - width: parent.width - Theme.spacingM * 2 - x: Theme.spacingM visible: root.editingLockFormat || root.isCustomFormat(SettingsData.lockDateFormat) placeholderText: I18n.tr("Enter custom lock screen format (e.g., dddd, MMMM d)") text: SettingsData.lockDateFormat @@ -294,92 +290,80 @@ Item { SettingsRow { visible: customFormatInput.visible || customLockFormatInput.visible - body: Rectangle { - width: parent.width - Theme.spacingM * 2 - x: Theme.spacingM - height: formatHelp.implicitHeight + Theme.spacingM * 2 - radius: Theme.cornerRadius - color: Theme.floatingWindowNestedSurface - border.color: Theme.outlineMedium - border.width: Theme.layerOutlineWidth + body: Column { + width: parent.width + spacing: Theme.spacingXS - Column { - id: formatHelp - anchors.fill: parent - anchors.margins: Theme.spacingM - spacing: Theme.spacingXS + StyledText { + text: I18n.tr("Format legend") + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + font.weight: Theme.fontWeightMedium + } - StyledText { - text: I18n.tr("Format legend") - font.pixelSize: Theme.fontSizeSmall - color: Theme.primary - font.weight: Theme.fontWeightMedium + Row { + width: parent.width + spacing: Theme.spacingL + + Column { + width: (parent.width - Theme.spacingL) / 2 + spacing: Theme.spacingXXS + + StyledText { + text: I18n.tr("• d - Day (1-31)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + StyledText { + text: I18n.tr("• dd - Day (01-31)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + StyledText { + text: I18n.tr("• ddd - Day name (Mon)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + StyledText { + text: I18n.tr("• dddd - Day name (Monday)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + StyledText { + text: I18n.tr("• M - Month (1-12)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } } - Row { - width: parent.width - spacing: Theme.spacingL + Column { + width: (parent.width - Theme.spacingL) / 2 + spacing: Theme.spacingXXS - Column { - width: (parent.width - Theme.spacingL) / 2 - spacing: Theme.spacingXXS - - StyledText { - text: I18n.tr("• d - Day (1-31)") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } - StyledText { - text: I18n.tr("• dd - Day (01-31)") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } - StyledText { - text: I18n.tr("• ddd - Day name (Mon)") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } - StyledText { - text: I18n.tr("• dddd - Day name (Monday)") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } - StyledText { - text: I18n.tr("• M - Month (1-12)") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } + StyledText { + text: I18n.tr("• MM - Month (01-12)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText } - - Column { - width: (parent.width - Theme.spacingL) / 2 - spacing: Theme.spacingXXS - - StyledText { - text: I18n.tr("• MM - Month (01-12)") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } - StyledText { - text: I18n.tr("• MMM - Month (Jan)") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } - StyledText { - text: I18n.tr("• MMMM - Month (January)") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } - StyledText { - text: I18n.tr("• yy - Year (24)") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } - StyledText { - text: I18n.tr("• yyyy - Year (2024)") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } + StyledText { + text: I18n.tr("• MMM - Month (Jan)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + StyledText { + text: I18n.tr("• MMMM - Month (January)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + StyledText { + text: I18n.tr("• yy - Year (24)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + StyledText { + text: I18n.tr("• yyyy - Year (2024)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText } } } @@ -438,7 +422,7 @@ Item { tags: ["weather", "location", "units"] title: I18n.tr("Weather") iconName: "partly_cloudy_day" - onClicked: root.parentModal?.navigateTo("weather") + onClicked: keyboard => root.parentModal?.navigateTo("weather", keyboard) } } } diff --git a/quickshell/Modules/Settings/TypographyMotionTab.qml b/quickshell/Modules/Settings/TypographyMotionTab.qml index f3c0ee224..1e8a23d4a 100644 --- a/quickshell/Modules/Settings/TypographyMotionTab.qml +++ b/quickshell/Modules/Settings/TypographyMotionTab.qml @@ -179,8 +179,9 @@ Item { Behavior on opacity { NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing + duration: Theme.expressiveDurations.expressiveFastEffects + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.expressiveEffects } } } diff --git a/quickshell/Modules/Settings/UsersTab.qml b/quickshell/Modules/Settings/UsersTab.qml index 0e09c19af..2aad56dcc 100644 --- a/quickshell/Modules/Settings/UsersTab.qml +++ b/quickshell/Modules/Settings/UsersTab.qml @@ -192,7 +192,7 @@ Item { DankFab { text: I18n.tr("Add user") iconName: "person_add" - onClicked: root.parentModal?.navigateTo("user_create") + onClicked: keyboard => root.parentModal?.navigateTo("user_create", keyboard) } } } diff --git a/quickshell/Modules/Settings/WallpaperColorsTab.qml b/quickshell/Modules/Settings/WallpaperColorsTab.qml index 7c2b9383f..ca0a3c0ae 100644 --- a/quickshell/Modules/Settings/WallpaperColorsTab.qml +++ b/quickshell/Modules/Settings/WallpaperColorsTab.qml @@ -211,6 +211,7 @@ Column { spacing: Theme.spacingM DankButtonGroup { + arrowKeysSelect: false id: modeGroup width: parent.width fillWidth: true @@ -254,7 +255,7 @@ Column { height: hero.stacked ? implicitHeight : Math.max(implicitHeight, side.navRowHeight) paddingH: Theme.spacingM paddingV: Theme.spacingM - onClicked: root.parentModal?.navigateTo("theme_schedule") + onClicked: keyboard => root.parentModal?.navigateTo("theme_schedule", keyboard) } SettingsNavRow { @@ -266,7 +267,7 @@ Column { height: hero.stacked ? implicitHeight : Math.max(implicitHeight, side.navRowHeight) paddingH: Theme.spacingM paddingV: Theme.spacingM - onClicked: root.parentModal?.navigateTo("theme") + onClicked: keyboard => root.parentModal?.navigateTo("theme", keyboard) leading: DankPaletteSwatch { width: SettingsMetrics.heroLeadingSize @@ -410,7 +411,7 @@ Column { title: I18n.tr("Automatic cycling") subtitle: SettingsTabs.page("wallpaper_cycling")?.hint ?? "" checked: root.cyclingEnabled - onNavigated: root.parentModal?.navigateTo("wallpaper_cycling") + onNavigated: keyboard => root.parentModal?.navigateTo("wallpaper_cycling", keyboard) onToggled: toggled => { if (root.perMonitor) { SessionData.setMonitorCyclingEnabled(root.selectedScreen, toggled); @@ -505,9 +506,9 @@ Column { iconName: "lock" title: I18n.tr("Lock screen") hint: SettingsData.lockScreenWallpaperPath ? SettingsData.lockScreenWallpaperPath.split("/").pop() : I18n.tr("Use desktop wallpaper") - onClicked: { + onClicked: keyboard => { SettingsSearchService.navigateToSection("lockScreenWallpaperPath"); - root.parentModal?.navigateTo("lock_screen"); + root.parentModal?.navigateTo("lock_screen", keyboard); } } diff --git a/quickshell/Modules/Settings/WeatherSettingsTab.qml b/quickshell/Modules/Settings/WeatherSettingsTab.qml index 37ed55916..533c930c5 100644 --- a/quickshell/Modules/Settings/WeatherSettingsTab.qml +++ b/quickshell/Modules/Settings/WeatherSettingsTab.qml @@ -138,13 +138,13 @@ Item { SettingsNavRow { title: I18n.tr("Dashboard") iconName: "space_dashboard" - onClicked: root.parentModal?.navigateTo("dank_dash") + onClicked: keyboard => root.parentModal?.navigateTo("dank_dash", keyboard) } SettingsNavRow { title: I18n.tr("Bar widgets") iconName: "widgets" - onClicked: root.parentModal?.navigateTo("dankbar_widgets") + onClicked: keyboard => root.parentModal?.navigateTo("dankbar_widgets", keyboard) } } } diff --git a/quickshell/Modules/Settings/Widgets/AppBrowserPopup.qml b/quickshell/Modules/Settings/Widgets/AppBrowserPopup.qml index e501845fb..89e0c82d4 100644 --- a/quickshell/Modules/Settings/Widgets/AppBrowserPopup.qml +++ b/quickshell/Modules/Settings/Widgets/AppBrowserPopup.qml @@ -180,8 +180,9 @@ DankFloatingWindow { Behavior on color { ColorAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing + duration: Theme.expressiveDurations.expressiveFastEffects + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.expressiveEffects } } } diff --git a/quickshell/Modules/Settings/Widgets/PluginSettingsHost.qml b/quickshell/Modules/Settings/Widgets/PluginSettingsHost.qml index 7c72c4587..63acd1a10 100644 --- a/quickshell/Modules/Settings/Widgets/PluginSettingsHost.qml +++ b/quickshell/Modules/Settings/Widgets/PluginSettingsHost.qml @@ -13,10 +13,6 @@ FocusScope { implicitHeight: loader.item ? loader.item.implicitHeight : 0 - Keys.onPressed: event => { - event.accepted = true; - } - Loader { id: loader anchors.fill: parent @@ -34,12 +30,6 @@ FocusScope { item.pluginService = root.pluginService; if ("popoutService" in item) item.popoutService = PopoutService; - Qt.callLater(() => { - if (!loader.item) - return; - root.focus = true; - loader.item.forceActiveFocus(); - }); } } } diff --git a/quickshell/Modules/Settings/Widgets/SettingsButtonGroupRow.qml b/quickshell/Modules/Settings/Widgets/SettingsButtonGroupRow.qml index 8b8ba795b..13f55bda3 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsButtonGroupRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsButtonGroupRow.qml @@ -54,6 +54,7 @@ SettingsRow { } DankButtonGroup { + arrowKeysSelect: false id: buttonGroup parent: root.compact ? bodyHost : trailingHost x: root.compact ? (parent.width - width) / 2 : 0 diff --git a/quickshell/Modules/Settings/Widgets/SettingsCard.qml b/quickshell/Modules/Settings/Widgets/SettingsCard.qml index 8cf141531..d231d494b 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsCard.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsCard.qml @@ -15,6 +15,7 @@ Column { property string settingKey: "" property string title: "" + // Not drawn; extract_settings_index.py reads it for the search result icon property string iconName: "" property bool collapsible: false property bool expanded: true diff --git a/quickshell/Modules/Settings/Widgets/SettingsDeleteButton.qml b/quickshell/Modules/Settings/Widgets/SettingsDeleteButton.qml new file mode 100644 index 000000000..21b23f5bf --- /dev/null +++ b/quickshell/Modules/Settings/Widgets/SettingsDeleteButton.qml @@ -0,0 +1,28 @@ +import QtQuick +import qs.Common +import qs.Widgets + +DankActionButton { + id: root + + property bool confirming: false + + signal deleteRequested + + iconName: confirming ? "warning" : "delete" + iconColor: confirming ? Theme.error : Theme.surfaceVariantText + tooltipText: confirming ? I18n.tr("Confirm Delete") : I18n.tr("Delete") + + onActiveFocusChanged: { + if (!activeFocus) + confirming = false; + } + onClicked: { + if (!confirming) { + confirming = true; + return; + } + confirming = false; + deleteRequested(); + } +} diff --git a/quickshell/Modules/Settings/Widgets/SettingsDivider.qml b/quickshell/Modules/Settings/Widgets/SettingsDivider.qml index cd0e8d5fc..14963e21d 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsDivider.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsDivider.qml @@ -2,8 +2,11 @@ import QtQuick import qs.Common Rectangle { - width: parent?.width ?? 0 - height: Theme.dividerWidth + property bool vertical: false + + width: vertical ? Theme.dividerWidth : parent?.width ?? 0 + height: vertical ? SettingsMetrics.splitDividerHeight : Theme.dividerWidth color: Theme.outlineVariant visible: !(parent?.isSettingsGroupHost ?? false) + anchors.verticalCenter: vertical ? parent?.verticalCenter : undefined } diff --git a/quickshell/Modules/Settings/Widgets/SettingsDropdownRow.qml b/quickshell/Modules/Settings/Widgets/SettingsDropdownRow.qml index c9e83b0cf..ddd76c34d 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsDropdownRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsDropdownRow.qml @@ -41,6 +41,7 @@ SettingsRow { DankDropdown { id: dropdown + downKeyOpens: false enabled: root.enabled Accessible.name: root.text Accessible.description: root.description + (root.description ? " · " : "") + currentValue diff --git a/quickshell/Modules/Settings/Widgets/SettingsInstanceRow.qml b/quickshell/Modules/Settings/Widgets/SettingsInstanceRow.qml index 1fe4f0699..5fe35d395 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsInstanceRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsInstanceRow.qml @@ -11,7 +11,6 @@ SettingsRow { property bool toggleVisible: true property bool toggleEnabled: true property bool deletable: true - property bool confirmingDelete: false signal toggled(bool checked) signal deleteRequested @@ -19,8 +18,8 @@ SettingsRow { clickable: true iconName: selected ? "radio_button_checked" : "radio_button_unchecked" iconColor: selected ? Theme.primary : Theme.surfaceVariantText - subtitle: confirmingDelete ? I18n.tr("Confirm Delete") : summary - subtitleColor: confirmingDelete ? Theme.error : Theme.surfaceVariantText + subtitle: deleteButton.confirming ? I18n.tr("Confirm Delete") : summary + subtitleColor: deleteButton.confirming ? Theme.error : Theme.surfaceVariantText DankToggle { visible: root.toggleVisible @@ -32,11 +31,9 @@ SettingsRow { onToggled: value => root.toggled(value) } - DankActionButton { + SettingsDeleteButton { + id: deleteButton visible: root.deletable - iconName: root.confirmingDelete ? "warning" : "delete" - iconColor: root.confirmingDelete ? Theme.error : Theme.surfaceVariantText - Accessible.name: root.confirmingDelete ? I18n.tr("Confirm Delete") : I18n.tr("Remove") - onClicked: root.deleteRequested() + onDeleteRequested: root.deleteRequested() } } diff --git a/quickshell/Modules/Settings/Widgets/SettingsLayoutPicker.qml b/quickshell/Modules/Settings/Widgets/SettingsLayoutPicker.qml index 3e41dd2a2..83af55d7d 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsLayoutPicker.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsLayoutPicker.qml @@ -111,20 +111,24 @@ GridLayout { border.color: isActive ? Theme.primary : Theme.outlineMedium activeFocusOnTab: true - Accessible.role: Accessible.RadioButton + Accessible.role: Accessible.Button Accessible.name: modelData.label + Accessible.checkable: true Accessible.checked: isActive Accessible.onPressAction: root.selected(modelData.key) Keys.onSpacePressed: root.selected(modelData.key) + Keys.onEnterPressed: root.selected(modelData.key) Keys.onReturnPressed: root.selected(modelData.key) - FocusRing {} + FocusRing { + id: cardRing + } Rectangle { anchors.fill: parent radius: parent.radius color: Theme.primary - opacity: modeMouse.containsMouse ? Theme.stateLayerHover : 0 + opacity: cardRing.visible ? Theme.stateLayerFocus : modeMouse.containsMouse ? Theme.stateLayerHover : 0 } Column { @@ -252,7 +256,11 @@ GridLayout { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: root.selected(modeCard.modelData.key) + onClicked: { + cardRing.pointerFocused = true; + modeCard.forceActiveFocus(Qt.MouseFocusReason); + root.selected(modeCard.modelData.key); + } } } } diff --git a/quickshell/Modules/Settings/Widgets/SettingsMetrics.qml b/quickshell/Modules/Settings/Widgets/SettingsMetrics.qml index 5f6e713a0..1c3b05743 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsMetrics.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsMetrics.qml @@ -43,7 +43,9 @@ Singleton { readonly property real bannerTextMinWidth: 100 readonly property real fontMenuExtraWidth: 100 readonly property real swatchTileMinWidth: 96 + readonly property real previewTileMinWidth: 140 readonly property real emptyStateHeight: 100 + readonly property real noteMaxHeight: 160 readonly property color rowColor: Theme.foregroundColor(Theme.cardSurface, true) readonly property color rowHighlightColor: Theme.withAlpha(Theme.primary, highlightBlend) readonly property color selectedRowColor: Theme.selectedContainer diff --git a/quickshell/Modules/Settings/Widgets/SettingsNoteRow.qml b/quickshell/Modules/Settings/Widgets/SettingsNoteRow.qml index 22b889822..b5df938ef 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsNoteRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsNoteRow.qml @@ -9,33 +9,47 @@ SettingsRow { property string noteIconName: "warning" property color tint: Theme.warning property color tintBackground: Theme.warningHover + property bool monospace: false + property real maxHeight: 0 body: Rectangle { + readonly property real naturalHeight: noteRow.implicitHeight + Theme.spacingS * 2 + width: parent.width - height: noteRow.implicitHeight + Theme.spacingS * 2 + height: root.maxHeight > 0 ? Math.min(root.maxHeight, naturalHeight) : naturalHeight radius: Theme.cornerRadius color: root.tintBackground - Row { - id: noteRow + DankFlickable { anchors.fill: parent anchors.margins: Theme.spacingS - spacing: Theme.spacingS + contentHeight: noteRow.implicitHeight + interactive: contentHeight > height + wheelEnabled: interactive + clip: interactive - DankIcon { - name: root.noteIconName - size: Theme.iconSizeSmall - color: root.tint - anchors.verticalCenter: parent.verticalCenter - } + Row { + id: noteRow + width: parent.width + spacing: Theme.spacingS - StyledText { - width: parent.width - parent.spacing - Theme.iconSizeSmall - text: root.text - font.pixelSize: Theme.fontSizeSmall - color: root.tint - wrapMode: Text.WordWrap - anchors.verticalCenter: parent.verticalCenter + DankIcon { + visible: root.noteIconName !== "" + name: root.noteIconName + size: Theme.iconSizeSmall + color: root.tint + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + width: root.noteIconName !== "" ? parent.width - parent.spacing - Theme.iconSizeSmall : parent.width + text: root.text + font.pixelSize: Theme.fontSizeSmall + isMonospace: root.monospace + color: root.tint + wrapMode: root.monospace ? Text.Wrap : Text.WordWrap + anchors.verticalCenter: parent.verticalCenter + } } } } diff --git a/quickshell/Modules/Settings/Widgets/SettingsRenameDialog.qml b/quickshell/Modules/Settings/Widgets/SettingsRenameDialog.qml new file mode 100644 index 000000000..f9e620aaf --- /dev/null +++ b/quickshell/Modules/Settings/Widgets/SettingsRenameDialog.qml @@ -0,0 +1,114 @@ +import QtQuick +import qs.Common +import qs.Widgets + +// Parent to the modal focus scope so it covers the window; focus returns to the opener on close +Loader { + id: root + + property string title: I18n.tr("Rename") + property string supportingText: "" + property string labelText: I18n.tr("Name") + property string leftIconName: "edit" + property Component aboveField: null + property Component belowField: null + property string text: "" + readonly property bool opened: _opened + + property bool _opened: false + property Item _returnFocusTo: null + + signal accepted(string name) + + anchors.fill: parent + z: 1000 + active: false + onLoaded: item.show() + + function show(initialText) { + if (_opened) + return; + text = initialText ?? ""; + _returnFocusTo = Window.window?.activeFocusItem ?? null; + _opened = true; + if (item) + item.show(); + else + active = true; + } + + function hide() { + if (!_opened) + return; + _opened = false; + if (item) + item.opened = false; + const focusItem = _returnFocusTo; + _returnFocusTo = null; + if (focusItem?.visible && focusItem.enabled) + focusItem.forceActiveFocus(Qt.OtherFocusReason); + } + + sourceComponent: DankDialog { + id: dialog + + embedded: nativeWindow + opened: nativeWindow + maximumWidth: SettingsMetrics.formDialogWidth + surfaceColor: Theme.hostSurface + title: root.title + supportingText: root.supportingText + acceptEnabled: root.text.trim() !== "" + onAccepted: root.accepted(root.text.trim()) + onRejected: root.hide() + onActiveChanged: { + if (!active && !root._opened) + root.active = false; + } + + function show() { + nameInput.text = root.text; + opened = true; + forceActiveFocus(); + nameInput.forceActiveFocus(); + nameInput.selectAll(); + } + + actions: [ + DankButton { + text: I18n.tr("Cancel") + backgroundColor: "transparent" + textColor: Theme.primary + onClicked: dialog.rejected() + }, + DankButton { + text: I18n.tr("Save") + iconName: "check" + enabled: dialog.acceptEnabled + onClicked: dialog.accepted() + } + ] + + Loader { + width: parent.width + visible: status === Loader.Ready && item.visible + sourceComponent: root.aboveField + } + + DankTextField { + id: nameInput + outlined: true + leftIconName: root.leftIconName + labelText: root.labelText + width: parent.width + showClearButton: true + onTextChanged: root.text = text + } + + Loader { + width: parent.width + visible: status === Loader.Ready && item.visible + sourceComponent: root.belowField + } + } +} diff --git a/quickshell/Modules/Settings/Widgets/SettingsReorderRow.qml b/quickshell/Modules/Settings/Widgets/SettingsReorderRow.qml index c8f1e50b4..d78642218 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsReorderRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsReorderRow.qml @@ -42,6 +42,7 @@ SettingsRow { id: handle coordinateItem: root.reorderList + upDownKeysMove: false label: root.title enabled: root.reorderEnabled && (root.reorderList.externalDrag || root.reorderList.count > 1) dragging: root.dragging diff --git a/quickshell/Modules/Settings/Widgets/SettingsRow.qml b/quickshell/Modules/Settings/Widgets/SettingsRow.qml index cc8105cbe..25596c0d8 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsRow.qml @@ -52,13 +52,25 @@ T.Control { readonly property bool hasBody: bodySlot.height > 0 readonly property bool hasText: title !== "" || subtitle !== "" readonly property bool isHighlighted: settingKey !== "" && SettingsSearchService.highlightSection === settingKey + property bool highlighted: isHighlighted readonly property bool isFirstInGroup: _edge(true) readonly property bool isLastInGroup: _edge(false) property real topRadius: isFirstInGroup ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius property real bottomRadius: isLastInGroup ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius property real minHeight: subtitle !== "" ? Theme.listItemTwoLineHeight : Theme.listItemHeight + // Keyboard focus on the row or any control inside it tints the row, so navigation reads without a focus ring + readonly property Item activeItem: Window.activeFocusItem + readonly property bool focusWithin: (activeItem?.visualFocus ?? false) && _contains(activeItem) - signal clicked + function _contains(item) { + for (let p = item; p; p = p.parent) { + if (p === root) + return true; + } + return false; + } + + signal clicked(bool keyboard) signal resetRequested onResetRequested: { @@ -72,7 +84,7 @@ T.Control { Accessible.description: subtitle Accessible.onPressAction: { if (clickable && enabled) - clicked(); + clicked(true); } Keys.onPressed: event => { @@ -82,7 +94,7 @@ T.Control { case Qt.Key_Space: case Qt.Key_Return: case Qt.Key_Enter: - root.clicked(); + root.clicked(true); event.accepted = true; break; } @@ -126,7 +138,7 @@ T.Control { Rectangle { anchors.fill: parent visible: root.paintBackground - color: root.active ? Theme.selectedContainer : root.isHighlighted ? Theme.blend(root.rowColor, Theme.primary, SettingsMetrics.highlightBlend) : root.rowColor + color: root.active ? Theme.selectedContainer : root.highlighted ? Theme.blend(root.rowColor, Theme.primary, SettingsMetrics.highlightBlend) : root.rowColor border.width: Theme.layerOutlineWidth border.color: Theme.outlineMedium topLeftRadius: root.topRadius @@ -137,7 +149,7 @@ T.Control { Rectangle { anchors.fill: parent - visible: !root.paintBackground && (root.active || root.isHighlighted) + visible: !root.paintBackground && (root.active || root.highlighted) color: root.active ? Theme.selectedContainer : SettingsMetrics.rowHighlightColor topLeftRadius: root.topRadius topRightRadius: root.topRadius @@ -148,9 +160,9 @@ T.Control { Rectangle { id: stateLayer anchors.fill: parent - visible: root.clickable + visible: root.clickable || root.focusWithin color: root.contentColor - opacity: !root.enabled ? 0 : (clickControl.down ? Theme.stateLayerPressed : (clickControl.hovered ? Theme.stateLayerHover : 0)) + opacity: !root.enabled ? 0 : (clickControl.down ? Theme.stateLayerPressed : root.focusWithin ? Theme.stateLayerFocus : (clickControl.hovered ? Theme.stateLayerHover : 0)) topLeftRadius: root.topRadius topRightRadius: root.topRadius bottomLeftRadius: root.bottomRadius @@ -194,14 +206,16 @@ T.Control { anchors.fill: parent enabled: root.clickable && root.enabled hoverEnabled: root.clickable - focusPolicy: Qt.ClickFocus + focusPolicy: Qt.NoFocus background: null Accessible.ignored: true onPressedChanged: { - if (pressed) + if (pressed) { + root.forceActiveFocus(Qt.MouseFocusReason); ripple.trigger(pressX, pressY); + } } - onClicked: root.clicked() + onClicked: root.clicked(false) HoverHandler { cursorShape: root.clickable ? Qt.PointingHandCursor : Qt.ArrowCursor diff --git a/quickshell/Modules/Settings/Widgets/SettingsSectionLabel.qml b/quickshell/Modules/Settings/Widgets/SettingsSectionLabel.qml index 1f19ea88d..ae57cdafd 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsSectionLabel.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsSectionLabel.qml @@ -34,8 +34,9 @@ Item { } FocusRing { + id: labelRing radius: Theme.cornerRadiusS + Theme.focusRingOffset - visible: root.activeFocus && root.collapsible + visible: parentFocused && !pointerFocused && root.collapsible } signal toggleRequested @@ -104,7 +105,12 @@ Item { StateLayer { stateColor: Theme.primary cornerRadius: parent.radius - onClicked: root.toggleRequested() + focused: labelRing.visible + onClicked: { + labelRing.pointerFocused = true; + root.forceActiveFocus(Qt.MouseFocusReason); + root.toggleRequested(); + } } } } diff --git a/quickshell/Modules/Settings/Widgets/SettingsSidebarItem.qml b/quickshell/Modules/Settings/Widgets/SettingsSidebarItem.qml index d0a0ded67..10d68cc62 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsSidebarItem.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsSidebarItem.qml @@ -12,20 +12,20 @@ Rectangle { property bool active: false property bool highlighted: false - signal clicked + signal clicked(bool keyboard) activeFocusOnTab: enabled Accessible.role: Accessible.Button Accessible.name: title Accessible.description: hint - Accessible.onPressAction: clicked() + Accessible.onPressAction: clicked(true) Keys.onPressed: event => { switch (event.key) { case Qt.Key_Space: case Qt.Key_Return: case Qt.Key_Enter: - root.clicked(); + root.clicked(true); event.accepted = true; break; } @@ -83,7 +83,7 @@ Rectangle { bottomLeftRadius: root.bottomRadius bottomRightRadius: root.bottomRadius color: Theme.surfaceText - opacity: mouseArea.pressed ? Theme.stateLayerPressed : (mouseArea.containsMouse || root.highlighted ? Theme.stateLayerHover : 0) + opacity: mouseArea.pressed ? Theme.stateLayerPressed : focusRing.visible ? Theme.stateLayerFocus : (mouseArea.containsMouse || root.highlighted ? Theme.stateLayerHover : 0) Behavior on opacity { enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None @@ -169,7 +169,20 @@ Rectangle { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onPressed: mouse => ripple.trigger(mouse.x, mouse.y) - onClicked: root.clicked() + onPressed: mouse => { + focusRing.pointerFocused = true; + root.forceActiveFocus(Qt.MouseFocusReason); + ripple.trigger(mouse.x, mouse.y); + } + onClicked: root.clicked(false) + } + + FocusRing { + id: focusRing + anchors.margins: Theme.focusRingWidth / 2 + topLeftRadius: Math.max(0, root.topRadius - Theme.focusRingWidth / 2) + topRightRadius: topLeftRadius + bottomLeftRadius: Math.max(0, root.bottomRadius - Theme.focusRingWidth / 2) + bottomRightRadius: bottomLeftRadius } } diff --git a/quickshell/Modules/Settings/Widgets/SettingsSliderRow.qml b/quickshell/Modules/Settings/Widgets/SettingsSliderRow.qml index c9c8a8b74..021c54382 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsSliderRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsSliderRow.qml @@ -65,6 +65,7 @@ SettingsRow { DankSlider { id: slider + upDownKeysStep: false Accessible.name: root.text Accessible.description: root.description size: "s" diff --git a/quickshell/Modules/Settings/Widgets/SettingsSplitRow.qml b/quickshell/Modules/Settings/Widgets/SettingsSplitRow.qml index 23ebbe6a7..788dc503f 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsSplitRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsSplitRow.qml @@ -6,35 +6,37 @@ SettingsRow { id: root property bool checked: false + property bool navigable: true - signal navigated + signal navigated(bool keyboard) signal toggled(bool checked) - clickable: true - onClicked: navigated() + clickable: navigable + onClicked: keyboard => navigated(keyboard) DankIcon { name: "chevron_right" size: Theme.iconSize color: Theme.surfaceVariantText rotation: I18n.isRtl ? 180 : 0 + visible: root.navigable anchors.verticalCenter: parent.verticalCenter } - Rectangle { - width: Theme.dividerWidth - height: SettingsMetrics.splitDividerHeight - color: Theme.outlineVariant - anchors.verticalCenter: parent.verticalCenter + SettingsDivider { + vertical: true + visible: root.navigable } Item { width: Theme.spacingS height: parent.height + visible: root.navigable } DankToggle { hideText: true + text: root.title checked: root.checked enabled: root.enabled onToggled: value => root.toggled(value) diff --git a/quickshell/Modules/Settings/Widgets/SettingsSwatchGrid.qml b/quickshell/Modules/Settings/Widgets/SettingsSwatchGrid.qml index 76980cefa..5bce9119f 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsSwatchGrid.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsSwatchGrid.qml @@ -13,7 +13,10 @@ Flow { property bool compact: false property real maxHeight: 0 property real minTileWidth: compact ? Theme.minimumTouchTargetSize + Theme.spacingM : SettingsMetrics.swatchTileMinWidth + // option => {preview, badge, primary, secondary, tertiary}, re-evaluated per tile so `options` stays stable across theme changes + property var resolveOption: null signal selected(string value) + signal deleteRequested(string value) readonly property int columns: Math.max(1, Math.floor((width + spacing) / (minTileWidth + spacing))) readonly property real tileWidth: Math.floor((width - spacing * (columns - 1)) / columns) @@ -31,54 +34,114 @@ Flow { id: tile required property var modelData + readonly property var option: root.resolveOption ? Object.assign({}, modelData, root.resolveOption(modelData)) : modelData readonly property bool isActive: root.currentValue === modelData.value + readonly property bool hasPreview: !root.compact && !!option.preview + readonly property bool deletable: !!modelData.deletable width: root.tileWidth - height: root.compact ? root.compactTileHeight : swatch.height + label.implicitHeight + Theme.spacingS * 3 + height: root.compact ? root.compactTileHeight : media.height + label.implicitHeight + Theme.spacingS * 3 radius: Theme.cornerRadiusM color: Theme.floatingWindowFieldColor border.width: isActive ? Theme.outlineWidthFocused : Theme.layerOutlineWidth border.color: isActive ? Theme.primary : Theme.outlineMedium activeFocusOnTab: true - Accessible.role: Accessible.RadioButton + Accessible.role: Accessible.Button Accessible.name: modelData.label + Accessible.checkable: true Accessible.checked: isActive Accessible.onPressAction: root.selected(modelData.value) Keys.onSpacePressed: root.selected(modelData.value) + Keys.onEnterPressed: root.selected(modelData.value) Keys.onReturnPressed: root.selected(modelData.value) - - FocusRing {} - - Rectangle { - anchors.fill: parent - radius: parent.radius - color: Theme.primary - opacity: tileMouse.containsMouse ? Theme.stateLayerHover : 0 + Keys.onDeletePressed: event => { + if (!tile.deletable) + return; + root.deleteRequested(tile.modelData.value); + event.accepted = true; } - DankPaletteSwatch { - id: swatch - width: Theme.minimumTouchTargetSize - height: Theme.minimumTouchTargetSize + FocusRing { + id: tileRing + } + + StateLayer { + id: tileState + anchors.fill: parent + stateColor: Theme.primary + focused: tileRing.visible + tooltipText: root.compact ? tile.modelData.label : null + onClicked: { + tileRing.pointerFocused = true; + tile.forceActiveFocus(Qt.MouseFocusReason); + root.selected(tile.modelData.value); + } + } + + Item { + id: media anchors.top: parent.top anchors.topMargin: Theme.spacingS anchors.horizontalCenter: parent.horizontalCenter - primaryColor: tile.modelData.primary - secondaryColor: tile.modelData.secondary ?? tile.modelData.primary - tertiaryColor: tile.modelData.tertiary ?? tile.modelData.secondary ?? tile.modelData.primary + width: tile.hasPreview ? parent.width - Theme.spacingS * 2 : Theme.minimumTouchTargetSize + height: tile.hasPreview ? Math.round(width * SettingsMetrics.choiceCardPreviewRatio) : Theme.minimumTouchTargetSize + + Rectangle { + anchors.fill: parent + visible: tile.hasPreview + radius: Theme.cornerRadiusS + color: Theme.foregroundColor(Theme.chipSurfaceNested, true) + clip: true + + Image { + id: previewImage + anchors.fill: parent + source: tile.hasPreview ? "file://" + tile.option.preview : "" + fillMode: Image.PreserveAspectFit + asynchronous: true + smooth: true + mipmap: true + } + } + + DankPaletteSwatch { + anchors.centerIn: parent + width: Theme.minimumTouchTargetSize + height: Theme.minimumTouchTargetSize + visible: !tile.hasPreview || previewImage.status !== Image.Ready + primaryColor: tile.option.primary + secondaryColor: tile.option.secondary ?? tile.option.primary + tertiaryColor: tile.option.tertiary ?? tile.option.secondary ?? tile.option.primary + } } - DankTooltipHost { - text: root.compact ? tile.modelData.label : null - target: tile - hoverArea: tileMouse + DankBadge { + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: Theme.spacingXS + visible: !root.compact && !!tile.option.badge + text: tile.option.badge ?? "" + color: Theme.secondaryContainer + textColor: Theme.onSecondaryContainer + } + + DankActionButton { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Theme.spacingXS + visible: tile.deletable && !root.compact && (tile.activeFocus || activeFocus || hovered || tileState.containsMouse) + iconName: "delete" + iconColor: Theme.error + backgroundColor: Theme.floatingWindowFieldColor + tooltipText: I18n.tr("Delete") + onClicked: root.deleteRequested(tile.modelData.value) } StyledText { id: label visible: !root.compact - anchors.top: swatch.bottom + anchors.top: media.bottom anchors.topMargin: Theme.spacingS anchors.left: parent.left anchors.right: parent.right @@ -93,14 +156,6 @@ Flow { maximumLineCount: 2 elide: Text.ElideRight } - - MouseArea { - id: tileMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: root.selected(tile.modelData.value) - } } } } diff --git a/quickshell/Modules/Settings/Widgets/SettingsToggleRow.qml b/quickshell/Modules/Settings/Widgets/SettingsToggleRow.qml index b48f3a73c..5e9362122 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsToggleRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsToggleRow.qml @@ -25,7 +25,7 @@ SettingsRow { Accessible.role: Accessible.CheckBox Accessible.checkable: true Accessible.checked: checked - Accessible.onToggleAction: root.clicked() + Accessible.onToggleAction: root.clicked(true) onClicked: { if (!enabled || toggling) return; diff --git a/quickshell/Modules/Settings/WidgetsTab.qml b/quickshell/Modules/Settings/WidgetsTab.qml index dc5c0f684..ea52bdbfa 100644 --- a/quickshell/Modules/Settings/WidgetsTab.qml +++ b/quickshell/Modules/Settings/WidgetsTab.qml @@ -127,6 +127,12 @@ Item { return; var ctrl = (event.modifiers & Qt.ControlModifier) !== 0; if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) { + var focusedRow = focusedWidgetRow(); + if (focusedRow) { + highlightedSection = focusedRow.section; + highlightedId = focusedRow.id; + } + widgetsTab.forceActiveFocus(); var dir = event.key === Qt.Key_Down ? 1 : -1; if (ctrl) { if (highlightedId !== "") @@ -154,11 +160,16 @@ Item { if (highlightedId !== "") moveAcrossSections(highlightedSection, highlightedId, event.key === Qt.Key_Right ? 1 : -1); event.accepted = true; - } else if (event.key === Qt.Key_Space || event.key === Qt.Key_Return) { + } else if (event.key === Qt.Key_Space) { if (highlightedId !== "") { toggleHighlighted(); event.accepted = true; } + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + if (highlightedId !== "") { + configureHighlighted(); + event.accepted = true; + } } } @@ -380,6 +391,23 @@ Item { SettingsData.updateBarConfig(selectedBarId, updates); } + // A row reached with Tab has focus but no cursor, so the cursor starts from it + function focusedWidgetRow() { + for (var item = Window.activeFocusItem; item; item = item.parent) { + if (item.reorderList === undefined || !item.modelData?.id) + continue; + for (var section = item.parent; section; section = section.parent) { + if (section.sectionId !== undefined) + return { + "section": section.sectionId, + "id": item.modelData.id + }; + } + return null; + } + return null; + } + function flatList() { var out = []; ["left", "center", "right"].forEach(s => { @@ -425,6 +453,15 @@ Item { handleItemEnabledChanged(highlightedSection, highlightedId, !en); } + function configureHighlighted() { + const index = getWidgetsForSection(highlightedSection).findIndex(x => (typeof x === "string" ? x : x.id) === highlightedId); + if (index < 0) + return; + if (!BarWidgetCatalog.configurable(getItemsForSection(highlightedSection)[index])) + return; + configureWidget(highlightedSection, index); + } + function handleSpacerSizeChanged(sectionId, widgetIndex, newSize) { var widgets = getWidgetsForSection(sectionId).slice(); if (widgetIndex < 0 || widgetIndex >= widgets.length) @@ -536,6 +573,10 @@ Item { SettingsPage { id: mainColumn + TapHandler { + onTapped: widgetsTab.forceActiveFocus() + } + SettingsCard { iconName: "toolbar" title: I18n.tr("Bar") @@ -543,6 +584,7 @@ Item { SettingsRow { body: DankButtonGroup { + arrowKeysSelect: false id: barSelectorGroup width: parent.width model: SettingsData.barConfigs.map(cfg => cfg.name || ("Bar " + (SettingsData.barConfigs.indexOf(cfg) + 1))) diff --git a/quickshell/Modules/Settings/WidgetsTabSection.qml b/quickshell/Modules/Settings/WidgetsTabSection.qml index c88937685..9a9eff47a 100644 --- a/quickshell/Modules/Settings/WidgetsTabSection.qml +++ b/quickshell/Modules/Settings/WidgetsTabSection.qml @@ -69,10 +69,10 @@ Column { required property var modelData - readonly property bool configurable: BarWidgetCatalog.hasOptions(modelData.id) || !!modelData.pluginId - readonly property bool highlighted: root.highlightedId === modelData.id && root.highlightedSection === root.sectionId + readonly property bool configurable: BarWidgetCatalog.configurable(modelData) reorderList: reorderArea + highlighted: !dragging && root.highlightedId === modelData.id && root.highlightedSection === root.sectionId opacity: dragging && reorderArea.crossSectionActive ? 0 : 1 title: modelData.text iconName: modelData.icon @@ -125,12 +125,9 @@ Column { anchors.verticalCenter: parent.verticalCenter } - Rectangle { - width: Theme.dividerWidth - height: SettingsMetrics.splitDividerHeight - color: Theme.outlineVariant + SettingsDivider { + vertical: true visible: widgetRow.configurable - anchors.verticalCenter: parent.verticalCenter } DankNumberStepper { @@ -158,20 +155,6 @@ Column { anchors.verticalCenter: parent.verticalCenter onClicked: root.removeWidget(root.sectionId, widgetRow.index) } - - Rectangle { - parent: widgetRow - anchors.fill: parent - anchors.margins: Theme.focusRingWidth / 2 - topLeftRadius: Math.max(0, widgetRow.topRadius - Theme.focusRingWidth / 2) - topRightRadius: topLeftRadius - bottomLeftRadius: Math.max(0, widgetRow.bottomRadius - Theme.focusRingWidth / 2) - bottomRightRadius: bottomLeftRadius - color: "transparent" - border.width: Theme.focusRingWidth - border.color: Theme.focusRingColor - visible: widgetRow.highlighted && !widgetRow.dragging - } } } diff --git a/quickshell/Modules/Settings/WindowRulesTab.qml b/quickshell/Modules/Settings/WindowRulesTab.qml index 9e4efe1ce..39ada45dd 100644 --- a/quickshell/Modules/Settings/WindowRulesTab.qml +++ b/quickshell/Modules/Settings/WindowRulesTab.qml @@ -67,6 +67,16 @@ Item { return [rule.matchCriteria || {}]; } + function actionChips(actions) { + const a = actions || {}; + return Object.keys(a).filter(k => a[k] !== undefined && a[k] !== null && a[k] !== "").map(k => { + const label = root.actionLabels[k] || k; + if (typeof a[k] === "boolean") + return a[k] ? label : label + ": " + I18n.tr("Off"); + return label + ": " + a[k]; + }); + } + function formatCriteria(obj, labels) { let out = []; const keys = Object.keys(obj || {}); @@ -333,6 +343,7 @@ Item { DankDropdown { id: windowSelector + downKeyOpens: false anchors.verticalCenter: parent.verticalCenter dropdownWidth: Math.min(400, createRuleRow.width - SettingsMetrics.rowPaddingH * 2) compactMode: true @@ -373,10 +384,9 @@ Item { DankIcon { name: "select_window" - size: 40 + size: Theme.iconSizeLarge color: Theme.surfaceVariantText anchors.horizontalCenter: parent.horizontalCenter - opacity: 0.5 } StyledText { @@ -442,35 +452,15 @@ Item { Repeater { id: actionRepeater - model: { - const actions = ruleRow.liveRuleData.actions || {}; - const labels = root.actionLabels; - return Object.keys(actions).filter(key => actions[key] !== undefined && actions[key] !== null && actions[key] !== "").map(key => { - const value = actions[key]; - if (typeof value === "boolean") - return value ? (labels[key] || key) : (labels[key] || key) + ": " + I18n.tr("Off"); - return (labels[key] || key) + ": " + value; - }); - } + model: root.actionChips(ruleRow.liveRuleData.actions) - delegate: Rectangle { + delegate: DankBadge { required property string modelData - - width: Math.min(parent?.width ?? 0, chipText.implicitWidth + Theme.spacingS * 2) - height: chipText.height + Theme.spacingXS * 2 - radius: Theme.cornerRadiusS + maximumWidth: parent?.width ?? 0 + text: modelData color: Theme.primaryContainer - - StyledText { - id: chipText - anchors.centerIn: parent - width: Math.min(implicitWidth, parent.width - Theme.spacingS * 2) - text: modelData - font.weight: Theme.fontWeightMedium - font.pixelSize: Theme.fontSizeSmall - color: Theme.onPrimaryContainer - wrapMode: Text.Wrap - } + textColor: Theme.onPrimaryContainer + tooltipText: truncated ? modelData : null } } } @@ -536,25 +526,21 @@ Item { visible: externalCard.sourceFile.length > 0 anchors.verticalCenter: parent.verticalCenter text: externalCard.sourceFile - color: Theme.withAlpha(Theme.surfaceVariantText, 0.15) + color: Theme.floatingWindowFieldColor textColor: Theme.surfaceVariantText } DankIcon { name: externalCard.expanded ? "expand_less" : "expand_more" - size: 20 + size: Theme.iconSize color: Theme.surfaceVariantText anchors.verticalCenter: parent.verticalCenter } DankActionButton { - buttonSize: 28 iconName: "content_copy" - iconSize: 16 - backgroundColor: "transparent" iconColor: Theme.surfaceVariantText enabled: !root.readOnly - opacity: enabled ? 1 : 0.5 anchors.verticalCenter: parent.verticalCenter tooltipText: I18n.tr("Convert to DMS") tooltipSide: "left" @@ -572,22 +558,15 @@ Item { visible: externalCard.hasActions Repeater { - model: { - const a = externalCard.modelData.actions || {}; - const labels = root.actionLabels; - return Object.keys(a).filter(k => a[k] !== undefined && a[k] !== null && a[k] !== "").map(k => { - const val = a[k]; - if (typeof val === "boolean") - return val ? (labels[k] || k) : (labels[k] || k) + ": " + I18n.tr("Off"); - return (labels[k] || k) + ": " + val; - }); - } + model: root.actionChips(externalCard.modelData.actions) delegate: DankBadge { required property string modelData + maximumWidth: parent?.width ?? 0 text: modelData - color: Theme.withAlpha(Theme.primary, 0.15) - textColor: Theme.primary + color: Theme.primaryContainer + textColor: Theme.onPrimaryContainer + tooltipText: truncated ? modelData : null } } } diff --git a/quickshell/Services/PluginService.qml b/quickshell/Services/PluginService.qml index 462a504fa..775969dad 100644 --- a/quickshell/Services/PluginService.qml +++ b/quickshell/Services/PluginService.qml @@ -469,6 +469,15 @@ Singleton { } function updatePlugin(pluginId, callback) { + // Installed entries carry the registry's requires_dms, i.e. the version an update would fetch + const installed = (DMSService.installedPlugins || []).find(plugin => plugin.id === pluginId); + if (installed?.requires_dms && !checkPluginCompatibility(installed.requires_dms)) { + if (callback) + callback({ + error: I18n.tr("Requires DMS %1", "plugin incompatibility notice, %1 is the required DMS version").arg(installed.requires_dms) + }); + return; + } DMSService.update(pluginId, response => { if (!response.error) forceRescanPlugin(pluginId); diff --git a/quickshell/Widgets/DankColorButton.qml b/quickshell/Widgets/DankColorButton.qml new file mode 100644 index 000000000..e47ce9bdc --- /dev/null +++ b/quickshell/Widgets/DankColorButton.qml @@ -0,0 +1,3 @@ +import qs.DankCommon.Widgets as DankCommon + +DankCommon.DankColorButton {} diff --git a/quickshell/Widgets/DankDetailChip.qml b/quickshell/Widgets/DankDetailChip.qml new file mode 100644 index 000000000..c570cd704 --- /dev/null +++ b/quickshell/Widgets/DankDetailChip.qml @@ -0,0 +1,3 @@ +import qs.DankCommon.Widgets as DankCommon + +DankCommon.DankDetailChip {} diff --git a/quickshell/Widgets/FocusRing.qml b/quickshell/Widgets/FocusRing.qml index fac6df179..4ebad6ca5 100644 --- a/quickshell/Widgets/FocusRing.qml +++ b/quickshell/Widgets/FocusRing.qml @@ -1,3 +1,14 @@ import qs.DankCommon.Widgets as DankCommon -DankCommon.FocusRing {} +DankCommon.FocusRing { + // Plain Items have no visualFocus; a tile sets this on its own press so a mouse click draws no ring + property bool pointerFocused: false + readonly property bool parentFocused: parent?.activeFocus ?? false + + visible: parentFocused && !pointerFocused + + onParentFocusedChanged: { + if (!parentFocused) + pointerFocused = false; + } +} diff --git a/quickshell/tests/plugin-reload.test.mjs b/quickshell/tests/plugin-reload.test.mjs index 17af83ca3..c341ab09d 100644 --- a/quickshell/tests/plugin-reload.test.mjs +++ b/quickshell/tests/plugin-reload.test.mjs @@ -24,7 +24,8 @@ function service({ enabled = true, type = "widget" } = {}) { log: { error() {}, warn() {} }, Paths: { toFileUrl: path => path.startsWith("file://") ? path : "file://" + path }, SettingsData: { getPluginSetting: () => enabled }, - DMSService: { update: (id, callback) => requests.push(callback) }, + DMSService: { update: (id, callback) => requests.push(callback), installedPlugins: [] }, + ShellVersionService: { checkVersionRequirement: requirement => !requirement.startsWith(">=99"), getParsedShellVersion: () => ({}) }, Component: { Error: 3, PreferSynchronous: 0 }, I18n: { tr: text => ({ arg: value => text.replace("%1", value) }) }, ToastService: { showError() {} }, @@ -67,6 +68,19 @@ test("successful updates replace cached components and reread the manifest", () assert.equal(context.availablePlugins.fixture.version, "2.0.0"); }); +test("an update whose registry entry needs a newer shell is refused before the download", () => { + const { context, requests, pending } = service(); + context.DMSService.installedPlugins = [{ id: "fixture", requires_dms: ">=99.0.0" }]; + let response; + context.updatePlugin("fixture", value => response = value); + assert.equal(requests.length, 0); + assert.equal(pending.length, 0); + assert.match(response.error, /99\.0\.0/); + context.DMSService.installedPlugins = [{ id: "fixture", requires_dms: ">=1.0.0" }]; + context.updatePlugin("fixture"); + assert.equal(requests.length, 1); +}); + test("failed updates preserve the running plugin and forward the error", () => { const { context, requests, pending } = service(); const component = context.pluginWidgetComponents.fixture; diff --git a/quickshell/tests/qml/settings-keyboard.qml b/quickshell/tests/qml/settings-keyboard.qml new file mode 100644 index 000000000..775ef73b9 --- /dev/null +++ b/quickshell/tests/qml/settings-keyboard.qml @@ -0,0 +1,521 @@ +import QtQuick +import QtTest +import Quickshell +import Quickshell.Wayland +import qs.Common +import qs.Services +import qs.Modals.Settings +import qs.DankCommon.Common as DC +import qs.Modules.Settings.Widgets +import qs.Widgets + +ShellRoot { + id: root + + property bool failed: false + property int sidebarRequests: 0 + property string sidebarRequestedPage: "" + + function find(item, predicate) { + if (!item) + return null; + if (predicate(item)) + return item; + for (const child of item.children ?? []) { + const found = find(child, predicate); + if (found) + return found; + } + return null; + } + + function inside(ancestor, item) { + for (let p = item; p; p = p.parent) { + if (p === ancestor) + return true; + } + return false; + } + + function check(condition, label) { + if (condition) + return; + failed = true; + console.log("FIXTURE_FAIL " + label); + } + + function waitFor(condition, label) { + try { + input.tryVerify(condition, 20000); + } catch (error) { + throw new Error("timed out: " + label); + } + } + + QtObject { + id: modal + property bool shouldBeVisible: true + property bool isCompactMode: false + property bool menuVisible: true + property bool canGoBack: false + readonly property bool searchFocused: sidebar.searchFocused + readonly property string focusPane: navigation.activePane + readonly property Item modalFocusScope: navigation + property var pageHistory: [] + property int backs: 0 + property int searches: 0 + property int pageFocuses: 0 + function goBack() { + backs++; + } + function focusSearch() { + searches++; + navigation.focusSearch(); + } + function focusSidebar() { + searches++; + navigation.focusSidebar(); + } + function focusCurrentPage() { + pageFocuses++; + navigation.focusContent(); + } + function toggleMenu() { + } + } + + PanelWindow { + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + implicitWidth: 900 + implicitHeight: 700 + + TestCase { + id: input + name: "settings-keyboard" + when: false + } + + SettingsPaneNavigation { + id: navigation + anchors.fill: parent + sidebar: sidebar + content: content + parentModal: modal + + SettingsSidebar { + id: sidebar + width: 300 + height: parent.height + parentModal: modal + onPageRequested: pageId => { + root.sidebarRequests++; + root.sidebarRequestedPage = pageId; + } + } + + SettingsContent { + id: content + anchors.left: sidebar.right + anchors.right: parent.right + height: parent.height + parentModal: modal + } + } + } + + function run() { + waitFor(() => CompositorService.compositor !== "unknown", "compositor detected"); + waitFor(() => content.Window.active, "settings window takes keyboard focus"); + SettingsData.animationDuration = 0; + content.currentPage = "notifications"; + waitFor(() => find(content.currentPageItem, item => item.upDownKeysStep !== undefined && item.visible) !== null, "notifications page loads"); + const page = content.currentPageItem; + const focused = () => content.Window.activeFocusItem; + + page.forceActiveFocus(); + const before = focused(); + input.keyClick(Qt.Key_Down); + const first = focused(); + check(first !== before && inside(page, first), "Down moves focus to a control on the page"); + input.keyClick(Qt.Key_Down); + const second = focused(); + check(second !== first && inside(page, second), "Down moves to the next control"); + input.keyClick(Qt.Key_Up); + check(focused() === first, "Up returns to the previous control"); + + const slider = find(page, item => item.upDownKeysStep === false && item.visible && item.enabled); + slider.forceActiveFocus(Qt.TabFocusReason); + const value = slider.value; + input.keyClick(Qt.Key_Down); + check(slider.value === value && !inside(slider, focused()), "Down leaves a settings slider without stepping it"); + + const dropdown = find(page, item => item.downKeyOpens === false && item.visible && item.enabled); + dropdown.forceActiveFocus(Qt.TabFocusReason); + input.keyClick(Qt.Key_Down); + check(!dropdown.menuOpen && !inside(dropdown, focused()), "Down leaves a settings dropdown without opening it"); + + page.forceActiveFocus(); + input.keyClick(Qt.Key_Escape); + check(modal.searches === 1 && modal.backs === 0, "Escape on a top-level page returns to the sidebar"); + modal.canGoBack = true; + page.forceActiveFocus(); + input.keyClick(Qt.Key_Escape); + input.keyClick(Qt.Key_Left, Qt.AltModifier); + check(modal.backs === 2, "Escape and Alt+Left go back from a sub-page"); + + SettingsData.desktopWidgetInstances = [ + { + id: "a", + widgetType: "desktopClock", + name: "A", + enabled: true + }, + { + id: "b", + widgetType: "desktopClock", + name: "B", + enabled: true + } + ]; + content.currentPage = "desktop_widgets"; + const isHandle = item => item.upDownKeysMove === false && item.visible && item.enabled; + waitFor(() => find(content.currentPageItem, isHandle) !== null, "desktop widgets page shows reorder handles"); + const handle = find(content.currentPageItem, isHandle); + handle.forceActiveFocus(Qt.TabFocusReason); + input.keyClick(Qt.Key_Down); + check(SettingsData.desktopWidgetInstances[0].id === "a" && !handle.activeFocus, "Down leaves a reorder handle without moving its item"); + + const deleteButton = find(content.currentPageItem, item => item.confirming !== undefined && item.visible); + deleteButton.forceActiveFocus(Qt.TabFocusReason); + input.keyClick(Qt.Key_Space); + check(deleteButton.confirming && SettingsData.desktopWidgetInstances.length === 2, "first Space only arms the delete button"); + input.keyClick(Qt.Key_Tab); + check(!deleteButton.confirming, "leaving an armed delete button disarms it"); + deleteButton.forceActiveFocus(Qt.TabFocusReason); + input.keyClick(Qt.Key_Space); + input.keyClick(Qt.Key_Space); + check(SettingsData.desktopWidgetInstances.length === 1, "second Space deletes exactly once"); + + SettingsData.desktopWidgetGroups = [ + { + id: "g", + name: "Group" + } + ]; + const isRename = item => item.iconName === "edit" && item.tooltipText === "Rename" && item.visible; + waitFor(() => find(content.currentPageItem, isRename) !== null, "desktop widgets page shows the group rename button"); + const renameButton = find(content.currentPageItem, isRename); + renameButton.forceActiveFocus(Qt.TabFocusReason); + renameButton.clicked(); + waitFor(() => focused() !== renameButton && focused()?.cursorPosition !== undefined, "rename dialog focuses its field"); + const dialogField = focused(); + let renameDialog = dialogField; + while (renameDialog && renameDialog.Accessible.role !== Accessible.Dialog) + renameDialog = renameDialog.parent; + input.keyClick(Qt.Key_Tab, Qt.ControlModifier); + check(inside(renameDialog, focused()), "pane shortcut stays inside the rename dialog"); + input.keyClick(Qt.Key_F, Qt.ControlModifier); + check(inside(renameDialog, focused()), "Search shortcut does not leave the rename dialog"); + dialogField.forceActiveFocus(); + input.keyClick(Qt.Key_Escape); + check(focused() === renameButton, "closing the rename dialog returns focus to the rename button"); + + SettingsData.barConfigs = [ + { + id: "bar", + enabled: true, + visible: true, + position: 0, + leftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow", "clock"], + centerWidgets: [], + rightWidgets: [] + } + ]; + SettingsUiState.selectedBarId = "bar"; + content.currentPage = "dankbar_widgets"; + const isThirdRow = item => item.reorderList !== undefined && item.modelData?.id === "focusedWindow"; + waitFor(() => find(content.currentPageItem, isThirdRow) !== null, "bar widgets page lists the rows"); + const widgetsTab = find(content.currentPageItem, item => item.focusedWidgetRow !== undefined); + find(content.currentPageItem, isThirdRow).forceActiveFocus(Qt.TabFocusReason); + input.keyClick(Qt.Key_Down); + check(widgetsTab.highlightedSection === "left" && widgetsTab.highlightedId === "clock", "Down from a Tab-focused widget row moves the cursor to the next row"); + + sidebar.focusSearch(); + waitFor(() => focused()?.cursorPosition !== undefined && inside(sidebar, focused()), "sidebar search takes focus"); + input.keyClick(Qt.Key_Tab); + const afterTab = focused(); + check(inside(sidebar, afterTab) && afterTab.hint !== undefined && modal.pageFocuses === 0, "Tab from the sidebar search reaches the first sidebar row"); + + const sidebarScroll = find(sidebar, item => item.contentY !== undefined && typeof item.flick === "function"); + const categoryIds = sidebar.navigableIds(); + const originalSidebarHeight = sidebar.height; + const originalFocusRings = SettingsData.focusRingEnabled; + sidebar.height = 400; + sidebar.currentPage = categoryIds[0]; + sidebar.activeCategoryId = categoryIds[0]; + + function checkCategory(id) { + input.wait(0); + const row = focused(); + check(inside(sidebar, row) && row.modelData?.id === id, "Tab focuses sidebar category " + id); + const top = row.mapToItem(sidebarScroll.contentItem, 0, 0).y; + check(top >= sidebarScroll.contentY && top + row.height <= sidebarScroll.contentY + sidebarScroll.height, "focused sidebar category stays in view: " + id); + return row; + } + + // Rings off is the harder case: focus must still read through the row tint alone + SettingsData.focusRingEnabled = false; + sidebar.focusSearch(); + input.keyClick(Qt.Key_Tab); + const profile = focused(); + check(profile === afterTab && profile.highlighted, "profile row shows keyboard focus"); + input.keyClick(Qt.Key_Tab); + const firstRow = checkCategory(categoryIds[0]); + const ring = find(firstRow, item => item.pointerFocused !== undefined); + check(ring.visible, "sidebar category shows focus with rings off"); + for (const id of categoryIds.slice(1)) { + input.keyClick(Qt.Key_Tab); + checkCategory(id); + } + check(sidebar.currentPage === categoryIds[0] && root.sidebarRequests === 0, "Tab preserves the selected page"); + input.keyClick(Qt.Key_Tab); + check(inside(content, focused()), "Tab leaves the sidebar for page controls"); + input.keyClick(Qt.Key_Backtab, Qt.ShiftModifier); + checkCategory(categoryIds[categoryIds.length - 1]); + for (let i = categoryIds.length - 2; i >= 0; i--) { + input.keyClick(Qt.Key_Backtab, Qt.ShiftModifier); + checkCategory(categoryIds[i]); + } + input.keyClick(Qt.Key_Backtab, Qt.ShiftModifier); + check(focused() === profile && sidebarScroll.contentY === 0, "Shift+Tab reveals the profile row again"); + input.keyClick(Qt.Key_Backtab, Qt.ShiftModifier); + check(focused()?.cursorPosition !== undefined && inside(sidebar, focused()), "Shift+Tab returns to search"); + + input.keyClick(Qt.Key_Tab); + input.keyClick(Qt.Key_Tab); + input.keyClick(Qt.Key_Return); + check(root.sidebarRequests === 1 && root.sidebarRequestedPage === categoryIds[0], "Enter opens the Tab-focused category exactly once"); + waitFor(() => inside(content.currentPageItem, focused()), "category Enter actually enters the page"); + sidebar.focusSearch(); + focused().text = "bar"; + waitFor(() => SettingsSearchService.results.length > 1, "sidebar search finds several results"); + const resultRequests = root.sidebarRequests; + input.keyClick(Qt.Key_Tab, Qt.ControlModifier); + waitFor(() => inside(content.currentPageItem, focused()), "Ctrl+Tab leaves a populated Search without selecting a result"); + check(root.sidebarRequests === resultRequests, "pane switching does not activate Search results"); + input.keyClick(Qt.Key_Backtab, Qt.ControlModifier | Qt.ShiftModifier); + check(sidebar.searchFocused && sidebar.searchActive, "pane switching restores the populated Search"); + input.keyClick(Qt.Key_Tab); + check(sidebar.searchSelectedIndex === 1 && focused()?.cursorPosition !== undefined, "Tab in a populated search advances its result cursor"); + input.keyClick(Qt.Key_Escape); + check(!sidebar.searchActive, "Escape clears search before normal Tab navigation resumes"); + input.keyClick(Qt.Key_Down); + check(sidebar.keyboardHighlightId === categoryIds[1] && focused()?.cursorPosition !== undefined, "Down in empty search browses categories without moving focus"); + input.keyClick(Qt.Key_Tab); + check(sidebar.keyboardHighlightId === categoryIds[2] && focused()?.cursorPosition !== undefined, "Tab advances an existing sidebar row cursor"); + input.keyClick(Qt.Key_Escape); + input.keyClick(Qt.Key_Tab); + check(focused() === afterTab, "clearing the sidebar cursor restores the normal Tab chain"); + sidebar.height = originalSidebarHeight; + SettingsData.focusRingEnabled = originalFocusRings; + + sidebar.focusSearch(); + waitFor(() => focused()?.cursorPosition !== undefined && inside(sidebar, focused()), "sidebar search takes focus again"); + input.keyClick(Qt.Key_Return); + waitFor(() => inside(content.currentPageItem, focused()), "empty Search Enter actually focuses the page"); + check(modal.pageFocuses === 2, "Enter in the empty sidebar search requests page focus once"); + + checkPaneNavigation(categoryIds, afterTab, focused); + } + + function checkPaneNavigation(categoryIds, profile, focused) { + sidebar.focusSearch(); + input.keyClick(Qt.Key_Tab); + input.keyClick(Qt.Key_Down); + check(focused()?.modelData?.id === categoryIds[0], "Down after Tab moves from profile to the first category"); + input.keyClick(Qt.Key_Down); + const category = focused(); + check(category.modelData?.id === categoryIds[1], "Down moves category focus without opening it"); + const requestsBefore = sidebarRequests; + input.keyClick(Qt.Key_Up); + check(focused()?.modelData?.id === categoryIds[0] && sidebarRequests === requestsBefore, "Up changes focus without activating a page"); + input.keyClick(Qt.Key_Down); + input.keyClick(Qt.Key_Tab, Qt.ControlModifier); + waitFor(() => inside(content.currentPageItem, focused()), "Ctrl+Tab transfers actual sidebar focus into content"); + const pageControl = focused(); + input.keyClick(Qt.Key_Backtab, Qt.ControlModifier | Qt.ShiftModifier); + check(focused() === category, "Ctrl+Shift+Tab restores the sidebar row"); + input.keyClick(Qt.Key_Tab, Qt.ControlModifier); + waitFor(() => focused() === pageControl, "Ctrl+Tab restores the content control"); + input.waitForPolish(content.Window.window); + input.mouseClick(category, category.width / 2, category.height / 2); + input.wait(0); + check(focused() === category && sidebarRequests === requestsBefore + 1, "sidebar click activates once and retains actual sidebar focus"); + input.keyClick(Qt.Key_Up); + check(focused()?.modelData?.id === categoryIds[0], "sidebar arrows work after a mouse click"); + input.mouseClick(content, content.width - 2, 2); + waitFor(() => inside(content, focused()) && !inside(content.currentPageItem, focused()), "content background click parks keyboard focus without highlighting a control"); + input.keyClick(Qt.Key_Tab, Qt.ControlModifier); + check(inside(sidebar, focused()), "keyboard pane transfer works after a content background click"); + + content.currentPage = "notifications"; + waitFor(() => find(content.currentPageItem, item => item.upDownKeysStep === false) !== null, "content loads the pointer test page"); + waitFor(() => content.currentPageSettled, "pointer test page is presented"); + const slider = find(content.currentPageItem, item => item.upDownKeysStep === false && item.visible && item.enabled); + input.waitForPolish(content.Window.window); + content._reveal(slider); + input.waitForPolish(content.Window.window); + input.waitForRendering(slider); + sidebar.focusSearch(); + input.mouseClick(slider, slider.width / 2, slider.height / 2); + input.wait(0); + check(inside(slider, focused()), "content click preserves clicked slider focus"); + const sliderValue = slider.value; + input.keyClick(Qt.Key_Down); + check(!inside(slider, focused()) && slider.value === sliderValue, "content arrows work after clicking a slider"); + input.keyClick(Qt.Key_F, Qt.ControlModifier); + check(sidebar.searchFocused, "Ctrl+F restores actual Search focus"); + const pageScroll = find(content.currentPageItem, item => item.contentY !== undefined && typeof item.flick === "function"); + pageScroll.contentY = Math.max(0, pageScroll.contentHeight - pageScroll.height); + const pointerScroll = pageScroll.contentY; + input.mouseClick(content, content.width - 2, 2); + waitFor(() => inside(content, focused()) && !inside(content.currentPageItem, focused()), "background click parks focus after scrolling"); + check(Math.abs(pageScroll.contentY - pointerScroll) < 1, "background click does not jump the page"); + input.keyClick(Qt.Key_Down); + check(inside(content.currentPageItem, focused()) && pageScroll.contentY < pointerScroll, "the next arrow resumes at the remembered control and reveals it"); + checkPointerControls(focused); + checkKeyCapture(focused); + modal.focusCurrentPage(); + modal.focusSearch(); + input.wait(0); + check(sidebar.searchFocused, "a later sidebar interaction cancels deferred content focus"); + } + + function checkKeyCapture(focused) { + content.currentPage = "lock_screen"; + waitFor(() => content.currentPageSettled && find(content.currentPageItem, item => item.capturing !== undefined) !== null, "security key capture loads"); + const capture = find(content.currentPageItem, item => item.capturing !== undefined); + capture.forceActiveFocus(Qt.TabFocusReason); + input.keyClick(Qt.Key_Return); + check(capture.capturing, "Enter starts security key capture"); + const backs = modal.backs; + for (const key of [Qt.Key_Up, Qt.Key_Down, Qt.Key_Backspace]) + input.keyClick(key); + check(focused() === capture && capture.capturing && modal.backs === backs, "capture owns arrows and Backspace"); + input.keyClick(Qt.Key_Tab); + input.keyClick(Qt.Key_Backtab, Qt.ShiftModifier); + check(focused() === capture && capture.capturing, "capture consumes forward and reverse Tab traversal"); + content._focusPage(); + input.wait(0); + check(focused() === capture && capture.capturing, "deferred page focus preserves key capture"); + input.keyClick(Qt.Key_Tab, Qt.ControlModifier); + check(focused() === capture && !sidebar.searchFocused, "capture owns the pane shortcut"); + capture.startCapture(); + input.keyClick(Qt.Key_Escape); + check(!capture.capturing && focused() === capture && modal.backs === backs, "Escape ends capture before page navigation"); + modal.focusSearch(); + } + + Component { + id: pointerControls + Column { + objectName: "pointer-controls" + width: 400 + z: 100 + property int toggles: 0 + property int choices: 0 + DankTextField { + objectName: "pointer-text" + width: parent.width + text: "abcd" + } + SettingsToggleRow { + objectName: "pointer-toggle" + width: parent.width + text: "Fixture" + onToggled: value => { + checked = value; + parent.toggles++; + } + } + SettingsSwatchGrid { + objectName: "pointer-swatches" + width: parent.width + compact: true + options: [{value: "a", label: "A", primary: Theme.primary, secondary: Theme.secondary, tertiary: Theme.tertiary}, {value: "b", label: "B", primary: Theme.primary, secondary: Theme.secondary, tertiary: Theme.tertiary}] + onSelected: value => { + currentValue = value; + parent.choices++; + } + } + } + } + + function checkPointerControls(focused) { + const panel = pointerControls.createObject(content.currentPageItem); + input.waitForPolish(content.Window.window); + input.waitForRendering(panel); + const field = find(panel, item => item.objectName === "pointer-text"); + modal.focusSearch(); + input.mouseClick(field, field.width / 2, field.height / 2); + input.wait(0); + check(inside(field, focused()), "clicking a content text field preserves actual field focus"); + const editor = focused(); + editor.cursorPosition = editor.text.length; + const backs = modal.backs; + input.keyClick(Qt.Key_Left); + check(editor.cursorPosition === 3, "text field keeps horizontal caret navigation"); + input.keyClick(Qt.Key_Backspace); + check(editor.text === "abd" && modal.backs === backs, "Backspace edits content text without navigating back"); + const toggle = find(panel, item => item.objectName === "pointer-toggle"); + input.mouseClick(toggle, toggle.width / 2, toggle.height / 2); + input.wait(0); + check(focused() === toggle && panel.toggles === 1, "mouse toggle activates once and retains control focus"); + input.keyClick(Qt.Key_Space); + check(panel.toggles === 2 && !toggle.checked, "Space toggles the clicked control once"); + const rowTint = find(toggle, item => item.parent === toggle && item.color?.toString() === toggle.contentColor.toString()); + check(rowTint.opacity === 0, "a mouse-focused row stays untinted"); + const rings = SettingsData.focusRingEnabled; + SettingsData.focusRingEnabled = false; + modal.focusSearch(); + toggle.forceActiveFocus(Qt.TabFocusReason); + check(rowTint.visible && rowTint.opacity === Theme.stateLayerFocus, "a keyboard-focused row tints with rings off"); + SettingsData.focusRingEnabled = rings; + const tile = find(panel, item => item.modelData?.value === "b"); + input.mouseClick(tile, tile.width / 2, tile.height / 2); + input.wait(0); + check(focused() === tile && panel.choices === 1, "swatch click focuses and applies the actual tile once"); + const tileRing = find(tile, item => item.pointerFocused !== undefined); + const tileTint = find(tile, item => item.stateOpacity !== undefined); + check(!tileRing.visible, "a mouse-focused swatch draws no focus ring"); + input.keyClick(Qt.Key_Return); + check(panel.choices === 2, "Enter acts on the mouse-focused swatch"); + // Focus must actually move: forceActiveFocus keeps the old reason on an already focused item + toggle.forceActiveFocus(Qt.TabFocusReason); + tile.forceActiveFocus(Qt.TabFocusReason); + check(tileRing.visible && tileTint.stateOpacity === Theme.stateLayerFocus, "a keyboard-focused swatch draws its focus ring and tint"); + panel.destroy(); + modal.focusSearch(); + } + + Timer { + interval: 0 + running: true + onTriggered: { + try { + root.run(); + } catch (error) { + root.check(false, error.message); + } + console.log(root.failed ? "FIXTURE_FAIL see above" : "FIXTURE_PASS"); + Qt.quit(); + } + } + + Component.onCompleted: { + Quickshell.watchFiles = false; + DC.Style.theme = Theme; + DC.Style.settings = SettingsData; + DC.I18n.backend = I18n; + } +} diff --git a/quickshell/tests/qml/settings-panes.qml b/quickshell/tests/qml/settings-panes.qml new file mode 100644 index 000000000..3c68aee1e --- /dev/null +++ b/quickshell/tests/qml/settings-panes.qml @@ -0,0 +1,198 @@ +import QtQuick +import QtTest +import Quickshell +import qs.Common +import qs.Services +import qs.Modals.Settings +import qs.DankCommon.Common as DC + +ShellRoot { + id: root + + SettingsModal { + id: modal + } + + TestCase { + id: input + name: "settings-panes" + when: false + } + + function find(item, predicate) { + if (!item) + return null; + if (predicate(item)) + return item; + for (const child of item.children ?? []) { + const found = find(child, predicate); + if (found) + return found; + } + return null; + } + + function inside(ancestor, item) { + for (let p = item; p; p = p.parent) { + if (p === ancestor) + return true; + } + return false; + } + + function check(condition, label) { + if (!condition) + throw new Error(label); + } + + function until(condition, label) { + try { + input.tryVerify(condition, 20000); + } catch (error) { + throw new Error(label); + } + } + + function run() { + until(() => CompositorService.compositor !== "unknown", "compositor detected"); + SettingsData.animationDuration = 0; + modal.show(); + const navigation = modal.modalFocusScope; + const content = navigation.content; + const focused = () => navigation.Window.activeFocusItem; + until(() => modal.visible && navigation.Window.active && modal.searchFocused, "initial Settings load focuses Search"); + input.keyClick(Qt.Key_Tab); + input.keyClick(Qt.Key_Down); + const row = focused(); + check(inside(modal.sidebar, row) && row.modelData?.id, "initial Tab then Down focuses a category"); + input.mouseClick(row, row.width / 2, row.height / 2); + until(() => content.currentPageSettled, "clicked category finishes loading"); + check(focused() === row, "cold mouse category load retains sidebar focus"); + input.keyClick(Qt.Key_Return); + until(() => inside(content.currentPageItem, focused()), "Enter on the selected category enters content"); + + const profile = find(modal.sidebar, item => item.navigationRequested !== undefined); + input.mouseClick(profile, profile.width / 2, profile.height / 2); + check(focused() === profile, "profile click focuses the public navigation row"); + let profileRequests = 0; + modal.sidebar.pageRequested.connect(() => profileRequests++); + input.keyClick(Qt.Key_Return); + check(profileRequests === 1, "Enter on the profile row requests its page exactly once"); + until(() => inside(content.currentPageItem, focused()), "Enter after clicking profile enters its page"); + + modal.setPage("personalization"); + modal.focusCurrentPage(); + until(() => content.currentPageSettled && content.currentPageItem?.hubId === "personalization", "personalization hub loads"); + const group = find(content.currentPageItem, item => item.arrowKeysSelect === false && item.model?.[0]?.icon === "light_mode"); + group.requestFocus(false); + const mode = SessionData.isLightMode; + input.keyClick(Qt.Key_Right); + check(SessionData.isLightMode === mode && focused()?.selected === false, "Light/Dark arrows focus without applying a theme"); + input.keyClick(Qt.Key_Space); + check(SessionData.isLightMode !== mode, "Space applies the focused Light/Dark mode"); + input.keyClick(Qt.Key_Tab, Qt.ControlModifier); + check(inside(modal.sidebar, focused()), "Ctrl+Tab returns to sidebar"); + input.keyClick(Qt.Key_Tab, Qt.ControlModifier); + until(() => inside(group, focused()), "Ctrl+Tab restores the focused Light/Dark choice"); + + const opener = find(content.currentPageItem, item => item.settingKey === "themeNav" && item.clickable); + opener.forceActiveFocus(Qt.TabFocusReason); + content._reveal(opener); + const scroll = find(content.currentPageItem, item => item.contentY !== undefined && typeof item.flick === "function"); + const position = scroll.contentY; + input.keyClick(Qt.Key_Return); + until(() => modal.currentPage === "theme" && content.currentPageSettled && inside(content.currentPageItem, focused()), "keyboard opens subpage and focuses a control"); + input.keyClick(Qt.Key_Left, Qt.AltModifier); + until(() => modal.currentPage === "personalization" && focused() === opener, "Back restores the actual hub opener"); + check(Math.abs(scroll.contentY - position) < 1, "Back preserves hub scroll"); + + // Parked keyboard focus sits on the content anchor, so no control is highlighted + const parked = () => focused()?.parent === content; + input.mouseClick(opener, opener.width / 2, opener.height / 2); + until(() => modal.currentPage === "theme" && content.currentPageSettled && parked(), "mouse-opened subpage parks focus instead of highlighting a control"); + input.keyClick(Qt.Key_Down); + check(inside(content.currentPageItem, focused()), "Down after a mouse-opened subpage enters it"); + input.mouseClick(content, content.width / 2, content.height / 2, Qt.BackButton); + until(() => modal.currentPage === "personalization" && parked(), "mouse Back button returns a page and parks focus"); + input.keyClick(Qt.Key_Down); + check(focused() === opener, "Down after mouse Back resumes at the hub opener"); + + input.mouseClick(row, row.width / 2, row.height / 2); + input.mouseClick(content, content.width / 2, Theme.spacingM); + until(parked, "content background click parks keyboard focus without highlighting a control"); + input.keyClick(Qt.Key_Down); + check(focused() === opener, "Down after a background click resumes at the last control"); + + const hubPage = content.currentPageItem; + modal.setPage("typography"); + modal.focusSearch(); + until(() => modal.currentPage === "typography" && content.currentPageItem !== hubPage && content.currentPageSettled, "typography page loads"); + const textRow = find(content.currentPageItem, item => item.clickable === false && item.hasBody === true); + input.waitForPolish(navigation.Window.window); + input.mouseClick(textRow, textRow.width / 2, textRow.height / 2); + until(parked, "click on a page body row parks keyboard focus in content"); + + SettingsData.barConfigs = [{ + id: "fixture", enabled: true, visible: true, position: 0, + leftWidgets: ["clock"], centerWidgets: [], rightWidgets: [] + }]; + SettingsUiState.selectedBarId = "fixture"; + modal.focusSearch(); + focused().text = "clock"; + const widgetResult = () => find(modal.sidebar, item => item.modelData?.runtimeType === "barWidget" && item.modelData.runtimeId === "clock"); + until(() => widgetResult() !== null, "search finds the existing Clock widget"); + const result = widgetResult(); + modal.sidebar.ensureRowVisible(result); + input.waitForPolish(navigation.Window.window); + const previousPage = content.currentPageItem; + input.mouseClick(result, result.width / 2, result.height / 2); + until(() => modal.currentPage === "bar_widget" && content.currentPageItem !== previousPage && content.currentPageSettled, "mouse search result loads widget options"); + check(inside(modal.sidebar, focused()), "mouse search drilldown retains sidebar focus"); + input.keyClick(Qt.Key_Tab, Qt.ControlModifier); + until(() => inside(content.currentPageItem, focused()), "pane shortcut enters searched widget options"); + modal.focusSearch(); + input.keyClick(Qt.Key_Escape); + modal.setPage("personalization"); + modal.focusCurrentPage(); + until(() => content.currentPageItem?.hubId === "personalization" && content.currentPageSettled, "personalization returns after search drilldown"); + + // The test compositor owns tiled window width; exercise the compact layout directly. + modal.isCompactMode = true; + modal.focusSearch(); + input.keyClick(Qt.Key_Tab, Qt.ControlModifier); + until(() => !modal.menuVisible && inside(content.currentPageItem, focused()), "compact pane shortcut enters visible content"); + input.keyClick(Qt.Key_Tab, Qt.ControlModifier); + check(modal.menuVisible && inside(modal.sidebar, focused()), "compact pane shortcut reveals the sidebar"); + input.keyClick(Qt.Key_Tab); + input.keyClick(Qt.Key_Return); + until(() => !modal.menuVisible && inside(content.currentPageItem, focused()), "compact sidebar activation enters content"); + modal.hide(); + until(() => !content.currentPageItem, "closing Settings releases its pages"); + modal.menuVisible = false; + navigation.activePane = "content"; + modal.show(); + until(() => modal.visible && modal.menuVisible && modal.searchFocused, "compact reopening starts in visible Search"); + modal.hide(); + } + + Timer { + interval: 0 + running: true + onTriggered: { + try { + root.run(); + console.log("FIXTURE_PASS"); + } catch (error) { + console.log("FIXTURE_FAIL " + error.message); + } + Qt.quit(); + } + } + + Component.onCompleted: { + Quickshell.watchFiles = false; + DC.Style.theme = Theme; + DC.Style.settings = SettingsData; + DC.I18n.backend = I18n; + } +}