Enable keyboard control in convergence dock

Make the convergence dock fully usable from the keyboard.

Tab now reaches Home, favorites, running tasks, and Overview.
Left and right move across section boundaries, and Enter/Space
triggers the same actions as mouse clicks.

Also add accessible role/name/press actions for these controls
so screen readers expose meaningful button semantics.
This commit is contained in:
Marco Allegretti 2026-04-19 11:06:49 +02:00
parent e72beb7296
commit e9dbfa5ea1

View file

@ -125,6 +125,7 @@ MouseArea {
Rectangle {
id: homeButton
visible: root.convergenceMode
activeFocusOnTab: root.convergenceMode
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
@ -134,6 +135,26 @@ MouseArea {
: (homeMouseArea.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) : "transparent")
radius: Kirigami.Units.cornerRadius
Accessible.role: Accessible.Button
Accessible.name: i18n("Home")
Accessible.onPressAction: MobileShellState.ShellDBusClient.openHomeScreen()
Keys.onReturnPressed: MobileShellState.ShellDBusClient.openHomeScreen()
Keys.onEnterPressed: MobileShellState.ShellDBusClient.openHomeScreen()
Keys.onSpacePressed: MobileShellState.ShellDBusClient.openHomeScreen()
Keys.onRightPressed: {
let first = repeater.itemAt(0)
if (first) { first.keyboardFocus(); return }
let firstTask = taskRepeater.itemAt(0)
if (firstTask) { firstTask.forceActiveFocus(); return }
overviewButton.forceActiveFocus()
}
KeyboardHighlight {
anchors.fill: parent
visible: homeButton.activeFocus
}
Kirigami.Icon {
anchors.centerIn: parent
width: Math.min(parent.width, parent.height) * 0.75
@ -155,6 +176,7 @@ MouseArea {
Rectangle {
id: overviewButton
visible: root.convergenceMode
activeFocusOnTab: root.convergenceMode
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
@ -164,6 +186,26 @@ MouseArea {
: (overviewMouseArea.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) : "transparent")
radius: Kirigami.Units.cornerRadius
Accessible.role: Accessible.Button
Accessible.name: i18n("Overview")
Accessible.onPressAction: root.folio.triggerOverview()
Keys.onReturnPressed: root.folio.triggerOverview()
Keys.onEnterPressed: root.folio.triggerOverview()
Keys.onSpacePressed: root.folio.triggerOverview()
Keys.onLeftPressed: {
let lastTask = taskRepeater.itemAt(taskRepeater.count - 1)
if (lastTask) { lastTask.forceActiveFocus(); return }
let lastFav = repeater.itemAt(repeater.count - 1)
if (lastFav) { lastFav.keyboardFocus(); return }
homeButton.forceActiveFocus()
}
KeyboardHighlight {
anchors.fill: parent
visible: overviewButton.activeFocus
}
Kirigami.Icon {
anchors.centerIn: parent
width: Math.min(parent.width, parent.height) * 0.75
@ -320,9 +362,12 @@ MouseArea {
break;
case Qt.Key_Left:
if (isLocationBottom) {
let nextDelegate = repeater.itemAt(delegate.index - 1);
if (nextDelegate) {
nextDelegate.keyboardFocus();
let prevDelegate = repeater.itemAt(delegate.index - 1);
if (prevDelegate) {
prevDelegate.keyboardFocus();
event.accepted = true;
} else if (root.convergenceMode) {
homeButton.forceActiveFocus();
event.accepted = true;
}
}
@ -333,6 +378,14 @@ MouseArea {
if (nextDelegate) {
nextDelegate.keyboardFocus();
event.accepted = true;
} else if (root.convergenceMode) {
let firstTask = taskRepeater.itemAt(0);
if (firstTask) {
firstTask.forceActiveFocus();
} else {
overviewButton.forceActiveFocus();
}
event.accepted = true;
}
}
break;
@ -852,9 +905,49 @@ MouseArea {
required property int index
required property var model
activeFocusOnTab: root.convergenceMode
readonly property bool isLocationBottom: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom
readonly property string taskStorageId: root.runningTaskStorageId(taskDelegate.model)
Accessible.role: Accessible.Button
Accessible.name: taskDelegate.model.display || ""
Accessible.onPressAction: taskDelegate.activateTask()
function activateTask() {
var winIds = taskDelegate.model.WinIdList
if (winIds && winIds.length > 1) {
if (thumbnailPopup.opened && thumbnailPopup.taskIndex === taskDelegate.index) {
thumbnailPopup.close()
} else {
thumbnailPopup.targetDelegate = taskDelegate
thumbnailPopup.taskIndex = taskDelegate.index
thumbnailPopup.windowIds = winIds
thumbnailPopup.isGroup = taskDelegate.model.IsGroupParent === true
thumbnailPopup.open()
}
} else {
thumbnailPopup.close()
tasksModel.requestActivate(tasksModel.makeModelIndex(taskDelegate.index))
}
}
Keys.onReturnPressed: taskDelegate.activateTask()
Keys.onEnterPressed: taskDelegate.activateTask()
Keys.onSpacePressed: taskDelegate.activateTask()
Keys.onLeftPressed: {
let prev = taskRepeater.itemAt(taskDelegate.index - 1)
if (prev) { prev.forceActiveFocus(); return }
let lastFav = repeater.itemAt(repeater.count - 1)
if (lastFav) { lastFav.keyboardFocus(); return }
homeButton.forceActiveFocus()
}
Keys.onRightPressed: {
let next = taskRepeater.itemAt(taskDelegate.index + 1)
if (next) { next.forceActiveFocus(); return }
overviewButton.forceActiveFocus()
}
// Position after all favourites
property double fromCenterValue: (repeater.count + taskDelegate.index) - (root.totalItemCount / 2)
Behavior on fromCenterValue {
@ -881,6 +974,11 @@ MouseArea {
: (taskMouseArea.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) : "transparent")
}
KeyboardHighlight {
anchors.fill: parent
visible: taskDelegate.activeFocus
}
// Task icon
Kirigami.Icon {
anchors.centerIn: parent
@ -970,22 +1068,7 @@ MouseArea {
thumbnailShowTimer.stop()
taskContextMenu.open();
} else {
var winIds = taskDelegate.model.WinIdList
if (winIds && winIds.length > 1) {
// Multiple windows: toggle thumbnail popup
if (thumbnailPopup.opened && thumbnailPopup.taskIndex === taskDelegate.index) {
thumbnailPopup.close()
} else {
thumbnailPopup.targetDelegate = taskDelegate
thumbnailPopup.taskIndex = taskDelegate.index
thumbnailPopup.windowIds = winIds
thumbnailPopup.isGroup = taskDelegate.model.IsGroupParent === true
thumbnailPopup.open()
}
} else {
thumbnailPopup.close()
tasksModel.requestActivate(tasksModel.makeModelIndex(taskDelegate.index));
}
taskDelegate.activateTask()
}
}
onContainsMouseChanged: {