Android: Add preliminary support for child windows

Update the manual test case for embedded windows to have
native window on Android.

There are still some sharp corners, for example:

* The windows are implemented with SurfaceViews, which makes
  z-ordering with multiple of them a bit tricky. The Surfaces
  they instantiate are basically z-ordered to either be below
  everything, with a hole punched in the window, or on top of
  everything, with the Surfaces created later on top of the
  ones created earlier. Also, with the foreign views it looks
  like the native view is on top of the Surface, because it
  is created later. And since the child windows create their
  Surfaces before the parent, they would be behind the parent
  window, currently circumventing this with letting the
  parent be z-ordered behind everything, and the children
  on top of everything. A follow up commit addresses this by
  changing the native view class to TextureView when multiple
  windows are present.
* Parent window always gets the touch events - fixed in
  a follow up commit
* If a child window has a text edit, it does not receive
  focus when clicking on it

Task-number: QTBUG-116187
Change-Id: I32188ec5e3d3fce9fd8e3a931e317d1e081f691c
Reviewed-by: Assam Boudjelthia <assam.boudjelthia@qt.io>
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
This commit is contained in:
Tinja Paavoseppä 2023-09-19 14:55:12 +03:00
parent 0a92d881bb
commit 6ff88f97a6
15 changed files with 305 additions and 87 deletions

View File

@ -29,6 +29,7 @@ set(java_sources
src/org/qtproject/qt/android/QtClipboardManager.java
src/org/qtproject/qt/android/QtDisplayManager.java
src/org/qtproject/qt/android/UsedFromNativeCode.java
src/org/qtproject/qt/android/QtRootLayout.java
src/org/qtproject/qt/android/QtWindow.java
)

View File

