diff --git a/panels/notification/center/GroupNotify.qml b/panels/notification/center/GroupNotify.qml index dbd13c940..c4d0b1fe7 100644 --- a/panels/notification/center/GroupNotify.qml +++ b/panels/notification/center/GroupNotify.qml @@ -24,9 +24,30 @@ NotifyItem { return true } + // Focus the last button for Shift+Tab navigation into group + function focusLastButton() { + groupClearBtn.forceActiveFocus() + return true + } + Control { id: impl anchors.fill: parent + focus: true + + Keys.onTabPressed: function(event) { + if (root.focusFirstButton()) { + event.accepted = true + } else { + root.gotoNextItem() + event.accepted = true + } + } + + Keys.onBacktabPressed: function(event) { + root.gotoPrevItem() + event.accepted = true + } contentItem: RowLayout { NotifyHeaderTitleText { diff --git a/panels/notification/center/NormalNotify.qml b/panels/notification/center/NormalNotify.qml index 197287a1a..64fd89dc3 100644 --- a/panels/notification/center/NormalNotify.qml +++ b/panels/notification/center/NormalNotify.qml @@ -13,6 +13,7 @@ NotifyItem { id: root implicitWidth: impl.implicitWidth implicitHeight: impl.implicitHeight + property bool shouldShowClose: false // True when item gets focus from keyboard navigation signal gotoNextItem() signal gotoPrevItem() @@ -21,9 +22,31 @@ NotifyItem { return notifyContent.focusFirstButton() } + function focusLastButton() { + return notifyContent.focusLastButton() + } + Control { id: impl anchors.fill: parent + focus: true + + Keys.onTabPressed: function(event) { + // Mark that this item got focus from Tab navigation + root.shouldShowClose = true + if (notifyContent.focusFirstButton()) { + event.accepted = true + } else { + root.gotoNextItem() + event.accepted = true + } + } + + Keys.onBacktabPressed: function(event) { + root.shouldShowClose = true + root.gotoPrevItem() + event.accepted = true + } contentItem: NotifyItemContent { id: notifyContent @@ -35,7 +58,8 @@ NotifyItem { date: root.date actions: root.actions defaultAction: root.defaultAction - parentHovered: impl.hovered || root.activeFocus + // Show close button when: mouse hovers, or item has focus from keyboard navigation + parentHovered: impl.hovered || (root.activeFocus && root.shouldShowClose) strongInteractive: root.strongInteractive contentIcon: root.contentIcon contentRowCount: root.contentRowCount diff --git a/panels/notification/center/NotifyViewDelegate.qml b/panels/notification/center/NotifyViewDelegate.qml index 000ce4bca..212252a08 100644 --- a/panels/notification/center/NotifyViewDelegate.qml +++ b/panels/notification/center/NotifyViewDelegate.qml @@ -25,7 +25,10 @@ DelegateChooser { view.positionViewAtIndex(currentIndex + 1, ListView.Contain) Qt.callLater(function() { let nextItem = view.itemAtIndex(currentIndex + 1) - if (nextItem && nextItem.enabled) nextItem.forceActiveFocus() + if (nextItem && nextItem.enabled) { + // Focus on the item itself first, not directly to buttons + nextItem.forceActiveFocus() + } }) } else { view.gotoHeaderFirst() @@ -39,7 +42,10 @@ DelegateChooser { view.positionViewAtIndex(currentIndex - 1, ListView.Contain) Qt.callLater(function() { let prevItem = view.itemAtIndex(currentIndex - 1) - if (prevItem && prevItem.enabled) prevItem.forceActiveFocus() + if (prevItem && prevItem.enabled) { + // Focus on the item itself first, not directly to buttons + prevItem.forceActiveFocus() + } }) } else { view.gotoHeaderLast() @@ -56,27 +62,8 @@ DelegateChooser { activeFocusOnTab: false z: index - Keys.onTabPressed: function(event) { - groupNotify.focusFirstButton() - event.accepted = true - } - Keys.onBacktabPressed: function(event) { - root.navigateToPrevItem(index) - event.accepted = true - } - onGotoNextItem: root.navigateToNextItem(index) - onGotoPrevItem: groupNotify.focusFirstButton() - - Loader { - anchors.fill: parent - active: groupNotify.activeFocus && NotifyAccessor.debugging - - sourceComponent: FocusBoxBorder { - radius: groupNotify.radius - color: groupNotify.palette.highlight - } - } + onGotoPrevItem: root.navigateToPrevItem(index) onCollapse: function () { console.log("collapse group", model.appName) @@ -108,23 +95,8 @@ DelegateChooser { activeFocusOnTab: false z: index - Keys.onTabPressed: function(event) { - if (normalNotify.focusFirstButton()) { - event.accepted = true - return - } - Qt.callLater(function() { - if (normalNotify.focusFirstButton()) return - root.navigateToNextItem(index) - }) - event.accepted = true - } - Keys.onBacktabPressed: function(event) { - root.navigateToPrevItem(index) - event.accepted = true - } onGotoNextItem: root.navigateToNextItem(index) - onGotoPrevItem: normalNotify.forceActiveFocus() + onGotoPrevItem: root.navigateToPrevItem(index) appName: model.appName iconName: model.iconName @@ -138,16 +110,6 @@ DelegateChooser { defaultAction: model.defaultAction indexInGroup: model.indexInGroup - Loader { - anchors.fill: parent - active: normalNotify.activeFocus && NotifyAccessor.debugging - - sourceComponent: FocusBoxBorder { - radius: normalNotify.radius - color: normalNotify.palette.highlight - } - } - TapHandler { acceptedButtons: Qt.RightButton onTapped: function (eventPoint, button) { @@ -191,23 +153,8 @@ DelegateChooser { activeFocusOnTab: false z: index - Keys.onTabPressed: function(event) { - if (overlapNotify.focusFirstButton()) { - event.accepted = true - return - } - Qt.callLater(function() { - if (overlapNotify.focusFirstButton()) return - root.navigateToNextItem(index) - }) - event.accepted = true - } - Keys.onBacktabPressed: function(event) { - root.navigateToPrevItem(index) - event.accepted = true - } onGotoNextItem: root.navigateToNextItem(index) - onGotoPrevItem: overlapNotify.forceActiveFocus() + onGotoPrevItem: root.navigateToPrevItem(index) count: model.overlapCount appName: model.appName @@ -221,15 +168,23 @@ DelegateChooser { contentRowCount: model.contentRowCount enableDismissed: false notifyContent.clearButton: AnimationSettingButton { + id: clearBtn icon.name: "clean-alone" text: qsTr("Clean All") activeFocusOnTab: false focusBorderVisible: activeFocus Keys.onTabPressed: function(event) { + // Try to focus first action button + if (overlapNotify.focusFirstButton()) { + event.accepted = true + return + } + // No enabled action buttons, go to next item overlapNotify.gotoNextItem() event.accepted = true } Keys.onBacktabPressed: function(event) { + // Shift+Tab: go back to previous item (clear button is first in tab order) overlapNotify.gotoPrevItem() event.accepted = true } @@ -238,14 +193,9 @@ DelegateChooser { } } - Loader { - anchors.fill: parent - active: overlapNotify.activeFocus && NotifyAccessor.debugging - - sourceComponent: FocusBoxBorder { - radius: overlapNotify.overlapItemRadius - color: overlapNotify.palette.highlight - } + Component.onCompleted: { + // Pass clear button reference to OverlapNotify + overlapNotify.clearButton = clearBtn } TapHandler { diff --git a/panels/notification/center/OverlapNotify.qml b/panels/notification/center/OverlapNotify.qml index 00fc0f1eb..c5700145b 100644 --- a/panels/notification/center/OverlapNotify.qml +++ b/panels/notification/center/OverlapNotify.qml @@ -17,6 +17,7 @@ NotifyItem { property int count: 1 readonly property int overlapItemRadius: 12 property bool enableDismissed: true + property bool shouldShowClose: false // True when item gets focus from keyboard navigation property var removedCallback property alias notifyContent: notifyContent @@ -24,8 +25,29 @@ NotifyItem { signal gotoNextItem() signal gotoPrevItem() + property var clearButton: null // Reference to the externally defined clear button + function focusFirstButton() { - return notifyContent.focusFirstButton() + // Focus clear button first, then action buttons + if (clearButton && clearButton.enabled && clearButton.visible) { + clearButton.forceActiveFocus() + return true + } + // Try action buttons (skip clear button to avoid loop) + return notifyContent.focusFirstActionOnly() + } + + function focusLastButton() { + // Focus last action button, then clear button + if (notifyContent.focusLastButton()) { + return true + } + // Try clear button + if (clearButton && clearButton.enabled && clearButton.visible) { + clearButton.forceActiveFocus() + return true + } + return false } states: [ @@ -52,6 +74,23 @@ NotifyItem { Control { id: impl anchors.fill: parent + focus: true + + Keys.onTabPressed: function(event) { + root.shouldShowClose = true + if (notifyContent.focusFirstButton()) { + event.accepted = true + } else { + root.gotoNextItem() + event.accepted = true + } + } + + Keys.onBacktabPressed: function(event) { + root.shouldShowClose = true + root.gotoPrevItem() + event.accepted = true + } contentItem: Item { width: parent.width @@ -66,13 +105,13 @@ NotifyItem { date: root.date actions: root.actions defaultAction: root.defaultAction - // Don't override closeVisible - use parentHovered to pass external hover/focus state - parentHovered: impl.hovered || root.activeFocus + // Show close button when: mouse hovers, or item has focus from keyboard navigation + parentHovered: impl.hovered || (root.activeFocus && root.shouldShowClose) strongInteractive: root.strongInteractive contentIcon: root.contentIcon contentRowCount: root.contentRowCount - enableDismissed: root.enableDismissed indexInGroup: root.indexInGroup + enableDismissed: root.enableDismissed onRemove: function () { root.remove() } diff --git a/panels/notification/plugin/NotifyAction.qml b/panels/notification/plugin/NotifyAction.qml index 290e2ed05..a39175420 100644 --- a/panels/notification/plugin/NotifyAction.qml +++ b/panels/notification/plugin/NotifyAction.qml @@ -13,26 +13,57 @@ Control { id: root property var actions: [] + readonly property bool hasEnabledAction: { + if (actions.length === 0) return false + for (let i = 0; i < actions.length; i++) { + if (actions[i] && actions[i].enabled) return true + } + return false + } signal actionInvoked(var actionId) signal gotoNextButton() // Signal to Tab to next button (X button or next notify) signal gotoPrevItem() // Signal to Shift+Tab to previous notify item // Focus the first action button for Tab navigation + // Returns true if an enabled button was found and focused function focusFirstButton() { - if (actions.length > 0) { + if (actions.length > 0 && actions[0] && actions[0].enabled) { firstActionBtn.forceActiveFocus() + return true } + // If first action is disabled, try to find the first enabled action + for (let i = 1; i < actions.length; i++) { + if (actions[i] && actions[i].enabled) { + if (i === 1 && secondActionLoader.item) { + secondActionLoader.item.forceActiveFocus() + return true + } else if (i > 1 && moreActionsLoader.item) { + moreActionsLoader.item.forceActiveFocus() + return true + } + } + } + return false } // Focus the last action button for Shift+Tab navigation function focusLastButton() { - if (actions.length === 2 && secondActionLoader.item) { - secondActionLoader.item.forceActiveFocus() - } else if (actions.length > 2 && moreActionsLoader.item) { - moreActionsLoader.item.forceActiveFocus() - } else if (actions.length > 0) { - firstActionBtn.forceActiveFocus() + // Find the last enabled action button + for (let i = actions.length - 1; i >= 0; i--) { + if (actions[i] && actions[i].enabled) { + if (i === 1 && secondActionLoader.item) { + secondActionLoader.item.forceActiveFocus() + return true + } else if (i > 1 && moreActionsLoader.item) { + moreActionsLoader.item.forceActiveFocus() + return true + } else if (i === 0) { + firstActionBtn.forceActiveFocus() + return true + } + } } + return false } contentItem: RowLayout { @@ -52,12 +83,23 @@ Control { event.accepted = true } Keys.onTabPressed: function(event) { + // Try to go to next action button or next notification item if (actions.length === 1) { + // Only one action button, go to next item root.gotoNextButton() - } else if (actions.length === 2 && secondActionLoader.item) { - secondActionLoader.item.forceActiveFocus() - } else if (actions.length > 2 && moreActionsLoader.item) { - moreActionsLoader.item.forceActiveFocus() + } else if (actions.length === 2) { + // Try to focus second action button + if (secondActionLoader.item && secondActionLoader.item.enabled) { + secondActionLoader.item.forceActiveFocus() + } else { + // Second button is disabled, go to next item + root.gotoNextButton() + } + } else if (actions.length > 2) { + // Try to focus more actions combo + if (moreActionsLoader.item) { + moreActionsLoader.item.forceActiveFocus() + } } event.accepted = true } @@ -75,10 +117,17 @@ Control { actionData: actions.length > 1 ? actions[1] : null activeFocusOnTab: false Keys.onBacktabPressed: function(event) { - firstActionBtn.forceActiveFocus() + // Go back to first action button + if (firstActionBtn.enabled) { + firstActionBtn.forceActiveFocus() + } else { + // First button is disabled, go to previous item + root.gotoPrevItem() + } event.accepted = true } Keys.onTabPressed: function(event) { + // Last action button, go to next item root.gotoNextButton() event.accepted = true } @@ -99,10 +148,17 @@ Control { model: expandActions activeFocusOnTab: false Keys.onBacktabPressed: function(event) { - firstActionBtn.forceActiveFocus() + // Go back to first action button + if (firstActionBtn.enabled) { + firstActionBtn.forceActiveFocus() + } else { + // First button is disabled, go to previous item + root.gotoPrevItem() + } event.accepted = true } Keys.onTabPressed: function(event) { + // Last action button, go to next item root.gotoNextButton() event.accepted = true } diff --git a/panels/notification/plugin/NotifyItemContent.qml b/panels/notification/plugin/NotifyItemContent.qml index edf68b25c..d25ae9c76 100644 --- a/panels/notification/plugin/NotifyItemContent.qml +++ b/panels/notification/plugin/NotifyItemContent.qml @@ -24,36 +24,44 @@ NotifyItem { signal gotoNextItem() // Signal to navigate to next notify item signal gotoPrevItem() // Signal to navigate to previous notify item - // Focus first interactive button (action buttons first, then X button) + // Focus first interactive button (close button first, then action buttons) + // Returns true if a button was successfully focused, false if no enabled button found function focusFirstButton() { - if (actionLoader.item && actionLoader.item.enabled) { - actionLoader.item.focusFirstButton() - return true - } + // Check close button first if (clearLoader.item && clearLoader.item.enabled) { clearLoader.item.forceActiveFocus() return true } - // Retry if clearLoader not yet created - function tryFocusClear(retries) { - if (clearLoader.item && clearLoader.item.enabled) { - clearLoader.item.forceActiveFocus() - } else if (retries > 0) { - Qt.callLater(function() { tryFocusClear(retries - 1) }) - } + // Check action buttons (from left to right) + if (actionLoader.item && actionLoader.item.enabled && actionLoader.item.hasEnabledAction) { + let focused = actionLoader.item.focusFirstButton() + if (focused) return true + } + // No enabled button found - return false so caller can skip to next item + return false + } + + // Focus action buttons only (skip close button) + // Used by OverlapNotify to avoid infinite loop + function focusFirstActionOnly() { + if (actionLoader.item && actionLoader.item.enabled && actionLoader.item.hasEnabledAction) { + let focused = actionLoader.item.focusFirstButton() + if (focused) return true } - Qt.callLater(function() { tryFocusClear(root.maxFocusRetries) }) - return true + return false } - // Focus last interactive button (X button first, then action buttons) + // Focus last interactive button (action buttons first, then close button) function focusLastButton() { + // Check action buttons first (find last enabled action button) + if (actionLoader.item && actionLoader.item.enabled && actionLoader.item.hasEnabledAction) { + let focused = actionLoader.item.focusLastButton() + if (focused) return true + } + // Check close button if (clearLoader.item && clearLoader.item.enabled) { clearLoader.item.forceActiveFocus() return true - } else if (actionLoader.item && actionLoader.item.enabled) { - actionLoader.item.focusLastButton() - return true } return false } @@ -113,15 +121,20 @@ NotifyItem { activeFocusOnTab: false focusBorderVisible: activeFocus Keys.onTabPressed: function(event) { + // Try to focus first action button + if (actionLoader.item && actionLoader.item.enabled && actionLoader.item.hasEnabledAction) { + if (actionLoader.item.focusFirstButton()) { + event.accepted = true + return + } + } + // No enabled action buttons, go to next item root.gotoNextItem() event.accepted = true } Keys.onBacktabPressed: function(event) { - if (actionLoader.item) { - actionLoader.item.focusLastButton() - } else { - root.gotoPrevItem() - } + // Shift+Tab: go back to previous item (close button is first in tab order) + root.gotoPrevItem() event.accepted = true } onClicked: function () { @@ -318,12 +331,8 @@ NotifyItem { root.actionInvoked(actionId) } onGotoNextButton: { - // Navigate to clear button or next notification item - if (clearLoader.item) { - clearLoader.item.forceActiveFocus() - } else { - root.gotoNextItem() - } + // From action buttons, go directly to next notification item + root.gotoNextItem() } onGotoPrevItem: root.gotoPrevItem() }