From 5dce4bc7166c1e0295665f6acda9f74736d72097 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Sat, 4 Nov 2023 23:22:55 -0700 Subject: [PATCH] Makes SDLInputConnection and DummyEdit public classes (thanks Cole!) I've added an additional patch that expands on the same basic idea as the first one; it makes SDLInputConnection and DummyEdit into public classes so that they can be overridden from the Xamarin end if their functionality needs to be extended. (In my case, I need to change the type of software keyboard that's displayed.) Fixes https://github.com/libsdl-org/SDL/issues/2785 --- .../main/java/org/libsdl/app/SDLActivity.java | 190 +----------------- .../java/org/libsdl/app/SDLDummyEdit.java | 62 ++++++ .../org/libsdl/app/SDLInputConnection.java | 136 +++++++++++++ 3 files changed, 200 insertions(+), 188 deletions(-) create mode 100644 android-project/app/src/main/java/org/libsdl/app/SDLDummyEdit.java create mode 100644 android-project/app/src/main/java/org/libsdl/app/SDLInputConnection.java diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java index 41497a71e..82ba742a4 100644 --- a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -23,9 +23,6 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; -import android.text.Editable; -import android.text.InputType; -import android.text.Selection; import android.util.DisplayMetrics; import android.util.Log; import android.util.SparseArray; @@ -39,12 +36,9 @@ import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; -import android.view.inputmethod.BaseInputConnection; -import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.Button; -import android.widget.EditText; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; @@ -210,7 +204,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh // Main components protected static SDLActivity mSingleton; protected static SDLSurface mSurface; - protected static DummyEdit mTextEdit; + protected static SDLDummyEdit mTextEdit; protected static boolean mScreenKeyboardShown; protected static ViewGroup mLayout; protected static SDLClipboardHandler mClipboardHandler; @@ -1353,7 +1347,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh params.topMargin = y; if (mTextEdit == null) { - mTextEdit = new DummyEdit(SDL.getContext()); + mTextEdit = new SDLDummyEdit(SDL.getContext()); mLayout.addView(mTextEdit, params); } else { @@ -1975,186 +1969,6 @@ class SDLMain implements Runnable { } } -/* This is a fake invisible editor view that receives the input and defines the - * pan&scan region - */ -class DummyEdit extends View implements View.OnKeyListener { - InputConnection ic; - - public DummyEdit(Context context) { - super(context); - setFocusableInTouchMode(true); - setFocusable(true); - setOnKeyListener(this); - } - - @Override - public boolean onCheckIsTextEditor() { - return true; - } - - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - return SDLActivity.handleKeyEvent(v, keyCode, event, ic); - } - - // - @Override - public boolean onKeyPreIme (int keyCode, KeyEvent event) { - // As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event - // FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639 - // FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not - // FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout - // FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android - // FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :) - if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { - if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) { - SDLActivity.onNativeKeyboardFocusLost(); - } - } - return super.onKeyPreIme(keyCode, event); - } - - @Override - public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - ic = new SDLInputConnection(this, true); - - outAttrs.inputType = InputType.TYPE_CLASS_TEXT | - InputType.TYPE_TEXT_FLAG_MULTI_LINE; - outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI | - EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */; - - return ic; - } -} - -class SDLInputConnection extends BaseInputConnection { - - protected EditText mEditText; - protected String mCommittedText = ""; - - public SDLInputConnection(View targetView, boolean fullEditor) { - super(targetView, fullEditor); - mEditText = new EditText(SDL.getContext()); - } - - @Override - public Editable getEditable() { - return mEditText.getEditableText(); - } - - @Override - public boolean sendKeyEvent(KeyEvent event) { - /* - * This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard) - * However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses - * and so we need to generate them ourselves in commitText. To avoid duplicates on the handful of keys - * that still do, we empty this out. - */ - - /* - * Return DOES still generate a key event, however. So rather than using it as the 'click a button' key - * as we do with physical keyboards, let's just use it to hide the keyboard. - */ - - if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { - if (SDLActivity.onNativeSoftReturnKey()) { - return true; - } - } - - return super.sendKeyEvent(event); - } - - @Override - public boolean commitText(CharSequence text, int newCursorPosition) { - if (!super.commitText(text, newCursorPosition)) { - return false; - } - updateText(); - return true; - } - - @Override - public boolean setComposingText(CharSequence text, int newCursorPosition) { - if (!super.setComposingText(text, newCursorPosition)) { - return false; - } - updateText(); - return true; - } - - @Override - public boolean deleteSurroundingText(int beforeLength, int afterLength) { - if (Build.VERSION.SDK_INT <= 29 /* Android 10.0 (Q) */) { - // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection - // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265 - if (beforeLength > 0 && afterLength == 0) { - // backspace(s) - while (beforeLength-- > 0) { - nativeGenerateScancodeForUnichar('\b'); - } - return true; - } - } - - if (!super.deleteSurroundingText(beforeLength, afterLength)) { - return false; - } - updateText(); - return true; - } - - protected void updateText() { - final Editable content = getEditable(); - if (content == null) { - return; - } - - String text = content.toString(); - int compareLength = Math.min(text.length(), mCommittedText.length()); - int matchLength, offset; - - /* Backspace over characters that are no longer in the string */ - for (matchLength = 0; matchLength < compareLength; ) { - int codePoint = mCommittedText.codePointAt(matchLength); - if (codePoint != text.codePointAt(matchLength)) { - break; - } - matchLength += Character.charCount(codePoint); - } - /* FIXME: This doesn't handle graphemes, like '🌬️' */ - for (offset = matchLength; offset < mCommittedText.length(); ) { - int codePoint = mCommittedText.codePointAt(offset); - nativeGenerateScancodeForUnichar('\b'); - offset += Character.charCount(codePoint); - } - - if (matchLength < text.length()) { - String pendingText = text.subSequence(matchLength, text.length()).toString(); - for (offset = 0; offset < pendingText.length(); ) { - int codePoint = pendingText.codePointAt(offset); - if (codePoint == '\n') { - if (SDLActivity.onNativeSoftReturnKey()) { - return; - } - } - /* Higher code points don't generate simulated scancodes */ - if (codePoint < 128) { - nativeGenerateScancodeForUnichar((char)codePoint); - } - offset += Character.charCount(codePoint); - } - SDLInputConnection.nativeCommitText(pendingText, 0); - } - mCommittedText = text; - } - - public static native void nativeCommitText(String text, int newCursorPosition); - - public static native void nativeGenerateScancodeForUnichar(char c); -} - class SDLClipboardHandler implements ClipboardManager.OnPrimaryClipChangedListener { diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLDummyEdit.java b/android-project/app/src/main/java/org/libsdl/app/SDLDummyEdit.java new file mode 100644 index 000000000..dca28145e --- /dev/null +++ b/android-project/app/src/main/java/org/libsdl/app/SDLDummyEdit.java @@ -0,0 +1,62 @@ +package org.libsdl.app; + +import android.content.*; +import android.text.InputType; +import android.view.*; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + +/* This is a fake invisible editor view that receives the input and defines the + * pan&scan region + */ +public class SDLDummyEdit extends View implements View.OnKeyListener +{ + InputConnection ic; + + public SDLDummyEdit(Context context) { + super(context); + setFocusableInTouchMode(true); + setFocusable(true); + setOnKeyListener(this); + } + + @Override + public boolean onCheckIsTextEditor() { + return true; + } + + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + return SDLActivity.handleKeyEvent(v, keyCode, event, ic); + } + + // + @Override + public boolean onKeyPreIme (int keyCode, KeyEvent event) { + // As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event + // FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639 + // FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not + // FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout + // FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android + // FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :) + if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) { + SDLActivity.onNativeKeyboardFocusLost(); + } + } + return super.onKeyPreIme(keyCode, event); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + ic = new SDLInputConnection(this, true); + + outAttrs.inputType = InputType.TYPE_CLASS_TEXT | + InputType.TYPE_TEXT_FLAG_MULTI_LINE; + outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI | + EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */; + + return ic; + } +} + diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLInputConnection.java b/android-project/app/src/main/java/org/libsdl/app/SDLInputConnection.java new file mode 100644 index 000000000..e1d29a889 --- /dev/null +++ b/android-project/app/src/main/java/org/libsdl/app/SDLInputConnection.java @@ -0,0 +1,136 @@ +package org.libsdl.app; + +import android.content.*; +import android.os.Build; +import android.text.Editable; +import android.view.*; +import android.view.inputmethod.BaseInputConnection; +import android.widget.EditText; + +public class SDLInputConnection extends BaseInputConnection +{ + protected EditText mEditText; + protected String mCommittedText = ""; + + public SDLInputConnection(View targetView, boolean fullEditor) { + super(targetView, fullEditor); + mEditText = new EditText(SDL.getContext()); + } + + @Override + public Editable getEditable() { + return mEditText.getEditableText(); + } + + @Override + public boolean sendKeyEvent(KeyEvent event) { + /* + * This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard) + * However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses + * and so we need to generate them ourselves in commitText. To avoid duplicates on the handful of keys + * that still do, we empty this out. + */ + + /* + * Return DOES still generate a key event, however. So rather than using it as the 'click a button' key + * as we do with physical keyboards, let's just use it to hide the keyboard. + */ + + if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { + if (SDLActivity.onNativeSoftReturnKey()) { + return true; + } + } + + return super.sendKeyEvent(event); + } + + @Override + public boolean commitText(CharSequence text, int newCursorPosition) { + if (!super.commitText(text, newCursorPosition)) { + return false; + } + updateText(); + return true; + } + + @Override + public boolean setComposingText(CharSequence text, int newCursorPosition) { + if (!super.setComposingText(text, newCursorPosition)) { + return false; + } + updateText(); + return true; + } + + @Override + public boolean deleteSurroundingText(int beforeLength, int afterLength) { + if (Build.VERSION.SDK_INT <= 29 /* Android 10.0 (Q) */) { + // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection + // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265 + if (beforeLength > 0 && afterLength == 0) { + // backspace(s) + while (beforeLength-- > 0) { + nativeGenerateScancodeForUnichar('\b'); + } + return true; + } + } + + if (!super.deleteSurroundingText(beforeLength, afterLength)) { + return false; + } + updateText(); + return true; + } + + protected void updateText() { + final Editable content = getEditable(); + if (content == null) { + return; + } + + String text = content.toString(); + int compareLength = Math.min(text.length(), mCommittedText.length()); + int matchLength, offset; + + /* Backspace over characters that are no longer in the string */ + for (matchLength = 0; matchLength < compareLength; ) { + int codePoint = mCommittedText.codePointAt(matchLength); + if (codePoint != text.codePointAt(matchLength)) { + break; + } + matchLength += Character.charCount(codePoint); + } + /* FIXME: This doesn't handle graphemes, like '🌬️' */ + for (offset = matchLength; offset < mCommittedText.length(); ) { + int codePoint = mCommittedText.codePointAt(offset); + nativeGenerateScancodeForUnichar('\b'); + offset += Character.charCount(codePoint); + } + + if (matchLength < text.length()) { + String pendingText = text.subSequence(matchLength, text.length()).toString(); + for (offset = 0; offset < pendingText.length(); ) { + int codePoint = pendingText.codePointAt(offset); + if (codePoint == '\n') { + if (SDLActivity.onNativeSoftReturnKey()) { + return; + } + } + /* Higher code points don't generate simulated scancodes */ + if (codePoint < 128) { + nativeGenerateScancodeForUnichar((char)codePoint); + } + offset += Character.charCount(codePoint); + } + SDLInputConnection.nativeCommitText(pendingText, 0); + } + mCommittedText = text; + } + + public static native void nativeCommitText(String text, int newCursorPosition); + + public static native void nativeGenerateScancodeForUnichar(char c); +} +