Accessibility fixs :

* Fix focus on first relevant element after changing page with navbar #LINQT-2202
* Improve focus navigation on call history list #LINQT-2201
* Fix missing accessible button name in dialer #LINQT-2221
* Switch from ScrollView to Flickable in parameters
* Add auto scroll on keyboard navigation in settings #LINQT-2219
* Correct back button in settings #LINQT-2209
* Fix focus when open settings page #LINQT-2208
* Arrow in vertical tab bar now change button focus instead of changing page #LINQT-2194
* Fix focus and accessibility label in magic search bar #LINQT-2205
This commit is contained in:
Alexandre Jörgensen 2025-12-29 16:21:29 +01:00
parent ca452efe80
commit d2413f33a9
25 changed files with 1269 additions and 712 deletions

View file

@ -114,6 +114,7 @@
#include "tool/request/RequestDialog.hpp" #include "tool/request/RequestDialog.hpp"
#include "tool/thread/Thread.hpp" #include "tool/thread/Thread.hpp"
#include "tool/ui/DashRectangle.hpp" #include "tool/ui/DashRectangle.hpp"
#include "tool/ui/FocusNavigator.hpp"
#if defined(Q_OS_MACOS) #if defined(Q_OS_MACOS)
#include "core/event-count-notifier/EventCountNotifierMacOs.hpp" #include "core/event-count-notifier/EventCountNotifierMacOs.hpp"
@ -714,6 +715,7 @@ void App::initCore() {
#endif #endif
mEngine->rootContext()->setContextProperty("applicationName", APPLICATION_NAME); mEngine->rootContext()->setContextProperty("applicationName", APPLICATION_NAME);
mEngine->rootContext()->setContextProperty("executableName", EXECUTABLE_NAME); mEngine->rootContext()->setContextProperty("executableName", EXECUTABLE_NAME);
mEngine->rootContext()->setContextProperty("FocusNavigator", new FocusNavigator(mEngine));
initCppInterfaces(); initCppInterfaces();
mEngine->addImageProvider(ImageProvider::ProviderId, new ImageProvider()); mEngine->addImageProvider(ImageProvider::ProviderId, new ImageProvider());

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -27,6 +27,7 @@ list(APPEND _LINPHONEAPP_SOURCES
tool/file/TemporaryFile.cpp tool/file/TemporaryFile.cpp
tool/ui/DashRectangle.cpp tool/ui/DashRectangle.cpp
tool/ui/FocusNavigator.cpp
tool/accessibility/AccessibilityHelper.cpp tool/accessibility/AccessibilityHelper.cpp
tool/accessibility/KeyboardShortcuts.cpp tool/accessibility/KeyboardShortcuts.cpp

View file

@ -34,6 +34,7 @@ bool FocusHelperAttached::eventFilter(QObject *watched, QEvent *event) {
auto fe = static_cast<QFocusEvent *>(event); auto fe = static_cast<QFocusEvent *>(event);
if (fe) { if (fe) {
int focusReason = fe->reason(); int focusReason = fe->reason();
// qDebug() << "FocusReason" << focusReason; // Usefull to debug focus problems
m_keyboardFocus = (focusReason == Qt::TabFocusReason || focusReason == Qt::BacktabFocusReason); m_keyboardFocus = (focusReason == Qt::TabFocusReason || focusReason == Qt::BacktabFocusReason);
m_otherFocus = focusReason == Qt::OtherFocusReason; m_otherFocus = focusReason == Qt::OtherFocusReason;
emit keyboardFocusChanged(); emit keyboardFocusChanged();

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2010-2025 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "FocusNavigator.hpp"
#include <QGuiApplication>
#include <QQuickItem>
FocusNavigator::FocusNavigator(QObject *parent) : QObject(parent) {
connect(qApp, &QGuiApplication::focusObjectChanged, this, &FocusNavigator::onFocusObjectChanged);
qApp->installEventFilter(this);
}
bool FocusNavigator::doesLastFocusWasKeyboard() {
return mLastFocusWasKeyboard;
}
bool FocusNavigator::eventFilter(QObject *, QEvent *event) {
switch (event->type()) {
case QEvent::FocusIn: {
auto fe = static_cast<QFocusEvent *>(event);
if (fe) {
int focusReason = fe->reason();
mLastFocusWasKeyboard = (focusReason == Qt::TabFocusReason || focusReason == Qt::BacktabFocusReason);
}
break;
}
default:
break;
}
return false;
}
void FocusNavigator::onFocusObjectChanged(QObject *obj) {
// qDebug() << "New focus object" << obj; // Usefull to debug focus problems
auto item = qobject_cast<QQuickItem *>(obj);
if (!item) return;
emit focusChanged(item, mLastFocusWasKeyboard);
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2010-2025 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QObject>
#include <QQuickItem>
class FocusNavigator : public QObject {
Q_OBJECT
public:
explicit FocusNavigator(QObject *parent = nullptr);
Q_INVOKABLE bool doesLastFocusWasKeyboard();
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
signals:
void focusChanged(QQuickItem *item, bool keyboardFocus);
private:
bool mLastFocusWasKeyboard = false;
void onFocusObjectChanged(QObject *obj);
};

View file

@ -23,7 +23,8 @@ Button {
: mainItem.hovered || mainItem.hasNavigationFocus : mainItem.hovered || mainItem.hasNavigationFocus
? mainItem.hoveredColor ? mainItem.hoveredColor
: mainItem.color : mainItem.color
border.color: mainItem.borderColor border.color: mainItem.keyboardFocus ? mainItem.keyboardFocusedBorderColor : mainItem.borderColor
border.width: mainItem.keyboardFocus ? mainItem.keyboardFocusedBorderWidth : mainItem.borderWidth
} }
contentItem: EffectImage { contentItem: EffectImage {

View file

@ -19,6 +19,9 @@ Control.TabBar {
property int visibleCount: 0 property int visibleCount: 0
signal enterPressed()
signal spacePressed()
// Call it after model is ready. If done before, Repeater will not be updated // Call it after model is ready. If done before, Repeater will not be updated
function initButtons(){ function initButtons(){
actionsRepeater.model = mainItem.model actionsRepeater.model = mainItem.model
@ -96,6 +99,8 @@ Control.TabBar {
onVisibleChanged: mainItem.updateVisibleCount() onVisibleChanged: mainItem.updateVisibleCount()
text: modelData.accessibilityLabel text: modelData.accessibilityLabel
property bool keyboardFocus: FocusHelper.keyboardFocus property bool keyboardFocus: FocusHelper.keyboardFocus
focusPolicy: Qt.StrongFocus
activeFocusOnTab: true
UnreadNotification { UnreadNotification {
unread: !defaultAccount unread: !defaultAccount
? -1 ? -1
@ -165,6 +170,23 @@ Control.TabBar {
mainItem.implicitWidth = Math.max(mainItem.implicitWidth, advanceWidth + buttonIcon.buttonSize) mainItem.implicitWidth = Math.max(mainItem.implicitWidth, advanceWidth + buttonIcon.buttonSize)
} }
} }
Keys.onPressed: event => {
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
mainItem.enterPressed()
} else if(event.key === Qt.Key_Space){
mainItem.spacePressed()
} else if(event.key === Qt.Key_Down){
event.accepted = true;
if(TabBar.index >= mainItem.visibleCount - 1)
return;
tabButton.nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocusReason)
} else if(event.key === Qt.Key_Up){
event.accepted = true;
if(TabBar.index <= 0)
return;
tabButton.nextItemInFocusChain(false).forceActiveFocus(Qt.BacktabFocusReason)
}
}
onClicked: { onClicked: {
mainItem.setCurrentIndex(TabBar.index) mainItem.setCurrentIndex(TabBar.index)
} }

View file

@ -5,17 +5,22 @@ import QtQuick.Controls.Basic as Control
import Linphone import Linphone
import UtilsCpp import UtilsCpp
import SettingsCpp import SettingsCpp
import CustomControls 1.0
import "qrc:/qt/qml/Linphone/view/Style/buttonStyle.js" as ButtonStyle import "qrc:/qt/qml/Linphone/view/Style/buttonStyle.js" as ButtonStyle
import "qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js" as Utils import "qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js" as Utils
ListView { ListView {
id: mainItem id: mainItem
clip: true clip: true
keyNavigationEnabled: false // We will reimplement the keyNavigation
activeFocusOnTab: true
property SearchBar searchBar property SearchBar searchBar
property bool loading: false property bool loading: false
property string searchText: searchBar?.text property string searchText: searchBar?.text
property real busyIndicatorSize: Utils.getSizeWithScreenRatio(60) property real busyIndicatorSize: Utils.getSizeWithScreenRatio(60)
property bool keyboardFocus: FocusHelper.keyboardFocus
property bool lastFocusByNavigationKeyboard: false // Workaround to get the correct focusReason
signal resultsReceived signal resultsReceived
@ -41,7 +46,26 @@ ListView {
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key == Qt.Key_Escape) { if (event.key == Qt.Key_Escape) {
console.log("Back") console.log("Back")
searchBar.forceActiveFocus() searchBar.forceActiveFocus(Qt.BacktabFocusReason)
event.accepted = true
}
// Re-implement key navigation to have Qt.TabFocusReason and Qt.BacktabFocusReason instead of Qt.OtherFocusReason when using arrows to navigate in listView
else if (event.key === Qt.Key_Up) {
if(currentIndex === 0){
searchBar.forceActiveFocus(Qt.BacktabFocusReason)
lastFocusByNavigationKeyboard = false
}else{
decrementCurrentIndex()
currentItem.forceActiveFocus(Qt.BacktabFocusReason) // The focusReason is created by QT later, need to create a workaround
lastFocusByNavigationKeyboard = true
}
event.accepted = true
}
else if(event.key === Qt.Key_Down){
incrementCurrentIndex()
currentItem.forceActiveFocus(Qt.TabFocusReason) // The focusReason is created by QT later, need to create a workaround
lastFocusByNavigationKeyboard = true
event.accepted = true event.accepted = true
} }
} }
@ -113,6 +137,8 @@ ListView {
delegate: FocusScope { delegate: FocusScope {
width: mainItem.width width: mainItem.width
height: Utils.getSizeWithScreenRatio(56) height: Utils.getSizeWithScreenRatio(56)
Accessible.role: Accessible.ListItem
RowLayout { RowLayout {
z: 1 z: 1
anchors.fill: parent anchors.fill: parent
@ -188,12 +214,14 @@ ListView {
} }
} }
BigButton { BigButton {
id: callButton
visible: !modelData.core.isConference || !SettingsCpp.disableMeetingsFeature visible: !modelData.core.isConference || !SettingsCpp.disableMeetingsFeature
style: ButtonStyle.noBackground style: ButtonStyle.noBackground
icon.source: AppIcons.phone icon.source: AppIcons.phone
focus: true focus: false
activeFocusOnTab: false activeFocusOnTab: false
asynchronous: false asynchronous: false
//: Call %1 //: Call %1
Accessible.name: qsTr("call_name_accessible_button").arg(historyAvatar.displayNameVal) Accessible.name: qsTr("call_name_accessible_button").arg(historyAvatar.displayNameVal)
onClicked: { onClicked: {
@ -206,12 +234,38 @@ ListView {
UtilsCpp.createCall(modelData.core.remoteAddress) UtilsCpp.createCall(modelData.core.remoteAddress)
} }
} }
Keys.onPressed: event => {
if (event.key === Qt.Key_Left){
backgroundMouseArea.forceActiveFocus(Qt.BacktabFocusReason)
lastFocusByNavigationKeyboard = true;
}
}
onActiveFocusChanged: {
if (!activeFocus) {
console.log("Unfocus button");
callButton.focus = false // Make sure to be unfocusable (could be when called by forceActiveFocus)
backgroundMouseArea.focus = true
}
}
} }
} }
MouseArea { MouseArea {
id: backgroundMouseArea
hoverEnabled: true hoverEnabled: true
anchors.fill: parent anchors.fill: parent
focus: true focus: true
property bool keyboardFocus: FocusHelper.keyboardFocus || activeFocus && lastFocusByNavigationKeyboard
//: %1 - %2 - %3 - right arrow for call-back button
Accessible.name: qsTr("call_history_entry_accessible_name").arg(
//: "Appel manqué"
modelData.core.status === LinphoneEnums.CallStatus.Missed ? qsTr("notification_missed_call_title")
//: "Appel sortant"
: modelData.core.isOutgoing ? qsTr("call_outgoing")
//: "Appel entrant"
: qsTr("call_audio_incoming")
).arg(historyAvatar.displayNameVal).arg(UtilsCpp.formatDate(modelData.core.date))
onContainsMouseChanged: { onContainsMouseChanged: {
if (containsMouse) if (containsMouse)
mainItem.lastMouseContainsIndex = index mainItem.lastMouseContainsIndex = index
@ -224,12 +278,20 @@ ListView {
radius: Utils.getSizeWithScreenRatio(8) radius: Utils.getSizeWithScreenRatio(8)
color: mainItem.currentIndex color: mainItem.currentIndex
=== index ? DefaultStyle.main2_200 : DefaultStyle.main2_100 === index ? DefaultStyle.main2_200 : DefaultStyle.main2_100
border.color: backgroundMouseArea.keyboardFocus ? DefaultStyle.main2_900 : "transparent"
border.width: backgroundMouseArea.keyboardFocus ? Utils.getSizeWithScreenRatio(3) : 0
visible: mainItem.lastMouseContainsIndex === index visible: mainItem.lastMouseContainsIndex === index
|| mainItem.currentIndex === index || mainItem.currentIndex === index
} }
onPressed: { onPressed: {
mainItem.currentIndex = model.index mainItem.currentIndex = model.index
mainItem.forceActiveFocus() mainItem.forceActiveFocus()
mainItem.lastFocusByNavigationKeyboard = false
}
Keys.onPressed: event => {
if(event.key === Qt.Key_Right){
callButton.forceActiveFocus(Qt.TabFocusReason)
}
} }
} }
} }

View file

@ -185,7 +185,7 @@ Flickable {
direction) direction)
if (newItem) { if (newItem) {
newItem.selectIndex( newItem.selectIndex(
direction > 0 ? -1 : newItem.model.count - 1) direction > 0 ? -1 : newItem.model.count - 1, direction > 0 ? Qt.BacktabFocusReason : Qt.TabFocusReason)
event.accepted = true event.accepted = true
} }
} }

View file

@ -6,6 +6,7 @@ import Linphone
import UtilsCpp import UtilsCpp
import ConstantsCpp import ConstantsCpp
import SettingsCpp import SettingsCpp
import CustomControls 1.0
import "qrc:/qt/qml/Linphone/view/Style/buttonStyle.js" as ButtonStyle import "qrc:/qt/qml/Linphone/view/Style/buttonStyle.js" as ButtonStyle
import "qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js" as Utils import "qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js" as Utils
@ -149,6 +150,9 @@ FocusScope {
onClicked: UtilsCpp.createCall(mainItem.addressFromFilter) onClicked: UtilsCpp.createCall(mainItem.addressFromFilter)
KeyNavigation.left: chatButton KeyNavigation.left: chatButton
KeyNavigation.right: videoCallButton KeyNavigation.right: videoCallButton
//: "Call %1"
Accessible.name: qsTr("call_with_contact_name_accessible_button").arg(mainItem.displayName)
keyboardFocus: FocusHelper.keyboardFocus || FocusHelper.otherFocus
} }
IconButton { IconButton {
id: videoCallButton id: videoCallButton
@ -164,6 +168,9 @@ FocusScope {
onClicked: UtilsCpp.createCall(mainItem.addressFromFilter, {"localVideoEnabled": true}) onClicked: UtilsCpp.createCall(mainItem.addressFromFilter, {"localVideoEnabled": true})
KeyNavigation.left: callButton KeyNavigation.left: callButton
KeyNavigation.right: chatButton KeyNavigation.right: chatButton
//: "Video call %1"
Accessible.name: qsTr("video_call_with_contact_name_accessible_button").arg(mainItem.displayName)
keyboardFocus: FocusHelper.keyboardFocus || FocusHelper.otherFocus
} }
IconButton { IconButton {
id: chatButton id: chatButton
@ -184,6 +191,9 @@ FocusScope {
console.debug("[ContactListItem.qml] Open conversation") console.debug("[ContactListItem.qml] Open conversation")
mainWindow.displayChatPage(mainItem.addressFromFilter) mainWindow.displayChatPage(mainItem.addressFromFilter)
} }
//: "Message %1"
Accessible.name: qsTr("message_with_contact_name_accessible_button").arg(mainItem.displayName)
keyboardFocus: FocusHelper.keyboardFocus || FocusHelper.otherFocus
} }
} }
PopupButton { PopupButton {

View file

@ -68,13 +68,13 @@ ListView {
property bool _moveToIndex: false property bool _moveToIndex: false
function selectIndex(index){ function selectIndex(index, focusReason = Qt.OtherFocusReason){
if(mainItem.expanded && index >= 0){ if(mainItem.expanded && index >= 0){
mainItem.currentIndex = index mainItem.currentIndex = index
var item = itemAtIndex(mainItem.currentIndex) var item = itemAtIndex(mainItem.currentIndex)
if(item){// Item is ready and available if(item){// Item is ready and available
mainItem.highlightedContact = item.searchResultItem mainItem.highlightedContact = item.searchResultItem
item.forceActiveFocus() item.forceActiveFocus(focusReason)
updatePosition() updatePosition()
_moveToIndex = false _moveToIndex = false
}else{// Move on the next items load. }else{// Move on the next items load.
@ -85,7 +85,7 @@ ListView {
mainItem.currentIndex = -1 mainItem.currentIndex = -1
mainItem.highlightedContact = null mainItem.highlightedContact = null
if(headerItem) { if(headerItem) {
headerItem.forceActiveFocus() headerItem.forceActiveFocus(focusReason)
} }
_moveToIndex = false _moveToIndex = false
} }
@ -97,12 +97,12 @@ ListView {
if(event.key == Qt.Key_Up || event.key == Qt.Key_Down){ if(event.key == Qt.Key_Up || event.key == Qt.Key_Down){
if(event.key == Qt.Key_Up && !headerItem.activeFocus) { if(event.key == Qt.Key_Up && !headerItem.activeFocus) {
if(currentIndex >= 0 ) { if(currentIndex >= 0 ) {
selectIndex(mainItem.currentIndex-1) selectIndex(mainItem.currentIndex-1, Qt.BacktabFocusReason)
event.accepted = true; event.accepted = true;
} }
}else if(event.key == Qt.Key_Down && mainItem.expanded){ }else if(event.key == Qt.Key_Down && mainItem.expanded){
if(currentIndex < model.count - 1) { if(currentIndex < model.count - 1) {
selectIndex(mainItem.currentIndex+1) selectIndex(mainItem.currentIndex+1, Qt.TabFocusReason)
event.accepted = true; event.accepted = true;
} }
} }

View file

@ -20,6 +20,7 @@ Item {
//: %1 settings //: %1 settings
Accessible.name: qsTr("setting_tab_accessible_name").arg(titleText) Accessible.name: qsTr("setting_tab_accessible_name").arg(titleText)
Accessible.role: Accessible.ListItem
Keys.onPressed: (event)=>{ Keys.onPressed: (event)=>{
if(event.key == Qt.Key_Space || event.key == Qt.Key_Return || event.key == Qt.Key_Enter){ if(event.key == Qt.Key_Space || event.key == Qt.Key_Return || event.key == Qt.Key_Enter){

View file

@ -172,6 +172,9 @@ FocusScope {
radius: Utils.getSizeWithScreenRatio(71) radius: Utils.getSizeWithScreenRatio(71)
style: ButtonStyle.numericPad style: ButtonStyle.numericPad
//: %1 longpress %2
Accessible.name: longPressText.text ? qsTr("numpad_longpress_accessible_name").arg(pressText.text).arg(longPressText.text) : pressText.text
contentItem: Item { contentItem: Item {
anchors.fill: parent anchors.fill: parent
Text { Text {
@ -215,6 +218,9 @@ FocusScope {
radius: Utils.getSizeWithScreenRatio(71) radius: Utils.getSizeWithScreenRatio(71)
style: ButtonStyle.phoneGreen style: ButtonStyle.phoneGreen
//: Call
Accessible.name: qsTr("call_accessible_name")
onClicked: mainItem.launchCall() onClicked: mainItem.launchCall()
KeyNavigation.left: eraseButton KeyNavigation.left: eraseButton
@ -237,6 +243,9 @@ FocusScope {
Layout.Layout.preferredWidth: Utils.getSizeWithScreenRatio(38) Layout.Layout.preferredWidth: Utils.getSizeWithScreenRatio(38)
Layout.Layout.preferredHeight: Utils.getSizeWithScreenRatio(38) Layout.Layout.preferredHeight: Utils.getSizeWithScreenRatio(38)
//: Erase
Accessible.name: qsTr("erase_accessible_name")
onClicked: mainItem.wipe() onClicked: mainItem.wipe()
KeyNavigation.left: launchCallButton KeyNavigation.left: launchCallButton
@ -244,8 +253,13 @@ FocusScope {
KeyNavigation.up: numPadGrid.getButtonAt(11) KeyNavigation.up: numPadGrid.getButtonAt(11)
KeyNavigation.down: numPadGrid.getButtonAt(1) KeyNavigation.down: numPadGrid.getButtonAt(1)
background: Item { background: Rectangle {
visible: false width: eraseButton.width
height: eraseButton.height
color: "transparent"
border.color: eraseButton.keyboardFocus ? eraseButton.keyboardFocusedBorderColor : "transparent"
border.width: eraseButton.keyboardFocus ? eraseButton.keyboardFocusedBorderWidth : eraseButton.borderWidth
} }
} }
} }

View file

@ -69,6 +69,8 @@ Control.Popup {
icon.height: Utils.getSizeWithScreenRatio(24) icon.height: Utils.getSizeWithScreenRatio(24)
style: ButtonStyle.noBackground style: ButtonStyle.noBackground
onClicked: mainItem.close() onClicked: mainItem.close()
//: Close numeric pad
Accessible.name: qsTr("close_numeric_pad_accessible_name")
} }
} }
contentItem: NumericPad{ contentItem: NumericPad{

View file

@ -807,6 +807,17 @@ function infoDialog(window, message) {
}, function (status) {}) }, function (status) {})
} }
// Ensure that the item is visible in the view
function ensureVisibleY(item, view){
const itemPosition = item.mapToItem(view, 0, 0)
if (itemPosition.y < 0){
view.contentY = view.contentY + itemPosition.y
} else if (itemPosition.y + item.height > view.height){
view.contentY = itemPosition.y + view.contentY + item.height - view.height
}
}
// Set position of list.currentItem into the scrollItem // Set position of list.currentItem into the scrollItem
function updatePosition(scrollItem, list){ function updatePosition(scrollItem, list){
if(scrollItem.height == 0) return; if(scrollItem.height == 0) return;

View file

@ -48,6 +48,8 @@ AbstractMainPage {
onClicked: { onClicked: {
mainItem.goBackRequested() mainItem.goBackRequested()
} }
//: Back to previous menu
Accessible.name: qsTr("back_previous_menu_accessible_name")
} }
Text { Text {
text: titleText text: titleText
@ -69,6 +71,9 @@ AbstractMainPage {
property int selectedIndex: mainItem.defaultIndex != -1 ? mainItem.defaultIndex : 0 property int selectedIndex: mainItem.defaultIndex != -1 ? mainItem.defaultIndex : 0
activeFocusOnTab: true activeFocusOnTab: true
spacing: Utils.getSizeWithScreenRatio(5) spacing: Utils.getSizeWithScreenRatio(5)
Accessible.role: Accessible.List
//: Settings page selection
Accessible.name: qsTr("settings_page_selection_accessible_name")
delegate: SettingsMenuItem { delegate: SettingsMenuItem {
titleText: modelData.title titleText: modelData.title
@ -86,7 +91,9 @@ AbstractMainPage {
let initialEntry = mainItem.families[familiesList.selectedIndex] let initialEntry = mainItem.families[familiesList.selectedIndex]
rightPanelStackView.push(layoutUrl(initialEntry.layout), { titleText: initialEntry.title, model: initialEntry.model, container: rightPanelStackView}) rightPanelStackView.push(layoutUrl(initialEntry.layout), { titleText: initialEntry.title, model: initialEntry.model, container: rightPanelStackView})
familiesList.currentIndex = familiesList.selectedIndex familiesList.currentIndex = familiesList.selectedIndex
backButton.forceActiveFocus()
} }
} }
Control.StackView.onActivated: {
familiesList.forceActiveFocus(FocusNavigator.doesLastFocusWasKeyboard() ? Qt.TabFocusReason : Qt.MouseFocusReason)
}
} }

View file

@ -14,6 +14,7 @@ Item {
id: mainItem id: mainItem
property var callObj property var callObj
property var contextualMenuOpenedComponent: undefined property var contextualMenuOpenedComponent: undefined
property bool focusPageOnNextLoad: false // Focus the page on next load - usefull cause of loader
signal addAccountRequest signal addAccountRequest
signal openNewCallRequest signal openNewCallRequest
@ -184,6 +185,18 @@ Item {
mainStackView.currentItem.forceActiveFocus(); mainStackView.currentItem.forceActiveFocus();
} }
} }
/**
* Focus the page when user select the page with keyboard.
* Do not add this behavior on the arrows
*/
onEnterPressed: {
focusPageOnNextLoad = true
}
onSpacePressed: {
focusPageOnNextLoad = true
}
Component.onCompleted: { Component.onCompleted: {
if (SettingsCpp.shortcutCount > 0) { if (SettingsCpp.shortcutCount > 0) {
var shortcuts = SettingsCpp.shortcuts; var shortcuts = SettingsCpp.shortcuts;
@ -639,6 +652,10 @@ Item {
openContextualMenuComponent(page); openContextualMenuComponent(page);
} }
} }
onLoaded: {
if(focusPageOnNextLoad) item.forceActiveFocus(Qt.TabFocusReason)
focusPageOnNextLoad = false
}
} }
Loader { Loader {
active: mainStackLayout.currentIndex === 1 active: mainStackLayout.currentIndex === 1
@ -654,6 +671,10 @@ Item {
} }
} }
} }
onLoaded: {
if(focusPageOnNextLoad) item.forceActiveFocus(Qt.TabFocusReason)
focusPageOnNextLoad = false
}
} }
Loader { Loader {
active: mainStackLayout.currentIndex === 2 active: mainStackLayout.currentIndex === 2
@ -672,6 +693,10 @@ Item {
} }
} }
} }
onLoaded: {
if(focusPageOnNextLoad) item.forceActiveFocus(Qt.TabFocusReason)
focusPageOnNextLoad = false
}
} }
Loader { Loader {
@ -688,6 +713,10 @@ Item {
} }
} }
} }
onLoaded: {
if(focusPageOnNextLoad) item.forceActiveFocus(Qt.TabFocusReason)
focusPageOnNextLoad = false
}
} }
} }

