From: Po Lu Date: Sun, 12 Feb 2023 12:32:25 +0000 (+0800) Subject: Update Android port X-Git-Url: http://git.eshelyaron.com/gitweb/?a=commitdiff_plain;h=0198b8cffd82893412c738dae8e50c45a99286f1;p=emacs.git Update Android port * doc/emacs/android.texi (Android Environment): Document notifications permission. * java/org/gnu/emacs/EmacsEditable.java (EmacsEditable): * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection): New files. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): Load library dependencies in a less verbose fashion. * java/org/gnu/emacs/EmacsView.java (EmacsView): Make imManager public. (onCreateInputConnection): Set InputType to TYPE_NULL for now. * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow, onKeyDown) (onKeyUp, getEventUnicodeChar): Correctly handle key events with strings. * lisp/term/android-win.el (android-clear-preedit-text) (android-preedit-text): New special event handlers. * src/android.c (struct android_emacs_window): Add function lookup_string. (android_init_emacs_window): Adjust accordingly. (android_wc_lookup_string): New function. * src/androidgui.h (struct android_key_event): Improve commentary. (enum android_lookup_status): New enum. * src/androidterm.c (handle_one_android_event): Synchronize IM lookup code with X. * src/coding.c (from_unicode_buffer): Implement on Android. * src/coding.h: * src/sfnt.c: Fix commentary. --- diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index 8df26560f02..308bd70c59c 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -334,6 +334,8 @@ permissions upon installation: @code{android.permission.TRANSMIT_IR} @item @code{android.permission.WAKE_LOCK} +@item +@code{android.permission.POST_NOTIFICATIONS} @end itemize Other permissions must be granted by the user through the system diff --git a/java/org/gnu/emacs/EmacsEditable.java b/java/org/gnu/emacs/EmacsEditable.java new file mode 100644 index 00000000000..79af65a6ccd --- /dev/null +++ b/java/org/gnu/emacs/EmacsEditable.java @@ -0,0 +1,300 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +import android.text.InputFilter; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.SpanWatcher; +import android.text.Selection; + +import android.content.Context; + +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; + +import android.text.Spannable; + +import android.util.Log; + +import android.os.Build; + +/* Android input methods insist on having access to buffer contents. + Since Emacs is not designed like ``any other Android text editor'', + that is not possible. + + This file provides a fake editing buffer that is designed to weasel + as much information as possible out of an input method, without + actually providing buffer contents to Emacs. + + The basic idea is to have the fake editing buffer be initially + empty. + + When the input method inserts composed text, it sets a flag. + Updates to the buffer while the flag is set are sent to Emacs to be + displayed as ``preedit text''. + + Once some heuristics decide that composition has been completed, + the composed text is sent to Emacs, and the text that was inserted + in this editing buffer is erased. */ + +public class EmacsEditable extends SpannableStringBuilder + implements SpanWatcher +{ + private static final String TAG = "EmacsEditable"; + + /* Whether or not composition is currently in progress. */ + private boolean isComposing; + + /* The associated input connection. */ + private EmacsInputConnection connection; + + /* The associated IM manager. */ + private InputMethodManager imManager; + + /* Any extracted text an input method may be monitoring. */ + private ExtractedText extractedText; + + /* The corresponding text request. */ + private ExtractedTextRequest extractRequest; + + /* The number of nested batch edits. */ + private int batchEditCount; + + /* Whether or not invalidateInput should be called upon batch edits + ending. */ + private boolean pendingInvalidate; + + /* The ``composing span'' indicating the bounds of an ongoing + character composition. */ + private Object composingSpan; + + public + EmacsEditable (EmacsInputConnection connection) + { + /* Initialize the editable with one initial space, so backspace + always works. */ + super (); + + Object tem; + Context context; + + this.connection = connection; + + context = connection.view.getContext (); + tem = context.getSystemService (Context.INPUT_METHOD_SERVICE); + imManager = (InputMethodManager) tem; + + /* To watch for changes to text properties on Android, you + add... a text property. */ + setSpan (this, 0, 0, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + + public void + endBatchEdit () + { + if (batchEditCount < 1) + return; + + if (--batchEditCount == 0 && pendingInvalidate) + invalidateInput (); + } + + public void + beginBatchEdit () + { + ++batchEditCount; + } + + public void + setExtractedTextAndRequest (ExtractedText text, + ExtractedTextRequest request, + boolean monitor) + { + /* Extract the text. If monitor is set, also record it as the + text that is currently being extracted. */ + + text.startOffset = 0; + text.selectionStart = Selection.getSelectionStart (this); + text.selectionEnd = Selection.getSelectionStart (this); + text.text = this; + + if (monitor) + { + extractedText = text; + extractRequest = request; + } + } + + public void + compositionStart () + { + isComposing = true; + } + + public void + compositionEnd () + { + isComposing = false; + sendComposingText (null); + } + + private void + sendComposingText (String string) + { + EmacsWindow window; + long time, serial; + + window = connection.view.window; + + if (window.isDestroyed ()) + return; + + time = System.currentTimeMillis (); + + /* A composition event is simply a special key event with a + keycode of -1. */ + + synchronized (window.eventStrings) + { + serial + = EmacsNative.sendKeyPress (window.handle, time, 0, -1, -1); + + /* Save the string so that android_lookup_string can find + it. */ + if (string != null) + window.saveUnicodeString ((int) serial, string); + } + } + + private void + invalidateInput () + { + int start, end, composingSpanStart, composingSpanEnd; + + if (batchEditCount > 0) + { + Log.d (TAG, "invalidateInput: deferring for batch edit"); + pendingInvalidate = true; + return; + } + + pendingInvalidate = false; + + start = Selection.getSelectionStart (this); + end = Selection.getSelectionEnd (this); + + if (composingSpan != null) + { + composingSpanStart = getSpanStart (composingSpan); + composingSpanEnd = getSpanEnd (composingSpan); + } + else + { + composingSpanStart = -1; + composingSpanEnd = -1; + } + + Log.d (TAG, "invalidateInput: now " + start + ", " + end); + + /* Tell the input method that the cursor changed. */ + imManager.updateSelection (connection.view, start, end, + composingSpanStart, + composingSpanEnd); + + /* If there is any extracted text, tell the IME that it has + changed. */ + if (extractedText != null) + imManager.updateExtractedText (connection.view, + extractRequest.token, + extractedText); + } + + public SpannableStringBuilder + replace (int start, int end, CharSequence tb, int tbstart, + int tbend) + { + super.replace (start, end, tb, tbstart, tbend); + + /* If a change happens during composition, perform the change and + then send the text being composed. */ + + if (isComposing) + sendComposingText (toString ()); + + return this; + } + + private boolean + isSelectionSpan (Object span) + { + return ((Selection.SELECTION_START == span + || Selection.SELECTION_END == span) + && (getSpanFlags (span) + & Spanned.SPAN_INTERMEDIATE) == 0); + } + + @Override + public void + onSpanAdded (Spannable text, Object what, int start, int end) + { + Log.d (TAG, "onSpanAdded: " + text + " " + what + " " + + start + " " + end); + + /* Try to find the composing span. This isn't a public API. */ + + if (what.getClass ().getName ().contains ("ComposingText")) + composingSpan = what; + + if (isSelectionSpan (what)) + invalidateInput (); + } + + @Override + public void + onSpanChanged (Spannable text, Object what, int ostart, + int oend, int nstart, int nend) + { + Log.d (TAG, "onSpanChanged: " + text + " " + what + " " + + nstart + " " + nend); + + if (isSelectionSpan (what)) + invalidateInput (); + } + + @Override + public void + onSpanRemoved (Spannable text, Object what, + int start, int end) + { + Log.d (TAG, "onSpanRemoved: " + text + " " + what + " " + + start + " " + end); + + if (isSelectionSpan (what)) + invalidateInput (); + } + + public boolean + isInBatchEdit () + { + return batchEditCount > 0; + } +} diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java new file mode 100644 index 00000000000..897a393b984 --- /dev/null +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -0,0 +1,175 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.SurroundingText; +import android.view.KeyEvent; + +import android.text.Editable; + +import android.util.Log; + +/* Android input methods, take number six. + + See EmacsEditable for more details. */ + +public class EmacsInputConnection extends BaseInputConnection +{ + private static final String TAG = "EmacsInputConnection"; + public EmacsView view; + private EmacsEditable editable; + + /* The length of the last string to be committed. */ + private int lastCommitLength; + + int currentLargeOffset; + + public + EmacsInputConnection (EmacsView view) + { + super (view, false); + this.view = view; + this.editable = new EmacsEditable (this); + } + + @Override + public Editable + getEditable () + { + return editable; + } + + @Override + public boolean + setComposingText (CharSequence text, int newCursorPosition) + { + editable.compositionStart (); + super.setComposingText (text, newCursorPosition); + return true; + } + + @Override + public boolean + setComposingRegion (int start, int end) + { + int i; + + if (lastCommitLength != 0) + { + Log.d (TAG, "Restarting composition for: " + lastCommitLength); + + for (i = 0; i < lastCommitLength; ++i) + sendKeyEvent (new KeyEvent (KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_DEL)); + + lastCommitLength = 0; + } + + editable.compositionStart (); + super.setComposingRegion (start, end); + return true; + } + + @Override + public boolean + finishComposingText () + { + editable.compositionEnd (); + return super.finishComposingText (); + } + + @Override + public boolean + beginBatchEdit () + { + editable.beginBatchEdit (); + return super.beginBatchEdit (); + } + + @Override + public boolean + endBatchEdit () + { + editable.endBatchEdit (); + return super.endBatchEdit (); + } + + @Override + public boolean + commitText (CharSequence text, int newCursorPosition) + { + editable.compositionEnd (); + super.commitText (text, newCursorPosition); + + /* An observation is that input methods rarely recompose trailing + spaces. Avoid re-setting the commit length in that case. */ + + if (text.toString ().equals (" ")) + lastCommitLength += 1; + else + /* At this point, the editable is now empty. + + The input method may try to cancel the edit upon a subsequent + backspace by calling setComposingRegion with a region that is + the length of TEXT. + + Record this length in order to be able to send backspace + events to ``delete'' the text in that case. */ + lastCommitLength = text.length (); + + Log.d (TAG, "commitText: \"" + text + "\""); + + return true; + } + + /* Return a large offset, cycling through 100000, 30000, 0. + The offset is typically used to force the input method to update + its notion of ``surrounding text'', bypassing any caching that + it might have in progress. + + There must be another way to do this, but I can't find it. */ + + public int + largeSelectionOffset () + { + switch (currentLargeOffset) + { + case 0: + currentLargeOffset = 100000; + return 100000; + + case 100000: + currentLargeOffset = 30000; + return 30000; + + case 30000: + currentLargeOffset = 0; + return 0; + } + + currentLargeOffset = 0; + return -1; + } +} diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 939348ba420..f0219843d35 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -25,6 +25,10 @@ import android.content.res.AssetManager; public class EmacsNative { + /* List of native libraries that must be loaded during class + initialization. */ + private static final String[] libraryDeps; + /* Obtain the fingerprint of this build of Emacs. The fingerprint can be used to determine the dump file name. */ public static native String getFingerprint (); @@ -167,148 +171,26 @@ public class EmacsNative Every time you add a new shared library dependency to Emacs, please add it here as well. */ - try - { - System.loadLibrary ("png_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("selinux_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("crypto_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("pcre_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("packagelistparser_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("gnutls_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("gmp_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("nettle_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("p11-kit_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("tasn1_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("hogweed_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("jansson_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("jpeg_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("tiff_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("xml2_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("icuuc_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ + libraryDeps = new String[] { "png_emacs", "selinux_emacs", + "crypto_emacs", "pcre_emacs", + "packagelistparser_emacs", + "gnutls_emacs", "gmp_emacs", + "nettle_emacs", "p11-kit_emacs", + "tasn1_emacs", "hogweed_emacs", + "jansson_emacs", "jpeg_emacs", + "tiff_emacs", "xml2_emacs", + "icuuc_emacs", }; + + for (String dependency : libraryDeps) + { + try + { + System.loadLibrary (dependency); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } } System.loadLibrary ("emacs"); diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 4fc8104e31f..bc3716f6da8 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -94,7 +94,7 @@ public class EmacsView extends ViewGroup private long lastClipSerial; /* The InputMethodManager for this view's context. */ - private InputMethodManager imManager; + public InputMethodManager imManager; /* Whether or not this view is attached to a window. */ public boolean isAttachedToWindow; @@ -558,8 +558,9 @@ public class EmacsView extends ViewGroup box that obscures Emacs. */ info.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; - /* But don't return an InputConnection, in order to force the on - screen keyboard to work correctly. */ + /* Set a reasonable inputType. */ + info.inputType = InputType.TYPE_NULL; + return null; } diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 9e2f2f53270..0eca35cec61 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -23,6 +23,8 @@ import java.lang.IllegalStateException; import java.util.ArrayList; import java.util.List; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; import android.content.Context; @@ -100,7 +102,7 @@ public class EmacsWindow extends EmacsHandleObject /* The button state and keyboard modifier mask at the time of the last button press or release event. */ - private int lastButtonState, lastModifiers; + public int lastButtonState, lastModifiers; /* Whether or not the window is mapped, and whether or not it is deiconified. */ @@ -122,6 +124,10 @@ public class EmacsWindow extends EmacsHandleObject to quit Emacs. */ private long lastVolumeButtonRelease; + /* Linked list of character strings which were recently sent as + events. */ + public LinkedHashMap eventStrings; + public EmacsWindow (short handle, final EmacsWindow parent, int x, int y, int width, int height, boolean overrideRedirect) @@ -155,6 +161,19 @@ public class EmacsWindow extends EmacsHandleObject } scratchGC = new EmacsGC ((short) 0); + + /* Create the map of input method-committed strings. Keep at most + ten strings in the map. */ + + eventStrings + = new LinkedHashMap () { + @Override + protected boolean + removeEldestEntry (Map.Entry entry) + { + return size () > 10; + } + }; } public void @@ -507,10 +526,40 @@ public class EmacsWindow extends EmacsHandleObject return view.getBitmap (); } + /* event.getCharacters is used because older input methods still + require it. */ + @SuppressWarnings ("deprecation") + public int + getEventUnicodeChar (KeyEvent event, int state) + { + String characters; + + if (event.getUnicodeChar (state) != 0) + return event.getUnicodeChar (state); + + characters = event.getCharacters (); + + if (characters != null && characters.length () == 1) + return characters.charAt (0); + + return characters == null ? 0 : -1; + } + + public void + saveUnicodeString (int serial, String string) + { + eventStrings.put (serial, string); + } + + /* event.getCharacters is used because older input methods still + require it. */ + @SuppressWarnings ("deprecation") public void onKeyDown (int keyCode, KeyEvent event) { int state, state_1; + long serial; + String characters; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) state = event.getModifiers (); @@ -537,18 +586,28 @@ public class EmacsWindow extends EmacsHandleObject state_1 = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); - EmacsNative.sendKeyPress (this.handle, - event.getEventTime (), - state, keyCode, - event.getUnicodeChar (state_1)); - lastModifiers = state; + synchronized (eventStrings) + { + serial + = EmacsNative.sendKeyPress (this.handle, + event.getEventTime (), + state, keyCode, + getEventUnicodeChar (event, + state_1)); + lastModifiers = state; + + characters = event.getCharacters (); + + if (characters != null && characters.length () > 1) + saveUnicodeString ((int) serial, characters); + } } public void onKeyUp (int keyCode, KeyEvent event) { int state, state_1; - long time; + long time, serial; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) state = event.getModifiers (); @@ -575,10 +634,12 @@ public class EmacsWindow extends EmacsHandleObject state_1 = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); - EmacsNative.sendKeyRelease (this.handle, - event.getEventTime (), - state, keyCode, - event.getUnicodeChar (state_1)); + serial + = EmacsNative.sendKeyRelease (this.handle, + event.getEventTime (), + state, keyCode, + getEventUnicodeChar (event, + state_1)); lastModifiers = state; if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) @@ -1105,4 +1166,17 @@ public class EmacsWindow extends EmacsHandleObject } }); } + + public String + lookupString (int eventSerial) + { + String any; + + synchronized (eventStrings) + { + any = eventStrings.remove (eventSerial); + } + + return any; + } }; diff --git a/lisp/term/android-win.el b/lisp/term/android-win.el index aa5992a9f09..0984b4d5840 100644 --- a/lisp/term/android-win.el +++ b/lisp/term/android-win.el @@ -166,6 +166,42 @@ VALUE should be something suitable for passing to (android-set-clipboard string)) ((eq type 'PRIMARY) (setq android-primary-selection string))))) + +;;; Character composition display. + +(defvar android-preedit-overlay nil + "The overlay currently used to display preedit text from a compose sequence.") + +;; With some input methods, text gets inserted before Emacs is told to +;; remove any preedit text that was displayed, which causes both the +;; preedit overlay and the text to be visible for a brief period of +;; time. This pre-command-hook clears the overlay before any command +;; and should be set whenever a preedit overlay is visible. +(defun android-clear-preedit-text () + "Clear the pre-edit overlay and remove itself from pre-command-hook. +This function should be installed in `pre-command-hook' whenever +preedit text is displayed." + (when android-preedit-overlay + (delete-overlay android-preedit-overlay) + (setq android-preedit-overlay nil)) + (remove-hook 'pre-command-hook #'android-clear-preedit-text)) + +(defun android-preedit-text (event) + "Display preedit text from a compose sequence in EVENT. +EVENT is a preedit-text event." + (interactive "e") + (when android-preedit-overlay + (delete-overlay android-preedit-overlay) + (setq android-preedit-overlay nil) + (remove-hook 'pre-command-hook #'android-clear-preedit-text)) + (when (nth 1 event) + (let ((string (propertize (nth 1 event) 'face '(:underline t)))) + (setq android-preedit-overlay (make-overlay (point) (point))) + (add-hook 'pre-command-hook #'android-clear-preedit-text) + (overlay-put android-preedit-overlay 'window (selected-window)) + (overlay-put android-preedit-overlay 'before-string string)))) + +(define-key special-event-map [preedit-text] 'android-preedit-text) (provide 'android-win) ;; android-win.el ends here. diff --git a/src/android.c b/src/android.c index ebd374addbf..479eb9b10d4 100644 --- a/src/android.c +++ b/src/android.c @@ -125,6 +125,7 @@ struct android_emacs_window jclass class; jmethodID swap_buffers; jmethodID toggle_on_screen_keyboard; + jmethodID lookup_string; }; /* The API level of the current device. */ @@ -1829,6 +1830,7 @@ android_init_emacs_window (void) FIND_METHOD (swap_buffers, "swapBuffers", "()V"); FIND_METHOD (toggle_on_screen_keyboard, "toggleOnScreenKeyboard", "(Z)V"); + FIND_METHOD (lookup_string, "lookupString", "(I)Ljava/lang/String;"); #undef FIND_METHOD } @@ -4104,6 +4106,115 @@ android_sync (void) android_exception_check (); } +int +android_wc_lookup_string (android_key_pressed_event *event, + wchar_t *buffer_return, int wchars_buffer, + int *keysym_return, + enum android_lookup_status *status_return) +{ + enum android_lookup_status status; + int rc; + jobject window, string; + const jchar *characters; + jsize size; + size_t i; + + status = ANDROID_LOOKUP_NONE; + rc = 0; + + /* See if an actual lookup has to be made. Note that while + BUFFER_RETURN is wchar_t, the returned characters are always in + UCS. */ + + if (event->unicode_char != (uint32_t) -1) + { + if (event->unicode_char) + { + if (wchars_buffer < 1) + { + *status_return = ANDROID_BUFFER_OVERFLOW; + return 0; + } + else + { + buffer_return[0] = event->unicode_char; + status = ANDROID_LOOKUP_CHARS; + rc = 1; + } + } + + *keysym_return = event->keycode; + + if (status == ANDROID_LOOKUP_CHARS) + status = ANDROID_LOOKUP_BOTH; + else + { + status = ANDROID_LOOKUP_KEYSYM; + rc = 0; + } + + *status_return = status; + + return rc; + } + + /* Now look up the window. */ + rc = 0; + + if (!android_handles[event->window].handle + || (android_handles[event->window].type + != ANDROID_HANDLE_WINDOW)) + status = ANDROID_LOOKUP_NONE; + else + { + window = android_handles[event->window].handle; + string + = (*android_java_env)->CallObjectMethod (android_java_env, window, + window_class.lookup_string, + (jint) event->serial); + android_exception_check (); + + if (!string) + status = ANDROID_LOOKUP_NONE; + else + { + /* Now return this input method string. */ + characters = (*android_java_env)->GetStringChars (android_java_env, + string, NULL); + android_exception_check (); + + /* Figure out how big the string is. */ + size = (*android_java_env)->GetStringLength (android_java_env, + string); + + /* Copy over the string data. */ + for (i = 0; i < MIN ((unsigned int) wchars_buffer, size); ++i) + buffer_return[i] = characters[i]; + + if (i < size) + status = ANDROID_BUFFER_OVERFLOW; + else + status = ANDROID_LOOKUP_CHARS; + + /* Return the number of characters that should have been + written. */ + + if (size > INT_MAX) + rc = INT_MAX; + else + rc = size; + + (*android_java_env)->ReleaseStringChars (android_java_env, string, + characters); + android_exception_check (); + ANDROID_DELETE_LOCAL_REF (string); + } + } + + *status_return = status; + return rc; +} + /* Low level drawing primitives. */ diff --git a/src/androidgui.h b/src/androidgui.h index 619c5e44f15..1f0d34e67aa 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -209,7 +209,7 @@ typedef char Emacs_Cursor; #ifndef ANDROID_STUBIFY /* Universal NULL handle. */ -static const int ANDROID_NONE; +static const int ANDROID_NONE, ANDROID_NO_SYMBOL; /* Keep these as conceptually close to X as possible: that makes synchronizing code between the ports much easier. */ @@ -259,9 +259,14 @@ struct android_key_event android_time time; unsigned int state; unsigned int keycode; + + /* If this field is -1, then android_lookup_string should be called + to retrieve the associated individual characters. */ unsigned int unicode_char; }; +typedef struct android_key_event android_key_pressed_event; + /* These hard coded values are Android modifier keycodes derived through experimentation. */ @@ -449,6 +454,15 @@ enum ANDROID_CURRENT_TIME = 0L, }; +enum android_lookup_status + { + ANDROID_BUFFER_OVERFLOW, + ANDROID_LOOKUP_NONE, + ANDROID_LOOKUP_CHARS, + ANDROID_LOOKUP_KEYSYM, + ANDROID_LOOKUP_BOTH, + }; + extern int android_pending (void); extern void android_next_event (union android_event *); @@ -537,6 +551,10 @@ extern void android_translate_coordinates (android_window, int, int, int *, int *); extern void android_sync (void); +extern int android_wc_lookup_string (android_key_pressed_event *, + wchar_t *, int, int *, + enum android_lookup_status *); + #endif diff --git a/src/androidterm.c b/src/androidterm.c index 4aee1a90b68..a57d5623c66 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -541,6 +541,8 @@ handle_one_android_event (struct android_display_info *dpyinfo, Lisp_Object window; int scroll_height; double scroll_unit; + int keysym; + ptrdiff_t nchars, i; /* It is okay for this to not resemble handle_one_xevent so much. Differences in event handling code are much less nasty than @@ -550,6 +552,7 @@ handle_one_android_event (struct android_display_info *dpyinfo, hlinfo = &dpyinfo->mouse_highlight; *finish = ANDROID_EVENT_NORMAL; any = android_window_to_frame (dpyinfo, event->xany.window); + nchars = 0; if (any && any->wait_event_type == event->type) any->wait_event_type = 0; /* Indicates we got it. */ @@ -616,6 +619,13 @@ handle_one_android_event (struct android_display_info *dpyinfo, android_flush_dirty_back_buffer_on (mouse_frame); } + if (!f) + goto OTHER; + + wchar_t copy_buffer[129]; + wchar_t *copy_bufptr = copy_buffer; + int copy_bufsiz = 128 * sizeof (wchar_t); + event->xkey.state |= android_emacs_to_android_modifiers (dpyinfo, extra_keyboard_modifiers); @@ -627,31 +637,100 @@ handle_one_android_event (struct android_display_info *dpyinfo, = android_android_to_emacs_modifiers (dpyinfo, modifiers); inev.ie.timestamp = event->xkey.time; - /* First deal with keysyms which have defined translations to - characters. */ + keysym = event->xkey.keycode; + + { + enum android_lookup_status status_return; + + nchars = android_wc_lookup_string (&event->xkey, copy_bufptr, + copy_bufsiz, &keysym, + &status_return); + + /* android_lookup_string can't be called twice, so there's no + way to recover from buffer overflow. */ + if (status_return == ANDROID_BUFFER_OVERFLOW) + goto done_keysym; + else if (status_return == ANDROID_LOOKUP_NONE) + { + /* Don't skip preedit text events. */ + if (event->xkey.keycode != (uint32_t) -1) + goto done_keysym; + } + else if (status_return == ANDROID_LOOKUP_CHARS) + keysym = ANDROID_NO_SYMBOL; + else if (status_return != ANDROID_LOOKUP_KEYSYM + && status_return != ANDROID_LOOKUP_BOTH) + emacs_abort (); + + /* Deal with pre-edit text events. On Android, these are + simply encoded as events with associated strings and a + keycode set to ``-1''. */ + + if (event->xkey.keycode == (uint32_t) -1) + { + inev.ie.kind = PREEDIT_TEXT_EVENT; + inev.ie.arg = Qnil; + + /* If text was looked up, decode it and make it the + preedit text. */ + + if (status_return == ANDROID_LOOKUP_CHARS && nchars) + { + copy_bufptr[nchars] = 0; + inev.ie.arg = from_unicode_buffer (copy_bufptr); + } - if (event->xkey.unicode_char >= 32 - && event->xkey.unicode_char < 128) + goto done_keysym; + } + } + + if (nchars == 1 && copy_bufptr[0] >= 32) { - inev.ie.kind = ASCII_KEYSTROKE_EVENT; - inev.ie.code = event->xkey.unicode_char; + /* Deal with characters. */ + + if (copy_bufptr[0] < 128) + inev.ie.kind = ASCII_KEYSTROKE_EVENT; + else + inev.ie.kind = MULTIBYTE_CHAR_KEYSTROKE_EVENT; + + inev.ie.code = copy_bufptr[0]; } - else if (event->xkey.unicode_char < 32) + else if (nchars < 2 && keysym) { /* If the key is a modifier key, just return. */ - if (ANDROID_IS_MODIFIER_KEY (event->xkey.keycode)) + if (ANDROID_IS_MODIFIER_KEY (keysym)) goto done_keysym; /* Next, deal with special ``characters'' by giving the keycode to keyboard.c. */ inev.ie.kind = NON_ASCII_KEYSTROKE_EVENT; - inev.ie.code = event->xkey.keycode; + inev.ie.code = keysym; } else { - /* Finally, deal with Unicode characters. */ - inev.ie.kind = MULTIBYTE_CHAR_KEYSTROKE_EVENT; - inev.ie.code = event->xkey.unicode_char; + /* Finally, deal with strings. */ + + for (i = 0; i < nchars; ++i) + { + inev.ie.kind = (SINGLE_BYTE_CHAR_P (copy_bufptr[i]) + ? ASCII_KEYSTROKE_EVENT + : MULTIBYTE_CHAR_KEYSTROKE_EVENT); + inev.ie.code = copy_bufptr[i]; + + /* If the character is actually '\n', then change this + to RET. */ + + if (copy_bufptr[i] == '\n') + { + inev.ie.kind = NON_ASCII_KEYSTROKE_EVENT; + inev.ie.code = 66; + } + + kbd_buffer_store_buffered_event (&inev, hold_quit); + } + + count += nchars; + inev.ie.kind = NO_EVENT; /* Already stored above. */ } goto done_keysym; diff --git a/src/coding.c b/src/coding.c index a2e0d7040f8..be5a0252ede 100644 --- a/src/coding.c +++ b/src/coding.c @@ -8495,7 +8495,7 @@ preferred_coding_system (void) return CODING_ID_NAME (id); } -#if defined (WINDOWSNT) || defined (CYGWIN) +#if defined (WINDOWSNT) || defined (CYGWIN) || defined HAVE_ANDROID Lisp_Object from_unicode (Lisp_Object str) @@ -8513,10 +8513,34 @@ from_unicode (Lisp_Object str) Lisp_Object from_unicode_buffer (const wchar_t *wstr) { - /* We get one of the two final null bytes for free. */ - ptrdiff_t len = 1 + sizeof (wchar_t) * wcslen (wstr); - AUTO_STRING_WITH_LEN (str, (char *) wstr, len); - return from_unicode (str); + if (sizeof (wchar_t) == 2) + { + /* We get one of the two final null bytes for free. */ + ptrdiff_t len = 1 + sizeof (wchar_t) * wcslen (wstr); + AUTO_STRING_WITH_LEN (str, (char *) wstr, len); + return from_unicode (str); + } + else + { + /* This code is used only on Android, where little endian UTF-16 + strings are extended to 32-bit wchar_t. */ + + uint16_t *words; + size_t length, i; + + length = wcslen (wstr) + 1; + + USE_SAFE_ALLOCA; + SAFE_NALLOCA (words, sizeof *words, length); + + for (i = 0; i < length - 1; ++i) + words[i] = wstr[i]; + + words[i] = '\0'; + AUTO_STRING_WITH_LEN (str, (char *) words, + (length - 1) * sizeof *words); + return unbind_to (sa_count, from_unicode (str)); + } } wchar_t * @@ -8536,7 +8560,7 @@ to_unicode (Lisp_Object str, Lisp_Object *buf) return WCSDATA (*buf); } -#endif /* WINDOWSNT || CYGWIN */ +#endif /* WINDOWSNT || CYGWIN || HAVE_ANDROID */ /*** 8. Emacs Lisp library functions ***/ diff --git a/src/coding.h b/src/coding.h index b8d4f5f27e1..08c29c884a5 100644 --- a/src/coding.h +++ b/src/coding.h @@ -709,7 +709,7 @@ extern void encode_coding_object (struct coding_system *, /* Defined in this file. */ INLINE int surrogates_to_codepoint (int, int); -#if defined (WINDOWSNT) || defined (CYGWIN) +#if defined (WINDOWSNT) || defined (CYGWIN) || defined HAVE_ANDROID /* These functions use Lisp string objects to store the UTF-16LE strings that modern versions of Windows expect. These strings are @@ -732,7 +732,7 @@ extern Lisp_Object from_unicode (Lisp_Object str); /* Convert WSTR to an Emacs string. */ extern Lisp_Object from_unicode_buffer (const wchar_t *wstr); -#endif /* WINDOWSNT || CYGWIN */ +#endif /* WINDOWSNT || CYGWIN || HAVE_ANDROID */ /* Macros for backward compatibility. */ diff --git a/src/sfnt.c b/src/sfnt.c index cf36f388819..38f6984b93c 100644 --- a/src/sfnt.c +++ b/src/sfnt.c @@ -3758,16 +3758,7 @@ sfnt_edge_sort (struct sfnt_edge *edges, size_t size) coordinates, and incrementing the Y axis by SFNT_POLY_STEP instead of 1. SFNT_POLY_STEP is chosen to always keep Y aligned to a grid placed such that there are always 1 << SFNT_POLY_SHIFT positions - available for each integral pixel coordinate. - - Moving upwards is performed using Bresenham's algorithm. Prior to - an edge being created, a single slope error is computed. This is - how much X should increase for each increase in Y. - - Upon each increase in Y, X initially does not move. Instead, the - ``slope error'' is accumulated; once it exceeds the sample size, - one sample is subtracted from the accumulator, and X is increased - by one step. */ + available for each integral pixel coordinate. */ static void sfnt_poly_edges (struct sfnt_edge *edges, size_t size,