summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBernd Weimer <bernd.weimer@qt.io>2022-11-15 16:38:43 +0100
committerBernd Weimer <bernd.weimer@qt.io>2022-12-01 14:35:32 +0100
commita7d4f5324b3e80f794a73bbb4f2f4674214cf4a5 (patch)
treed16fd7abe6c5e7e6ebec02cf4841e7645d19f585
parent68888133e2aa7dd3ed2ef1c4d1095065749d7e7f (diff)
downloadqtapplicationmanager-a7d4f5324b3e80f794a73bbb4f2f4674214cf4a5.tar.gz
Forward keyboard focus to applications
Key events were not passed to applications, when the focus moved to a WindowItem in the System UI, since the item representing the application window is actually a child of the WindowItem. A possible solution would be to explicitly send the key events to the application item, though the approach taken here is to forward the keyboard focus. This results in key events always reaching the application first (even when the System UI would actually consume the events) and in single-process mode, if the application consumes the event the System UI won't get it any more. Fixes: QTBUG-108837 Change-Id: I1699556f1a67057e8be2848e37c846c2ee33a646 Reviewed-by: Robert Griebl <robert.griebl@qt.io>
-rw-r--r--doc/singlevsmultiprocess.qdoc10
-rw-r--r--src/manager-lib/inprocesssurfaceitem.cpp4
-rw-r--r--src/manager-lib/inprocesssurfaceitem.h4
-rw-r--r--src/window-lib/windowitem.cpp42
-rw-r--r--src/window-lib/windowitem.h11
-rw-r--r--tests/auto/qml/CMakeLists.txt1
-rw-r--r--tests/auto/qml/keyinput/CMakeLists.txt6
-rw-r--r--tests/auto/qml/keyinput/am-config.yaml7
-rw-r--r--tests/auto/qml/keyinput/apps/app1/info.yaml6
-rw-r--r--tests/auto/qml/keyinput/apps/app1/main.qml50
-rw-r--r--tests/auto/qml/keyinput/apps/app2/info.yaml6
-rw-r--r--tests/auto/qml/keyinput/apps/app2/main.qml50
-rw-r--r--tests/auto/qml/keyinput/tst_keyinput.qml212
13 files changed, 398 insertions, 11 deletions
diff --git a/doc/singlevsmultiprocess.qdoc b/doc/singlevsmultiprocess.qdoc
index 732ebd75..e09e2944 100644
--- a/doc/singlevsmultiprocess.qdoc
+++ b/doc/singlevsmultiprocess.qdoc
@@ -160,6 +160,16 @@ compatibility. The following are some examples:
\endlist
+\section1 Input
+
+There are some peculiarities in terms of keyboard input, caused amongst others by the fact, that
+Wayland clients (applications) cannot report back to the System UI, whether they have accepted a
+key event. In multi-process mode key events will be passed to both, the System UI and applications.
+This is also the case in single-process mode, however, if the application accepts the event, the
+System UI will not get it any more. This can be circumvented by using \l{Shortcut}s or installing a
+global event filter in the System UI.
+
+
\section1 Resource Consumption
CPU usage on a multi-core system may be more efficient in multi-process mode, since the OS can
diff --git a/src/manager-lib/inprocesssurfaceitem.cpp b/src/manager-lib/inprocesssurfaceitem.cpp
index 4ae43b83..e18f7ec6 100644
--- a/src/manager-lib/inprocesssurfaceitem.cpp
+++ b/src/manager-lib/inprocesssurfaceitem.cpp
@@ -25,7 +25,7 @@ static bool isName(const QByteArray &key)
}
InProcessSurfaceItem::InProcessSurfaceItem(QQuickItem *parent)
- : QQuickItem(parent)
+ : QQuickFocusScope(parent)
{
QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
setClip(true);
@@ -83,7 +83,7 @@ bool InProcessSurfaceItem::eventFilter(QObject *o, QEvent *e)
}
}
- return QQuickItem::eventFilter(o, e);
+ return QQuickFocusScope::eventFilter(o, e);
}
void InProcessSurfaceItem::setVisibleClientSide(bool value)
diff --git a/src/manager-lib/inprocesssurfaceitem.h b/src/manager-lib/inprocesssurfaceitem.h
index 2600ddcc..c0e0fa49 100644
--- a/src/manager-lib/inprocesssurfaceitem.h
+++ b/src/manager-lib/inprocesssurfaceitem.h
@@ -6,8 +6,8 @@
#pragma once
#include <QColor>
-#include <QQuickItem>
#include <QPointer>
+#include <private/qquickfocusscope_p.h>
#include <QtAppManCommon/global.h>
QT_BEGIN_NAMESPACE_AM
@@ -15,7 +15,7 @@ QT_BEGIN_NAMESPACE_AM
/*
* Item exposed to the System UI
*/
-class InProcessSurfaceItem : public QQuickItem
+class InProcessSurfaceItem : public QQuickFocusScope
{
Q_OBJECT
public:
diff --git a/src/window-lib/windowitem.cpp b/src/window-lib/windowitem.cpp
index c3dbaee1..5d540624 100644
--- a/src/window-lib/windowitem.cpp
+++ b/src/window-lib/windowitem.cpp
@@ -7,6 +7,7 @@
#include "window.h"
#if defined(AM_MULTI_PROCESS)
+#include <QWaylandQuickItem>
#include "waylandcompositor.h"
#include "waylandwindow.h"
#endif // AM_MULTI_PROCESS
@@ -108,6 +109,11 @@ WindowItem::WindowItem(QQuickItem *parent)
m_contentItem->setZ(2);
m_contentItem->setWidth(width());
m_contentItem->setHeight(height());
+
+ connect(this, &QQuickItem::activeFocusChanged, this, [this] () {
+ if (hasActiveFocus() && m_impl)
+ m_impl->forwardActiveFocus();
+ });
}
WindowItem::~WindowItem()
@@ -354,12 +360,36 @@ void WindowItem::InProcessImpl::setupSecondaryView()
}
}
+void WindowItem::InProcessImpl::forwardActiveFocus()
+{
+ m_inProcessWindow->rootItem()->forceActiveFocus();
+}
+
///////////////////////////////////////////////////////////////////////////////////////////////////
// WindowItem::WaylandImpl
///////////////////////////////////////////////////////////////////////////////////////////////////
#if defined(AM_MULTI_PROCESS)
+class WaylandQuickIgnoreKeyItem : public QWaylandQuickItem
+{
+public:
+ WaylandQuickIgnoreKeyItem(QQuickItem *parent) : QWaylandQuickItem(parent) {}
+
+protected:
+ void keyPressEvent(QKeyEvent *event) override
+ {
+ QWaylandQuickItem::keyPressEvent(event);
+ event->ignore();
+ }
+
+ void keyReleaseEvent(QKeyEvent *event) override
+ {
+ QWaylandQuickItem::keyReleaseEvent(event);
+ event->ignore();
+ }
+};
+
WindowItem::WaylandImpl::~WaylandImpl()
{
delete m_waylandItem;
@@ -377,18 +407,26 @@ void WindowItem::WaylandImpl::setup(Window *window)
m_waylandItem->setBufferLocked(false);
m_waylandItem->setSurface(m_waylandWindow->surface());
+
+ if (q->hasActiveFocus())
+ forwardActiveFocus();
}
void WindowItem::WaylandImpl::createWaylandItem()
{
- m_waylandItem = new QWaylandQuickItem(q);
+ m_waylandItem = new WaylandQuickIgnoreKeyItem(q);
- connect(m_waylandItem, &QWaylandQuickItem::surfaceDestroyed, q, [this]() {
+ connect(m_waylandItem, &WaylandQuickIgnoreKeyItem::surfaceDestroyed, q, [this]() {
// keep the buffer there to allow us to animate the window destruction
m_waylandItem->setBufferLocked(true);
});
}
+void WindowItem::WaylandImpl::forwardActiveFocus()
+{
+ m_waylandItem->forceActiveFocus();
+}
+
void WindowItem::WaylandImpl::tearDown()
{
m_waylandItem->setSurface(nullptr);
diff --git a/src/window-lib/windowitem.h b/src/window-lib/windowitem.h
index 19989542..33f51258 100644
--- a/src/window-lib/windowitem.h
+++ b/src/window-lib/windowitem.h
@@ -8,16 +8,13 @@
#include <QQuickItem>
#include <QtAppManCommon/global.h>
-#if defined(AM_MULTI_PROCESS)
-#include <QWaylandQuickItem>
-#endif // AM_MULTI_PROCESS
-
QT_BEGIN_NAMESPACE_AM
class Window;
class InProcessWindow;
#if defined(AM_MULTI_PROCESS)
class WaylandWindow;
+class WaylandQuickIgnoreKeyItem;
#endif // AM_MULTI_PROCESS
@@ -57,6 +54,7 @@ public:
protected:
void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override;
+
signals:
void windowChanged();
void primaryChanged();
@@ -78,6 +76,7 @@ private:
virtual Window *window() const = 0;
virtual void setupPrimaryView() = 0;
virtual void setupSecondaryView() = 0;
+ virtual void forwardActiveFocus() = 0;
WindowItem *q;
};
@@ -90,6 +89,7 @@ private:
Window *window() const override;
void setupPrimaryView() override;
void setupSecondaryView() override;
+ void forwardActiveFocus() override;
InProcessWindow *m_inProcessWindow{nullptr};
QQuickItem *m_shaderEffectSource{nullptr};
@@ -107,9 +107,10 @@ private:
void setupPrimaryView() override;
void setupSecondaryView() override;
void createWaylandItem();
+ void forwardActiveFocus() override;
WaylandWindow *m_waylandWindow{nullptr};
- QWaylandQuickItem *m_waylandItem{nullptr};
+ WaylandQuickIgnoreKeyItem *m_waylandItem{nullptr};
};
#endif // AM_MULTI_PROCESS
diff --git a/tests/auto/qml/CMakeLists.txt b/tests/auto/qml/CMakeLists.txt
index e1280bd0..ecbb58b5 100644
--- a/tests/auto/qml/CMakeLists.txt
+++ b/tests/auto/qml/CMakeLists.txt
@@ -18,6 +18,7 @@ if (TARGET Qt6::appman-qmltestrunner)
add_subdirectory(configs)
add_subdirectory(lifecycle)
add_subdirectory(resources)
+ add_subdirectory(keyinput)
if (QT_FEATURE_am_multi_process)
add_subdirectory(crash)
add_subdirectory(processtitle)
diff --git a/tests/auto/qml/keyinput/CMakeLists.txt b/tests/auto/qml/keyinput/CMakeLists.txt
new file mode 100644
index 00000000..16db54de
--- /dev/null
+++ b/tests/auto/qml/keyinput/CMakeLists.txt
@@ -0,0 +1,6 @@
+
+qt_am_internal_add_qml_test(tst_keyinput
+ CONFIG_YAML am-config.yaml
+ EXTRA_FILES apps
+ TEST_FILE tst_keyinput.qml
+)
diff --git a/tests/auto/qml/keyinput/am-config.yaml b/tests/auto/qml/keyinput/am-config.yaml
new file mode 100644
index 00000000..1aa8648a
--- /dev/null
+++ b/tests/auto/qml/keyinput/am-config.yaml
@@ -0,0 +1,7 @@
+formatVersion: 1
+formatType: am-configuration
+---
+applications:
+ builtinAppsManifestDir: "${CONFIG_PWD}/apps"
+ installationDir: "/tmp/am/apps"
+ documentDir: "/tmp/am/docs"
diff --git a/tests/auto/qml/keyinput/apps/app1/info.yaml b/tests/auto/qml/keyinput/apps/app1/info.yaml
new file mode 100644
index 00000000..48cbc46a
--- /dev/null
+++ b/tests/auto/qml/keyinput/apps/app1/info.yaml
@@ -0,0 +1,6 @@
+formatVersion: 1
+formatType: am-application
+---
+id: 'app1'
+code: 'main.qml'
+runtime: 'qml'
diff --git a/tests/auto/qml/keyinput/apps/app1/main.qml b/tests/auto/qml/keyinput/apps/app1/main.qml
new file mode 100644
index 00000000..84d201ce
--- /dev/null
+++ b/tests/auto/qml/keyinput/apps/app1/main.qml
@@ -0,0 +1,50 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtApplicationManager
+import QtApplicationManager.Application
+
+
+ApplicationManagerWindow {
+ id: root
+ width: 320
+ height: 240
+ visible: true
+
+ Rectangle {
+ id: rect
+ anchors.fill: parent
+ color: "lightsteelblue"
+ focus: true
+
+ Text {
+ text: `${rect.activeFocus}/${root.active}`
+ }
+
+ Text {
+ id: txt
+ anchors.centerIn: parent
+ font.pixelSize: 32
+ }
+
+ Timer {
+ id: tim
+ onTriggered: txt.text = "";
+ }
+
+ Keys.onReleased: (event) => {
+ IntentClient.sendIntentRequest("copythat", { app: 1, key: event.key });
+
+ if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
+ //console.info(`App1: ${event.key === Qt.Key_Up ? "UP" : "DOWN"}`);
+ txt.text = `App1: ${event.key === Qt.Key_Up ? "UP" : "DOWN"}`;
+ tim.restart();
+ } else if (event.key === Qt.Key_Right || event.key === Qt.Key_Left) {
+ //console.info(`App1: ${event.key === Qt.Key_Right ? "RIGHT" : "LEFT"}`);
+ } else {
+ console.info(`App1: ${event.key}`);
+ }
+ }
+ }
+}
diff --git a/tests/auto/qml/keyinput/apps/app2/info.yaml b/tests/auto/qml/keyinput/apps/app2/info.yaml
new file mode 100644
index 00000000..2ee09d6e
--- /dev/null
+++ b/tests/auto/qml/keyinput/apps/app2/info.yaml
@@ -0,0 +1,6 @@
+formatVersion: 1
+formatType: am-application
+---
+id: 'app2'
+code: 'main.qml'
+runtime: 'qml'
diff --git a/tests/auto/qml/keyinput/apps/app2/main.qml b/tests/auto/qml/keyinput/apps/app2/main.qml
new file mode 100644
index 00000000..14bd40db
--- /dev/null
+++ b/tests/auto/qml/keyinput/apps/app2/main.qml
@@ -0,0 +1,50 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtApplicationManager
+import QtApplicationManager.Application
+
+
+ApplicationManagerWindow {
+ id: root
+ width: 320
+ height: 240
+ visible: true
+
+ Rectangle {
+ id: rect
+ anchors.fill: parent
+ color: "lightsteelblue"
+ focus: true
+
+ Text {
+ text: `${rect.activeFocus}/${root.active}`
+ }
+
+ Text {
+ id: txt
+ anchors.centerIn: parent
+ font.pixelSize: 32
+ }
+
+ Timer {
+ id: tim
+ onTriggered: txt.text = "";
+ }
+
+ Keys.onReleased: (event) => {
+ IntentClient.sendIntentRequest("copythat", { app: 2, key: event.key });
+
+ if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
+ //console.info(`App2: ${event.key === Qt.Key_Up ? "UP" : "DOWN"}`);
+ txt.text = `App2: ${event.key === Qt.Key_Up ? "UP" : "DOWN"}`;
+ tim.restart();
+ } else if (event.key === Qt.Key_Right || event.key === Qt.Key_Left) {
+ //console.info(`App2: ${event.key === Qt.Key_Right ? "RIGHT" : "LEFT"}`);
+ } else {
+ console.info(`App2: ${event.key}`);
+ }
+ }
+ }
+}
diff --git a/tests/auto/qml/keyinput/tst_keyinput.qml b/tests/auto/qml/keyinput/tst_keyinput.qml
new file mode 100644
index 00000000..97b46143
--- /dev/null
+++ b/tests/auto/qml/keyinput/tst_keyinput.qml
@@ -0,0 +1,212 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtTest
+import QtApplicationManager.SystemUI
+
+TestCase {
+ id: root
+
+ readonly property int delay: 0 // [ms] increase to follow what's going on
+ readonly property int spyTimeout: 5000 * AmTest.timeoutFactor
+
+ component WindowChrome: FocusScope {
+ property alias winSurface: inner
+ signal keyEvent(int key, bool press)
+
+ width: inner.width
+ height: inner.height
+
+ WindowItem {
+ id: inner
+ focus: true
+
+ Keys.onPressed: (event) => {
+ keyEvent(event.key, true)
+ if (event.key === Qt.Key_Left || event.key === Qt.Key_Right)
+ event.accepted = true;
+ }
+
+ Keys.onReleased: (event) => {
+ keyEvent(event.key, false)
+ if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
+ txt.text = `System UI: ${event.key === Qt.Key_Up ? "UP" : "DOWN"}`;
+ tim.restart();
+ } else if (event.key === Qt.Key_Left) {
+ leftWin.focus = true;
+ event.accepted = true;
+ } else if (event.key === Qt.Key_Right) {
+ rightWin.focus = true;
+ event.accepted = true;
+ } else {
+ console.info(`System UI: ${event.key}`);
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.fill: inner
+ anchors.margins: -border.width
+ border.width: 3
+ border.color: parent.activeFocus ? "red" : "transparent"
+ color: "transparent"
+
+ Text {
+ id: txt
+ anchors.horizontalCenter: parent.horizontalCenter
+ font.pixelSize: 16
+ }
+
+ Timer {
+ id: tim
+ onTriggered: txt.text = "";
+ }
+ }
+ }
+
+ name: "KeyInput"
+ width: 652
+ height: 246
+ visible: true
+ when: windowShown
+
+ WindowChrome {
+ id: leftWin
+ x: 3; y: 3
+ focus: true
+ }
+
+ WindowChrome {
+ id: rightWin
+ x: 329; y: 3
+ }
+
+ Connections {
+ target: WindowManager
+
+ function onWindowAdded(window) {
+ if (window.application.id === "app1")
+ leftWin.winSurface.window = window;
+ else if (window.application.id === "app2")
+ rightWin.winSurface.window = window;
+ }
+
+ function onWindowContentStateChanged(window) {
+ if (window.contentState === WindowObject.NoSurface) {
+ if (window.application.id === "app1")
+ leftWin.winSurface.window = null;
+ else if (window.application.id === "app2")
+ rightWin.winSurface.window = null;
+ }
+ }
+ }
+
+ IntentServerHandler {
+ id: intentHandler
+ intentIds: [ "copythat" ]
+ visibility: IntentObject.Public
+ //onRequestReceived: (request) => { console.info(`System UI: key: ${request.parameters.key} ack.`); }
+ }
+
+ SignalSpy {
+        id: copyThatSpy
+        target: intentHandler
+        signalName: "requestReceived"
+    }
+
+ SignalSpy {
+        id: keyEventLeftSpy
+        target: leftWin
+        signalName: "keyEvent"
+    }
+
+ SignalSpy {
+        id: keyEventRightSpy
+        target: rightWin
+        signalName: "keyEvent"
+    }
+
+ function click(key) {
+ keyPress(key);
+ AmTest.aboutToBlock(); // see QTBUG-83422
+ wait(50);
+ keyRelease(key);
+ AmTest.aboutToBlock();
+ wait(root.delay);
+ }
+
+ function test_moveFocus() {
+ if (root.Window.window.flags & Qt.WindowDoesNotAcceptFocus)
+ skip("Test can only be run without AM_BACKGROUND_TEST set, since it requires input focus");
+
+ ApplicationManager.startApplication("app1");
+ ApplicationManager.startApplication("app2");
+
+ tryVerify(function() {
+ return leftWin.winSurface.window != null && rightWin.winSurface.window != null
+ }, spyTimeout);
+
+ wait(100);
+
+ //console.info("Inject Up");
+ click(Qt.Key_Up);
+ copyThatSpy.wait(spyTimeout);
+ compare(copyThatSpy.signalArguments[0][0].parameters.app, 1);
+ compare(copyThatSpy.signalArguments[0][0].parameters.key, Qt.Key_Up);
+ keyEventLeftSpy.wait(spyTimeout);
+ compare(keyEventLeftSpy.signalArguments[0][0], Qt.Key_Up);
+ verify(keyEventLeftSpy.signalArguments[0][1]);
+ keyEventLeftSpy.wait(spyTimeout);
+ compare(keyEventLeftSpy.signalArguments[1][0], Qt.Key_Up);
+ verify(!keyEventLeftSpy.signalArguments[1][1]);
+
+ //console.info("Inject Right");
+ click(Qt.Key_Right);
+ copyThatSpy.wait(spyTimeout); // unfortunately keys always go to clients first
+ compare(copyThatSpy.signalArguments[1][0].parameters.app, 1);
+ compare(copyThatSpy.signalArguments[1][0].parameters.key, Qt.Key_Right);
+ keyEventLeftSpy.wait(spyTimeout);
+ compare(keyEventLeftSpy.signalArguments[2][0], Qt.Key_Right);
+ verify(keyEventLeftSpy.signalArguments[2][1]);
+ keyEventLeftSpy.wait(spyTimeout);
+ compare(keyEventLeftSpy.signalArguments[3][0], Qt.Key_Right);
+ verify(!keyEventLeftSpy.signalArguments[3][1]);
+
+ //console.info("Inject Down");
+ click(Qt.Key_Down);
+ copyThatSpy.wait(spyTimeout);
+ compare(copyThatSpy.signalArguments[2][0].parameters.app, 2);
+ compare(copyThatSpy.signalArguments[2][0].parameters.key, Qt.Key_Down);
+ keyEventRightSpy.wait(spyTimeout);
+ compare(keyEventRightSpy.signalArguments[0][0], Qt.Key_Down);
+ verify(keyEventRightSpy.signalArguments[0][1]);
+ keyEventRightSpy.wait(spyTimeout);
+ compare(keyEventRightSpy.signalArguments[1][0], Qt.Key_Down);
+ verify(!keyEventRightSpy.signalArguments[1][1]);
+
+ //console.info("Inject Left");
+ click(Qt.Key_Left);
+ copyThatSpy.wait(spyTimeout); // unfortunately keys always go to clients first
+ compare(copyThatSpy.signalArguments[3][0].parameters.app, 2);
+ compare(copyThatSpy.signalArguments[3][0].parameters.key, Qt.Key_Left);
+ keyEventRightSpy.wait(spyTimeout);
+ compare(keyEventRightSpy.signalArguments[2][0], Qt.Key_Left);
+ verify(keyEventRightSpy.signalArguments[2][1]);
+ keyEventRightSpy.wait(spyTimeout);
+ compare(keyEventRightSpy.signalArguments[3][0], Qt.Key_Left);
+ verify(!keyEventRightSpy.signalArguments[3][1]);
+
+ //console.info("Inject Down");
+ click(Qt.Key_Down);
+ copyThatSpy.wait(spyTimeout);
+ compare(copyThatSpy.signalArguments[4][0].parameters.app, 1);
+ compare(copyThatSpy.signalArguments[4][0].parameters.key, Qt.Key_Down);
+ keyEventLeftSpy.wait(spyTimeout);
+ compare(keyEventLeftSpy.signalArguments[4][0], Qt.Key_Down);
+ verify(keyEventLeftSpy.signalArguments[4][1]);
+ keyEventLeftSpy.wait(spyTimeout);
+ compare(keyEventLeftSpy.signalArguments[5][0], Qt.Key_Down);
+ verify(!keyEventLeftSpy.signalArguments[5][1]);
+ }
+}