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) + } + } + } } }