View file

@ -100,17 +100,13 @@ Rectangle {
} }
} }
} }
Control.ScrollView { Flickable {
id: scrollView id: scrollView
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.top: header.bottom anchors.top: header.bottom
anchors.topMargin: Utils.getSizeWithScreenRatio(16) anchors.topMargin: Utils.getSizeWithScreenRatio(16)
// Workaround while the CI is made with Qt6.5.3
// When updated to 6.8, remove this Item and
// change the ScrollView with a Flickable
Item{anchors.fill: parent}
contentHeight: contentListView.contentHeight contentHeight: contentListView.contentHeight
Control.ScrollBar.vertical: ScrollBar { Control.ScrollBar.vertical: ScrollBar {
active: contentListView.contentHeight > scrollView.height active: contentListView.contentHeight > scrollView.height
@ -130,21 +126,21 @@ Rectangle {
model: mainItem.contentModel model: mainItem.contentModel
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.leftMargin: Utils.getSizeWithScreenRatio(45) anchors.leftMargin: Utils.getSizeWithScreenRatio(45)
anchors.rightMargin: Utils.getSizeWithScreenRatio(45) anchors.rightMargin: Utils.getSizeWithScreenRatio(45)
height: contentHeight height: contentHeight
spacing: Utils.getSizeWithScreenRatio(10) spacing: Utils.getSizeWithScreenRatio(10)
delegate: ColumnLayout { delegate: ColumnLayout {
visible: modelData.visible != undefined ? modelData.visible: true visible: modelData.visible != undefined ? modelData.visible: true
Component.onCompleted: if (!visible) height = 0 Component.onCompleted: if (!visible) height = 0
spacing: Utils.getSizeWithScreenRatio(16) spacing: Utils.getSizeWithScreenRatio(16)
width: contentListView.width width: contentListView.width
Rectangle { Rectangle {
visible: index !== 0 visible: index !== 0
Layout.topMargin: Utils.getSizeWithScreenRatio(modelData.hideTopSeparator ? 0 : 16) Layout.topMargin: Utils.getSizeWithScreenRatio(modelData.hideTopSeparator ? 0 : 16)
Layout.bottomMargin: Utils.getSizeWithScreenRatio(16) Layout.bottomMargin: Utils.getSizeWithScreenRatio(16)
Layout.fillWidth: true Layout.fillWidth: true
height: Utils.getSizeWithScreenRatio(1) height: Utils.getSizeWithScreenRatio(1)
color: modelData.hideTopSeparator ? 'transparent' : DefaultStyle.main2_500_main color: modelData.hideTopSeparator ? 'transparent' : DefaultStyle.main2_500_main
} }
GridLayout { GridLayout {
@ -152,12 +148,12 @@ Rectangle {
columns: mainItem.useVerticalLayout ? 1 : 2 columns: mainItem.useVerticalLayout ? 1 : 2
Layout.fillWidth: true Layout.fillWidth: true
// Layout.preferredWidth: parent.width // Layout.preferredWidth: parent.width
rowSpacing: modelData.title.length > 0 || modelData.subTitle.length > 0 ? Utils.getSizeWithScreenRatio(20) : 0 rowSpacing: modelData.title.length > 0 || modelData.subTitle.length > 0 ? Utils.getSizeWithScreenRatio(20) : 0
columnSpacing: Utils.getSizeWithScreenRatio(47) columnSpacing: Utils.getSizeWithScreenRatio(47)
ColumnLayout { ColumnLayout {
Layout.preferredWidth: Utils.getSizeWithScreenRatio(341) Layout.preferredWidth: Utils.getSizeWithScreenRatio(341)
Layout.maximumWidth: Utils.getSizeWithScreenRatio(341) Layout.maximumWidth: Utils.getSizeWithScreenRatio(341)
spacing: Utils.getSizeWithScreenRatio(3) spacing: Utils.getSizeWithScreenRatio(3)
Text { Text {
text: modelData.title text: modelData.title
visible: modelData.title.length > 0 visible: modelData.title.length > 0
@ -179,10 +175,10 @@ Rectangle {
} }
} }
RowLayout { RowLayout {
Layout.topMargin: modelData.hideTopMargin ? 0 : Utils.getSizeWithScreenRatio(mainItem.useVerticalLayout ? 10 : 21) Layout.topMargin: modelData.hideTopMargin ? 0 : Utils.getSizeWithScreenRatio(mainItem.useVerticalLayout ? 10 : 21)
Layout.bottomMargin: Utils.getSizeWithScreenRatio(21) Layout.bottomMargin: Utils.getSizeWithScreenRatio(21)
Layout.leftMargin: mainItem.useVerticalLayout ? 0 : Utils.getSizeWithScreenRatio(17) Layout.leftMargin: mainItem.useVerticalLayout ? 0 : Utils.getSizeWithScreenRatio(17)
Layout.preferredWidth: Utils.getSizeWithScreenRatio(modelData.customWidth > 0 ? modelData.customWidth : 545) Layout.preferredWidth: Utils.getSizeWithScreenRatio(modelData.customWidth > 0 ? modelData.customWidth : 545)
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
Loader { Loader {
id: contentLoader id: contentLoader
@ -190,12 +186,22 @@ Rectangle {
sourceComponent: modelData.contentComponent sourceComponent: modelData.contentComponent
} }
Item { Item {
Layout.preferredWidth: Utils.getSizeWithScreenRatio(modelData.customRightMargin > 0 ? modelData.customRightMargin : 17) Layout.preferredWidth: Utils.getSizeWithScreenRatio(modelData.customRightMargin > 0 ? modelData.customRightMargin : 17)
} }
} }
} }
} }
} }
Connections {
target: FocusNavigator
function onFocusChanged(item, keyboardFocus) {
if(Utils.isDescendant(item,scrollView) && keyboardFocus){
Utils.ensureVisibleY(item, scrollView)
}
}
}
} }
} }

View file

@ -24,6 +24,14 @@ AbstractMainPage {
goToCallHistory() goToCallHistory()
} }
/**
* Focus on the first pertinent element in the page (LINQT-2202)
* @override
*/
function forceActiveFocus(reason = undefined){
listStackView.currentItem?.newCallButton?.forceActiveFocus(reason)
}
//Group call properties //Group call properties
property ConferenceInfoGui confInfoGui property ConferenceInfoGui confInfoGui
property AccountProxy accounts: AccountProxy { property AccountProxy accounts: AccountProxy {
@ -134,6 +142,8 @@ AbstractMainPage {
FocusScope { FocusScope {
objectName: "historyListItem" objectName: "historyListItem"
property alias listView: historyListView property alias listView: historyListView
property alias newCallButton: newCallButton
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
@ -187,6 +197,7 @@ AbstractMainPage {
} }
Button { Button {
id: newCallButton id: newCallButton
focus: true
style: ButtonStyle.noBackground style: ButtonStyle.noBackground
icon.source: AppIcons.newCall icon.source: AppIcons.newCall
Layout.preferredWidth: Utils.getSizeWithScreenRatio(34) Layout.preferredWidth: Utils.getSizeWithScreenRatio(34)

View file

@ -16,6 +16,14 @@ AbstractMainPage {
emptyListText: qsTr("chat_empty_title") emptyListText: qsTr("chat_empty_title")
newItemIconSource: AppIcons.plusCircle newItemIconSource: AppIcons.plusCircle
/**
* Focus on the first pertinent element in the page (LINQT-2202)
* @override
*/
function forceActiveFocus(reason = undefined){
listStackView.currentItem?.newChatButton?.forceActiveFocus(reason)
}
property AccountProxy accounts: AccountProxy { property AccountProxy accounts: AccountProxy {
id: accountProxy id: accountProxy
} }
@ -116,6 +124,7 @@ AbstractMainPage {
FocusScope { FocusScope {
objectName: "chatListItem" objectName: "chatListItem"
property alias listView: chatListView property alias listView: chatListView
property alias newChatButton: newChatButton
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
@ -158,6 +167,7 @@ AbstractMainPage {
} }
Button { Button {
id: newChatButton id: newChatButton
focus: true
style: ButtonStyle.noBackground style: ButtonStyle.noBackground
icon.source: AppIcons.plusCircle icon.source: AppIcons.plusCircle
Layout.preferredWidth: Utils.getSizeWithScreenRatio(28) Layout.preferredWidth: Utils.getSizeWithScreenRatio(28)

View file

@ -17,6 +17,14 @@ AbstractMainPage {
emptyListText: qsTr("contacts_list_empty") emptyListText: qsTr("contacts_list_empty")
newItemIconSource: AppIcons.plusCircle newItemIconSource: AppIcons.plusCircle
/**
* Focus on the first pertinent element in the page (LINQT-2202)
* @override
*/
function forceActiveFocus(reason = undefined){
createContactButton.forceActiveFocus(reason)
}
// disable left panel contact list interaction while a contact is being edited // disable left panel contact list interaction while a contact is being edited
property bool leftPanelEnabled: !rightPanelStackView.currentItem property bool leftPanelEnabled: !rightPanelStackView.currentItem
|| rightPanelStackView.currentItem.objectName || rightPanelStackView.currentItem.objectName
@ -243,6 +251,7 @@ AbstractMainPage {
visible: !rightPanelStackView.currentItem visible: !rightPanelStackView.currentItem
|| rightPanelStackView.currentItem.objectName !== "contactEdition" || rightPanelStackView.currentItem.objectName !== "contactEdition"
style: ButtonStyle.noBackground style: ButtonStyle.noBackground
focus: true
icon.source: AppIcons.plusCircle icon.source: AppIcons.plusCircle
Layout.preferredWidth: Utils.getSizeWithScreenRatio(28) Layout.preferredWidth: Utils.getSizeWithScreenRatio(28)
Layout.preferredHeight: Utils.getSizeWithScreenRatio(28) Layout.preferredHeight: Utils.getSizeWithScreenRatio(28)

View file

@ -26,6 +26,15 @@ AbstractMainPage {
rightPanelStackTopMargin: Utils.getSizeWithScreenRatio(45) rightPanelStackTopMargin: Utils.getSizeWithScreenRatio(45)
rightPanelStackBottomMargin: Utils.getSizeWithScreenRatio(30) rightPanelStackBottomMargin: Utils.getSizeWithScreenRatio(30)
/**
* Focus on the first pertinent element in the page (LINQT-2202)
* @override
*/
function forceActiveFocus(reason = undefined){
leftPanelStackView.currentItem?.newConfButton?.forceActiveFocus(reason)
}
function createPreFilledMeeting(subject, addresses) { function createPreFilledMeeting(subject, addresses) {
mainItem.selectedConference = Qt.createQmlObject('import Linphone mainItem.selectedConference = Qt.createQmlObject('import Linphone
ConferenceInfoGui{ ConferenceInfoGui{
@ -137,6 +146,7 @@ AbstractMainPage {
id: listLayout id: listLayout
FocusScope{ FocusScope{
property string objectName: "listLayout" property string objectName: "listLayout"
property alias newConfButton: newConfButton
Control.StackView.onDeactivated: { Control.StackView.onDeactivated: {
mainItem.selectedConference = null mainItem.selectedConference = null
} }
@ -166,6 +176,7 @@ AbstractMainPage {
} }
Button { Button {
id: newConfButton id: newConfButton
focus: true
style: ButtonStyle.noBackground style: ButtonStyle.noBackground
icon.source: AppIcons.plusCircle icon.source: AppIcons.plusCircle
Layout.preferredWidth: Utils.getSizeWithScreenRatio(28) Layout.preferredWidth: Utils.getSizeWithScreenRatio(28)