diff options
Diffstat (limited to 'src/android/jar/src/org/qtproject/qt/android/QtInputDelegate.java')
-rw-r--r-- | src/android/jar/src/org/qtproject/qt/android/QtInputDelegate.java | 654 |
1 files changed, 654 insertions, 0 deletions
diff --git a/src/android/jar/src/org/qtproject/qt/android/QtInputDelegate.java b/src/android/jar/src/org/qtproject/qt/android/QtInputDelegate.java new file mode 100644 index 0000000000..cfa273e410 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtInputDelegate.java @@ -0,0 +1,654 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// 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.graphics.Rect; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.text.method.MetaKeyKeyListener; +import android.util.DisplayMetrics; +import android.view.InputDevice; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; + +import org.qtproject.qt.android.QtInputConnection.QtInputConnectionListener; + +/** @noinspection FieldCanBeLocal*/ +class QtInputDelegate implements QtInputConnection.QtInputConnectionListener { + + // keyboard methods + public static native void keyDown(int key, int unicode, int modifier, boolean autoRepeat); + public static native void keyUp(int key, int unicode, int modifier, boolean autoRepeat); + public static native void keyboardVisibilityChanged(boolean visibility); + public static native void keyboardGeometryChanged(int x, int y, int width, int height); + // keyboard methods + + // dispatch events methods + public static native boolean dispatchGenericMotionEvent(MotionEvent event); + public static native boolean dispatchKeyEvent(KeyEvent event); + // dispatch events methods + + // handle methods + public static native void handleLocationChanged(int id, int x, int y); + // handle methods + + private QtEditText m_currentEditText = null; + private final InputMethodManager m_imm; + + private boolean m_keyboardIsVisible = false; + private boolean m_isKeyboardHidingAnimationOngoing = false; + private long m_showHideTimeStamp = System.nanoTime(); + private int m_portraitKeyboardHeight = 0; + private int m_landscapeKeyboardHeight = 0; + private int m_probeKeyboardHeightDelayMs = 50; + private CursorHandle m_cursorHandle; + private CursorHandle m_leftSelectionHandle; + private CursorHandle m_rightSelectionHandle; + private EditPopupMenu m_editPopupMenu; + + private int m_softInputMode = 0; + + // Values coming from QAndroidInputContext::CursorHandleShowMode + private static final int CursorHandleNotShown = 0; + private static final int CursorHandleShowNormal = 1; + private static final int CursorHandleShowSelection = 2; + private static final int CursorHandleShowEdit = 0x100; + + // Handle IDs + public static final int IdCursorHandle = 1; + public static final int IdLeftHandle = 2; + public static final int IdRightHandle = 3; + + private static Boolean m_tabletEventSupported = null; + + private static int m_oldX, m_oldY; + + + private long m_metaState; + private int m_lastChar = 0; + private boolean m_backKeyPressedSent = false; + + // Note: because of the circular call to updateFullScreen() from the delegate, we need + // a listener to be able to do that call from the delegate, because that's where that + // logic lives + public interface KeyboardVisibilityListener { + void onKeyboardVisibilityChange(); + } + + private final KeyboardVisibilityListener m_keyboardVisibilityListener; + + QtInputDelegate(Activity activity, KeyboardVisibilityListener listener) + { + this.m_keyboardVisibilityListener = listener; + m_imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + } + + // QtInputConnectionListener methods + @Override + public void onSetClosing(boolean closing) { + if (!closing) + setKeyboardVisibility(true, System.nanoTime()); + } + + @Override + public void onHideKeyboardRunnableDone(boolean visibility, long hideTimeStamp) { + setKeyboardVisibility(visibility, hideTimeStamp); + } + + @Override + public void onSendKeyEventDefaultCase() { + hideSoftwareKeyboard(); + } + // QtInputConnectionListener methods + + public boolean isKeyboardVisible() + { + return m_keyboardIsVisible; + } + + // Is the keyboard fully visible i.e. visible and no ongoing animation + @UsedFromNativeCode + public boolean isSoftwareKeyboardVisible() + { + return isKeyboardVisible() && !m_isKeyboardHidingAnimationOngoing; + } + + void setSoftInputMode(int inputMode) + { + m_softInputMode = inputMode; + } + + QtEditText getCurrentQtEditText() + { + return m_currentEditText; + } + + void setEditPopupMenu(EditPopupMenu editPopupMenu) + { + m_editPopupMenu = editPopupMenu; + } + + private void keyboardVisibilityUpdated(boolean visibility) + { + m_isKeyboardHidingAnimationOngoing = false; + QtInputDelegate.keyboardVisibilityChanged(visibility); + } + + public void setKeyboardVisibility(boolean visibility, long timeStamp) + { + if (m_showHideTimeStamp > timeStamp) + return; + m_showHideTimeStamp = timeStamp; + + if (m_keyboardIsVisible == visibility) + return; + m_keyboardIsVisible = visibility; + keyboardVisibilityUpdated(m_keyboardIsVisible); + + // Hiding the keyboard clears the immersive mode, so we need to set it again. + if (!visibility) + m_keyboardVisibilityListener.onKeyboardVisibilityChange(); + + } + + @UsedFromNativeCode + public void resetSoftwareKeyboard() + { + if (m_imm == null || m_currentEditText == null) + return; + m_currentEditText.postDelayed(() -> { + m_imm.restartInput(m_currentEditText); + m_currentEditText.m_optionsChanged = false; + }, 5); + } + + void setFocusedView(QtEditText currentEditText) + { + m_currentEditText = currentEditText; + } + + public void showSoftwareKeyboard(Activity activity, QtLayout layout, + final int x, final int y, final int width, final int height, + final int inputHints, final int enterKeyType) + { + QtNative.runAction(() -> { + if (m_imm == null || m_currentEditText == null) + return; + + if (updateSoftInputMode(activity, height)) + return; + + m_currentEditText.setEditTextOptions(enterKeyType, inputHints); + + m_currentEditText.postDelayed(() -> { + m_imm.showSoftInput(m_currentEditText, 0, new ResultReceiver(new Handler()) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + switch (resultCode) { + case InputMethodManager.RESULT_SHOWN: + QtNativeInputConnection.updateCursorPosition(); + //FALLTHROUGH + case InputMethodManager.RESULT_UNCHANGED_SHOWN: + setKeyboardVisibility(true, System.nanoTime()); + if (m_softInputMode == 0) { + probeForKeyboardHeight(layout, activity, + x, y, width, height, inputHints, enterKeyType); + } + break; + case InputMethodManager.RESULT_HIDDEN: + case InputMethodManager.RESULT_UNCHANGED_HIDDEN: + setKeyboardVisibility(false, System.nanoTime()); + break; + } + } + }); + if (m_currentEditText.m_optionsChanged) { + m_imm.restartInput(m_currentEditText); + m_currentEditText.m_optionsChanged = false; + } + }, 15); + }); + } + + private boolean updateSoftInputMode(Activity activity, int height) + { + DisplayMetrics metrics = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); + + // If the screen is in portrait mode than we estimate that keyboard height + // will not be higher than 2/5 of the screen. Otherwise we estimate that keyboard height + // will not be higher than 2/3 of the screen + final int visibleHeight; + if (metrics.widthPixels < metrics.heightPixels) { + visibleHeight = m_portraitKeyboardHeight != 0 ? + m_portraitKeyboardHeight : metrics.heightPixels * 3 / 5; + } else { + visibleHeight = m_landscapeKeyboardHeight != 0 ? + m_landscapeKeyboardHeight : metrics.heightPixels / 3; + } + + if (m_softInputMode != 0) { + activity.getWindow().setSoftInputMode(m_softInputMode); + int stateHidden = WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN; + return (m_softInputMode & stateHidden) != 0; + } else { + int stateUnchanged = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED; + if (height > visibleHeight) { + int adjustResize = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + activity.getWindow().setSoftInputMode(stateUnchanged | adjustResize); + } else { + int adjustPan = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN; + activity.getWindow().setSoftInputMode(stateUnchanged | adjustPan); + } + } + return false; + } + + private void probeForKeyboardHeight(QtLayout layout, Activity activity, int x, int y, + int width, int height, int inputHints, int enterKeyType) + { + layout.postDelayed(() -> { + if (!m_keyboardIsVisible) + return; + DisplayMetrics metrics = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); + Rect r = new Rect(); + activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(r); + if (metrics.heightPixels != r.bottom) { + if (metrics.widthPixels > metrics.heightPixels) { // landscape + if (m_landscapeKeyboardHeight != r.bottom) { + m_landscapeKeyboardHeight = r.bottom; + showSoftwareKeyboard(activity, layout, x, y, width, height, + inputHints, enterKeyType); + } + } else { + if (m_portraitKeyboardHeight != r.bottom) { + m_portraitKeyboardHeight = r.bottom; + showSoftwareKeyboard(activity, layout, x, y, width, height, + inputHints, enterKeyType); + } + } + } else { + // no luck ? + // maybe the delay was too short, so let's make it longer + if (m_probeKeyboardHeightDelayMs < 1000) + m_probeKeyboardHeightDelayMs *= 2; + } + }, m_probeKeyboardHeightDelayMs); + } + + public void hideSoftwareKeyboard() + { + m_isKeyboardHidingAnimationOngoing = true; + QtNative.runAction(() -> { + if (m_imm == null || m_currentEditText == null) + return; + + m_imm.hideSoftInputFromWindow(m_currentEditText.getWindowToken(), 0, + new ResultReceiver(new Handler()) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + switch (resultCode) { + case InputMethodManager.RESULT_SHOWN: + case InputMethodManager.RESULT_UNCHANGED_SHOWN: + setKeyboardVisibility(true, System.nanoTime()); + break; + case InputMethodManager.RESULT_HIDDEN: + case InputMethodManager.RESULT_UNCHANGED_HIDDEN: + setKeyboardVisibility(false, System.nanoTime()); + break; + } + } + }); + }); + } + + @UsedFromNativeCode + public void updateSelection(final int selStart, final int selEnd, + final int candidatesStart, final int candidatesEnd) + { + QtNative.runAction(() -> { + if (m_imm == null) + return; + + m_imm.updateSelection(m_currentEditText, selStart, selEnd, candidatesStart, candidatesEnd); + }); + } + + @UsedFromNativeCode + public int getSelectHandleWidth() + { + int width = 0; + if (m_leftSelectionHandle != null && m_rightSelectionHandle != null) { + width = Math.max(m_leftSelectionHandle.width(), m_rightSelectionHandle.width()); + } else if (m_cursorHandle != null) { + width = m_cursorHandle.width(); + } + return width; + } + + /* called from the C++ code when the position of the cursor or selection handles needs to + be adjusted. + mode is one of QAndroidInputContext::CursorHandleShowMode + */ + @UsedFromNativeCode + public void updateHandles(Activity activity, QtLayout layout, int mode, + int editX, int editY, int editButtons, + int x1, int y1, int x2, int y2, boolean rtl) + { + QtNative.runAction(() -> updateHandleImpl(activity, layout, mode, editX, editY, editButtons, + x1, y1, x2, y2, rtl)); + } + + private void updateHandleImpl(Activity activity, QtLayout layout, int mode, + int editX, int editY, int editButtons, + int x1, int y1, int x2, int y2, boolean rtl) + { + switch (mode & 0xff) + { + case CursorHandleNotShown: + if (m_cursorHandle != null) { + m_cursorHandle.hide(); + m_cursorHandle = null; + } + if (m_rightSelectionHandle != null) { + m_rightSelectionHandle.hide(); + m_leftSelectionHandle.hide(); + m_rightSelectionHandle = null; + m_leftSelectionHandle = null; + } + if (m_editPopupMenu != null) + m_editPopupMenu.hide(); + break; + + case CursorHandleShowNormal: + if (m_cursorHandle == null) { + m_cursorHandle = new CursorHandle(activity, layout, IdCursorHandle, + android.R.attr.textSelectHandle, false); + } + m_cursorHandle.setPosition(x1, y1); + if (m_rightSelectionHandle != null) { + m_rightSelectionHandle.hide(); + m_leftSelectionHandle.hide(); + m_rightSelectionHandle = null; + m_leftSelectionHandle = null; + } + break; + + case CursorHandleShowSelection: + if (m_rightSelectionHandle == null) { + m_leftSelectionHandle = new CursorHandle(activity, layout, IdLeftHandle, + !rtl ? android.R.attr.textSelectHandleLeft : + android.R.attr.textSelectHandleRight, + rtl); + m_rightSelectionHandle = new CursorHandle(activity, layout, IdRightHandle, + !rtl ? android.R.attr.textSelectHandleRight : + android.R.attr.textSelectHandleLeft, + rtl); + } + m_leftSelectionHandle.setPosition(x1,y1); + m_rightSelectionHandle.setPosition(x2,y2); + if (m_cursorHandle != null) { + m_cursorHandle.hide(); + m_cursorHandle = null; + } + mode |= CursorHandleShowEdit; + break; + } + + if (!QtClipboardManager.hasClipboardText(activity)) + editButtons &= ~EditContextView.PASTE_BUTTON; + + if (m_editPopupMenu != null) { + if ((mode & CursorHandleShowEdit) == CursorHandleShowEdit && editButtons != 0) { + m_editPopupMenu.setPosition(editX, editY, editButtons, + m_cursorHandle, m_leftSelectionHandle, m_rightSelectionHandle); + } else { + m_editPopupMenu.hide(); + } + } + } + + public boolean onKeyDown(int keyCode, KeyEvent event) + { + m_metaState = MetaKeyKeyListener.handleKeyDown(m_metaState, keyCode, event); + int metaState = MetaKeyKeyListener.getMetaState(m_metaState) | event.getMetaState(); + int c = event.getUnicodeChar(metaState); + int lc = c; + m_metaState = MetaKeyKeyListener.adjustMetaAfterKeypress(m_metaState); + + if ((c & KeyCharacterMap.COMBINING_ACCENT) != 0) { + c = c & KeyCharacterMap.COMBINING_ACCENT_MASK; + c = KeyEvent.getDeadChar(m_lastChar, c); + } + + if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP + || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN + || keyCode == KeyEvent.KEYCODE_MUTE) + && System.getenv("QT_ANDROID_VOLUME_KEYS") == null) { + return false; + } + + m_lastChar = lc; + if (keyCode == KeyEvent.KEYCODE_BACK) { + m_backKeyPressedSent = !isKeyboardVisible(); + if (!m_backKeyPressedSent) + return true; + } + + QtInputDelegate.keyDown(keyCode, c, event.getMetaState(), event.getRepeatCount() > 0); + + return true; + } + + public boolean onKeyUp(int keyCode, KeyEvent event) + { + if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP + || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN + || keyCode == KeyEvent.KEYCODE_MUTE) + && System.getenv("QT_ANDROID_VOLUME_KEYS") == null) { + return false; + } + + if (keyCode == KeyEvent.KEYCODE_BACK && !m_backKeyPressedSent) { + hideSoftwareKeyboard(); + setKeyboardVisibility(false, System.nanoTime()); + return true; + } + + m_metaState = MetaKeyKeyListener.handleKeyUp(m_metaState, keyCode, event); + boolean autoRepeat = event.getRepeatCount() > 0; + QtInputDelegate.keyUp(keyCode, event.getUnicodeChar(), event.getMetaState(), autoRepeat); + + return true; + } + + public boolean handleDispatchKeyEvent(KeyEvent event) + { + if (event.getAction() == KeyEvent.ACTION_MULTIPLE + && event.getCharacters() != null + && event.getCharacters().length() == 1 + && event.getKeyCode() == 0) { + keyDown(0, event.getCharacters().charAt(0), event.getMetaState(), + event.getRepeatCount() > 0); + keyUp(0, event.getCharacters().charAt(0), event.getMetaState(), + event.getRepeatCount() > 0); + } + + return dispatchKeyEvent(event); + } + + public boolean handleDispatchGenericMotionEvent(MotionEvent event) + { + return dispatchGenericMotionEvent(event); + } + + ////////////////////////////// + // Mouse and Touch Input // + ////////////////////////////// + + // tablet methods + public static native boolean isTabletEventSupported(); + public static native void tabletEvent(int winId, int deviceId, long time, int action, + int pointerType, int buttonState, float x, float y, + float pressure); + // tablet methods + + // pointer methods + public static native void mouseDown(int winId, int x, int y, int mouseButtonState); + public static native void mouseUp(int winId, int x, int y, int mouseButtonState); + public static native void mouseMove(int winId, int x, int y); + public static native void mouseWheel(int winId, int x, int y, float hDelta, float vDelta); + public static native void touchBegin(int winId); + public static native void touchAdd(int winId, int pointerId, int action, boolean primary, + int x, int y, float major, float minor, float rotation, + float pressure); + public static native void touchEnd(int winId, int action); + public static native void touchCancel(int winId); + public static native void longPress(int winId, int x, int y); + // pointer methods + + static private int getAction(int index, MotionEvent event) + { + int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_MOVE) { + int hsz = event.getHistorySize(); + if (hsz > 0) { + float x = event.getX(index); + float y = event.getY(index); + for (int h = 0; h < hsz; ++h) { + if ( event.getHistoricalX(index, h) != x || + event.getHistoricalY(index, h) != y ) + return 1; + } + return 2; + } + return 1; + } + if (action == MotionEvent.ACTION_DOWN + || action == MotionEvent.ACTION_POINTER_DOWN && index == event.getActionIndex()) { + return 0; + } else if (action == MotionEvent.ACTION_UP + || action == MotionEvent.ACTION_POINTER_UP && index == event.getActionIndex()) { + return 3; + } + return 2; + } + + static public void sendTouchEvent(MotionEvent event, int id) + { + int pointerType = 0; + + if (m_tabletEventSupported == null) + m_tabletEventSupported = isTabletEventSupported(); + + switch (event.getToolType(0)) { + case MotionEvent.TOOL_TYPE_STYLUS: + pointerType = 1; // QTabletEvent::Pen + break; + case MotionEvent.TOOL_TYPE_ERASER: + pointerType = 3; // QTabletEvent::Eraser + break; + } + + if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) { + sendMouseEvent(event, id); + } else if (m_tabletEventSupported && pointerType != 0) { + tabletEvent(id, event.getDeviceId(), event.getEventTime(), event.getActionMasked(), + pointerType, event.getButtonState(), + event.getX(), event.getY(), event.getPressure()); + } else { + touchBegin(id); + for (int i = 0; i < event.getPointerCount(); ++i) { + touchAdd(id, + event.getPointerId(i), + getAction(i, event), + i == 0, + (int)event.getX(i), + (int)event.getY(i), + event.getTouchMajor(i), + event.getTouchMinor(i), + event.getOrientation(i), + event.getPressure(i)); + } + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + touchEnd(id, 0); + break; + + case MotionEvent.ACTION_UP: + touchEnd(id, 2); + break; + + case MotionEvent.ACTION_CANCEL: + touchCancel(id); + break; + + default: + touchEnd(id, 1); + } + } + } + + static public void sendTrackballEvent(MotionEvent event, int id) + { + sendMouseEvent(event,id); + } + + static public boolean sendGenericMotionEvent(MotionEvent event, int id) + { + int scrollOrHoverMove = MotionEvent.ACTION_SCROLL | MotionEvent.ACTION_HOVER_MOVE; + int pointerDeviceModifier = (event.getSource() & InputDevice.SOURCE_CLASS_POINTER); + boolean isPointerDevice = pointerDeviceModifier == InputDevice.SOURCE_CLASS_POINTER; + + if ((event.getAction() & scrollOrHoverMove) == 0 || !isPointerDevice ) + return false; + + return sendMouseEvent(event, id); + } + + static public boolean sendMouseEvent(MotionEvent event, int id) + { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_UP: + mouseUp(id, (int) event.getX(), (int) event.getY(), event.getButtonState()); + break; + + case MotionEvent.ACTION_DOWN: + mouseDown(id, (int) event.getX(), (int) event.getY(), event.getButtonState()); + m_oldX = (int) event.getX(); + m_oldY = (int) event.getY(); + break; + case MotionEvent.ACTION_HOVER_MOVE: + case MotionEvent.ACTION_MOVE: + if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) { + mouseMove(id, (int) event.getX(), (int) event.getY()); + } else { + int dx = (int) (event.getX() - m_oldX); + int dy = (int) (event.getY() - m_oldY); + if (Math.abs(dx) > 5 || Math.abs(dy) > 5) { + mouseMove(id, (int) event.getX(), (int) event.getY()); + m_oldX = (int) event.getX(); + m_oldY = (int) event.getY(); + } + } + break; + case MotionEvent.ACTION_SCROLL: + mouseWheel(id, (int) event.getX(), (int) event.getY(), + event.getAxisValue(MotionEvent.AXIS_HSCROLL), + event.getAxisValue(MotionEvent.AXIS_VSCROLL)); + break; + default: + return false; + } + return true; + } +} |