diff --git a/Linphone/core/App.cpp b/Linphone/core/App.cpp
index 330a776b5..02a0e8d1b 100644
--- a/Linphone/core/App.cpp
+++ b/Linphone/core/App.cpp
@@ -111,6 +111,7 @@
#include "tool/request/RequestDialog.hpp"
#include "tool/thread/Thread.hpp"
#include "tool/ui/DashRectangle.hpp"
+#include "tool/ui/FocusNavigator.hpp"
#if defined(Q_OS_MACOS)
#include "core/event-count-notifier/EventCountNotifierMacOs.hpp"
@@ -605,6 +606,7 @@ void App::initCore() {
#endif
mEngine->rootContext()->setContextProperty("applicationName", APPLICATION_NAME);
mEngine->rootContext()->setContextProperty("executableName", EXECUTABLE_NAME);
+ mEngine->rootContext()->setContextProperty("FocusNavigator", new FocusNavigator());
initCppInterfaces();
mEngine->addImageProvider(ImageProvider::ProviderId, new ImageProvider());
diff --git a/Linphone/tool/CMakeLists.txt b/Linphone/tool/CMakeLists.txt
index aafe1a72b..c8669c2f8 100644
--- a/Linphone/tool/CMakeLists.txt
+++ b/Linphone/tool/CMakeLists.txt
@@ -27,6 +27,7 @@ list(APPEND _LINPHONEAPP_SOURCES
tool/file/TemporaryFile.cpp
tool/ui/DashRectangle.cpp
+ tool/ui/FocusNavigator.cpp
tool/accessibility/AccessibilityHelper.cpp
tool/accessibility/KeyboardShortcuts.cpp
diff --git a/Linphone/tool/ui/FocusNavigator.cpp b/Linphone/tool/ui/FocusNavigator.cpp
new file mode 100644
index 000000000..d3489845a
--- /dev/null
+++ b/Linphone/tool/ui/FocusNavigator.cpp
@@ -0,0 +1,51 @@
+/*
+ * 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 .
+ */
+
+#include "FocusNavigator.hpp"
+
+#include
+#include
+
+FocusNavigator::FocusNavigator(QObject *parent) : QObject(parent) {
+ connect(qApp, &QGuiApplication::focusObjectChanged, this, &FocusNavigator::onFocusObjectChanged);
+ qApp->installEventFilter(this);
+}
+
+bool FocusNavigator::eventFilter(QObject *, QEvent *event) {
+ switch (event->type()) {
+ case QEvent::FocusIn: {
+ auto fe = static_cast(event);
+ if (fe) {
+ int focusReason = fe->reason();
+ m_lastFocusWasKeyboard = (focusReason == Qt::TabFocusReason || focusReason == Qt::BacktabFocusReason);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ return false;
+}
+
+void FocusNavigator::onFocusObjectChanged(QObject *obj) {
+ auto item = qobject_cast(obj);
+ if (!item) return;
+ emit focusChanged(item, m_lastFocusWasKeyboard);
+}
diff --git a/Linphone/tool/ui/FocusNavigator.hpp b/Linphone/tool/ui/FocusNavigator.hpp
new file mode 100644
index 000000000..eeaf72145
--- /dev/null
+++ b/Linphone/tool/ui/FocusNavigator.hpp
@@ -0,0 +1,41 @@
+/*
+ * 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 .
+ */
+
+#pragma once
+
+#include
+#include
+
+class FocusNavigator : public QObject {
+ Q_OBJECT
+
+public:
+ explicit FocusNavigator(QObject *parent = nullptr);
+
+protected:
+ bool eventFilter(QObject *obj, QEvent *event) override;
+
+signals:
+ void focusChanged(QQuickItem *item, bool keyboardFocus);
+
+private:
+ bool m_lastFocusWasKeyboard = false;
+ void onFocusObjectChanged(QObject *obj);
+};
diff --git a/Linphone/view/Control/Tool/Helper/utils.js b/Linphone/view/Control/Tool/Helper/utils.js
index 9b72b4c14..e7e99ed42 100644
--- a/Linphone/view/Control/Tool/Helper/utils.js
+++ b/Linphone/view/Control/Tool/Helper/utils.js
@@ -807,6 +807,17 @@ function infoDialog(window, message) {
}, 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
function updatePosition(scrollItem, list){
if(scrollItem.height == 0) return;
diff --git a/Linphone/view/Page/Layout/Settings/AbstractSettingsLayout.qml b/Linphone/view/Page/Layout/Settings/AbstractSettingsLayout.qml
index 8f8f8c852..1e175a822 100644
--- a/Linphone/view/Page/Layout/Settings/AbstractSettingsLayout.qml
+++ b/Linphone/view/Page/Layout/Settings/AbstractSettingsLayout.qml
@@ -126,21 +126,21 @@ Rectangle {
model: mainItem.contentModel
anchors.left: parent.left
anchors.right: parent.right
- anchors.leftMargin: Utils.getSizeWithScreenRatio(45)
- anchors.rightMargin: Utils.getSizeWithScreenRatio(45)
+ anchors.leftMargin: Utils.getSizeWithScreenRatio(45)
+ anchors.rightMargin: Utils.getSizeWithScreenRatio(45)
height: contentHeight
- spacing: Utils.getSizeWithScreenRatio(10)
+ spacing: Utils.getSizeWithScreenRatio(10)
delegate: ColumnLayout {
- visible: modelData.visible != undefined ? modelData.visible: true
- Component.onCompleted: if (!visible) height = 0
- spacing: Utils.getSizeWithScreenRatio(16)
+ visible: modelData.visible != undefined ? modelData.visible: true
+ Component.onCompleted: if (!visible) height = 0
+ spacing: Utils.getSizeWithScreenRatio(16)
width: contentListView.width
Rectangle {
visible: index !== 0
- Layout.topMargin: Utils.getSizeWithScreenRatio(modelData.hideTopSeparator ? 0 : 16)
- Layout.bottomMargin: Utils.getSizeWithScreenRatio(16)
+ Layout.topMargin: Utils.getSizeWithScreenRatio(modelData.hideTopSeparator ? 0 : 16)
+ Layout.bottomMargin: Utils.getSizeWithScreenRatio(16)
Layout.fillWidth: true
- height: Utils.getSizeWithScreenRatio(1)
+ height: Utils.getSizeWithScreenRatio(1)
color: modelData.hideTopSeparator ? 'transparent' : DefaultStyle.main2_500_main
}
GridLayout {
@@ -148,12 +148,12 @@ Rectangle {
columns: mainItem.useVerticalLayout ? 1 : 2
Layout.fillWidth: true
// Layout.preferredWidth: parent.width
- rowSpacing: modelData.title.length > 0 || modelData.subTitle.length > 0 ? Utils.getSizeWithScreenRatio(20) : 0
- columnSpacing: Utils.getSizeWithScreenRatio(47)
+ rowSpacing: modelData.title.length > 0 || modelData.subTitle.length > 0 ? Utils.getSizeWithScreenRatio(20) : 0
+ columnSpacing: Utils.getSizeWithScreenRatio(47)
ColumnLayout {
- Layout.preferredWidth: Utils.getSizeWithScreenRatio(341)
- Layout.maximumWidth: Utils.getSizeWithScreenRatio(341)
- spacing: Utils.getSizeWithScreenRatio(3)
+ Layout.preferredWidth: Utils.getSizeWithScreenRatio(341)
+ Layout.maximumWidth: Utils.getSizeWithScreenRatio(341)
+ spacing: Utils.getSizeWithScreenRatio(3)
Text {
text: modelData.title
visible: modelData.title.length > 0
@@ -175,10 +175,10 @@ Rectangle {
}
}
RowLayout {
- Layout.topMargin: modelData.hideTopMargin ? 0 : Utils.getSizeWithScreenRatio(mainItem.useVerticalLayout ? 10 : 21)
- Layout.bottomMargin: Utils.getSizeWithScreenRatio(21)
- Layout.leftMargin: mainItem.useVerticalLayout ? 0 : Utils.getSizeWithScreenRatio(17)
- Layout.preferredWidth: Utils.getSizeWithScreenRatio(modelData.customWidth > 0 ? modelData.customWidth : 545)
+ Layout.topMargin: modelData.hideTopMargin ? 0 : Utils.getSizeWithScreenRatio(mainItem.useVerticalLayout ? 10 : 21)
+ Layout.bottomMargin: Utils.getSizeWithScreenRatio(21)
+ Layout.leftMargin: mainItem.useVerticalLayout ? 0 : Utils.getSizeWithScreenRatio(17)
+ Layout.preferredWidth: Utils.getSizeWithScreenRatio(modelData.customWidth > 0 ? modelData.customWidth : 545)
Layout.alignment: Qt.AlignRight
Loader {
id: contentLoader
@@ -186,12 +186,22 @@ Rectangle {
sourceComponent: modelData.contentComponent
}
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)
+ }
+ }
+ }
}
}