@ -59,7 +59,8 @@ class QtAccessibilityDelegate extends View.AccessibilityDelegate
return dispatchHoverEvent(event);
}
}
// TODO do we want to have one QtAccessibilityDelegate for the whole app (QtRootLayout) or
// e.g. one per window?
public QtAccessibilityDelegate(QtLayout layout)
{
m_layout = layout;

View File

@ -37,13 +37,12 @@ class QtActivityDelegate
{
private Activity m_activity;
private QtLayout m_layout = null;
private QtRootLayout m_layout = null;
private HashMap<Integer, QtWindow> m_topLevelWindows;
private ImageView m_splashScreen = null;
private boolean m_splashScreenSticky = false;
private View m_dummyView = null;
private QtAccessibilityDelegate m_accessibilityDelegate = null;
private QtDisplayManager m_displayManager = null;
@ -135,7 +134,7 @@ class QtActivityDelegate
private void initMembers()
{
m_layout = new QtLayout(m_activity);
m_layout = new QtRootLayout(m_activity);
m_membersInitialized = true;
m_topLevelWindows = new HashMap<Integer, QtWindow>();

View File

@ -30,37 +30,6 @@ class QtLayout extends ViewGroup
super(context, attrs, defStyle);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
Activity activity = (Activity)getContext();
if (activity == null)
return;
DisplayMetrics realMetrics = new DisplayMetrics();
Display display = (Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
? activity.getWindowManager().getDefaultDisplay()
: activity.getDisplay();
if (display == null)
return;
display.getRealMetrics(realMetrics);
if ((realMetrics.widthPixels > realMetrics.heightPixels) != (w > h)) {
// This is an intermediate state during display rotation.
// The new size is still reported for old orientation, while
// realMetrics contain sizes for new orientation. Setting
// such parameters will produce inconsistent results, so
// we just skip them.
// We will have another onSizeChanged() with normal values
// a bit later.
return;
}
QtDisplayManager.setApplicationDisplayMetrics(activity, w, h);
QtDisplayManager.handleOrientationChanges(activity, true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
@ -79,11 +48,15 @@ class QtLayout extends ViewGroup
int childRight;
int childBottom;
QtLayout.LayoutParams lp
= (QtLayout.LayoutParams) child.getLayoutParams();
childRight = lp.x + child.getMeasuredWidth();
childBottom = lp.y + child.getMeasuredHeight();
if (child.getLayoutParams() instanceof QtLayout.LayoutParams) {
QtLayout.LayoutParams lp
= (QtLayout.LayoutParams) child.getLayoutParams();
childRight = lp.x + child.getMeasuredWidth();
childBottom = lp.y + child.getMeasuredHeight();
} else {
childRight = child.getMeasuredWidth();
childBottom = child.getMeasuredHeight();
}
maxWidth = Math.max(maxWidth, childRight);
maxHeight = Math.max(maxHeight, childBottom);
@ -181,6 +154,11 @@ class QtLayout extends ViewGroup
this.y = y;
}
public LayoutParams(int width, int height)
{
super(width, height);
}
/**
* {@inheritDoc}
*/

View File

@ -0,0 +1,74 @@
// Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2012 BogDan Vatra <bogdan@kde.org>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
package org.qtproject.qt.android;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.Display;
/**
A layout which corresponds to one Activity, i.e. is the root layout where the top level window
and handles orientation changes.
*/
public class QtRootLayout extends QtLayout
{
private int m_activityDisplayRotation = -1;
private int m_ownDisplayRotation = -1;
private int m_nativeOrientation = -1;
public QtRootLayout(Context context)
{
super(context);
}
public void setActivityDisplayRotation(int rotation)
{
m_activityDisplayRotation = rotation;
}
public void setNativeOrientation(int orientation)
{
m_nativeOrientation = orientation;
}
public int displayRotation()
{
return m_ownDisplayRotation;
}
@Override
protected void onSizeChanged (int w, int h, int oldw, int oldh)
{
Activity activity = (Activity)getContext();
if (activity == null)
return;
DisplayMetrics realMetrics = new DisplayMetrics();
Display display = (Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
? activity.getWindowManager().getDefaultDisplay()
: activity.getDisplay();
if (display == null)
return;
display.getRealMetrics(realMetrics);
if ((realMetrics.widthPixels > realMetrics.heightPixels) != (w > h)) {
// This is an intermediate state during display rotation.
// The new size is still reported for old orientation, while
// realMetrics contain sizes for new orientation. Setting
// such parameters will produce inconsistent results, so
// we just skip them.
// We will have another onSizeChanged() with normal values
// a bit later.
return;
}
QtDisplayManager.setApplicationDisplayMetrics(activity, w, h);
QtDisplayManager.handleOrientationChanges(activity, true);
}
}

View File

@ -4,8 +4,6 @@
package org.qtproject.qt.android;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Surface;
import android.view.View;
@ -18,14 +16,26 @@ public class QtWindow extends QtLayout implements QtSurface.SurfaceChangedCallba
private QtSurface m_surface;
private View m_nativeView;
private Handler m_androidHandler;
private HashMap<Integer, QtWindow> m_childWindows = new HashMap<Integer, QtWindow>();
private QtWindow m_parentWindow;
private static native void setSurface(int windowId, Surface surface);
public QtWindow(Context context)
public QtWindow(Context context, QtWindow parentWindow)
{
super(context);
setId(View.generateViewId());
setParent(parentWindow);
}
void setVisible(boolean visible) {
QtNative.runAction(() -> {
if (visible)
setVisibility(View.VISIBLE);
else
setVisibility(View.INVISIBLE);
});
}
@Override
@ -34,6 +44,12 @@ public class QtWindow extends QtLayout implements QtSurface.SurfaceChangedCallba
setSurface(getId(), surface);
}
public void removeWindow()
{
if (m_parentWindow != null)
m_parentWindow.removeChildWindow(getId());
}
public void createSurface(final boolean onTop,
final int x, final int y, final int w, final int h,
final int imageDepth)
@ -44,11 +60,20 @@ public class QtWindow extends QtLayout implements QtSurface.SurfaceChangedCallba
if (m_surface != null)
removeView(m_surface);
QtSurface surface = new QtSurface(getContext(),
QtWindow.this, QtWindow.this.getId(),
onTop, imageDepth);
surface.setLayoutParams(new QtLayout.LayoutParams(w, h, x, y));
setLayoutParams(new QtLayout.LayoutParams(w, h, x, y));
// TODO currently setting child windows to onTop, since their surfaces
// now get created earlier than the parents -> they are behind the parent window
// without this, and SurfaceView z-ordering is limited
boolean tempOnTop = onTop || (m_parentWindow != null);
QtSurface surface = new QtSurface(getContext(), QtWindow.this,
QtWindow.this.getId(), tempOnTop, imageDepth);
surface.setLayoutParams(new QtLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
// The QtSurface of this window will be added as the first of the stack.
// All other views are stacked based on the order they are created.
addView(surface, 0);
m_surface = surface;
}
@ -68,16 +93,34 @@ public class QtWindow extends QtLayout implements QtSurface.SurfaceChangedCallba
});
}
public void setSurfaceGeometry(final int x, final int y, final int w, final int h)
public void setGeometry(final int x, final int y, final int w, final int h)
{
QtNative.runAction(new Runnable() {
@Override
public void run() {
QtLayout.LayoutParams lp = new QtLayout.LayoutParams(w, h, x, y);
if (m_surface != null)
m_surface.setLayoutParams(lp);
else if (m_nativeView != null)
m_nativeView.setLayoutParams(lp);
QtWindow.this.setLayoutParams(new QtLayout.LayoutParams(w, h, x, y));
}
});
}
public void addChildWindow(QtWindow window)
{
QtNative.runAction(new Runnable() {
@Override
public void run() {
m_childWindows.put(window.getId(), window);
addView(window, getChildCount());
}
});
}
public void removeChildWindow(int id)
{
QtNative.runAction(new Runnable() {
@Override
public void run() {
if (m_childWindows.containsKey(id))
removeView(m_childWindows.remove(id));
}
});
}
@ -92,13 +135,35 @@ public class QtWindow extends QtLayout implements QtSurface.SurfaceChangedCallba
removeView(m_nativeView);
m_nativeView = view;
m_nativeView.setLayoutParams(new QtLayout.LayoutParams(w, h, x, y));
QtWindow.this.setLayoutParams(new QtLayout.LayoutParams(w, h, x, y));
m_nativeView.setLayoutParams(new QtLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
addView(m_nativeView);
}
});
}
public void bringChildToFront(int id)
{
View view = m_childWindows.get(id);
if (view != null) {
if (getChildCount() > 0)
moveChild(view, getChildCount() - 1);
}
}
public void bringChildToBack(int id) {
QtNative.runAction(new Runnable() {
@Override
public void run() {
View view = m_childWindows.get(id);
if (view != null) {
moveChild(view, 0);
}
}
});
}
public void removeNativeView()
{
QtNative.runAction(new Runnable() {
@ -111,4 +176,22 @@ public class QtWindow extends QtLayout implements QtSurface.SurfaceChangedCallba
}
});
}
void setParent(QtWindow parentWindow)
{
if (m_parentWindow == parentWindow)
return;
if (m_parentWindow != null)
m_parentWindow.removeChildWindow(getId());
m_parentWindow = parentWindow;
if (m_parentWindow != null)
m_parentWindow.addChildWindow(this);
}
QtWindow parent()
{
return m_parentWindow;
}
}

