Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
QtWindow.java
Go to the documentation of this file.
1// Copyright (C) 2023 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4package org.qtproject.qt.android;
5
6import android.annotation.SuppressLint;
7import android.app.ActionBar;
8import android.app.Activity;
9import android.content.Context;
10import android.content.res.TypedArray;
11import android.graphics.Insets;
12
13import android.view.DisplayCutout;
14import android.view.GestureDetector;
15import android.view.MotionEvent;
16import android.view.Surface;
17import android.view.View;
18import android.view.ViewGroup;
19import android.view.ViewTreeObserver;
20import android.view.Window;
21import android.view.WindowInsets;
22import android.view.WindowManager;
23import android.view.WindowInsetsController;
24import android.os.Build;
25
26import java.util.HashMap;
27import java.util.concurrent.ConcurrentLinkedQueue;
28
29@SuppressLint("ViewConstructor")
30class QtWindow extends QtLayout implements QtSurfaceInterface
31{
32 private View m_surfaceContainer;
33 // Read from native code on the Qt GUI thread, written on the UI thread.
34 private volatile Surface m_surface;
35 private boolean m_nativeSurfaceRetired = false;
36 private final ConcurrentLinkedQueue<Runnable> m_pendingSurfaceReleases = new ConcurrentLinkedQueue<>();
37 private View m_nativeView;
38 private final HashMap<Integer, QtWindow> m_childWindows = new HashMap<>();
39 private QtWindow m_parentWindow;
40 private GestureDetector m_gestureDetector;
41 private final QtEditText m_editText;
42 private boolean m_editTextFocusInitialized = false;
43 private final QtInputConnection.QtInputConnectionListener m_inputConnectionListener;
44 private boolean m_firstSafeMarginsDelivered = false;
45 private WindowInsets m_pendingSafeAreaInsets;
46 private int m_actionBarHeight = -1;
47
48 private static native void surfaceCreated(int windowId, Surface surface);
49 private static native void surfaceRedrawNeeded(int windowId, Runnable drawingFinished);
50 private static native void surfaceDestroyed(int windowId, Runnable surfaceReleased);
51 private static native void safeAreaMarginsChanged(Insets insets, int id);
52 static native void windowFocusChanged(boolean hasFocus, int id);
53 static native void updateWindows();
54
55 QtWindow(Context context, boolean isForeignWindow, QtWindow parentWindow,
56 QtInputConnection.QtInputConnectionListener listener)
57 {
58 super(context);
59 m_inputConnectionListener = listener;
60 setParent(parentWindow);
61 setFocusableInTouchMode(true);
62 setDefaultFocusHighlightEnabled(false);
63 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
64
65 // Views are by default visible, but QWindows are not. Set directly, the
66 // view isn't attached yet, so no queued action can undo a later commit.
67 setVisibility(View.INVISIBLE);
68
69 if (!isForeignWindow && context instanceof Activity) {
70 // TODO QTBUG-122552 - Service keyboard input not implemented
71 m_editText = new QtEditText(context, listener);
72 m_editText.setFocusable(false);
73 m_editText.setFocusableInTouchMode(false);
74 m_editText.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
75 LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
76 ViewGroup.LayoutParams.WRAP_CONTENT);
77 QtNative.runAction(() -> addView(m_editText, layoutParams));
78 } else {
79 m_editText = null;
80 }
81
82 QtNative.runAction(() -> {
83 m_gestureDetector =
84 new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
85 @Override
86 public void onLongPress(MotionEvent event) {
87 QtInputDelegate.longPress(getId(), (int) event.getX(), (int) event.getY(),
88 event.getMetaState());
89 }
90 });
91 m_gestureDetector.setIsLongpressEnabled(true);
92 });
93
94 registerSafeAreaMarginsListener();
95 }
96
97 // Reports safe margins for this window and its children. Also called from
98 // the decor listener, since the insets dispatch can be consumed above us.
99 void reportSafeAreaMarginsFromRoot(WindowInsets freshInsets)
100 {
101 if (!isAttachedToWindow())
102 return;
103 // Post so the layout settles before measuring. Inside a dispatch the
104 // root insets are still the old ones, so use the dispatched insets.
105 getRootView().post(() -> {
106 WindowInsets insets = freshInsets != null ? freshInsets : getRootWindowInsets();
107 if (insets != null)
108 updateSafeAreaMargins(insets);
109 });
110 for (QtWindow child : m_childWindows.values())
111 child.reportSafeAreaMarginsFromRoot(freshInsets);
112 }
113
114 void registerSafeAreaMarginsListener()
115 {
116 if (!(getContext() instanceof QtActivityBase))
117 return;
118
119 setOnApplyWindowInsetsListener((view, insets) -> {
120 WindowInsets windowInsets = view.onApplyWindowInsets(insets);
121 updateSafeAreaMargins(windowInsets);
122
123 return windowInsets;
124 });
125
126 // If the window is attached, try to directly deliver root insets
127 if (isAttachedToWindow()) {
128 WindowInsets insets = getRootWindowInsets();
129 if (insets != null) {
130 getRootView().post(() -> updateSafeAreaMargins(insets));
131 m_firstSafeMarginsDelivered = true;
132 }
133 } else { // Otherwise request it upon attachement
134 addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
135 @Override
136 public void onViewAttachedToWindow(View view) {
137 view.removeOnAttachStateChangeListener(this);
138 view.requestApplyInsets();
139 }
140
141 @Override
142 public void onViewDetachedFromWindow(View view) {}
143 });
144 }
145
146 // Further, tag into pre draw to deliver safe area margins early on
147 if (!m_firstSafeMarginsDelivered) {
148 ViewTreeObserver.OnPreDrawListener listener = new ViewTreeObserver.OnPreDrawListener() {
149 @Override
150 public boolean onPreDraw() {
151 if (isAttachedToWindow()) {
152 WindowInsets insets = getRootWindowInsets();
153 if (insets != null) {
154 getViewTreeObserver().removeOnPreDrawListener(this);
155 getRootView().post(() -> updateSafeAreaMargins(insets));
156 m_firstSafeMarginsDelivered = true;
157 return true;
158 }
159 }
160
161 requestApplyInsets();
162
163 return true;
164 }
165 };
166 getViewTreeObserver().addOnPreDrawListener(listener);
167 }
168 }
169
170 @Override
171 protected void onLayout(boolean changed, int l, int t, int r, int b)
172 {
173 // Report before the children are laid out, so Qt gets the safe area of
174 // this pass before a Surface the pass creates.
175 reportSafeAreaMargins();
176 super.onLayout(changed, l, t, r, b);
177 }
178
179 @SuppressWarnings("deprecation")
180 private boolean isAdjustResize()
181 {
182 if (!(getContext() instanceof Activity))
183 return false;
184 int mode = ((Activity) getContext()).getWindow().getAttributes().softInputMode
185 & WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
186 return mode == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
187 }
188
189 @SuppressWarnings("deprecation")
190 Insets getSafeInsets(View view, WindowInsets insets)
191 {
192 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
193 int types = WindowInsets.Type.displayCutout() | WindowInsets.Type.systemBars();
194 // The child dispatch folds the keyboard into the system bars under
195 // adjustResize, but root insets keep it apart, so add it for both.
196 if (isAdjustResize())
197 types |= WindowInsets.Type.ime();
198 return insets.getInsets(types);
199 }
200
201 // Android R and older
202 int left = insets.getSystemWindowInsetLeft();
203 int top = insets.getSystemWindowInsetTop();
204 int right = insets.getSystemWindowInsetRight();
205 int bottom = insets.getSystemWindowInsetBottom();
206
207 // Android 9 and 10 emulators don't seem to be able
208 // to handle this, but let's have the logic here anyway
209 DisplayCutout cutout = insets.getDisplayCutout();
210 if (cutout != null) {
211 left = Math.max(left, cutout.getSafeInsetLeft());
212 top = Math.max(top, cutout.getSafeInsetTop());
213 right = Math.max(right, cutout.getSafeInsetRight());
214 bottom = Math.max(bottom, cutout.getSafeInsetBottom());
215 }
216
217 // If a theme supports an action bar, sometimes it even if it's hidden
218 // the insets might report values including the bar's height, not entirely
219 // sure whether it's due to a delay or a bug, either way ensure the top
220 // margin doesn't include the action bar's height.
221 ActionBar actionBar = ((Activity) getContext()).getActionBar();
222 if (actionBar == null || !actionBar.isShowing()) {
223 int topWithoutActionBar = top - actionBarHeight();
224 if (topWithoutActionBar > 0)
225 top = topWithoutActionBar;
226 }
227
228 return Insets.of(left, top, right, bottom);
229 }
230
231 private void updateSafeAreaMargins(WindowInsets insets)
232 {
233 if (getVisibility() != View.VISIBLE)
234 return;
235 // Retain fresh animation insets until the pending bounds are applied.
236 m_pendingSafeAreaInsets = insets;
237 if (!isLayoutRequested() || isInLayout())
238 reportSafeAreaMargins();
239 }
240
241 private void reportSafeAreaMargins()
242 {
243 WindowInsets insets = m_pendingSafeAreaInsets;
244 m_pendingSafeAreaInsets = null;
245 if (getVisibility() != View.VISIBLE || !(getContext() instanceof QtActivityBase))
246 return;
247 if (insets == null)
248 insets = getRootWindowInsets();
249 if (insets == null)
250 return;
251 View rootView = getRootView();
252
253 int[] rootLocation = new int[2];
254 rootView.getLocationOnScreen(rootLocation);
255 int rootX = rootLocation[0];
256 int rootY = rootLocation[1];
257
258 int[] windowLocation = new int[2];
259 getLocationOnScreen(windowLocation);
260 int windowX = windowLocation[0];
261 int windowY = windowLocation[1];
262
263 // Offset values of window from root
264 int leftOffset = windowX - rootX;
265 int topOffset = windowY - rootY;
266 int rightOffset = (rootX + rootView.getWidth()) - (windowX + getWidth());
267 int bottomOffset = (rootY + rootView.getHeight()) - (windowY + getHeight());
268
269 // Find the remaining minimum safe margins
270 Insets safeInsets = getSafeInsets(rootView, insets);
271 int left = Math.max(0, Math.min(safeInsets.left, safeInsets.left - leftOffset));
272 int top = Math.max(0, Math.min(safeInsets.top, safeInsets.top - topOffset));
273 int right = Math.max(0, Math.min(safeInsets.right, safeInsets.right - rightOffset));
274 int bottom = Math.max(0, Math.min(safeInsets.bottom, safeInsets.bottom - bottomOffset));
275
276 safeAreaMarginsChanged(Insets.of(left, top, right, bottom), getId());
277 }
278
279 private int actionBarHeight()
280 {
281 if (m_actionBarHeight == -1) {
282 TypedArray ta = getContext().getTheme().obtainStyledAttributes(
283 new int[] { android.R.attr.actionBarSize });
284 try {
285 m_actionBarHeight = ta.getDimensionPixelSize(0, 0);
286 } finally {
287 ta.recycle();
288 }
289 }
290 return m_actionBarHeight;
291 }
292
293 @Override
294 public void onSurfaceCreated(Surface surface)
295 {
296 m_surface = surface;
297 surfaceCreated(getId(), surface);
298
299 // Initialize input when the surface exists, before the first touch.
300 if (surface != null && !m_editTextFocusInitialized && m_editText != null) {
301 enableEditTextFocus();
302 if (m_inputConnectionListener != null)
303 m_inputConnectionListener.onEditTextChanged(m_editText);
304 m_editText.requestFocus();
305 }
306 }
307
308 @Override
309 public void onSurfaceRedrawNeeded(Runnable drawingFinished)
310 {
311 surfaceRedrawNeeded(getId(), drawingFinished);
312 }
313
314 @Override
315 public void onSurfaceDestroyed(Runnable surfaceReleased)
316 {
317 m_surface = null;
318 if (m_nativeSurfaceRetired) {
319 surfaceReleased.run();
320 return;
321 }
322 m_pendingSurfaceReleases.add(surfaceReleased);
323 surfaceDestroyed(getId(), () -> {
324 if (m_pendingSurfaceReleases.remove(surfaceReleased))
325 surfaceReleased.run();
326 });
327 }
328
329 // Lets the Qt GUI thread fetch the current Surface synchronously instead
330 // of waiting for the queued surfaceCreated dispatch to land.
331 @UsedFromNativeCode
332 Surface getSurface()
333 {
334 return m_surface;
335 }
336
337 @Override
338 public boolean onTouchEvent(MotionEvent event)
339 {
340 enableEditTextFocus();
341
342 windowFocusChanged(true, getId());
343 if (m_editText != null && m_inputConnectionListener != null)
344 m_inputConnectionListener.onEditTextChanged(m_editText);
345
346 QtInputDelegate.sendTouchEvent(event, getId());
347 if (m_gestureDetector != null)
348 m_gestureDetector.onTouchEvent(event);
349 return true;
350 }
351
352 private void enableEditTextFocus()
353 {
354 if (m_editTextFocusInitialized || m_editText == null)
355 return;
356 m_editTextFocusInitialized = true;
357 m_editText.setFocusable(true);
358 m_editText.setFocusableInTouchMode(true);
359 }
360
361 @Override
362 public boolean onTrackballEvent(MotionEvent event)
363 {
364 QtInputDelegate.sendTrackballEvent(event, getId());
365 return true;
366 }
367
368 @Override
369 public boolean onGenericMotionEvent(MotionEvent event)
370 {
371 return QtInputDelegate.sendGenericMotionEvent(event, getId());
372 }
373
374 @Override
375 protected void onAttachedToWindow()
376 {
377 super.onAttachedToWindow();
378 setOnDragListener(QtDragManager.getInstance());
379 }
380
381 @Override
382 protected void onDetachedFromWindow()
383 {
384 QtDragManager.getInstance().onSourceWindowDetached(this);
385 setOnDragListener(null);
386 super.onDetachedFromWindow();
387 }
388
389 @UsedFromNativeCode
390 void removeWindow()
391 {
392 if (m_parentWindow != null)
393 m_parentWindow.removeChildWindow(getId());
394 }
395
396 // Qt's latest values, applied together once per UI thread turn, so the
397 // view system never lays out an intermediate state. Guarded by this.
398 private int m_x, m_y, m_width, m_height;
399 private boolean m_visible;
400 private boolean m_needsSurface, m_surfaceOnTop, m_surfaceOpaque;
401 private int m_surfaceContainerType;
402 private final Runnable m_applyUpdate = this::applyUpdate;
403
404 @UsedFromNativeCode
405 void configureSurface(boolean onTop, boolean isOpaque, int containerType)
406 {
407 synchronized (this) {
408 m_needsSurface = true;
409 m_surfaceOnTop = onTop;
410 m_surfaceOpaque = isOpaque;
411 m_surfaceContainerType = containerType;
412 }
413 }
414
415 @UsedFromNativeCode
416 void updateWindow(int x, int y, int width, int height, boolean visible)
417 {
418 synchronized (this) {
419 m_x = x;
420 m_y = y;
421 m_width = width;
422 m_height = height;
423 m_visible = visible;
424 }
425 removeCallbacks(m_applyUpdate);
426 post(m_applyUpdate);
427 }
428
429 private void applyUpdate()
430 {
431 final int x, y, width, height, containerType;
432 final boolean visible, needsSurface, onTop, isOpaque;
433 synchronized (this) {
434 x = m_x;
435 y = m_y;
436 width = m_width;
437 height = m_height;
438 visible = m_visible;
439 needsSurface = m_needsSurface;
440 onTop = m_surfaceOnTop;
441 isOpaque = m_surfaceOpaque;
442 containerType = m_surfaceContainerType;
443 }
444 if (getContext() instanceof QtActivityBase && !hasLayoutGeometry(x, y, width, height))
445 setLayoutParams(new QtLayout.LayoutParams(width, height, x, y));
446 // Added after the geometry, so the first Surface has the final size.
447 if (visible && needsSurface && m_surfaceContainer == null)
448 addSurfaceContainer(onTop, isOpaque, containerType);
449 final boolean shown = visible && getVisibility() != View.VISIBLE;
450 setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
451 // A hidden view reports nothing, so report once when shown.
452 if (shown)
453 requestLayout();
454 }
455
456 private boolean hasLayoutGeometry(int x, int y, int w, int h)
457 {
458 if (!(getLayoutParams() instanceof QtLayout.LayoutParams))
459 return false;
460 QtLayout.LayoutParams params = (QtLayout.LayoutParams) getLayoutParams();
461 return params.x == x && params.y == y && params.width == w && params.height == h;
462 }
463
464 private void addSurfaceContainer(boolean onTop, boolean isOpaque, int surfaceContainerType)
465 {
466 if (surfaceContainerType == 0)
467 m_surfaceContainer = new QtSurface(getContext(), QtWindow.this, onTop, 32);
468 else
469 m_surfaceContainer = new QtTextureView(getContext(), QtWindow.this, isOpaque);
470 m_surfaceContainer.setLayoutParams(new QtLayout.LayoutParams(
471 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
472 // The surface container of this window will be added as the first of the stack.
473 // All other views are stacked based on the order they are created.
474 addView(m_surfaceContainer, 0);
475 }
476
477 @UsedFromNativeCode
478 void destroySurface()
479 {
480 QtNative.runAction(this::destroySurfaceOnUiThread, false);
481 }
482
483 private void destroySurfaceOnUiThread()
484 {
485 // Native teardown precedes this action, including delayed callbacks.
486 m_nativeSurfaceRetired = true;
487 Throwable removalFailure = null;
488 try {
489 if (m_surfaceContainer != null) {
490 removeView(m_surfaceContainer);
491 m_surfaceContainer = null;
492 }
493 } catch (RuntimeException | Error failure) {
494 removalFailure = failure;
495 throw failure;
496 } finally {
497 try {
498 releasePendingSurfaces();
499 } catch (RuntimeException | Error failure) {
500 if (removalFailure == null)
501 throw failure;
502 if (removalFailure != failure)
503 removalFailure.addSuppressed(failure);
504 }
505 }
506 }
507
508 private void releasePendingSurfaces()
509 {
510 Throwable firstFailure = null;
511 Runnable released;
512 while ((released = m_pendingSurfaceReleases.poll()) != null) {
513 try {
514 released.run();
515 } catch (RuntimeException | Error failure) {
516 if (firstFailure == null)
517 firstFailure = failure;
518 else if (firstFailure != failure)
519 firstFailure.addSuppressed(failure);
520 }
521 }
522 if (firstFailure instanceof RuntimeException)
523 throw (RuntimeException) firstFailure;
524 if (firstFailure instanceof Error)
525 throw (Error) firstFailure;
526 }
527
528 void addChildWindow(QtWindow window)
529 {
530 QtNative.runAction(()-> {
531 m_childWindows.put(window.getId(), window);
532 addView(window, getChildCount());
533 });
534 }
535
536 void removeChildWindow(int id)
537 {
538 QtNative.runAction(()-> {
539 if (m_childWindows.containsKey(id))
540 removeView(m_childWindows.remove(id));
541 });
542 }
543
544 @UsedFromNativeCode
545 void setNativeView(final View view)
546 {
547 QtNative.runAction(()-> {
548 if (m_nativeView != null)
549 removeView(m_nativeView);
550
551 m_nativeView = view;
552 m_nativeView.setLayoutParams(new QtLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
553 ViewGroup.LayoutParams.MATCH_PARENT));
554 addView(m_nativeView);
555 });
556 }
557
558 @UsedFromNativeCode
559 void bringChildToFront(int id)
560 {
561 QtNative.runAction(()-> {
562 View view = m_childWindows.get(id);
563 if (view != null) {
564 if (getChildCount() > 0)
565 moveChild(view, getChildCount() - 1);
566 }
567 });
568 }
569
570 @UsedFromNativeCode
571 void bringChildToBack(int id) {
572 QtNative.runAction(()-> {
573 View view = m_childWindows.get(id);
574 if (view != null) {
575 moveChild(view, 0);
576 }
577 });
578 }
579
580 @UsedFromNativeCode
581 void removeNativeView()
582 {
583 QtNative.runAction(()-> {
584 if (m_nativeView != null) {
585 removeView(m_nativeView);
586 m_nativeView = null;
587 }
588 });
589 }
590
591 void setParent(QtWindow parentWindow)
592 {
593 if (m_parentWindow == parentWindow)
594 return;
595
596 if (m_parentWindow != null)
597 m_parentWindow.removeChildWindow(getId());
598
599 m_parentWindow = parentWindow;
600 if (m_parentWindow != null)
601 m_parentWindow.addChildWindow(this);
602 }
603
604 @UsedFromNativeCode
605 void updateFocusedEditText()
606 {
607 if (m_editText != null && m_inputConnectionListener != null)
608 m_inputConnectionListener.onEditTextChanged(m_editText);
609 }
610}
QPainter Context
Napi::TypedArray TypedArray
Definition qnapi_p.h:76
self window
[2]
GLvoid ** params
GLboolean GLboolean GLboolean b
GLdouble GLdouble right
GLint GLint GLint GLint GLint x
GLenum mode
GLfloat GLfloat GLfloat w
[0]
GLint GLint bottom
GLboolean r
GLint GLsizei width
GLdouble GLdouble GLdouble GLdouble top
GLint y
GLfloat GLfloat GLfloat GLfloat h
GLint left
struct _cl_event * event
GLdouble GLdouble t
Definition qopenglext.h:243
QLayoutItem * child
[0]
file setParent(multiPart)
manager post(request, myJson, this, [this](QRestReply &reply) { if(!reply.isSuccess()) { } if(std::optional json=reply.readJson()) { } })
QQuickView * view
[0]
if(foo.startsWith("("+type+") 0x")) ... QString hello("hello")
[0]