diff --git a/.agents/skills/dms-plugin-dev/references/dash-plugin-guide.md b/.agents/skills/dms-plugin-dev/references/dash-plugin-guide.md index 3ebe342d7..8f3c33e54 100644 --- a/.agents/skills/dms-plugin-dev/references/dash-plugin-guide.md +++ b/.agents/skills/dms-plugin-dev/references/dash-plugin-guide.md @@ -93,7 +93,7 @@ Injected: `pluginId`, `pluginService`, `popoutService`, `entryId`, `dashHost` (` Base provides: `options`, `pluginData`, `getData(key, def)`, `setData(key, value)`. -Optional: `implicitHeight`, `focusTarget`, `restoreFocus()`, `handleKeyEvent(event)` (return true to consume; Ctrl+Tab is reserved), `blocksTabNavigation`, `menuActions`, `signal tabRequested(string id)`, `signal navFocusRequested`. +Optional: `implicitHeight`, `focusTarget`, `restoreFocus()`, `handleKeyEvent(event)` (return true to consume; Ctrl+Tab and Ctrl+PgUp/PgDown are reserved), `blocksTabNavigation`, `menuActions`, `signal tabRequested(string id)`, `signal navFocusRequested`. For tiles inside a tab, use `Card` from `qs.Modules.DankDash.Overview` so colors, radius and tones match the overview. diff --git a/dux-qml-common b/dux-qml-common index dbf7e6c3d..604ba61ee 160000 --- a/dux-qml-common +++ b/dux-qml-common @@ -1 +1 @@ -Subproject commit dbf7e6c3d4f52e2558da191905f6de5daa32547b +Subproject commit 604ba61ee559f5f6c6dc7e78f9fb8e004a54e686 diff --git a/quickshell/Modules/DDash/DDashPopout.qml b/quickshell/Modules/DDash/DDashPopout.qml index e17c3d329..c032a64cf 100644 --- a/quickshell/Modules/DDash/DDashPopout.qml +++ b/quickshell/Modules/DDash/DDashPopout.qml @@ -8,7 +8,6 @@ import qs.DCommon.Widgets import qs.Widgets import qs.Modules.ControlCenter.Widgets import qs.Modules.DDash.Overview -import "../../DCommon/Common/FocusNavigation.js" as FocusNavigation DPopout { id: root @@ -313,15 +312,8 @@ DPopout { onCanceled: DashMetrics.panelPreview = null } - function headerFocusTargets() { - const targets = root.editMode ? pageActions.focusTargets : [tabBar, pageTitle.focusTarget]; - return targets.filter(item => item.visible && item.enabled); - } - function focusNavigation(backwards) { - const targets = headerFocusTargets(); - const target = backwards ? targets[targets.length - 1] : targets[0]; - target?.forceActiveFocus(backwards ? Qt.BacktabFocusReason : Qt.TabFocusReason); + keyboard.focusHeader(backwards); } function focusInitial() { @@ -339,39 +331,6 @@ DPopout { pages.currentItem?.restoreFocus?.(); } - function contentEntryFor(key) { - switch (key) { - case Qt.Key_Down: - case Qt.Key_Right: - return "forwards"; - case Qt.Key_Up: - case Qt.Key_Left: - return "backwards"; - } - return ""; - } - - function enterContent(backwards) { - const target = backwards ? (pages.currentItem?.previousFocusTarget ?? pages.focusTarget) : pages.focusTarget; - FocusNavigation.focusItem(target, backwards); - } - - function cycleRegion(backwards) { - if (pages.currentHost?.activeFocus) { - if (pages.currentItem?.cycleFocus?.(backwards) === true) - return; - focusNavigation(backwards); - return; - } - const targets = headerFocusTargets(); - const inHeader = targets.some(FocusNavigation.containsFocus); - if (inHeader && FocusNavigation.moveFocus(targets, backwards)) - return; - if (inHeader && pages.currentItem?.cycleFocus?.(backwards) === true) - return; - enterContent(backwards); - } - function resetScroll() { pages.contentY = 0; } @@ -410,159 +369,34 @@ DPopout { focusInitial(); } - Keys.onPressed: function (event) { - if (pageActions.menuOpen) - return; - if (tabOptions.shown) { - if (event.key !== Qt.Key_Escape) - return; - tabOptions.dismiss(); - event.accepted = true; - return; + Keys.onPressed: event => event.accepted = keyboard.handleKeyEvent(event) + + DashKeyboard { + id: keyboard + + active: root.shouldBeVisible + editable: root.activeTabId !== "" + editMode: root.editMode + optionsShown: tabOptions.shown + menuOpen: pageActions.menuOpen + tabItem: pages.currentItem + contentScope: pages.currentHost + contentTarget: pages.focusTarget + headerTargets: root.editMode ? pageActions.focusTargets : [tabBar, pageTitle.focusTarget] + headerEdge: root.verticalNavigation ? (root.navigationEdge === SettingsData.Right ? "right" : "left") : (root.navigationEdge === SettingsData.Bottom ? "down" : "up") + onTabCycleRequested: direction => { + root.cycleTab(direction); + if (!mainContainer.navigationFocused) + mainContainer.focusInitial(); } - - const current = pages.currentItem; - if (current && typeof current.handleKeyEvent === "function" && current.handleKeyEvent(event) === true) { - event.accepted = true; - return; + onEditModeRequested: on => root.editMode = on + onOptionsDismissRequested: tabOptions.dismiss() + onCloseRequested: { + if (root.detailTabId !== "") + root.closeDetail(); + else + root.dashVisible = false; } - - const entry = contentEntryFor(event.key); - if (entry !== "" && !pages.currentHost?.activeFocus) { - enterContent(entry === "backwards"); - event.accepted = true; - return; - } - - if (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab) { - if (root.editMode) { - mainContainer.focusNavigation(event.key === Qt.Key_Backtab || !!(event.modifiers & Qt.ShiftModifier)); - event.accepted = true; - return; - } - if (current?.blocksTabNavigation) - return; - root.cycleTab(event.key === Qt.Key_Backtab || (event.modifiers & Qt.ShiftModifier) ? -1 : 1); - event.accepted = true; - return; - } - if (event.key !== Qt.Key_Escape) - return; - if (root.editMode) { - root.editMode = false; - event.accepted = true; - return; - } - if (root.detailTabId !== "") { - root.closeDetail(); - event.accepted = true; - return; - } - root.dashVisible = false; - event.accepted = true; - } - - Shortcut { - sequence: "Tab" - enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation - context: Qt.WindowShortcut - onActivated: { - root.cycleTab(1); - mainContainer.focusInitial(); - } - } - - Shortcut { - sequence: "Shift+Tab" - enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation - context: Qt.WindowShortcut - onActivated: { - root.cycleTab(-1); - mainContainer.focusInitial(); - } - } - - Shortcut { - sequence: "Ctrl+Tab" - enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen - context: Qt.WindowShortcut - onActivated: { - root.cycleTab(1); - mainContainer.focusInitial(); - } - } - - Shortcut { - sequence: "Ctrl+Shift+Tab" - enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen - context: Qt.WindowShortcut - onActivated: { - root.cycleTab(-1); - mainContainer.focusInitial(); - } - } - - Shortcut { - sequences: ["F2", "Ctrl+E"] - enabled: root.shouldBeVisible && root.activeTabId !== "" && !root.editMode && !tabOptions.shown && !pageActions.menuOpen && !pages.currentItem?.blocksTabNavigation - context: Qt.WindowShortcut - onActivated: root.editMode = true - } - - Shortcut { - sequence: "F6" - enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation - context: Qt.WindowShortcut - onActivated: mainContainer.cycleRegion(false) - } - - Shortcut { - sequence: "Shift+F6" - enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation - context: Qt.WindowShortcut - onActivated: mainContainer.cycleRegion(true) - } - - Shortcut { - sequence: "Alt+Tab" - enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation - context: Qt.WindowShortcut - onActivated: pages.currentItem?.cycleCardFocus?.(false) - } - - Shortcut { - sequence: "Alt+Shift+Tab" - enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation - context: Qt.WindowShortcut - onActivated: pages.currentItem?.cycleCardFocus?.(true) - } - - Shortcut { - sequences: ["Alt+Left", "Alt+H"] - enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation - context: Qt.WindowShortcut - onActivated: pages.currentItem?.moveCardFocus?.("left") - } - - Shortcut { - sequences: ["Alt+Right", "Alt+L"] - enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation - context: Qt.WindowShortcut - onActivated: pages.currentItem?.moveCardFocus?.("right") - } - - Shortcut { - sequences: ["Alt+Up", "Alt+K"] - enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation - context: Qt.WindowShortcut - onActivated: pages.currentItem?.moveCardFocus?.("up") - } - - Shortcut { - sequences: ["Alt+Down", "Alt+J"] - enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation - context: Qt.WindowShortcut - onActivated: pages.currentItem?.moveCardFocus?.("down") } DashOptionsSheet { @@ -634,6 +468,7 @@ DPopout { onEditRequested: root.editMode = true visible: root.showTabs && !root.editMode orientation: root.verticalNavigation ? Qt.Vertical : Qt.Horizontal + tabHighlights: true currentIndex: root.currentTabIndex nextFocusTarget: pageActions.focusTargets[0] ?? pages.focusTarget previousFocusTarget: pages.currentItem?.previousFocusTarget ?? pages.focusTarget diff --git a/quickshell/Modules/DDash/DashKeyboard.qml b/quickshell/Modules/DDash/DashKeyboard.qml new file mode 100644 index 000000000..d36225da4 --- /dev/null +++ b/quickshell/Modules/DDash/DashKeyboard.qml @@ -0,0 +1,261 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import "../../DCommon/Common/FocusNavigation.js" as FocusNavigation + +Item { + id: root + + property bool active: false + property bool editable: true + property bool editMode: false + property bool optionsShown: false + property bool menuOpen: false + property var tabItem: null + property Item contentScope: null + property Item contentTarget: null + property var headerTargets: [] + property string headerEdge: "up" + + readonly property bool idle: active && !optionsShown && !menuOpen + readonly property bool contentFocused: contentScope?.activeFocus ?? false + readonly property bool headerFocused: headerTargets.some(item => item?.activeFocus ?? false) + readonly property bool browsing: idle && !editMode && !(tabItem?.blocksTabNavigation ?? false) + + signal tabCycleRequested(int direction) + signal editModeRequested(bool on) + signal optionsDismissRequested + signal closeRequested + + function visibleHeaderTargets() { + return headerTargets.filter(item => item && item.visible && item.enabled); + } + + function focusHeader(backwards) { + const targets = visibleHeaderTargets(); + const target = backwards ? targets[targets.length - 1] : targets[0]; + target?.forceActiveFocus(backwards ? Qt.BacktabFocusReason : Qt.TabFocusReason); + } + + function enterContent(backwards) { + const target = backwards ? (tabItem?.previousFocusTarget ?? contentTarget) : contentTarget; + FocusNavigation.focusItem(target, backwards); + } + + function cycleRegion(backwards) { + const targets = visibleHeaderTargets(); + if (!targets.some(FocusNavigation.containsFocus) && targets.length > 0) { + focusHeader(backwards); + return; + } + if (FocusNavigation.moveFocus(targets, backwards)) + return; + enterContent(backwards); + } + + function inContent(item) { + for (let p = item; p; p = p.parent) { + if (p === contentScope) + return true; + } + return false; + } + + // Tab wraps inside the content, as in Settings. + function walkContent(backwards) { + if (!contentFocused) { + enterContent(backwards); + return; + } + let item = Window.activeFocusItem; + for (let step = 0; item && step < 1000; step++) { + item = item.nextItemInFocusChain(!backwards); + if (inContent(item)) { + item.forceActiveFocus(backwards ? Qt.BacktabFocusReason : Qt.TabFocusReason); + return; + } + } + } + + // A tab without restoreFocus parks focus on the host, which counts as content. + function toggleRegion() { + if (headerFocused || visibleHeaderTargets().length === 0) + enterContent(false); + else + focusHeader(false); + } + + function moveCard(direction) { + if (!contentFocused) { + if (direction === headerEdge) + focusHeader(false); + else + enterContent(false); + return; + } + if (tabItem?.moveCardFocus?.(direction) !== true && direction === headerEdge) + focusHeader(false); + } + + function arrowDirection(event) { + if ((event.modifiers & ~Qt.KeypadModifier) !== Qt.NoModifier) + return ""; + switch (event.key) { + case Qt.Key_Left: + return "left"; + case Qt.Key_Right: + return "right"; + case Qt.Key_Up: + return "up"; + case Qt.Key_Down: + return "down"; + } + return ""; + } + + function contentEntryFor(key) { + switch (key) { + case Qt.Key_Down: + case Qt.Key_Right: + return "forwards"; + case Qt.Key_Up: + case Qt.Key_Left: + return "backwards"; + } + return ""; + } + + function handleKeyEvent(event) { + if (!active || menuOpen) + return false; + if (optionsShown) { + if (event.key !== Qt.Key_Escape) + return false; + optionsDismissRequested(); + return true; + } + const forTab = !headerFocused || event.key === Qt.Key_Escape; + if (forTab && typeof tabItem?.handleKeyEvent === "function" && tabItem.handleKeyEvent(event) === true) + return true; + const direction = arrowDirection(event); + if (direction !== "" && contentFocused) + return !tabItem?.blocksTabNavigation && tabItem?.moveCardFocus?.(direction) === true; + const entry = contentEntryFor(event.key); + if (entry !== "" && !contentFocused) { + enterContent(entry === "backwards"); + return true; + } + if (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab) { + if (!editMode) + return false; + focusHeader(event.key === Qt.Key_Backtab || !!(event.modifiers & Qt.ShiftModifier)); + return true; + } + if (event.key !== Qt.Key_Escape) + return false; + if (editMode) + editModeRequested(false); + else + closeRequested(); + return true; + } + + // The tab row takes Tab itself to move its highlight. + Shortcut { + sequence: "Tab" + enabled: root.browsing && !root.headerFocused + context: Qt.WindowShortcut + onActivated: root.walkContent(false) + } + + Shortcut { + sequence: "Shift+Tab" + enabled: root.browsing && !root.headerFocused + context: Qt.WindowShortcut + onActivated: root.walkContent(true) + } + + Shortcut { + sequences: ["Ctrl+Tab", "Ctrl+Shift+Tab"] + enabled: root.browsing + context: Qt.WindowShortcut + onActivated: root.toggleRegion() + } + + Shortcut { + sequence: "Ctrl+PgDown" + enabled: root.idle + context: Qt.WindowShortcut + onActivated: root.tabCycleRequested(1) + } + + Shortcut { + sequence: "Ctrl+PgUp" + enabled: root.idle + context: Qt.WindowShortcut + onActivated: root.tabCycleRequested(-1) + } + + Shortcut { + sequences: ["F2", "Ctrl+E"] + enabled: root.browsing && root.editable + context: Qt.WindowShortcut + onActivated: root.editModeRequested(true) + } + + Shortcut { + sequence: "F6" + enabled: root.browsing + context: Qt.WindowShortcut + onActivated: root.cycleRegion(false) + } + + Shortcut { + sequence: "Shift+F6" + enabled: root.browsing + context: Qt.WindowShortcut + onActivated: root.cycleRegion(true) + } + + Shortcut { + sequence: "Alt+Tab" + enabled: root.browsing + context: Qt.WindowShortcut + onActivated: root.tabItem?.cycleCardFocus?.(false) + } + + Shortcut { + sequence: "Alt+Shift+Tab" + enabled: root.browsing + context: Qt.WindowShortcut + onActivated: root.tabItem?.cycleCardFocus?.(true) + } + + Shortcut { + sequences: ["Alt+Left", "Alt+H"] + enabled: root.browsing + context: Qt.WindowShortcut + onActivated: root.moveCard("left") + } + + Shortcut { + sequences: ["Alt+Right", "Alt+L"] + enabled: root.browsing + context: Qt.WindowShortcut + onActivated: root.moveCard("right") + } + + Shortcut { + sequences: ["Alt+Up", "Alt+K"] + enabled: root.browsing + context: Qt.WindowShortcut + onActivated: root.moveCard("up") + } + + Shortcut { + sequences: ["Alt+Down", "Alt+J"] + enabled: root.browsing + context: Qt.WindowShortcut + onActivated: root.moveCard("down") + } +} diff --git a/quickshell/Modules/DDash/Media/BentoChrome.qml b/quickshell/Modules/DDash/Media/BentoChrome.qml index a2b10671f..497c449e9 100644 --- a/quickshell/Modules/DDash/Media/BentoChrome.qml +++ b/quickshell/Modules/DDash/Media/BentoChrome.qml @@ -75,7 +75,17 @@ MediaChromeBase { Rectangle { id: viewTogglePill - readonly property bool keyboardFocused: viewToggle.activeFocus && (root.Window.activeFocusItem?.visualFocus ?? false) + // Tab can focus a segment while the group itself has none. + readonly property bool keyboardFocused: { + const item = root.Window.activeFocusItem; + if (!(item?.visualFocus ?? false)) + return false; + for (let p = item; p; p = p.parent) { + if (p === viewToggle) + return true; + } + return false; + } readonly property bool revealed: artHover.hovered || keyboardFocused anchors.horizontalCenter: parent.horizontalCenter @@ -102,6 +112,7 @@ MediaChromeBase { DButtonGroup { id: viewToggle + focusRingColor: MediaAccentService.focusRing anchors.centerIn: parent size: "small" checkEnabled: false @@ -219,7 +230,10 @@ MediaChromeBase { width: parent.width height: parent.height - controls.y active: root.lyricsColumn && root.player.live - onLoaded: item.forceActiveFocus(Qt.PopupFocusReason) + onLoaded: { + if (root.player.ownsFocus()) + item.forceActiveFocus(Qt.PopupFocusReason); + } sourceComponent: LyricsOverlay { player: root.player @@ -397,6 +411,7 @@ MediaChromeBase { DButton { id: playerButton + focusRingColor: MediaAccentService.focusRing readonly property string panelId: "players" diff --git a/quickshell/Modules/DDash/Media/LyricsOverlay.qml b/quickshell/Modules/DDash/Media/LyricsOverlay.qml index e5080bc43..52a1731e0 100644 --- a/quickshell/Modules/DDash/Media/LyricsOverlay.qml +++ b/quickshell/Modules/DDash/Media/LyricsOverlay.qml @@ -358,6 +358,7 @@ FocusScope { DButton { id: followButton + focusRingColor: MediaAccentService.focusRing objectName: "followPlayback" anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom @@ -431,6 +432,7 @@ FocusScope { } DButton { + focusRingColor: MediaAccentService.focusRing anchors.horizontalCenter: parent.horizontalCenter text: I18n.tr("Retry") visible: root.controller.state === "error" diff --git a/quickshell/Modules/DDash/Media/MaterialChrome.qml b/quickshell/Modules/DDash/Media/MaterialChrome.qml index 822625ecf..4c28281e0 100644 --- a/quickshell/Modules/DDash/Media/MaterialChrome.qml +++ b/quickshell/Modules/DDash/Media/MaterialChrome.qml @@ -132,7 +132,10 @@ MediaChromeBase { anchors.bottom: seekBlock.visible ? seekBlock.top : parent.bottom anchors.bottomMargin: seekBlock.visible ? Theme.spacingM : 0 active: root.player.lyricsOpen && root.player.live - onLoaded: item.forceActiveFocus(Qt.PopupFocusReason) + onLoaded: { + if (root.player.ownsFocus()) + item.forceActiveFocus(Qt.PopupFocusReason); + } sourceComponent: LyricsOverlay { player: root.player } diff --git a/quickshell/Modules/DDash/Media/MaterialSourceGroup.qml b/quickshell/Modules/DDash/Media/MaterialSourceGroup.qml index 8394a5e05..75cf6468c 100644 --- a/quickshell/Modules/DDash/Media/MaterialSourceGroup.qml +++ b/quickshell/Modules/DDash/Media/MaterialSourceGroup.qml @@ -22,6 +22,7 @@ Row { DIconButton { id: lyricsButton + focusRingColor: MediaAccentService.focusRing variant: "outlined" checkable: true checked: root.player.lyricsOpen @@ -67,6 +68,8 @@ Row { component GroupButton: DIconButton { required property string panelId + focusRingColor: MediaAccentService.focusRing + readonly property bool active: root.player.panel === panelId variant: "outlined" diff --git a/quickshell/Modules/DDash/Media/MediaArtView.qml b/quickshell/Modules/DDash/Media/MediaArtView.qml index 1d8377387..4c21ff631 100644 --- a/quickshell/Modules/DDash/Media/MediaArtView.qml +++ b/quickshell/Modules/DDash/Media/MediaArtView.qml @@ -32,7 +32,10 @@ Item { Loader { anchors.fill: parent active: root.showLyrics && root.player.lyricsOpen && root.player.live - onLoaded: item.forceActiveFocus(Qt.PopupFocusReason) + onLoaded: { + if (root.player.ownsFocus()) + item.forceActiveFocus(Qt.PopupFocusReason); + } sourceComponent: LyricsOverlay { player: root.player diff --git a/quickshell/Modules/DDash/Media/MediaDeviceRow.qml b/quickshell/Modules/DDash/Media/MediaDeviceRow.qml index 2f750bcc7..1e66eb0ea 100644 --- a/quickshell/Modules/DDash/Media/MediaDeviceRow.qml +++ b/quickshell/Modules/DDash/Media/MediaDeviceRow.qml @@ -13,6 +13,7 @@ SettingsRow { required property var node property bool selected: false property bool playing: false + readonly property color insetColor: Theme.blend(MediaAccentService.accentContainer, Theme.surface, 0.7) signal activated @@ -27,8 +28,8 @@ SettingsRow { clickable: true paddingH: Theme.spacingL paddingV: Theme.spacingL - rowColor: selected ? Theme.selectedContainer : Theme.chipSurface - titleColor: selected ? Theme.onSelectedContainer : Theme.surfaceText + rowColor: selected ? MediaAccentService.accentContainer : Theme.chipSurface + titleColor: selected ? MediaAccentService.onAccentContainer : Theme.surfaceText subtitleColor: titleColor topRadius: Theme.cornerRadiusLIncreased bottomRadius: Theme.cornerRadiusLIncreased @@ -39,13 +40,13 @@ SettingsRow { width: Theme.iconButtonSize height: width radius: Theme.fullRadius(width, height) - color: root.selected ? Theme.onPrimary : Theme.chipSurfaceNested + color: root.selected ? root.insetColor : Theme.chipSurfaceNested DIcon { anchors.centerIn: parent name: AudioService.sinkIcon(root.node) size: Theme.iconSizeMedium - color: root.selected ? Theme.onSelectedContainer : Theme.onSurfaceVariant + color: root.selected ? root.titleColor : Theme.onSurfaceVariant } } @@ -58,6 +59,7 @@ SettingsRow { spacing: Theme.spacingS DActionButton { + focusRingColor: MediaAccentService.focusRing buttonSize: Theme.iconButtonSize iconName: volumeSlider.volumeIcon iconColor: root.titleColor @@ -73,10 +75,10 @@ SettingsRow { volume: root.node?.audio?.volume ?? 0 muted: root.node?.audio?.muted ?? false maximumVolume: AudioService.getMaxVolumePercent(root.node) - fillColor: Theme.onSelectedContainer - fillTextColor: Theme.selectedContainer - trackColor: Theme.onPrimary - trackTextColor: Theme.onSelectedContainer + fillColor: root.titleColor + fillTextColor: root.rowColor + trackColor: root.insetColor + trackTextColor: root.titleColor Accessible.name: I18n.tr("Volume") + ": " + root.title onVolumeChangedByUser: volume => { const audio = root.node?.audio; diff --git a/quickshell/Modules/DDash/Media/MediaSeekbar.qml b/quickshell/Modules/DDash/Media/MediaSeekbar.qml index 70c3bef2e..7dd8918fd 100644 --- a/quickshell/Modules/DDash/Media/MediaSeekbar.qml +++ b/quickshell/Modules/DDash/Media/MediaSeekbar.qml @@ -15,6 +15,7 @@ DSeekbar { accentColor: root.player.accent accentTrackColor: MediaAccentService.accentTrack accentSubtleColor: MediaAccentService.accentSubtle + focusRingColor: MediaAccentService.focusRing isSeeking: root.player.isSeeking onIsSeekingChanged: root.player.isSeeking = isSeeking } diff --git a/quickshell/Modules/DDash/Media/MediaSourceGroup.qml b/quickshell/Modules/DDash/Media/MediaSourceGroup.qml index ef4c81086..4387ddbe9 100644 --- a/quickshell/Modules/DDash/Media/MediaSourceGroup.qml +++ b/quickshell/Modules/DDash/Media/MediaSourceGroup.qml @@ -44,6 +44,7 @@ Item { DActionButton { id: volumeButton + focusRingColor: MediaAccentService.focusRing readonly property string panelId: "volume" @@ -65,6 +66,7 @@ Item { DButton { id: deviceChip + focusRingColor: MediaAccentService.focusRing readonly property string panelId: "devices" @@ -87,6 +89,7 @@ Item { DActionButton { id: deviceIcon + focusRingColor: MediaAccentService.focusRing readonly property string panelId: "devices" diff --git a/quickshell/Modules/DDash/Media/MediaTransportButton.qml b/quickshell/Modules/DDash/Media/MediaTransportButton.qml index c87293395..fadbcc7b0 100644 --- a/quickshell/Modules/DDash/Media/MediaTransportButton.qml +++ b/quickshell/Modules/DDash/Media/MediaTransportButton.qml @@ -9,6 +9,8 @@ import qs.DCommon.Widgets DIconButton { id: root + focusRingColor: MediaAccentService.focusRing + required property var player required property string mediaAction property bool keepNavigation: false diff --git a/quickshell/Modules/DDash/Media/MediaVolumeSlider.qml b/quickshell/Modules/DDash/Media/MediaVolumeSlider.qml index c8e032c55..16c3e8627 100644 --- a/quickshell/Modules/DDash/Media/MediaVolumeSlider.qml +++ b/quickshell/Modules/DDash/Media/MediaVolumeSlider.qml @@ -19,6 +19,7 @@ DSlider { signal muteRequested size: "m" + focusRingColor: MediaAccentService.focusRing insetIcon: volumeIcon insetIconClickable: true insetIconLabel: muteLabel diff --git a/quickshell/Modules/DDash/MediaPlayerTab.qml b/quickshell/Modules/DDash/MediaPlayerTab.qml index c98f29a52..1ca42be13 100644 --- a/quickshell/Modules/DDash/MediaPlayerTab.qml +++ b/quickshell/Modules/DDash/MediaPlayerTab.qml @@ -174,6 +174,15 @@ Item { lyricsOpen = true; } + // A plain Item's activeFocus stays false while a child holds focus. + function ownsFocus() { + for (let item = Window.activeFocusItem; item; item = item.parent) { + if (item === root) + return true; + } + return false; + } + function toggleLyrics(opener) { if (!lyricsEnabled) return; diff --git a/quickshell/Modules/DDash/Overview/MediaOverviewCard.qml b/quickshell/Modules/DDash/Overview/MediaOverviewCard.qml index d2bf523ff..b8a6205d4 100644 --- a/quickshell/Modules/DDash/Overview/MediaOverviewCard.qml +++ b/quickshell/Modules/DDash/Overview/MediaOverviewCard.qml @@ -154,6 +154,7 @@ Card { DSeekbar { id: seekbar + focusRingColor: MediaAccentService.focusRing anchors.fill: parent activePlayer: root.activePlayer stableLength: MprisController.activePlayerStableLength diff --git a/quickshell/Modules/DIsland/Activities/DashTabFace.qml b/quickshell/Modules/DIsland/Activities/DashTabFace.qml index f1f5a4d98..61beb18fe 100644 --- a/quickshell/Modules/DIsland/Activities/DashTabFace.qml +++ b/quickshell/Modules/DIsland/Activities/DashTabFace.qml @@ -68,24 +68,17 @@ FocusScope { clip: true LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.childrenInherit: true - KeyNavigation.tab: !root.editMode && !tabOptions.shown && !pageActions.menuOpen ? root.tab?.focusTarget ?? null : null function focusFace() { pageActions.clearFocus(); - const target = root.tab?.focusTarget ?? root; - if (typeof target.requestFocus === "function") { - target.requestFocus(false, Qt.OtherFocusReason); - return true; - } - target.forceActiveFocus(Qt.OtherFocusReason); + sheetHandle.focusTarget.focus = false; + tabLoader.focus = false; + root.forceActiveFocus(Qt.OtherFocusReason); + if (!root.editMode) + root.tab?.restoreFocus?.(); return true; } - function focusHeader(backwards) { - const targets = root.editMode ? pageActions.focusTargets : [sheetHandle.focusTarget]; - targets[backwards ? targets.length - 1 : 0]?.forceActiveFocus(backwards ? Qt.BacktabFocusReason : Qt.TabFocusReason); - } - function reportHeight() { root.controller.setDashboardContentHeight(root.activityId, root.contentHeight + DashMetrics.islandHandleChromeHeight); } @@ -98,44 +91,39 @@ FocusScope { pageActions.closeMenu(); tabOptions.dismiss(); } + // A switched-to face stays disabled until its fade is halfway, so take focus then. + onEnabledChanged: { + if (enabled && live && root.controller.keyboardDismissRequested && !activeFocus) + focusFace(); + } onEditModeChanged: { root.controller.setEditing(root.activityId, editMode, DashMetrics.islandEditRoom); - if (!editMode) { + if (!editMode) panelResizer.cancel(); - return; - } Qt.callLater(() => { - if (!root.live || !root.editMode) - return; - pageActions.clearFocus(); - sheetHandle.focusTarget.focus = false; - tabLoader.focus = false; - root.forceActiveFocus(Qt.OtherFocusReason); + if (root.live) + root.focusFace(); }); } - Keys.onPressed: event => { - if (tabOptions.shown || pageActions.menuOpen) - return; - if (root.editMode && (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab)) { - root.focusHeader(event.key === Qt.Key_Backtab || !!(event.modifiers & Qt.ShiftModifier)); - event.accepted = true; - return; - } - if (root.tab?.handleKeyEvent?.(event) === true) { - event.accepted = true; - return; - } - if (!root.editMode && (event.key === Qt.Key_F2 || (event.key === Qt.Key_E && (event.modifiers & Qt.ControlModifier)))) { - root.editMode = true; - event.accepted = true; - return; - } - if (event.key !== Qt.Key_Escape || !root.editMode) - return; - root.editMode = false; - root.focusFace(); - event.accepted = true; + Keys.onPressed: event => event.accepted = keyboard.handleKeyEvent(event) + + DashKeyboard { + id: keyboard + + active: root.live + editable: root.entryId !== "" + editMode: root.editMode + optionsShown: tabOptions.shown + menuOpen: pageActions.menuOpen + tabItem: root.tab + contentScope: tabLoader + contentTarget: root.tab?.focusTarget ?? null + headerTargets: root.editMode ? pageActions.focusTargets : [sheetHandle.focusTarget] + onTabCycleRequested: direction => root.controller.cycleDashTab(direction) + onEditModeRequested: on => root.editMode = on + onOptionsDismissRequested: tabOptions.dismiss() + onCloseRequested: root.controller.requestCollapse() } Item { @@ -172,10 +160,7 @@ FocusScope { editMode: root.editMode panelResizable: true onOptionsRequested: tabOptions.presentFor(root.entryId) - onFinished: { - root.editMode = false; - root.focusFace(); - } + onFinished: root.editMode = false } } @@ -217,7 +202,7 @@ FocusScope { target: root.tab ignoreUnknownSignals: true function onNavFocusRequested(backwards) { - root.focusHeader(backwards); + keyboard.focusHeader(backwards); } } diff --git a/quickshell/Modules/DIsland/IslandController.qml b/quickshell/Modules/DIsland/IslandController.qml index b946f461b..17b674892 100644 --- a/quickshell/Modules/DIsland/IslandController.qml +++ b/quickshell/Modules/DIsland/IslandController.qml @@ -150,6 +150,38 @@ QtObject { return activityId; } + function dashActivityFor(entryId) { + switch (entryId) { + case DashRegistry.overviewId: + return "home"; + case "notifications": + return "notificationcenter"; + case "media": + return mediaAvailable ? "media" : ""; + case "weather": + case "wellbeing": + case "wallpaper": + return entryId; + } + return ""; + } + + function requestDashTab(entryId) { + if (entryId === "notifications") + return requestNotificationCenter(false); + const activityId = dashActivityFor(entryId); + return activityId !== "" && requestActivity(activityId, true, true); + } + + function cycleDashTab(direction) { + const ids = DashRegistry.visibleTabIds.filter(id => dashActivityFor(id) !== ""); + if (ids.length === 0) + return false; + const pos = ids.indexOf(dashEntryIdFor(activeActivity)); + const next = ids[pos < 0 ? (direction > 0 ? 0 : ids.length - 1) : (pos + direction + ids.length) % ids.length]; + return next === dashEntryIdFor(activeActivity) || requestDashTab(next); + } + function dashboardWidthFor(activityId) { return Math.min(dashboardAvailableWidth, DashMetrics.widthFor(SettingsData.showWeekNumber, undefined, DashMetrics.panelColumnsFor(dashEntryIdFor(activityId)))); } diff --git a/quickshell/Services/MediaAccentService.qml b/quickshell/Services/MediaAccentService.qml index 4233f0d87..6e00ae6b5 100644 --- a/quickshell/Services/MediaAccentService.qml +++ b/quickshell/Services/MediaAccentService.qml @@ -65,6 +65,8 @@ Singleton { readonly property color accentTrack: Theme.withAlpha(accent, 0.28) readonly property color accentSubtle: Theme.withAlpha(accent, 0.55) + // A ring colour the user changed from primary wins over the accent. + readonly property color focusRing: Qt.colorEqual(Theme.focusRingColor, Theme.primary) ? accent : Theme.focusRingColor readonly property string artUrl: TrackArtService.resolvedArtUrl property var _accent: _pickAccent(TrackArtService.artwork.colors) diff --git a/quickshell/Widgets/DSeekbar.qml b/quickshell/Widgets/DSeekbar.qml index 1fb5c1240..dc91a9f22 100644 --- a/quickshell/Widgets/DSeekbar.qml +++ b/quickshell/Widgets/DSeekbar.qml @@ -83,8 +83,10 @@ Item { FocusRing { radius: Theme.cornerRadiusXS + Theme.focusRingOffset visible: root.activeFocus + border.color: root.focusRingColor } property color accentColor: Theme.primary + property color focusRingColor: Theme.focusRingColor property color accentTrackColor: Theme.withAlpha(accentColor, 0.28) property color accentSubtleColor: Theme.withAlpha(accentColor, 0.55)