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 { Rectangle {
id: homeButton id: homeButton
visible: root.convergenceMode visible: root.convergenceMode
activeFocusOnTab: root.convergenceMode
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom 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") : (homeMouseArea.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) : "transparent")
radius: Kirigami.Units.cornerRadius 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 { Kirigami.Icon {
anchors.centerIn: parent anchors.centerIn: parent
width: Math.min(parent.width, parent.height) * 0.75 width: Math.min(parent.width, parent.height) * 0.75
@ -155,6 +176,7 @@ MouseArea {
Rectangle { Rectangle {
id: overviewButton id: overviewButton
visible: root.convergenceMode visible: root.convergenceMode
activeFocusOnTab: root.convergenceMode
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom 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") : (overviewMouseArea.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) : "transparent")
radius: Kirigami.Units.cornerRadius 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 { Kirigami.Icon {
anchors.centerIn: parent anchors.centerIn: parent
width: Math.min(parent.width, parent.height) * 0.75 width: Math.min(parent.width, parent.height) * 0.75
@ -320,9 +362,12 @@ MouseArea {
break; break;
case Qt.Key_Left: case Qt.Key_Left:
if (isLocationBottom) { if (isLocationBottom) {
let nextDelegate = repeater.itemAt(delegate.index - 1); let prevDelegate = repeater.itemAt(delegate.index - 1);
if (nextDelegate) { if (prevDelegate) {
nextDelegate.keyboardFocus(); prevDelegate.keyboardFocus();
event.accepted = true;
} else if (root.convergenceMode) {
homeButton.forceActiveFocus();
event.accepted = true; event.accepted = true;
} }
} }
@ -333,6 +378,14 @@ MouseArea {
if (nextDelegate) { if (nextDelegate) {
nextDelegate.keyboardFocus(); nextDelegate.keyboardFocus();
event.accepted = true; event.accepted = true;
} else if (root.convergenceMode) {
let firstTask = taskRepeater.itemAt(0);
if (firstTask) {
firstTask.forceActiveFocus();
} else {
overviewButton.forceActiveFocus();
}
event.accepted = true;
} }
} }
break; break;
@ -852,9 +905,49 @@ MouseArea {
required property int index required property int index
required property var model required property var model
activeFocusOnTab: root.convergenceMode
readonly property bool isLocationBottom: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom readonly property bool isLocationBottom: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom
readonly property string taskStorageId: root.runningTaskStorageId(taskDelegate.model) 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 // Position after all favourites
property double fromCenterValue: (repeater.count + taskDelegate.index) - (root.totalItemCount / 2) property double fromCenterValue: (repeater.count + taskDelegate.index) - (root.totalItemCount / 2)
Behavior on fromCenterValue { 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") : (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 // Task icon
Kirigami.Icon { Kirigami.Icon {
anchors.centerIn: parent anchors.centerIn: parent
@ -970,22 +1068,7 @@ MouseArea {
thumbnailShowTimer.stop() thumbnailShowTimer.stop()
taskContextMenu.open(); taskContextMenu.open();
} else { } else {
var winIds = taskDelegate.model.WinIdList taskDelegate.activateTask()
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));
}
} }
} }
onContainsMouseChanged: { onContainsMouseChanged: {