View File

@ -32,7 +32,7 @@ void QAndroidPlatformForeignWindow::setGeometry(const QRect &rect)
QAndroidPlatformWindow::setGeometry(rect);
if (m_nativeViewInserted)
setSurfaceGeometry(rect);
setNativeGeometry(rect);
}
void QAndroidPlatformForeignWindow::setVisible(bool visible)
@ -63,11 +63,6 @@ void QAndroidPlatformForeignWindow::applicationStateChanged(Qt::ApplicationState
QAndroidPlatformWindow::applicationStateChanged(state);
}
void QAndroidPlatformForeignWindow::setParent(const QPlatformWindow *window)
{
Q_UNUSED(window);
}
void QAndroidPlatformForeignWindow::addViewToWindow()
{
jint x = 0, y = 0, w = -1, h = -1;

View File

@ -20,7 +20,6 @@ public:
void setGeometry(const QRect &rect) override;
void setVisible(bool visible) override;
void applicationStateChanged(Qt::ApplicationState state) override;
void setParent(const QPlatformWindow *window) override;
bool isForeignWindow() const override { return true; }
private:

View File

@ -45,7 +45,9 @@ void QAndroidPlatformOpenGLWindow::setGeometry(const QRect &rect)
m_oldGeometry = geometry();
QAndroidPlatformWindow::setGeometry(rect);
setSurfaceGeometry(rect);
setNativeGeometry(rect);
QRect availableGeometry = screen()->availableGeometry();
if (rect.width() > 0
@ -59,7 +61,7 @@ void QAndroidPlatformOpenGLWindow::setGeometry(const QRect &rect)
EGLSurface QAndroidPlatformOpenGLWindow::eglSurface(EGLConfig config)
{
if (QAndroidEventDispatcherStopper::stopped() ||
QGuiApplication::applicationState() == Qt::ApplicationSuspended || !window()->isTopLevel()) {
QGuiApplication::applicationState() == Qt::ApplicationSuspended) {
return m_eglSurface;
}

View File

@ -44,7 +44,6 @@ public:
void removeWindow(QAndroidPlatformWindow *window);
void raise(QAndroidPlatformWindow *window);
void lower(QAndroidPlatformWindow *window);
void topVisibleWindowChanged();
int displayId() const override;

View File

@ -39,7 +39,8 @@ void QAndroidPlatformVulkanWindow::setGeometry(const QRect &rect)
m_oldGeometry = geometry();
QAndroidPlatformWindow::setGeometry(rect);
setSurfaceGeometry(rect);
if (m_surfaceCreated)
setNativeGeometry(rect);
QRect availableGeometry = screen()->availableGeometry();
if (rect.width() > 0

View File

@ -19,10 +19,9 @@ Q_LOGGING_CATEGORY(lcQpaWindow, "qt.qpa.window")
Q_CONSTINIT static QBasicAtomicInt winIdGenerator = Q_BASIC_ATOMIC_INITIALIZER(0);
QAndroidPlatformWindow::QAndroidPlatformWindow(QWindow *window)
: QPlatformWindow(window), m_nativeQtWindow(QNativeInterface::QAndroidApplication::context()),
: QPlatformWindow(window), m_nativeQtWindow(nullptr), m_nativeParentQtWindow(nullptr),
m_androidSurfaceObject(nullptr)
{
m_nativeViewId = m_nativeQtWindow.callMethod<jint>("getId");
m_windowFlags = Qt::Widget;
m_windowState = Qt::WindowNoState;
// the surfaceType is overwritten in QAndroidPlatformOpenGLWindow ctor so let's save
@ -48,22 +47,44 @@ QAndroidPlatformWindow::QAndroidPlatformWindow(QWindow *window)
if (requestedNativeGeometry != finalNativeGeometry)
setGeometry(finalNativeGeometry);
}
platformScreen()->addWindow(this);
if (parent())
m_nativeParentQtWindow = static_cast<QAndroidPlatformWindow*>(parent())->nativeWindow();
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([this]() {
m_nativeQtWindow = QJniObject::construct<QtJniTypes::QtWindow>(
QNativeInterface::QAndroidApplication::context(),
m_nativeParentQtWindow);
m_nativeViewId = m_nativeQtWindow.callMethod<jint>("getId");
}).waitForFinished();
if (window->isTopLevel())
platformScreen()->addWindow(this);
}
QAndroidPlatformWindow::~QAndroidPlatformWindow()
{
platformScreen()->removeWindow(this);
if (window()->isTopLevel())
platformScreen()->removeWindow(this);
}
void QAndroidPlatformWindow::lower()
{
if (m_nativeParentQtWindow.isValid()) {
m_nativeParentQtWindow.callMethod<void>("bringChildToBack", nativeViewId());
return;
}
platformScreen()->lower(this);
}
void QAndroidPlatformWindow::raise()
{
if (m_nativeParentQtWindow.isValid()) {
m_nativeParentQtWindow.callMethod<void>("bringChildToFront", nativeViewId());
QWindowSystemInterface::handleFocusWindowChanged(window(), Qt::ActiveWindowFocusReason);
return;
}
updateSystemUiVisibility();
platformScreen()->raise(this);
}
@ -87,16 +108,20 @@ void QAndroidPlatformWindow::setGeometry(const QRect &rect)
void QAndroidPlatformWindow::setVisible(bool visible)
{
m_nativeQtWindow.callMethod<void>("setVisible", visible);
if (visible) {
updateSystemUiVisibility();
if ((m_windowState & Qt::WindowFullScreen)
|| ((m_windowState & Qt::WindowMaximized) && (window()->flags() & Qt::MaximizeUsingFullscreenGeometryHint))) {
setGeometry(platformScreen()->geometry());
} else if (m_windowState & Qt::WindowMaximized) {
setGeometry(platformScreen()->availableGeometry());
if (window()->isTopLevel()) {
updateSystemUiVisibility();
if ((m_windowState & Qt::WindowFullScreen)
|| ((m_windowState & Qt::WindowMaximized) && (window()->flags() & Qt::MaximizeUsingFullscreenGeometryHint))) {
setGeometry(platformScreen()->geometry());
} else if (m_windowState & Qt::WindowMaximized) {
setGeometry(platformScreen()->availableGeometry());
}
requestActivateWindow();
}
requestActivateWindow();
} else if (window() == qGuiApp->focusWindow()) {
} else if (window()->isTopLevel() && window() == qGuiApp->focusWindow()) {
platformScreen()->topVisibleWindowChanged();
}
@ -132,13 +157,22 @@ Qt::WindowFlags QAndroidPlatformWindow::windowFlags() const
void QAndroidPlatformWindow::setParent(const QPlatformWindow *window)
{
// even though we do not yet support child windows properly, any windows getting a parent
// should be removed from screen's window stack which is only for top level windows,
// and respectively any window becoming top level should go in there
using namespace QtJniTypes;
if (window) {
platformScreen()->removeWindow(this);
// If we were a top level window, remove from screen
if (!m_nativeParentQtWindow.isValid())
platformScreen()->removeWindow(this);
const QAndroidPlatformWindow *androidWindow =
static_cast<const QAndroidPlatformWindow*>(window);
const QtWindow parentWindow = androidWindow->nativeWindow();
// If this was a child window of another window, the java method takes care of that
m_nativeQtWindow.callMethod<void, QtWindow>("setParent", parentWindow.object());
m_nativeParentQtWindow = parentWindow;
} else {
m_nativeQtWindow.callMethod<void, QtWindow>("setParent", nullptr);
platformScreen()->addWindow(this);
m_nativeParentQtWindow = QJniObject();
}
}
@ -154,6 +188,7 @@ void QAndroidPlatformWindow::propagateSizeHints()
void QAndroidPlatformWindow::requestActivateWindow()
{
// raise() will handle differences between top level and child windows, and requesting focus
if (!blockedByModal())
raise();
}
@ -214,7 +249,7 @@ void QAndroidPlatformWindow::destroySurface()
}
}
void QAndroidPlatformWindow::setSurfaceGeometry(const QRect &geometry)
void QAndroidPlatformWindow::setNativeGeometry(const QRect &geometry)
{
if (!m_surfaceCreated)
return;
@ -229,7 +264,7 @@ void QAndroidPlatformWindow::setSurfaceGeometry(const QRect &geometry)
w = geometry.width();
h = geometry.height();
}
m_nativeQtWindow.callMethod<void>("setSurfaceGeometry", x, y, w, h);
m_nativeQtWindow.callMethod<void>("setGeometry", x, y, w, h);
}
void QAndroidPlatformWindow::onSurfaceChanged(QtJniTypes::Surface surface)

View File

@ -65,7 +65,7 @@ protected:
void unlockSurface() { m_surfaceMutex.unlock(); }
void createSurface();
void destroySurface();
void setSurfaceGeometry(const QRect &geometry);
void setNativeGeometry(const QRect &geometry);
void sendExpose() const;
bool blockedByModal() const;
@ -76,6 +76,7 @@ protected:
WId m_windowId;
QtJniTypes::QtWindow m_nativeQtWindow;
QtJniTypes::QtWindow m_nativeParentQtWindow;
// The Android Surface, accessed from multiple threads, guarded by m_surfaceMutex
QtJniTypes::Surface m_androidSurfaceObject;
QWaitCondition m_surfaceWaitCondition;

View File

@ -3,7 +3,7 @@
#include <QtGui>
#if defined(Q_OS_MACOS) || defined(Q_OS_IOS) || defined(Q_OS_WIN) || QT_CONFIG(xcb)
#if defined(Q_OS_MACOS) || defined(Q_OS_IOS) || defined(Q_OS_WIN) || QT_CONFIG(xcb) || defined(ANDROID)
#include "../../shared/nativewindow.h"
#define HAVE_NATIVE_WINDOW
#endif

View File

@ -14,6 +14,12 @@
# include <winuser.h>
#elif QT_CONFIG(xcb)
# include <xcb/xcb.h>
#elif defined(ANDROID)
# include <QtCore/qjniobject.h>
# include <QtCore/qjnitypes.h>
# include <QtCore/qnativeinterface.h>
Q_DECLARE_JNI_CLASS(View, "android/view/View")
Q_DECLARE_JNI_CLASS(ViewParent, "android/view/ViewParent")
#endif
class NativeWindow
@ -28,6 +34,8 @@ public:
using Handle = HWND;
#elif QT_CONFIG(xcb)
using Handle = xcb_window_t;
#elif defined(ANDROID)
using Handle = QtJniTypes::View;;
#endif
NativeWindow();
@ -249,6 +257,48 @@ void NativeWindow::setParent(WId parent)
parent ? Handle(parent) : screen->root, 0, 0);
}
#elif defined (ANDROID)
NativeWindow::NativeWindow()
{
m_handle = QJniObject::construct<QtJniTypes::View, QtJniTypes::Context>(
QNativeInterface::QAndroidApplication::context());
m_handle.callMethod<void>("setBackgroundColor", 0xffffaaff);
}
NativeWindow::~NativeWindow()
{
}
NativeWindow::operator WId() const
{
return reinterpret_cast<WId>(m_handle.object());
}
void NativeWindow::setGeometry(const QRect &rect)
{
// No-op, the view geometry is handled by the QWindow constructed from it
}
QRect NativeWindow::geometry() const
{
int x = m_handle.callMethod<jint>("getX");
int y = m_handle.callMethod<jint>("getY");
int w = m_handle.callMethod<jint>("getWidth");
int h = m_handle.callMethod<jint>("getHeight");
return QRect(x, y, w, h);
}
WId NativeWindow::parentWinId() const
{
// TODO note, the returned object is a ViewParent, not necessarily
// a View - what is this used for?
using namespace QtJniTypes;
ViewParent parentView = m_handle.callMethod<ViewParent>("getParent");
if (parentView.isValid())
return reinterpret_cast<WId>(parentView.object());
return 0L;
}
#endif
#endif // NATIVEWINDOW_H