Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion panels/notification/center/NormalNotify.qml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ NotifyItem {
date: root.date
actions: root.actions
defaultAction: root.defaultAction
closeVisible: impl.hovered || root.activeFocus
parentHovered: impl.hovered || root.activeFocus
strongInteractive: root.strongInteractive
contentIcon: root.contentIcon
contentRowCount: root.contentRowCount
Expand Down
38 changes: 35 additions & 3 deletions panels/notification/center/NotifyCenter.qml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ FocusScope {
property alias viewPanelShown: view.viewPanelShown
property int maxViewHeight: 400
property int stagingViewCount: 0
readonly property int viewCount: view.viewCount

signal gotoStagingLast() // Signal to Shift+Tab to staging last button
signal gotoStagingFirst() // Signal to Tab cycle to staging first item

// Focus header first button (for Tab from staging)
function focusHeaderFirst() {
header.focusFirstButton()
}

// Focus header last button (for Shift+Tab from staging)
function focusHeaderLast() {
header.focusLastButton()
}

// Focus view last item (for Shift+Tab when no staging)
function focusViewLastItem() {
view.focusLastItem()
}

NotifyModel {
id: notifyModel
Expand All @@ -40,8 +59,14 @@ FocusScope {
if (view.viewCount === 0 || !view.focusItemAtIndex(0)) header.focusFirstButton()
}
onGotoLastNotify: {
if (view.viewCount === 0) header.focusLastButton()
else view.focusLastItem()
// First try to go to staging area if it has items
if (root.stagingViewCount > 0) {
root.gotoStagingLast()
} else if (view.viewCount === 0) {
header.focusLastButton()
} else {
view.focusLastItem()
}
}
}

Expand All @@ -58,7 +83,14 @@ FocusScope {

height: Math.min(maxViewHeight, viewHeight)
notifyModel: notifyModel
onGotoHeaderFirst: header.focusFirstButton()
onGotoHeaderFirst: {
// If staging has items, go to staging first; otherwise go to header
if (root.stagingViewCount > 0) {
root.gotoStagingFirst()
} else {
header.focusFirstButton()
}
}
onGotoHeaderLast: header.focusLastButton()
}

Expand Down
79 changes: 78 additions & 1 deletion panels/notification/center/NotifyStaging.qml
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,73 @@ FocusScope {
property var model: notifyModel
readonly property int viewCount: view.count

signal gotoHeaderFirst() // Signal to Tab to header first button
signal gotoHeaderLast() // Signal to Shift+Tab to header last button

// Focus the first item in staging for Tab cycle
function focusFirstItem() {
if (view.count > 0) {
view.currentIndex = 0
Qt.callLater(function() {
let firstItem = view.itemAtIndex(0)
if (firstItem && firstItem.enabled) {
firstItem.forceActiveFocus()
}
})
}
}

// Focus the last button in staging for Shift+Tab from header
function focusLastButton() {
if (view.count > 0) {
let lastItem = view.itemAtIndex(view.count - 1)
if (lastItem) {
lastItem.forceActiveFocus()
Qt.callLater(function() {
if (lastItem.notifyContent) {
lastItem.notifyContent.focusLastButton()
}
})
}
}
}

NotifyStagingModel {
id: notifyModel
}

// Navigate to next item or go to header
function navigateToNextItem(currentIndex) {
if (currentIndex < view.count - 1) {
view.currentIndex = currentIndex + 1
Qt.callLater(function() {
let nextItem = view.itemAtIndex(currentIndex + 1)
if (nextItem && nextItem.enabled) nextItem.forceActiveFocus()
})
} else {
// Last item, go to header
root.gotoHeaderFirst()
}
}

// Navigate to previous item or go to header
function navigateToPrevItem(currentIndex) {
if (currentIndex > 0) {
view.currentIndex = currentIndex - 1
Qt.callLater(function() {
let prevItem = view.itemAtIndex(currentIndex - 1)
if (prevItem && prevItem.enabled) prevItem.forceActiveFocus()
})
} else {
// First item, go to header last button
root.gotoHeaderLast()
}
}

ListView {
id: view
spacing: 10
snapMode: ListView.SnapToItem
// activeFocusOnTab: true
width: root.width
height: contentHeight

Expand All @@ -48,6 +106,25 @@ FocusScope {
contentIcon: model.contentIcon
contentRowCount: model.contentRowCount

// Tab key navigation: focus internal buttons first
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()

onRemove: function () {
console.log("remove overlap", model.id)
notifyModel.closeNotify(model.id, NotifyItem.Closed)
Expand Down
3 changes: 2 additions & 1 deletion panels/notification/center/OverlapNotify.qml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ NotifyItem {
date: root.date
actions: root.actions
defaultAction: root.defaultAction
closeVisible: impl.hovered || root.activeFocus
// Don't override closeVisible - use parentHovered to pass external hover/focus state
parentHovered: impl.hovered || root.activeFocus
strongInteractive: root.strongInteractive
contentIcon: root.contentIcon
contentRowCount: root.contentRowCount
Expand Down
21 changes: 21 additions & 0 deletions panels/notification/center/package/main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ Window {
right: parent.right
rightMargin: contentPadding
}
// Tab navigation: staging -> header
onGotoHeaderFirst: notifyCenter.focusHeaderFirst()
onGotoHeaderLast: notifyCenter.focusHeaderLast()
Connections {
target: Panel
function onVisibleChanged() {
Expand All @@ -125,6 +128,24 @@ Window {
right: parent.right
bottom: parent.bottom
}
// Shift+Tab navigation: header -> staging (or view last item if no staging)
onGotoStagingLast: {
if (notifyStaging.viewCount > 0) {
notifyStaging.focusLastButton()
} else if (notifyCenter.viewCount > 0) {
notifyCenter.focusViewLastItem()
} else {
notifyCenter.focusHeaderLast()
}
}
// Tab cycle: view last item -> staging first item
onGotoStagingFirst: {
if (notifyStaging.viewCount > 0) {
notifyStaging.focusFirstItem()
} else {
notifyCenter.focusHeaderFirst()
}
}

Connections {
target: Panel
Expand Down
6 changes: 4 additions & 2 deletions panels/notification/plugin/NotifyItemContent.qml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ NotifyItem {
implicitHeight: impl.implicitHeight
// Maximum retry attempts for focus operations when loader content is pending
readonly property int maxFocusRetries: 5
property bool closeVisible: activeFocus || impl.hovered || (clearLoader.item && clearLoader.item.activeFocus)
property bool parentHovered: false // External hover state from parent component
property bool closeVisible: activeFocus || impl.hovered || parentHovered || (clearLoader.item && clearLoader.item.activeFocus)
property int miniContentHeight: NotifyStyle.contentItem.miniHeight
property bool enableDismissed: true
property alias clearButton: clearLoader.sourceComponent
Expand Down Expand Up @@ -102,7 +103,8 @@ NotifyItem {
id: clearLoader
anchors.right: parent.right
// Show when mouse hovers or notification item has focus
active: !(root.strongInteractive && root.actions.length > 0) && (root.closeVisible || closePlaceHolder.hovered)
// Keep active when button itself has focus to prevent unloading during Tab navigation
active: !(root.strongInteractive && root.actions.length > 0) && (root.closeVisible || closePlaceHolder.hovered || (clearLoader.item && clearLoader.item.activeFocus))
sourceComponent: SettingActionButton {
id: closeBtn
objectName: "closeNotify-" + root.appName
Expand Down