From 0198b8cffd82893412c738dae8e50c45a99286f1 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 12 Feb 2023 20:32:25 +0800 Subject: 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. --- java/org/gnu/emacs/EmacsInputConnection.java | 175 +++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 java/org/gnu/emacs/EmacsInputConnection.java (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') 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; + } +} -- cgit v1.3 From a158c1d5b964fda36f752998cef076d581dc4df4 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 15 Feb 2023 12:23:03 +0800 Subject: Update Android port * configure.ac (HAVE_TEXT_CONVERSION): Define on Android. * doc/emacs/input.texi (On-Screen Keyboards): Document ``text conversion'' slightly. * doc/lispref/commands.texi (Misc Events): Document new `text-conversion' event. * java/org/gnu/emacs/EmacsContextMenu.java (display): Use `syncRunnable'. * java/org/gnu/emacs/EmacsDialog.java (display): Likewise. * java/org/gnu/emacs/EmacsEditable.java: Delete file. * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection): Reimplement from scratch. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): Add new functions. * java/org/gnu/emacs/EmacsService.java (EmacsService, getEmacsView) (getLocationOnScreen, sync, getClipboardManager, restartEmacs): Use syncRunnable. (syncRunnable): New function. (updateIC, resetIC): New functions. * java/org/gnu/emacs/EmacsView.java (EmacsView): New field `inputConnection' and `icMode'. (onCreateInputConnection): Update accordingly. (setICMode, getICMode): New functions. * lisp/bindings.el (global-map): Ignore text conversion events. * src/alloc.c (mark_frame): Mark text conversion data. * src/android.c (struct android_emacs_service): New fields `update_ic' and `reset_ic'. (event_serial): Export. (android_query_sem): New function. (android_init_events): Initialize new semaphore. (android_write_event): Export. (android_select): Check for UI thread code. (setEmacsParams, android_init_emacs_service): Initialize new methods. (android_check_query, android_begin_query, android_end_query) (android_run_in_emacs_thread): (android_update_ic, android_reset_ic): New functions for managing synchronous queries from one thread to another. * src/android.h: Export new functions. * src/androidgui.h (enum android_event_type): Add input method events. (enum android_ime_operation, struct android_ime_event) (union android_event, enum android_ic_mode): New structs and enums. * src/androidterm.c (android_window_to_frame): Allow DPYINFO to be NULL. (android_decode_utf16, android_handle_ime_event) (handle_one_android_event, android_sync_edit) (android_copy_java_string, beginBatchEdit, endBatchEdit) (commitCompletion, deleteSurroundingText, finishComposingText) (getSelectedtext, getTextAfterCursor, getTextBeforeCursor) (setComposingText, setComposingRegion, setSelection, getSelection) (performEditorAction, getExtractedText): New functions. (struct android_conversion_query_context): (android_perform_conversion_query): (android_text_to_string): (struct android_get_selection_context): (android_get_selection): (struct android_get_extracted_text_context): (android_get_extracted_text): (struct android_extracted_text_request_class): (struct android_extracted_text_class): (android_update_selection): (android_reset_conversion): (android_set_point): (android_compose_region_changed): (android_notify_conversion): (text_conversion_interface): New functions and structures. (android_term_init): Initialize text conversion. * src/coding.c (syms_of_coding): Define Qutf_16le on Android. * src/frame.c (make_frame): Clear conversion data. (delete_frame): Reset conversion state. * src/frame.h (enum text_conversion_operation) (struct text_conversion_action, struct text_conversion_state) (GCALIGNED_STRUCT): Update structures. * src/keyboard.c (read_char, readable_events, kbd_buffer_get_event) (syms_of_keyboard): Handle text conversion events. * src/lisp.h: * src/process.c: Fix includes. * src/textconv.c (enum textconv_batch_edit_flags, textconv_query) (reset_frame_state, detect_conversion_events) (restore_selected_window, really_commit_text) (really_finish_composing_text, really_set_composing_text) (really_set_composing_region, really_delete_surrounding_text) (really_set_point, complete_edit) (handle_pending_conversion_events_1) (handle_pending_conversion_events, start_batch_edit) (end_batch_edit, commit_text, finish_composing_text) (set_composing_text, set_composing_region, textconv_set_point) (delete_surrounding_text, get_extracted_text) (report_selected_window_change, report_point_change) (register_texconv_interface): New functions. * src/textconv.h (struct textconv_interface) (TEXTCONV_SKIP_CONVERSION_REGION): Update prototype. * src/xdisp.c (mark_window_display_accurate_1): * src/xfns.c (xic_string_conversion_callback): * src/xterm.c (init_xterm): Adjust accordingly. --- configure.ac | 2 +- doc/emacs/input.texi | 17 + doc/lispref/commands.texi | 9 + java/org/gnu/emacs/EmacsContextMenu.java | 15 +- java/org/gnu/emacs/EmacsDialog.java | 15 +- java/org/gnu/emacs/EmacsEditable.java | 300 -------- java/org/gnu/emacs/EmacsInputConnection.java | 187 ++--- java/org/gnu/emacs/EmacsNative.java | 46 ++ java/org/gnu/emacs/EmacsService.java | 117 +-- java/org/gnu/emacs/EmacsView.java | 55 +- lisp/bindings.el | 3 + src/alloc.c | 14 + src/android.c | 295 +++++++- src/android.h | 10 + src/androidgui.h | 60 ++ src/androidterm.c | 1037 +++++++++++++++++++++++++- src/coding.c | 2 +- src/frame.c | 17 + src/frame.h | 62 ++ src/keyboard.c | 50 ++ src/lisp.h | 3 + src/process.c | 1 + src/textconv.c | 906 +++++++++++++++++++++- src/textconv.h | 38 +- src/xdisp.c | 28 + src/xfns.c | 2 +- src/xterm.c | 2 +- 27 files changed, 2787 insertions(+), 506 deletions(-) delete mode 100644 java/org/gnu/emacs/EmacsEditable.java (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') diff --git a/configure.ac b/configure.ac index 44529260892..fc95bcd09b6 100644 --- a/configure.ac +++ b/configure.ac @@ -7217,7 +7217,7 @@ if test "$window_system" != "none"; then [Define if you poll periodically to detect C-g.]) WINDOW_SYSTEM_OBJ="fontset.o fringe.o image.o" - if test "$window_system" = "x11"; then + if test "$window_system" = "x11" || test "$REALLY_ANDROID" = "yes"; then AC_DEFINE([HAVE_TEXT_CONVERSION], [1], [Define if the window system has text conversion support.]) WINDOW_SYSTEM_OBJ="$WINDOW_SYSTEM_OBJ textconv.o" diff --git a/doc/emacs/input.texi b/doc/emacs/input.texi index b306a63b6cb..2463a75edcd 100644 --- a/doc/emacs/input.texi +++ b/doc/emacs/input.texi @@ -109,3 +109,20 @@ Emacs quitting. @xref{Quitting}. The exact button is used to do this varies by system: on X, it is defined in the variable @code{x-quit-keysym}, and on Android, it is always the volume down button. + +@cindex text conversion, keyboards + Most input methods designed to work with on-screen keyboards perform +buffer edits differently from desktop input methods. + + On a conventional desktop windowing system, an input method will +simply display the contents of any on going character compositions on +screen, and send the appropriate key events to Emacs after completion. + + However, on screen keyboard input methods directly perform edits to +the selected window of each frame; this is known as ``text +conversion'', or ``string conversion'' under the X Window System. + + Text conversion is performed asynchronously whenever Emacs receives +a request to perform the conversion from the input method. After the +conversion completes, a @code{text-conversion} event is sent. +@xref{Misc Events,,, elisp, the Emacs Reference Manual}. diff --git a/doc/lispref/commands.texi b/doc/lispref/commands.texi index 2c0787521a5..2807d3d61b2 100644 --- a/doc/lispref/commands.texi +++ b/doc/lispref/commands.texi @@ -2200,6 +2200,15 @@ the buffer in which the xwidget will be displayed, using A few other event types represent occurrences within the system. @table @code +@cindex @code{text-conversion} event +@item text-conversion +This kind of event is sent @strong{after} a system-wide input method +performs an edit to one or more buffers. + +Once the event is sent, the input method may already have made changes +to multiple frames. @c TODO: allow querying which frames to which +@c changes have been made. + @cindex @code{delete-frame} event @item (delete-frame (@var{frame})) This kind of event indicates that the user gave the window manager diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 92429410d03..184c611bf9d 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -279,20 +279,7 @@ public class EmacsContextMenu } }; - synchronized (runnable) - { - EmacsService.SERVICE.runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } - + EmacsService.syncRunnable (runnable); return rc.thing; } diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index bd5e9ba8ee7..bc0333e99b9 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -317,20 +317,7 @@ public class EmacsDialog implements DialogInterface.OnDismissListener } }; - synchronized (runnable) - { - EmacsService.SERVICE.runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } - + EmacsService.syncRunnable (runnable); return rc.thing; } diff --git a/java/org/gnu/emacs/EmacsEditable.java b/java/org/gnu/emacs/EmacsEditable.java deleted file mode 100644 index 79af65a6ccd..00000000000 --- a/java/org/gnu/emacs/EmacsEditable.java +++ /dev/null @@ -1,300 +0,0 @@ -/* 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 index 897a393b984..3cf4419838b 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -25,6 +25,7 @@ import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.SurroundingText; +import android.view.inputmethod.TextSnapshot; import android.view.KeyEvent; import android.text.Editable; @@ -38,138 +39,156 @@ import android.util.Log; 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; + private EmacsView view; + private short windowHandle; public EmacsInputConnection (EmacsView view) { - super (view, false); + super (view, true); + this.view = view; - this.editable = new EmacsEditable (this); + this.windowHandle = view.window.handle; } @Override - public Editable - getEditable () + public boolean + beginBatchEdit () { - return editable; + Log.d (TAG, "beginBatchEdit"); + EmacsNative.beginBatchEdit (windowHandle); + return true; } @Override public boolean - setComposingText (CharSequence text, int newCursorPosition) + endBatchEdit () { - editable.compositionStart (); - super.setComposingText (text, newCursorPosition); + Log.d (TAG, "endBatchEdit"); + EmacsNative.endBatchEdit (windowHandle); return true; } @Override public boolean - setComposingRegion (int start, int end) + commitCompletion (CompletionInfo info) { - 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)); + Log.d (TAG, "commitCompletion: " + info); + EmacsNative.commitCompletion (windowHandle, + info.getText ().toString (), + info.getPosition ()); + return true; + } - lastCommitLength = 0; - } + @Override + public boolean + commitText (CharSequence text, int newCursorPosition) + { + Log.d (TAG, "commitText: " + text + " " + newCursorPosition); + EmacsNative.commitText (windowHandle, text.toString (), + newCursorPosition); + return true; + } - editable.compositionStart (); - super.setComposingRegion (start, end); + @Override + public boolean + deleteSurroundingText (int leftLength, int rightLength) + { + Log.d (TAG, ("deleteSurroundingText: " + + leftLength + " " + rightLength)); + EmacsNative.deleteSurroundingText (windowHandle, leftLength, + rightLength); return true; } - + @Override public boolean finishComposingText () { - editable.compositionEnd (); - return super.finishComposingText (); + Log.d (TAG, "finishComposingText"); + + EmacsNative.finishComposingText (windowHandle); + return true; + } + + @Override + public String + getSelectedText (int flags) + { + Log.d (TAG, "getSelectedText: " + flags); + + return EmacsNative.getSelectedText (windowHandle, flags); + } + + @Override + public String + getTextAfterCursor (int length, int flags) + { + Log.d (TAG, "getTextAfterCursor: " + length + " " + flags); + + return EmacsNative.getTextAfterCursor (windowHandle, length, + flags); + } + + @Override + public String + getTextBeforeCursor (int length, int flags) + { + Log.d (TAG, "getTextBeforeCursor: " + length + " " + flags); + + return EmacsNative.getTextBeforeCursor (windowHandle, length, + flags); } @Override public boolean - beginBatchEdit () + setComposingText (CharSequence text, int newCursorPosition) { - editable.beginBatchEdit (); - return super.beginBatchEdit (); + Log.d (TAG, "setComposingText: " + newCursorPosition); + + EmacsNative.setComposingText (windowHandle, text.toString (), + newCursorPosition); + return true; } @Override public boolean - endBatchEdit () + setComposingRegion (int start, int end) { - editable.endBatchEdit (); - return super.endBatchEdit (); + Log.d (TAG, "setComposingRegion: " + start + " " + end); + + EmacsNative.setComposingRegion (windowHandle, start, end); + return true; } - + @Override public boolean - commitText (CharSequence text, int newCursorPosition) + performEditorAction (int editorAction) { - 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 + "\""); + Log.d (TAG, "performEditorAction: " + editorAction); + EmacsNative.performEditorAction (windowHandle, editorAction); 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. + @Override + public ExtractedText + getExtractedText (ExtractedTextRequest request, int flags) + { + Log.d (TAG, "getExtractedText: " + request + " " + flags); + + return EmacsNative.getExtractedText (windowHandle, request, + flags); + } - There must be another way to do this, but I can't find it. */ + + /* Override functions which are not implemented. */ - public int - largeSelectionOffset () + @Override + public TextSnapshot + takeSnapshot () { - switch (currentLargeOffset) - { - case 0: - currentLargeOffset = 100000; - return 100000; - - case 100000: - currentLargeOffset = 30000; - return 30000; - - case 30000: - currentLargeOffset = 0; - return 0; - } - - currentLargeOffset = 0; - return -1; + Log.d (TAG, "takeSnapshot"); + return null; } } diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index f0219843d35..aaac9446510 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -22,6 +22,8 @@ package org.gnu.emacs; import java.lang.System; import android.content.res.AssetManager; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; public class EmacsNative { @@ -161,6 +163,50 @@ public class EmacsNative descriptor, or NULL if there is none. */ public static native byte[] getProcName (int fd); + /* Notice that the Emacs thread will now start waiting for the main + thread's looper to respond. */ + public static native void beginSynchronous (); + + /* Notice that the Emacs thread will has finished waiting for the + main thread's looper to respond. */ + public static native void endSynchronous (); + + + + /* Input connection functions. These mostly correspond to their + counterparts in Android's InputConnection. */ + + public static native void beginBatchEdit (short window); + public static native void endBatchEdit (short window); + public static native void commitCompletion (short window, String text, + int position); + public static native void commitText (short window, String text, + int position); + public static native void deleteSurroundingText (short window, + int leftLength, + int rightLength); + public static native void finishComposingText (short window); + public static native String getSelectedText (short window, int flags); + public static native String getTextAfterCursor (short window, int length, + int flags); + public static native String getTextBeforeCursor (short window, int length, + int flags); + public static native void setComposingText (short window, String text, + int newCursorPosition); + public static native void setComposingRegion (short window, int start, + int end); + public static native void setSelection (short window, int start, int end); + public static native void performEditorAction (short window, + int editorAction); + public static native ExtractedText getExtractedText (short window, + ExtractedTextRequest req, + int flags); + + + /* Return the current value of the selection, or -1 upon + failure. */ + public static native int getSelection (short window); + static { /* Older versions of Android cannot link correctly with shared diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 2ec2ddf9bda..855a738a30f 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -80,6 +80,11 @@ public class EmacsService extends Service private EmacsThread thread; private Handler handler; + /* Keep this in synch with androidgui.h. */ + public static final int IC_MODE_NULL = 0; + public static final int IC_MODE_ACTION = 1; + public static final int IC_MODE_TEXT = 2; + /* Display metrics used by font backends. */ public DisplayMetrics metrics; @@ -258,20 +263,7 @@ public class EmacsService extends Service } }; - synchronized (runnable) - { - runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } - + syncRunnable (runnable); return view.thing; } @@ -292,19 +284,7 @@ public class EmacsService extends Service } }; - synchronized (runnable) - { - runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } + syncRunnable (runnable); } public void @@ -502,19 +482,7 @@ public class EmacsService extends Service } }; - synchronized (runnable) - { - runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } + syncRunnable (runnable); } @@ -594,20 +562,7 @@ public class EmacsService extends Service } }; - synchronized (runnable) - { - runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } - + syncRunnable (runnable); return manager.thing; } @@ -622,4 +577,58 @@ public class EmacsService extends Service startActivity (intent); System.exit (0); } + + /* Wait synchronously for the specified RUNNABLE to complete in the + UI thread. Must be called from the Emacs thread. */ + + public static void + syncRunnable (Runnable runnable) + { + EmacsNative.beginSynchronous (); + + synchronized (runnable) + { + SERVICE.runOnUiThread (runnable); + + while (true) + { + try + { + runnable.wait (); + break; + } + catch (InterruptedException e) + { + continue; + } + } + } + + EmacsNative.endSynchronous (); + } + + public void + updateIC (EmacsWindow window, int newSelectionStart, + int newSelectionEnd, int composingRegionStart, + int composingRegionEnd) + { + Log.d (TAG, ("updateIC: " + window + " " + newSelectionStart + + " " + newSelectionEnd + " " + + composingRegionStart + " " + + composingRegionEnd)); + window.view.imManager.updateSelection (window.view, + newSelectionStart, + newSelectionEnd, + composingRegionStart, + composingRegionEnd); + } + + public void + resetIC (EmacsWindow window, int icMode) + { + Log.d (TAG, "resetIC: " + window); + + window.view.setICMode (icMode); + window.view.imManager.restartInput (window.view); + } }; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index bc3716f6da8..52da6d41f7d 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -103,6 +103,13 @@ public class EmacsView extends ViewGroup displayed whenever possible. */ public boolean isCurrentlyTextEditor; + /* The associated input connection. */ + private EmacsInputConnection inputConnection; + + /* The current IC mode. See `android_reset_ic' for more + details. */ + private int icMode; + public EmacsView (EmacsWindow window) { @@ -554,14 +561,46 @@ public class EmacsView extends ViewGroup public InputConnection onCreateInputConnection (EditorInfo info) { + int selection, mode; + + /* Figure out what kind of IME behavior Emacs wants. */ + mode = getICMode (); + /* Make sure the input method never displays a full screen input box that obscures Emacs. */ info.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; /* Set a reasonable inputType. */ - info.inputType = InputType.TYPE_NULL; + info.inputType = InputType.TYPE_CLASS_TEXT; + + /* Obtain the current position of point and set it as the + selection. */ + selection = EmacsNative.getSelection (window.handle); + + Log.d (TAG, "onCreateInputConnection: current selection is: " + selection); + + /* If this fails or ANDROID_IC_MODE_NULL was requested, then don't + initialize the input connection. */ + if (selection == -1 || mode == EmacsService.IC_MODE_NULL) + { + info.inputType = InputType.TYPE_NULL; + return null; + } + + if (mode == EmacsService.IC_MODE_ACTION) + info.imeOptions |= EditorInfo.IME_ACTION_DONE; - return null; + /* Set the initial selection fields. */ + info.initialSelStart = selection; + info.initialSelEnd = selection; + + /* Create the input connection if necessary. */ + + if (inputConnection == null) + inputConnection = new EmacsInputConnection (this); + + /* Return the input connection. */ + return inputConnection; } @Override @@ -572,4 +611,16 @@ public class EmacsView extends ViewGroup keyboard. */ return isCurrentlyTextEditor; } + + public synchronized void + setICMode (int icMode) + { + this.icMode = icMode; + } + + public synchronized int + getICMode () + { + return icMode; + } }; diff --git a/lisp/bindings.el b/lisp/bindings.el index c77b64c05da..057c870958d 100644 --- a/lisp/bindings.el +++ b/lisp/bindings.el @@ -1521,6 +1521,9 @@ if `inhibit-field-text-motion' is non-nil." (define-key special-event-map [sigusr1] 'ignore) (define-key special-event-map [sigusr2] 'ignore) +;; Text conversion +(define-key global-map [text-conversion] 'ignore) + ;; Don't look for autoload cookies in this file. ;; Local Variables: ;; no-update-autoloads: t diff --git a/src/alloc.c b/src/alloc.c index bc43f22005d..6d8658e7bb0 100644 --- a/src/alloc.c +++ b/src/alloc.c @@ -6939,6 +6939,11 @@ static void mark_frame (struct Lisp_Vector *ptr) { struct frame *f = (struct frame *) ptr; +#ifdef HAVE_TEXT_CONVERSION + struct text_conversion_action *tem; +#endif + + mark_vectorlike (&ptr->header); mark_face_cache (f->face_cache); #ifdef HAVE_WINDOW_SYSTEM @@ -6950,6 +6955,15 @@ mark_frame (struct Lisp_Vector *ptr) mark_vectorlike (&font->header); } #endif + +#ifdef HAVE_TEXT_CONVERSION + mark_object (f->conversion.compose_region_start); + mark_object (f->conversion.compose_region_end); + mark_object (f->conversion.compose_region_overlay); + + for (tem = f->conversion.actions; tem; tem = tem->next) + mark_object (tem->data); +#endif } static void diff --git a/src/android.c b/src/android.c index 479eb9b10d4..8f446224dab 100644 --- a/src/android.c +++ b/src/android.c @@ -98,6 +98,8 @@ struct android_emacs_service jmethodID sync; jmethodID browse_url; jmethodID restart_emacs; + jmethodID update_ic; + jmethodID reset_ic; }; struct android_emacs_pixmap @@ -207,7 +209,7 @@ static struct android_emacs_window window_class; /* The last event serial used. This is a 32 bit value, but it is stored in unsigned long to be consistent with X. */ -static unsigned int event_serial; +unsigned int event_serial; /* Unused pointer used to control compiler optimizations. */ void *unused_pointer; @@ -408,6 +410,10 @@ android_handle_sigusr1 (int sig, siginfo_t *siginfo, void *arg) #endif +/* Semaphore used to indicate completion of a query. + This should ideally be defined further down. */ +static sem_t android_query_sem; + /* Set up the global event queue by initializing the mutex and two condition variables, and the linked list of events. This must be called before starting the Emacs thread. Also, initialize the @@ -438,6 +444,7 @@ android_init_events (void) sem_init (&android_pselect_sem, 0, 0); sem_init (&android_pselect_start_sem, 0, 0); + sem_init (&android_query_sem, 0, 0); event_queue.events.next = &event_queue.events; event_queue.events.last = &event_queue.events; @@ -538,7 +545,7 @@ android_next_event (union android_event *event_return) pthread_mutex_unlock (&event_queue.mutex); } -static void +void android_write_event (union android_event *event) { struct android_event_container *container; @@ -576,6 +583,10 @@ android_select (int nfds, fd_set *readfds, fd_set *writefds, static char byte; #endif + /* Check for and run anything the UI thread wants to run on the main + thread. */ + android_check_query (); + pthread_mutex_lock (&event_queue.mutex); if (event_queue.num_events) @@ -634,6 +645,10 @@ android_select (int nfds, fd_set *readfds, fd_set *writefds, if (nfds_return < 0) errno = EINTR; + /* Now check for and run anything the UI thread wants to run in the + main thread. */ + android_check_query (); + return nfds_return; } @@ -1431,7 +1446,7 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, /* This may be called from multiple threads. setEmacsParams should only ever be called once. */ - if (__atomic_fetch_add (&emacs_initialized, -1, __ATOMIC_RELAXED)) + if (__atomic_fetch_add (&emacs_initialized, -1, __ATOMIC_SEQ_CST)) { ANDROID_THROW (env, "java/lang/IllegalArgumentException", "Emacs was already initialized!"); @@ -1705,6 +1720,10 @@ android_init_emacs_service (void) FIND_METHOD (browse_url, "browseUrl", "(Ljava/lang/String;)" "Ljava/lang/String;"); FIND_METHOD (restart_emacs, "restartEmacs", "()V"); + FIND_METHOD (update_ic, "updateIC", + "(Lorg/gnu/emacs/EmacsWindow;IIII)V"); + FIND_METHOD (reset_ic, "resetIC", + "(Lorg/gnu/emacs/EmacsWindow;I)V"); #undef FIND_METHOD } @@ -1834,7 +1853,7 @@ android_init_emacs_window (void) #undef FIND_METHOD } -extern JNIEXPORT void JNICALL +JNIEXPORT void JNICALL NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv, jobject dump_file_object, jint api_level) { @@ -1928,19 +1947,19 @@ NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv, emacs_abort (); } -extern JNIEXPORT void JNICALL +JNIEXPORT void JNICALL NATIVE_NAME (emacsAbort) (JNIEnv *env, jobject object) { emacs_abort (); } -extern JNIEXPORT void JNICALL +JNIEXPORT void JNICALL NATIVE_NAME (quit) (JNIEnv *env, jobject object) { Vquit_flag = Qt; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendConfigureNotify) (JNIEnv *env, jobject object, jshort window, jlong time, jint x, jint y, jint width, @@ -1961,7 +1980,7 @@ NATIVE_NAME (sendConfigureNotify) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, jshort window, jlong time, jint state, jint keycode, @@ -1981,7 +2000,7 @@ NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, jshort window, jlong time, jint state, jint keycode, @@ -2001,7 +2020,7 @@ NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendFocusIn) (JNIEnv *env, jobject object, jshort window, jlong time) { @@ -2016,7 +2035,7 @@ NATIVE_NAME (sendFocusIn) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendFocusOut) (JNIEnv *env, jobject object, jshort window, jlong time) { @@ -2031,7 +2050,7 @@ NATIVE_NAME (sendFocusOut) (JNIEnv *env, jobject object, return ++event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendWindowAction) (JNIEnv *env, jobject object, jshort window, jint action) { @@ -2046,7 +2065,7 @@ NATIVE_NAME (sendWindowAction) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendEnterNotify) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time) @@ -2064,7 +2083,7 @@ NATIVE_NAME (sendEnterNotify) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendLeaveNotify) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time) @@ -2082,7 +2101,7 @@ NATIVE_NAME (sendLeaveNotify) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendMotionNotify) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time) @@ -2100,7 +2119,7 @@ NATIVE_NAME (sendMotionNotify) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendButtonPress) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint state, @@ -2121,7 +2140,7 @@ NATIVE_NAME (sendButtonPress) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendButtonRelease) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint state, @@ -2142,7 +2161,7 @@ NATIVE_NAME (sendButtonRelease) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint pointer_id) @@ -2161,7 +2180,7 @@ NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint pointer_id) @@ -2180,7 +2199,7 @@ NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchMove) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint pointer_id) @@ -2199,7 +2218,7 @@ NATIVE_NAME (sendTouchMove) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendWheel) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint state, @@ -2221,7 +2240,7 @@ NATIVE_NAME (sendWheel) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendIconified) (JNIEnv *env, jobject object, jshort window) { @@ -2235,7 +2254,7 @@ NATIVE_NAME (sendIconified) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendDeiconified) (JNIEnv *env, jobject object, jshort window) { @@ -2249,7 +2268,7 @@ NATIVE_NAME (sendDeiconified) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendContextMenu) (JNIEnv *env, jobject object, jshort window, jint menu_event_id) { @@ -2264,7 +2283,7 @@ NATIVE_NAME (sendContextMenu) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendExpose) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jint width, jint height) @@ -2283,6 +2302,23 @@ NATIVE_NAME (sendExpose) (JNIEnv *env, jobject object, return event_serial; } +/* Forward declarations of deadlock prevention functions. */ + +static void android_begin_query (void); +static void android_end_query (void); + +JNIEXPORT void JNICALL +NATIVE_NAME (beginSynchronous) (JNIEnv *env, jobject object) +{ + android_begin_query (); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (endSynchronous) (JNIEnv *env, jobject object) +{ + android_end_query (); +} + #ifdef __clang__ #pragma clang diagnostic pop #else @@ -5155,6 +5191,215 @@ android_get_current_api_level (void) +/* Whether or not a query is currently being made. */ +static bool android_servicing_query; + +/* Function that is waiting to be run in the Emacs thread. */ +static void (*android_query_function) (void *); + +/* Context for that function. */ +static void *android_query_context; + +/* Deadlock protection. The UI thread and the Emacs thread must + sometimes make synchronous queries to each other, which are + normally answered inside each thread's respective event loop. + Deadlocks can happen when both threads simultaneously make such + synchronous queries and block waiting for each others responses. + + The Emacs thread can be interrupted to service any queries made by + the UI thread, but is not possible the other way around. + + To avoid such deadlocks, an atomic counter is provided. This + counter is incremented every time a query starts, and is set to + zerp every time one ends. If the UI thread tries to make a query + and sees that the counter is non-zero, it simply returns so that + its event loop can proceed to perform and respond to the query. If + the Emacs thread sees the same thing, then it stops to service all + queries being made by the input method, then proceeds to make its + query. */ + +/* Run any function that the UI thread has asked to run, and then + signal its completion. */ + +void +android_check_query (void) +{ + void (*proc) (void *); + void *closure; + + if (!__atomic_load_n (&android_servicing_query, __ATOMIC_SEQ_CST)) + return; + + /* First, load the procedure and closure. */ + __atomic_load (&android_query_context, &closure, __ATOMIC_SEQ_CST); + __atomic_load (&android_query_function, &proc, __ATOMIC_SEQ_CST); + + if (!proc) + return; + + proc (closure); + + /* Finish the query. */ + __atomic_store_n (&android_query_context, NULL, __ATOMIC_SEQ_CST); + __atomic_store_n (&android_query_function, NULL, __ATOMIC_SEQ_CST); + __atomic_clear (&android_servicing_query, __ATOMIC_SEQ_CST); + + /* Signal completion. */ + sem_post (&android_query_sem); +} + +/* Notice that the Emacs thread will start blocking waiting for a + response from the UI thread. Process any pending queries from the + UI thread. + + This function may be called from Java. */ + +static void +android_begin_query (void) +{ + if (__atomic_test_and_set (&android_servicing_query, + __ATOMIC_SEQ_CST)) + { + /* Answer the query that is currently being made. */ + assert (android_query_function != NULL); + android_check_query (); + + /* Wait for that query to complete. */ + while (__atomic_load_n (&android_servicing_query, + __ATOMIC_SEQ_CST)) + ;; + } +} + +/* Notice that a query has stopped. This function may be called from + Java. */ + +static void +android_end_query (void) +{ + __atomic_clear (&android_servicing_query, __ATOMIC_SEQ_CST); +} + +/* Synchronously ask the Emacs thread to run the specified PROC with + the given CLOSURE. Return if this fails, or once PROC is run. + + PROC may be run from inside maybe_quit. + + It is not okay to run Lisp code which signals or performs non + trivial tasks inside PROC. + + Return 1 if the Emacs thread is currently waiting for the UI thread + to respond and PROC could not be run, or 0 otherwise. */ + +int +android_run_in_emacs_thread (void (*proc) (void *), void *closure) +{ + union android_event event; + + event.xaction.type = ANDROID_WINDOW_ACTION; + event.xaction.serial = ++event_serial; + event.xaction.window = 0; + event.xaction.action = 0; + + /* Set android_query_function and android_query_context. */ + __atomic_store_n (&android_query_context, closure, __ATOMIC_SEQ_CST); + __atomic_store_n (&android_query_function, proc, __ATOMIC_SEQ_CST); + + /* Don't allow deadlocks to happen; make sure the Emacs thread is + not waiting for something to be done. */ + + if (__atomic_test_and_set (&android_servicing_query, + __ATOMIC_SEQ_CST)) + { + __atomic_store_n (&android_query_context, NULL, + __ATOMIC_SEQ_CST); + __atomic_store_n (&android_query_function, NULL, + __ATOMIC_SEQ_CST); + + return 1; + } + + /* Send a dummy event. `android_check_query' will be called inside + wait_reading_process_output after the event arrives. + + Otherwise, android_select will call android_check_thread the next + time it is entered. */ + android_write_event (&event); + + /* Start waiting for the function to be executed. */ + while (sem_wait (&android_query_sem) < 0) + ;; + + return 0; +} + + + +/* Input method related functions. */ + +/* Change WINDOW's active selection to the characters between + SELECTION_START and SELECTION_END. + + Also, update the composing region to COMPOSING_REGION_START and + COMPOSING_REGION_END. + + If any value cannot fit in jint, then the behavior of the input + method is undefined. */ + +void +android_update_ic (android_window window, ptrdiff_t selection_start, + ptrdiff_t selection_end, ptrdiff_t composing_region_start, + ptrdiff_t composing_region_end) +{ + jobject object; + + object = android_resolve_handle (window, ANDROID_HANDLE_WINDOW); + + (*android_java_env)->CallVoidMethod (android_java_env, + emacs_service, + service_class.update_ic, + object, + (jint) selection_start, + (jint) selection_end, + (jint) composing_region_start, + (jint) composing_region_end); + android_exception_check (); +} + +/* Reinitialize any ongoing input method connection on WINDOW. + + Any input method that is connected to WINDOW will invalidate its + cache of the buffer contents. + + MODE controls certain aspects of the input method's behavior: + + - If MODE is ANDROID_IC_MODE_NULL, the input method will be + deactivated, and an ASCII only keyboard will be displayed + instead. + + - If MODE is ANDROID_IC_MODE_ACTION, the input method will + edit text normally, but send ``return'' as a key event. + This is useful inside the mini buffer. + + - If MODE is ANDROID_IC_MODE_TEXT, the input method is free + to behave however it wants. */ + +void +android_reset_ic (android_window window, enum android_ic_mode mode) +{ + jobject object; + + object = android_resolve_handle (window, ANDROID_HANDLE_WINDOW); + + (*android_java_env)->CallVoidMethod (android_java_env, + emacs_service, + service_class.reset_ic, + object, (jint) mode); + android_exception_check (); +} + + + #else /* ANDROID_STUBIFY */ /* X emulation functions for Android. */ diff --git a/src/android.h b/src/android.h index 9006f5f34c5..ec4fa33dfc3 100644 --- a/src/android.h +++ b/src/android.h @@ -108,6 +108,16 @@ extern void android_closedir (struct android_dir *); extern Lisp_Object android_browse_url (Lisp_Object); + + +/* Event loop functions. */ + +extern void android_check_query (void); +extern int android_run_in_emacs_thread (void (*) (void *), void *); +extern void android_write_event (union android_event *); + +extern unsigned int event_serial; + #endif /* JNI functions should not be built when Emacs is stubbed out for the diff --git a/src/androidgui.h b/src/androidgui.h index 1f0d34e67aa..25dc6754fff 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -235,6 +235,7 @@ enum android_event_type ANDROID_DEICONIFIED, ANDROID_CONTEXT_MENU, ANDROID_EXPOSE, + ANDROID_INPUT_METHOD, }; struct android_any_event @@ -419,6 +420,52 @@ struct android_menu_event int menu_event_id; }; +enum android_ime_operation + { + ANDROID_IME_COMMIT_TEXT, + ANDROID_IME_DELETE_SURROUNDING_TEXT, + ANDROID_IME_FINISH_COMPOSING_TEXT, + ANDROID_IME_SET_COMPOSING_TEXT, + ANDROID_IME_SET_COMPOSING_REGION, + ANDROID_IME_SET_POINT, + ANDROID_IME_START_BATCH_EDIT, + ANDROID_IME_END_BATCH_EDIT, + }; + +struct android_ime_event +{ + /* Type of the event. */ + enum android_event_type type; + + /* The event serial. */ + unsigned long serial; + + /* The associated window. */ + android_window window; + + /* What operation is being performed. */ + enum android_ime_operation operation; + + /* The details of the operation. START and END provide buffer + indices, and may actually mean ``left'' and ``right''. */ + ptrdiff_t start, end, position; + + /* The number of characters in TEXT. */ + size_t length; + + /* TEXT is either NULL, or a pointer to LENGTH bytes of malloced + UTF-16 encoded text that must be decoded by Emacs. + + POSITION is where point should end up after the text is + committed, relative to TEXT. If POSITION is less than 0, it is + relative to TEXT's start; otherwise, it is relative to its + end. */ + unsigned short *text; + + /* Value to set the counter to after the operation completes. */ + unsigned long counter; +}; + union android_event { enum android_event_type type; @@ -447,6 +494,9 @@ union android_event /* This is only used to transmit selected menu items. */ struct android_menu_event menu; + + /* This is used to dispatch input method editing requests. */ + struct android_ime_event ime; }; enum @@ -463,6 +513,13 @@ enum android_lookup_status ANDROID_LOOKUP_BOTH, }; +enum android_ic_mode + { + ANDROID_IC_MODE_NULL = 0, + ANDROID_IC_MODE_ACTION = 1, + ANDROID_IC_MODE_TEXT = 2, + }; + extern int android_pending (void); extern void android_next_event (union android_event *); @@ -554,6 +611,9 @@ extern void android_sync (void); extern int android_wc_lookup_string (android_key_pressed_event *, wchar_t *, int, int *, enum android_lookup_status *); +extern void android_update_ic (android_window, ptrdiff_t, ptrdiff_t, + ptrdiff_t, ptrdiff_t); +extern void android_reset_ic (android_window, enum android_ic_mode); #endif diff --git a/src/androidterm.c b/src/androidterm.c index a57d5623c66..a44bae954da 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -20,6 +20,9 @@ along with GNU Emacs. If not, see . */ #include #include #include +#include +#include +#include #include "lisp.h" #include "androidterm.h" @@ -28,6 +31,8 @@ along with GNU Emacs. If not, see . */ #include "android.h" #include "buffer.h" #include "window.h" +#include "textconv.h" +#include "coding.h" /* This is a chain of structures for all the X displays currently in use. */ @@ -59,6 +64,12 @@ enum ANDROID_EVENT_DROP, }; +/* Find the frame whose window has the identifier WDESC. + + This is like x_window_to_frame in xterm.c, except that DPYINFO may + be NULL, as there is only at most one Android display, and is only + specified in order to stay consistent with X. */ + static struct frame * android_window_to_frame (struct android_display_info *dpyinfo, android_window wdesc) @@ -73,7 +84,7 @@ android_window_to_frame (struct android_display_info *dpyinfo, { f = XFRAME (frame); - if (!FRAME_ANDROID_P (f) || FRAME_DISPLAY_INFO (f) != dpyinfo) + if (!FRAME_ANDROID_P (f)) continue; if (FRAME_ANDROID_WINDOW (f) == wdesc) @@ -527,6 +538,103 @@ android_find_tool (struct frame *f, int pointer_id) return NULL; } +/* Decode STRING, an array of N little endian UTF-16 characters, into + a Lisp string. Return Qnil if the string is too large, and the + encoded string otherwise. */ + +static Lisp_Object +android_decode_utf16 (unsigned short *utf16, size_t n) +{ + struct coding_system coding; + ptrdiff_t size; + + if (INT_MULTIPLY_WRAPV (n, sizeof *utf16, &size)) + return Qnil; + + /* Set up the coding system. Decoding a UTF-16 string (with no BOM) + should not signal. */ + + memset (&coding, 0, sizeof coding); + + setup_coding_system (Qutf_16le, &coding); + coding.source = (const unsigned char *) utf16; + decode_coding_object (&coding, Qnil, 0, 0, size, + size, Qt); + + return coding.dst_object; +} + +/* Handle a single input method event EVENT, delivered to the frame + F. + + Perform the text conversion action specified inside. */ + +static void +android_handle_ime_event (union android_event *event, struct frame *f) +{ + Lisp_Object text; + + /* First, decode the text if necessary. */ + + switch (event->ime.operation) + { + case ANDROID_IME_COMMIT_TEXT: + case ANDROID_IME_FINISH_COMPOSING_TEXT: + case ANDROID_IME_SET_COMPOSING_TEXT: + text = android_decode_utf16 (event->ime.text, + event->ime.length); + xfree (event->ime.text); + break; + + default: + break; + } + + /* Finally, perform the appropriate conversion action. */ + + switch (event->ime.operation) + { + case ANDROID_IME_COMMIT_TEXT: + commit_text (f, text, event->ime.position, + event->ime.counter); + break; + + case ANDROID_IME_DELETE_SURROUNDING_TEXT: + delete_surrounding_text (f, event->ime.start, + event->ime.end, + event->ime.counter); + break; + + case ANDROID_IME_FINISH_COMPOSING_TEXT: + finish_composing_text (f, event->ime.counter); + break; + + case ANDROID_IME_SET_COMPOSING_TEXT: + set_composing_text (f, text, event->ime.position, + event->ime.counter); + break; + + case ANDROID_IME_SET_COMPOSING_REGION: + set_composing_region (f, event->ime.start, + event->ime.end, + event->ime.counter); + break; + + case ANDROID_IME_SET_POINT: + textconv_set_point (f, event->ime.position, + event->ime.counter); + break; + + case ANDROID_IME_START_BATCH_EDIT: + start_batch_edit (f, event->ime.counter); + break; + + case ANDROID_IME_END_BATCH_EDIT: + end_batch_edit (f, event->ime.counter); + break; + } +} + static int handle_one_android_event (struct android_display_info *dpyinfo, union android_event *event, int *finish, @@ -745,6 +853,17 @@ handle_one_android_event (struct android_display_info *dpyinfo, case ANDROID_WINDOW_ACTION: + /* This is a special event sent by android_run_in_emacs_thread + used to make Android run stuff. */ + + if (!event->xaction.window && !event->xaction.action) + { + /* Check for and run anything the UI thread wants to run on the main + thread. */ + android_check_query (); + goto OTHER; + } + f = any; if (event->xaction.action == 0) @@ -1334,6 +1453,19 @@ handle_one_android_event (struct android_display_info *dpyinfo, goto OTHER; + /* Input method events. textconv.c functions are called here to + queue events, which are then executed in a safe context + inside keyboard.c. */ + case ANDROID_INPUT_METHOD: + + if (!any) + /* Free any text allocated for this event. */ + xfree (event->ime.text); + else + android_handle_ime_event (event, any); + + goto OTHER; + default: goto OTHER; } @@ -4148,6 +4280,904 @@ android_draw_window_divider (struct window *w, int x0, int x1, int y0, int y1) +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wmissing-prototypes" +#else +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-prototypes" +#endif + +/* Input method related functions. Some of these are called from Java + within the UI thread. */ + +/* A counter used to decide when an editing request completes. */ +static unsigned long edit_counter; + +/* The last counter known to have completed. */ +static unsigned long last_edit_counter; + +/* Semaphore posted every time the counter increases. */ +static sem_t edit_sem; + +/* Try to synchronize with the UI thread, waiting a certain amount of + time for outstanding editing requests to complete. + + Every time one of the text retrieval functions is called and an + editing request is made, Emacs gives the main thread approximately + 50 ms to process it, in order to mostly keep the input method in + sync with the buffer contents. */ + +static void +android_sync_edit (void) +{ + struct timespec start, end, rem; + + if (__atomic_load_n (&last_edit_counter, + __ATOMIC_SEQ_CST) + == edit_counter) + return; + + start = current_timespec (); + end = timespec_add (start, make_timespec (0, 50000000)); + + while (true) + { + rem = timespec_sub (end, current_timespec ()); + + /* Timeout. */ + if (timespec_sign (rem) < 0) + break; + + if (__atomic_load_n (&last_edit_counter, + __ATOMIC_SEQ_CST) + == edit_counter) + break; + + sem_timedwait (&edit_sem, &end); + } +} + +/* Return a copy of the specified Java string and its length in + *LENGTH. Use the JNI environment ENV. Value is NULL if copying + *the string fails. */ + +static unsigned short * +android_copy_java_string (JNIEnv *env, jstring string, size_t *length) +{ + jsize size, i; + const jchar *java; + unsigned short *buffer; + + size = (*env)->GetStringLength (env, string); + buffer = malloc (size * sizeof *buffer); + + if (!buffer) + return NULL; + + java = (*env)->GetStringChars (env, string, NULL); + + if (!java) + { + free (buffer); + return NULL; + } + + for (i = 0; i < size; ++i) + buffer[i] = java[i]; + + *length = size; + (*env)->ReleaseStringChars (env, string, java); + return buffer; +} + +JNIEXPORT void JNICALL +NATIVE_NAME (beginBatchEdit) (JNIEnv *env, jobject object, jshort window) +{ + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_START_BATCH_EDIT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = 0; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (endBatchEdit) (JNIEnv *env, jobject object, jshort window) +{ + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_END_BATCH_EDIT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = 0; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (commitCompletion) (JNIEnv *env, jobject object, jshort window, + jstring completion_text, jint position) +{ + union android_event event; + unsigned short *text; + size_t length; + + /* First, obtain a copy of the Java string. */ + text = android_copy_java_string (env, completion_text, &length); + + if (!text) + return; + + /* Next, populate the event. Events will always eventually be + delivered on Android, so handle_one_android_event can be relied + on to free text. */ + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_COMMIT_TEXT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = min (length, PTRDIFF_MAX); + event.ime.position = position; + event.ime.text = text; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (commitText) (JNIEnv *env, jobject object, jshort window, + jstring commit_text, jint position) +{ + union android_event event; + unsigned short *text; + size_t length; + + /* First, obtain a copy of the Java string. */ + text = android_copy_java_string (env, commit_text, &length); + + if (!text) + return; + + /* Next, populate the event. Events will always eventually be + delivered on Android, so handle_one_android_event can be relied + on to free text. */ + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_COMMIT_TEXT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = min (length, PTRDIFF_MAX); + event.ime.position = position; + event.ime.text = text; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (deleteSurroundingText) (JNIEnv *env, jobject object, + jshort window, jint left_length, + jint right_length) +{ + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_DELETE_SURROUNDING_TEXT; + event.ime.start = left_length; + event.ime.end = right_length; + event.ime.length = 0; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (finishComposingText) (JNIEnv *env, jobject object, + jshort window) +{ + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_FINISH_COMPOSING_TEXT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = 0; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +/* Structure describing the context used for a text query. */ + +struct android_conversion_query_context +{ + /* The conversion request. */ + struct textconv_callback_struct query; + + /* The window the request is being made on. */ + android_window window; + + /* Whether or not the request was successful. */ + bool success; +}; + +/* Obtain the text from the frame whose window is that specified in + DATA using the text conversion query specified there. + + Adjust the query position to skip over any active composing region. + + Set ((struct android_conversion_query_context *) DATA)->success on + success. */ + +static void +android_perform_conversion_query (void *data) +{ + struct android_conversion_query_context *context; + struct frame *f; + + context = data; + + /* Find the frame associated with the window. */ + f = android_window_to_frame (NULL, context->window); + + if (!f) + return; + + textconv_query (f, &context->query, + TEXTCONV_SKIP_CONVERSION_REGION); + + /* context->query.text will have been set even if textconv_query + returns 1. */ + + context->success = true; +} + +/* Convert a string BUFFERS containing N characters in Emacs's + internal multibyte encoding to a Java string utilizing the + specified JNI environment. + + If N is equal to BYTES, then BUFFER is a single byte buffer. + Otherwise, BUFFER is a multibyte buffer. + + Make sure N and BYTES are absolutely correct, or you are asking for + trouble. + + Value is the string upon success, NULL otherwise. Any exceptions + generated are not cleared. */ + +static jstring +android_text_to_string (JNIEnv *env, char *buffer, ptrdiff_t n, + ptrdiff_t bytes) +{ + jchar *utf16; + size_t size, index; + jstring string; + int encoded; + + if (n == bytes) + { + /* This buffer holds no multibyte characters. */ + + if (INT_MULTIPLY_WRAPV (n, sizeof *utf16, &size)) + return NULL; + + utf16 = malloc (size); + index = 0; + + if (!utf16) + return NULL; + + while (n--) + { + utf16[index] = buffer[index]; + index++; + } + + string = (*env)->NewString (env, utf16, bytes); + free (utf16); + + return string; + } + + /* Allocate enough to hold N characters. */ + + if (INT_MULTIPLY_WRAPV (n, sizeof *utf16, &size)) + return NULL; + + utf16 = malloc (size); + index = 0; + + if (!utf16) + return NULL; + + while (n--) + { + eassert (CHAR_HEAD_P (*buffer)); + encoded = STRING_CHAR ((unsigned char *) buffer); + + /* Now figure out how to save ENCODED into the string. + Emacs operates on multibyte characters, not UTF-16 + characters with surrogate pairs as Android does. + + However, character positions in Java are represented in 2 + byte units, meaning that the text position reported to + Android can become out of sync if characters are found in a + buffer that require surrogate pairs. + + The hack used by Emacs is to simply replace each multibyte + character that doesn't fit in a jchar with the NULL + character. */ + + if (encoded >= 65536) + encoded = 0; + + utf16[index++] = encoded; + buffer += BYTES_BY_CHAR_HEAD (*buffer); + } + + /* Create the string. */ + string = (*env)->NewString (env, utf16, index); + free (utf16); + return string; +} + +JNIEXPORT jstring JNICALL +NATIVE_NAME (getSelectedText) (JNIEnv *env, jobject object, + jshort window) +{ + return NULL; +} + +JNIEXPORT jstring JNICALL +NATIVE_NAME (getTextAfterCursor) (JNIEnv *env, jobject object, jshort window, + jint length, jint flags) +{ + struct android_conversion_query_context context; + jstring string; + + /* First, set up the conversion query. */ + context.query.position = 0; + context.query.direction = TEXTCONV_FORWARD_CHAR; + context.query.factor = min (length, 65535); + context.query.operation = TEXTCONV_RETRIEVAL; + + /* Next, set the rest of the context. */ + context.window = window; + context.success = false; + + /* Now try to perform the query. */ + android_sync_edit (); + if (android_run_in_emacs_thread (android_perform_conversion_query, + &context)) + return NULL; + + if (!context.success) + return NULL; + + /* context->query.text now contains the text in Emacs's internal + UTF-8 based encoding. + + Convert it to Java's UTF-16 encoding, which is the same as + UTF-16, except that NULL bytes are encoded as surrogate pairs. + + This assumes that `free' can free data allocated with xmalloc. */ + + string = android_text_to_string (env, context.query.text.text, + context.query.text.length, + context.query.text.bytes); + free (context.query.text.text); + + return string; +} + +JNIEXPORT jstring JNICALL +NATIVE_NAME (getTextBeforeCursor) (JNIEnv *env, jobject object, jshort window, + jint length, jint flags) +{ + struct android_conversion_query_context context; + jstring string; + + /* First, set up the conversion query. */ + context.query.position = 0; + context.query.direction = TEXTCONV_BACKWARD_CHAR; + context.query.factor = min (length, 65535); + context.query.operation = TEXTCONV_RETRIEVAL; + + /* Next, set the rest of the context. */ + context.window = window; + context.success = false; + + /* Now try to perform the query. */ + android_sync_edit (); + if (android_run_in_emacs_thread (android_perform_conversion_query, + &context)) + return NULL; + + if (!context.success) + return NULL; + + /* context->query.text now contains the text in Emacs's internal + UTF-8 based encoding. + + Convert it to Java's UTF-16 encoding, which is the same as + UTF-16, except that NULL bytes are encoded as surrogate pairs. + + This assumes that `free' can free data allocated with xmalloc. */ + + string = android_text_to_string (env, context.query.text.text, + context.query.text.length, + context.query.text.bytes); + free (context.query.text.text); + + return string; +} + +JNIEXPORT void JNICALL +NATIVE_NAME (setComposingText) (JNIEnv *env, jobject object, jshort window, + jstring composing_text, + jint new_cursor_position) +{ + union android_event event; + unsigned short *text; + size_t length; + + /* First, obtain a copy of the Java string. */ + text = android_copy_java_string (env, composing_text, &length); + + if (!text) + return; + + /* Next, populate the event. Events will always eventually be + delivered on Android, so handle_one_android_event can be relied + on to free text. */ + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_SET_COMPOSING_TEXT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = min (length, PTRDIFF_MAX); + event.ime.position = new_cursor_position; + event.ime.text = text; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (setComposingRegion) (JNIEnv *env, jobject object, jshort window, + jint start, jint end) +{ + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_SET_COMPOSING_REGION; + event.ime.start = start; + event.ime.end = end; + event.ime.length = 0; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (setSelection) (JNIEnv *env, jobject object, jshort window, + jint start, jint end) +{ + union android_event event; + + /* While IMEs want access to the entire selection, Emacs only + supports setting the point. */ + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_SET_POINT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = 0; + event.ime.position = start; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; +} + +/* Structure describing the context for `getSelection'. */ + +struct android_get_selection_context +{ + /* The window in question. */ + android_window window; + + /* The position of the window's point when it was last + redisplayed. */ + ptrdiff_t point; +}; + +/* Function run on the main thread by `getSelection'. + Place the character position of point in PT. */ + +static void +android_get_selection (void *data) +{ + struct android_get_selection_context *context; + struct frame *f; + struct window *w; + + context = data; + + /* Look up the associated frame and its selected window. */ + f = android_window_to_frame (NULL, context->window); + + if (!f) + context->point = -1; + else + { + w = XWINDOW (f->selected_window); + + /* Return W's point at the time of the last redisplay. This is + rather important to keep the input method consistent with the + contents of the display. */ + context->point = w->last_point; + } +} + +JNIEXPORT jint JNICALL +NATIVE_NAME (getSelection) (JNIEnv *env, jobject object, jshort window) +{ + struct android_get_selection_context context; + + context.window = window; + + android_sync_edit (); + if (android_run_in_emacs_thread (android_get_selection, + &context)) + return -1; + + if (context.point == -1) + return -1; + + return min (context.point, TYPE_MAXIMUM (jint)); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (performEditorAction) (JNIEnv *env, jobject object, + jshort window, int action) +{ + union android_event event; + + /* Undocumented behavior: performEditorAction is apparently expected + to finish composing any text. */ + + NATIVE_NAME (finishComposingText) (env, object, window); + + event.xkey.type = ANDROID_KEY_PRESS; + event.xkey.serial = ++event_serial; + event.xkey.window = window; + event.xkey.time = 0; + event.xkey.state = 0; + event.xkey.keycode = 66; + event.xkey.unicode_char = 0; + + android_write_event (&event); +} + +struct android_get_extracted_text_context +{ + /* The parameters of the request. */ + int hint_max_chars; + + /* Token for the request. */ + int token; + + /* The returned text, or NULL. */ + char *text; + + /* The size of that text in characters and bytes. */ + ptrdiff_t length, bytes; + + /* Offsets into that text. */ + ptrdiff_t start, offset; + + /* The window. */ + android_window window; +}; + +/* Return the extracted text in the extracted text context specified + by DATA. */ + +static void +android_get_extracted_text (void *data) +{ + struct android_get_extracted_text_context *request; + struct frame *f; + + request = data; + + /* Find the frame associated with the window. */ + f = android_window_to_frame (NULL, request->window); + + if (!f) + return; + + /* Now get the extracted text. */ + request->text + = get_extracted_text (f, min (request->hint_max_chars, 600), + &request->start, &request->offset, + &request->length, &request->bytes); +} + +/* Structure describing the `ExtractedTextRequest' class. + Valid only on the UI thread. */ + +struct android_extracted_text_request_class +{ + bool initialized; + jfieldID hint_max_chars; + jfieldID token; +}; + +/* Structure describing the `ExtractedText' class. + Valid only on the UI thread. */ + +struct android_extracted_text_class +{ + jclass class; + jmethodID constructor; + jfieldID partial_start_offset; + jfieldID partial_end_offset; + jfieldID selection_start; + jfieldID selection_end; + jfieldID start_offset; + jfieldID text; +}; + +JNIEXPORT jobject JNICALL +NATIVE_NAME (getExtractedText) (JNIEnv *env, jobject ignored_object, + jshort window, jobject request, + jint flags) +{ + struct android_get_extracted_text_context context; + static struct android_extracted_text_request_class request_class; + static struct android_extracted_text_class text_class; + jstring string; + jclass class; + jobject object; + + /* TODO: report changes to extracted text. */ + + /* Initialize both classes if necessary. */ + + if (!request_class.initialized) + { + class + = (*env)->FindClass (env, ("android/view/inputmethod" + "/ExtractedTextRequest")); + assert (class); + + request_class.hint_max_chars + = (*env)->GetFieldID (env, class, "hintMaxChars", "I"); + assert (request_class.hint_max_chars); + + request_class.token + = (*env)->GetFieldID (env, class, "token", "I"); + assert (request_class.token); + + request_class.initialized = true; + } + + if (!text_class.class) + { + text_class.class + = (*env)->FindClass (env, ("android/view/inputmethod" + "/ExtractedText")); + assert (text_class.class); + + class + = text_class.class + = (*env)->NewGlobalRef (env, text_class.class); + assert (text_class.class); + + text_class.partial_start_offset + = (*env)->GetFieldID (env, class, "partialStartOffset", "I"); + text_class.partial_end_offset + = (*env)->GetFieldID (env, class, "partialEndOffset", "I"); + text_class.selection_start + = (*env)->GetFieldID (env, class, "selectionStart", "I"); + text_class.selection_end + = (*env)->GetFieldID (env, class, "selectionEnd", "I"); + text_class.start_offset + = (*env)->GetFieldID (env, class, "startOffset", "I"); + text_class.text + = (*env)->GetFieldID (env, class, "text", "Ljava/lang/CharSequence;"); + text_class.constructor + = (*env)->GetMethodID (env, class, "", "()V"); + } + + context.hint_max_chars + = (*env)->GetIntField (env, request, request_class.hint_max_chars); + context.token + = (*env)->GetIntField (env, request, request_class.token); + context.text = NULL; + context.window = window; + + android_sync_edit (); + if (android_run_in_emacs_thread (android_get_extracted_text, + &context)) + return NULL; + + if (!context.text) + return NULL; + + /* Encode the returned text. */ + string = android_text_to_string (env, context.text, context.length, + context.bytes); + free (context.text); + + if (!string) + return NULL; + + /* Create an ExtractedText object containing this information. */ + object = (*android_java_env)->NewObject (env, text_class.class, + text_class.constructor); + if (!object) + return NULL; + + (*env)->SetIntField (env, object, text_class.partial_start_offset, -1); + (*env)->SetIntField (env, object, text_class.partial_end_offset, -1); + (*env)->SetIntField (env, object, text_class.selection_start, + min (context.offset, TYPE_MAXIMUM (jint))); + (*env)->SetIntField (env, object, text_class.selection_end, + min (context.offset, TYPE_MAXIMUM (jint))); + (*env)->SetIntField (env, object, text_class.start_offset, + min (context.start, TYPE_MAXIMUM (jint))); + (*env)->SetObjectField (env, object, text_class.text, string); + return object; +} + +#ifdef __clang__ +#pragma clang diagnostic pop +#else +#pragma GCC diagnostic pop +#endif + + + +/* Tell the input method where the composing region and selection of + F's selected window is located. W should be F's selected window; + if it is NULL, then F->selected_window is used in its place. */ + +static void +android_update_selection (struct frame *f, struct window *w) +{ + ptrdiff_t start, end, point; + + if (MARKERP (f->conversion.compose_region_start)) + { + eassert (MARKERP (f->conversion.compose_region_end)); + + start = marker_position (f->conversion.compose_region_start); + end = marker_position (f->conversion.compose_region_end); + } + else + start = -1, end = -1; + + /* Now constrain START and END to the maximium size of a Java + integer. */ + start = min (start, TYPE_MAXIMUM (jint)); + end = min (end, TYPE_MAXIMUM (jint)); + + if (!w) + w = XWINDOW (f->selected_window); + + /* Figure out where the point is. */ + point = min (w->last_point, TYPE_MAXIMUM (jint)); + + /* Send the update. */ + android_update_ic (FRAME_ANDROID_WINDOW (f), point, point, + start, end); +} + +/* Notice that the input method connection to F should be reset as a + result of a change to its contents. */ + +static void +android_reset_conversion (struct frame *f) +{ + /* Reset the input method. + + Pick an appropriate ``input mode'' based on whether or not the + minibuffer window is selected; this controls whether or not + ``RET'' inserts a newline or sends an actual key event. */ + android_reset_ic (FRAME_ANDROID_WINDOW (f), + (EQ (f->selected_window, + f->minibuffer_window) + ? ANDROID_IC_MODE_ACTION + : ANDROID_IC_MODE_TEXT)); + + /* Move its selection to the specified position. */ + android_update_selection (f, NULL); +} + +/* Notice that point has moved in the F's selected window's selected + buffer. W is the window, and BUFFER is that buffer. */ + +static void +android_set_point (struct frame *f, struct window *w, + struct buffer *buffer) +{ + android_update_selection (f, w); +} + +/* Notice that the composition region on F's old selected window has + changed. */ + +static void +android_compose_region_changed (struct frame *f) +{ + android_update_selection (f, XWINDOW (f->old_selected_window)); +} + +/* Notice that the text conversion has completed. */ + +static void +android_notify_conversion (unsigned long counter) +{ + int sval; + + if (last_edit_counter < counter) + __atomic_store_n (&last_edit_counter, counter, + __ATOMIC_SEQ_CST); + + sem_getvalue (&edit_sem, &sval); + + if (sval <= 0) + sem_post (&edit_sem); +} + +/* Android text conversion interface. */ + +static struct textconv_interface text_conversion_interface = + { + android_reset_conversion, + android_set_point, + android_compose_region_changed, + android_notify_conversion, + }; + + + extern frame_parm_handler android_frame_parm_handlers[]; #endif /* !ANDROID_STUBIFY */ @@ -4327,6 +5357,11 @@ android_term_init (void) /* Set the baud rate to the same value it gets set to on X. */ baud_rate = 19200; + +#ifndef ANDROID_STUBIFY + sem_init (&edit_sem, false, 0); + register_textconv_interface (&text_conversion_interface); +#endif } diff --git a/src/coding.c b/src/coding.c index be5a0252ede..3ac4ada2939 100644 --- a/src/coding.c +++ b/src/coding.c @@ -11759,7 +11759,7 @@ syms_of_coding (void) DEFSYM (Qutf_8_unix, "utf-8-unix"); DEFSYM (Qutf_8_emacs, "utf-8-emacs"); -#if defined (WINDOWSNT) || defined (CYGWIN) +#if defined (WINDOWSNT) || defined (CYGWIN) || defined HAVE_ANDROID /* No, not utf-16-le: that one has a BOM. */ DEFSYM (Qutf_16le, "utf-16le"); #endif diff --git a/src/frame.c b/src/frame.c index a1d73d0799d..874e8c4cac1 100644 --- a/src/frame.c +++ b/src/frame.c @@ -997,6 +997,16 @@ make_frame (bool mini_p) f->select_mini_window_flag = false; /* This one should never be zero. */ f->change_stamp = 1; + +#ifdef HAVE_TEXT_CONVERSION + f->conversion.compose_region_start = Qnil; + f->conversion.compose_region_end = Qnil; + f->conversion.compose_region_overlay = Qnil; + f->conversion.batch_edit_count = 0; + f->conversion.batch_edit_flags = 0; + f->conversion.actions = NULL; +#endif + root_window = make_window (); rw = XWINDOW (root_window); if (mini_p) @@ -2264,6 +2274,13 @@ delete_frame (Lisp_Object frame, Lisp_Object force) f->terminal = 0; /* Now the frame is dead. */ unblock_input (); + /* Clear markers and overlays set by F on behalf of an input + method. */ +#ifdef HAVE_TEXT_CONVERSION + if (FRAME_WINDOW_P (f)) + reset_frame_state (f); +#endif + /* If needed, delete the terminal that this frame was on. (This must be done after the frame is killed.) */ terminal->reference_count--; diff --git a/src/frame.h b/src/frame.h index 4405c2df860..27484936ff1 100644 --- a/src/frame.h +++ b/src/frame.h @@ -76,6 +76,63 @@ enum ns_appearance_type #endif #endif /* HAVE_WINDOW_SYSTEM */ +#ifdef HAVE_TEXT_CONVERSION + +enum text_conversion_operation + { + TEXTCONV_START_BATCH_EDIT, + TEXTCONV_END_BATCH_EDIT, + TEXTCONV_COMMIT_TEXT, + TEXTCONV_FINISH_COMPOSING_TEXT, + TEXTCONV_SET_COMPOSING_TEXT, + TEXTCONV_SET_COMPOSING_REGION, + TEXTCONV_SET_POINT, + TEXTCONV_DELETE_SURROUNDING_TEXT, + }; + +/* Structure describing a single edit being performed by the input + method that should be executed in the context of + kbd_buffer_get_event. */ + +struct text_conversion_action +{ + /* The next text conversion action. */ + struct text_conversion_action *next; + + /* Any associated data. */ + Lisp_Object data; + + /* The operation being performed. */ + enum text_conversion_operation operation; + + /* Counter value. */ + unsigned long counter; +}; + +/* Structure describing the text conversion state associated with a + frame. */ + +struct text_conversion_state +{ + /* List of text conversion actions associated with this frame. */ + struct text_conversion_action *actions; + + /* Markers representing the composing region. */ + Lisp_Object compose_region_start, compose_region_end; + + /* Overlay representing the composing region. */ + Lisp_Object compose_region_overlay; + + /* The number of ongoing ``batch edits'' that are causing point + reporting to be delayed. */ + int batch_edit_count; + + /* Mask containing what must be updated after batch edits end. */ + int batch_edit_flags; +}; + +#endif + /* The structure representing a frame. */ struct frame @@ -664,6 +721,11 @@ struct frame enum ns_appearance_type ns_appearance; bool_bf ns_transparent_titlebar; #endif + +#ifdef HAVE_TEXT_CONVERSION + /* Text conversion state used by certain input methods. */ + struct text_conversion_state conversion; +#endif } GCALIGNED_STRUCT; /* Most code should use these functions to set Lisp fields in struct frame. */ diff --git a/src/keyboard.c b/src/keyboard.c index 11fa1e62c89..538fdffc199 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -44,6 +44,11 @@ along with GNU Emacs. If not, see . */ #include "atimer.h" #include "process.h" #include "menu.h" + +#ifdef HAVE_TEXT_CONVERSION +#include "textconv.h" +#endif + #include #ifdef HAVE_PTHREAD @@ -3020,6 +3025,10 @@ read_char (int commandflag, Lisp_Object map, { struct buffer *prev_buffer = current_buffer; last_input_event = c; + + /* All a `text-conversion' event does is prevent Emacs from + staying idle. It is not useful. */ + call4 (Qcommand_execute, tem, Qnil, Fvector (1, &last_input_event), Qt); if (CONSP (c) && !NILP (Fmemq (XCAR (c), Vwhile_no_input_ignore_events)) @@ -3582,6 +3591,11 @@ readable_events (int flags) return 1; #endif +#ifdef HAVE_TEXT_CONVERSION + if (detect_conversion_events ()) + return 1; +#endif + if (!(flags & READABLE_EVENTS_IGNORE_SQUEEZABLES) && some_mouse_moved ()) return 1; if (single_kboard) @@ -3914,6 +3928,11 @@ kbd_buffer_get_event (KBOARD **kbp, had_pending_selection_requests = false; #endif +#ifdef HAVE_TEXT_CONVERSION + bool had_pending_conversion_events; + + had_pending_conversion_events = false; +#endif #ifdef subprocesses if (kbd_on_hold_p () && kbd_buffer_nr_stored () < KBD_BUFFER_SIZE / 4) @@ -3977,6 +3996,13 @@ kbd_buffer_get_event (KBOARD **kbp, had_pending_selection_requests = true; break; } +#endif +#ifdef HAVE_TEXT_CONVERSION + if (detect_conversion_events ()) + { + had_pending_conversion_events = true; + break; + } #endif if (end_time) { @@ -4024,6 +4050,14 @@ kbd_buffer_get_event (KBOARD **kbp, x_handle_pending_selection_requests (); #endif +#ifdef HAVE_TEXT_CONVERSION + /* Handle pending ``text conversion'' requests from an input + method. */ + + if (had_pending_conversion_events) + handle_pending_conversion_events (); +#endif + if (CONSP (Vunread_command_events)) { Lisp_Object first; @@ -4380,12 +4414,25 @@ kbd_buffer_get_event (KBOARD **kbp, #ifdef HAVE_X_WINDOWS else if (had_pending_selection_requests) obj = Qnil; +#endif +#ifdef HAVE_TEXT_CONVERSION + /* This is an internal event used to prevent Emacs from becoming + idle immediately after a text conversion operation. */ + else if (had_pending_conversion_events) + obj = Qtext_conversion; #endif else /* We were promised by the above while loop that there was something for us to read! */ emacs_abort (); +#ifdef HAVE_TEXT_CONVERSION + /* While not implemented as keyboard commands, changes made by the + input method still mean that Emacs is no longer idle. */ + if (had_pending_conversion_events) + timer_stop_idle (); +#endif + input_pending = readable_events (0); Vlast_event_frame = internal_last_event_frame; @@ -12902,6 +12949,9 @@ See also `pre-command-hook'. */); DEFSYM (Qcoding, "coding"); DEFSYM (Qtouchscreen, "touchscreen"); +#ifdef HAVE_TEXT_CONVERSION + DEFSYM (Qtext_conversion, "text-conversion"); +#endif Fset (Qecho_area_clear_hook, Qnil); diff --git a/src/lisp.h b/src/lisp.h index 894b939b7b9..2dc51d32481 100644 --- a/src/lisp.h +++ b/src/lisp.h @@ -5230,7 +5230,10 @@ extern char *emacs_root_dir (void); #ifdef HAVE_TEXT_CONVERSION /* Defined in textconv.c. */ +extern void reset_frame_state (struct frame *); extern void report_selected_window_change (struct frame *); +extern void report_point_change (struct frame *, struct window *, + struct buffer *); #endif #ifdef HAVE_NATIVE_COMP diff --git a/src/process.c b/src/process.c index 9a8b0d7fd85..e7ccb2c604e 100644 --- a/src/process.c +++ b/src/process.c @@ -121,6 +121,7 @@ static struct rlimit nofile_limit; #ifdef HAVE_ANDROID #include "android.h" +#include "androidterm.h" #endif #ifdef HAVE_WINDOW_SYSTEM diff --git a/src/textconv.c b/src/textconv.c index d5db6d11717..b62ed7d1365 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -25,7 +25,10 @@ along with GNU Emacs. If not, see . */ ability to ``undo'' or ``edit'' previously composed text. This is most commonly seen in input methods for CJK laguages for X Windows, and is extensively used throughout Android by input methods for all - kinds of scripts. */ + kinds of scripts. + + In addition, these input methods may also need to make detailed + edits to the content of a buffer. That is also handled here. */ #include @@ -44,6 +47,15 @@ along with GNU Emacs. If not, see . */ static struct textconv_interface *text_interface; +/* Flags used to determine what must be sent after a batch edit + ends. */ + +enum textconv_batch_edit_flags + { + PENDING_POINT_CHANGE = 1, + PENDING_COMPOSE_CHANGE = 2, + }; + /* Copy the portion of the current buffer described by BEG, BEG_BYTE, @@ -94,14 +106,18 @@ copy_buffer (ptrdiff_t beg, ptrdiff_t beg_byte, Then, either delete that text from the buffer if QUERY->operation is TEXTCONV_SUBSTITUTION, or return 0. + If FLAGS & TEXTCONV_SKIP_CONVERSION_REGION, then first move PT past + the conversion region in the specified direction if it is inside. + Value is 0 if QUERY->operation was not TEXTCONV_SUBSTITUTION or if deleting the text was successful, and 1 otherwise. */ int -textconv_query (struct frame *f, struct textconv_callback_struct *query) +textconv_query (struct frame *f, struct textconv_callback_struct *query, + int flags) { specpdl_ref count; - ptrdiff_t pos, pos_byte, end, end_byte; + ptrdiff_t pos, pos_byte, end, end_byte, start; ptrdiff_t temp, temp1; char *buffer; @@ -113,14 +129,46 @@ textconv_query (struct frame *f, struct textconv_callback_struct *query) /* Inhibit quitting. */ specbind (Qinhibit_quit, Qt); - /* Temporarily switch to F's selected window. */ - Fselect_window (f->selected_window, Qt); + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window ((WINDOW_LIVE_P (f->old_selected_window) + ? f->old_selected_window + : f->selected_window), Qt); /* Now find the appropriate text bounds for QUERY. First, move point QUERY->position steps forward or backwards. */ pos = PT; + /* Next, if POS lies within the conversion region and the caller + asked for it to be moved away, move it away from the conversion + region. */ + + if (flags & TEXTCONV_SKIP_CONVERSION_REGION + && MARKERP (f->conversion.compose_region_start)) + { + start = marker_position (f->conversion.compose_region_start); + end = marker_position (f->conversion.compose_region_end); + + if (pos >= start && pos < end) + { + switch (query->direction) + { + case TEXTCONV_FORWARD_CHAR: + case TEXTCONV_FORWARD_WORD: + case TEXTCONV_CARET_DOWN: + case TEXTCONV_NEXT_LINE: + case TEXTCONV_LINE_START: + pos = end; + break; + + default: + pos = max (BEGV, start - 1); + break; + } + } + } + /* If pos is outside the accessible part of the buffer or if it overflows, move back to point or to the extremes of the accessible region. */ @@ -287,6 +335,828 @@ textconv_query (struct frame *f, struct textconv_callback_struct *query) return 0; } +/* Reset F's text conversion state. Delete any overlays or + markers inside. */ + +void +reset_frame_state (struct frame *f) +{ + struct text_conversion_action *last, *next; + + /* Make the composition region markers point elsewhere. */ + + if (!NILP (f->conversion.compose_region_start)) + { + Fset_marker (f->conversion.compose_region_start, Qnil, Qnil); + Fset_marker (f->conversion.compose_region_end, Qnil, Qnil); + f->conversion.compose_region_start = Qnil; + f->conversion.compose_region_end = Qnil; + } + + /* Delete the composition region overlay. */ + + if (!NILP (f->conversion.compose_region_overlay)) + Fdelete_overlay (f->conversion.compose_region_overlay); + + /* Delete each text conversion action queued up. */ + + next = f->conversion.actions; + while (next) + { + last = next; + next = next->next; + + /* Say that the conversion is finished. */ + if (text_interface && text_interface->notify_conversion) + text_interface->notify_conversion (last->counter); + + xfree (last); + } + f->conversion.actions = NULL; + + /* Clear batch edit state. */ + f->conversion.batch_edit_count = 0; + f->conversion.batch_edit_flags = 0; +} + +/* Return whether or not there are pending edits from an input method + on any frame. */ + +bool +detect_conversion_events (void) +{ + Lisp_Object tail, frame; + + FOR_EACH_FRAME (tail, frame) + { + if (XFRAME (frame)->conversion.actions) + return true; + } + + return false; +} + +/* Restore the selected window WINDOW. */ + +static void +restore_selected_window (Lisp_Object window) +{ + /* FIXME: not sure what to do if WINDOW has been deleted. */ + Fselect_window (window, Qt); +} + +/* Commit the given text in the composing region. If there is no + composing region, then insert the text after F's selected window's + last point instead. Finally, remove the composing region. */ + +static void +really_commit_text (struct frame *f, EMACS_INT position, + Lisp_Object text) +{ + specpdl_ref count; + ptrdiff_t wanted, start, end; + + /* If F's old selected window is no longer live, fail. */ + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return; + + count = SPECPDL_INDEX (); + record_unwind_protect (restore_selected_window, + selected_window); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window (f->old_selected_window, Qt); + + /* Now detect whether or not there is a composing region. + If there is, then replace it with TEXT. Don't do that + otherwise. */ + + if (MARKERP (f->conversion.compose_region_start)) + { + /* Replace its contents. */ + start = marker_position (f->conversion.compose_region_start); + end = marker_position (f->conversion.compose_region_end); + safe_del_range (start, end); + Finsert (1, &text); + + /* Move to a the position specified in POSITION. */ + + if (position < 0) + { + wanted + = marker_position (f->conversion.compose_region_start); + + if (INT_SUBTRACT_WRAPV (wanted, position, &wanted) + || wanted < BEGV) + wanted = BEGV; + + if (wanted > ZV) + wanted = ZV; + + set_point (wanted); + } + else + { + wanted + = marker_position (f->conversion.compose_region_end); + + if (INT_ADD_WRAPV (wanted, position - 1, &wanted) + || wanted > ZV) + wanted = ZV; + + if (wanted < BEGV) + wanted = BEGV; + + set_point (wanted); + } + + /* Make the composition region markers point elsewhere. */ + + if (!NILP (f->conversion.compose_region_start)) + { + Fset_marker (f->conversion.compose_region_start, Qnil, Qnil); + Fset_marker (f->conversion.compose_region_end, Qnil, Qnil); + f->conversion.compose_region_start = Qnil; + f->conversion.compose_region_end = Qnil; + } + + /* Delete the composition region overlay. */ + + if (!NILP (f->conversion.compose_region_overlay)) + Fdelete_overlay (f->conversion.compose_region_overlay); + } + else + { + /* Otherwise, move the text and point to an appropriate + location. */ + wanted = PT; + Finsert (1, &text); + + if (position < 0) + { + if (INT_SUBTRACT_WRAPV (wanted, position, &wanted) + || wanted < BEGV) + wanted = BEGV; + + if (wanted > ZV) + wanted = ZV; + + set_point (wanted); + } + else + { + wanted = PT; + + if (INT_ADD_WRAPV (wanted, position - 1, &wanted) + || wanted > ZV) + wanted = ZV; + + if (wanted < BEGV) + wanted = BEGV; + + set_point (wanted); + } + } + + unbind_to (count, Qnil); +} + +/* Remove the composition region on the frame F, while leaving its + contents intact. */ + +static void +really_finish_composing_text (struct frame *f) +{ + if (!NILP (f->conversion.compose_region_start)) + { + Fset_marker (f->conversion.compose_region_start, Qnil, Qnil); + Fset_marker (f->conversion.compose_region_end, Qnil, Qnil); + f->conversion.compose_region_start = Qnil; + f->conversion.compose_region_end = Qnil; + } + + /* Delete the composition region overlay. */ + + if (!NILP (f->conversion.compose_region_overlay)) + Fdelete_overlay (f->conversion.compose_region_overlay); +} + +/* Set the composing text on F to TEXT. Then, move point to an + appropriate position relative to POSITION, and call + `compose_region_changed' in the text conversion interface should + point not have been changed relative to F's old selected window's + last point. */ + +static void +really_set_composing_text (struct frame *f, ptrdiff_t position, + Lisp_Object text) +{ + specpdl_ref count; + ptrdiff_t start, wanted, end; + struct window *w; + + /* If F's old selected window is no longer live, fail. */ + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return; + + count = SPECPDL_INDEX (); + record_unwind_protect (restore_selected_window, + selected_window); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + w = XWINDOW (f->old_selected_window); + Fselect_window (f->old_selected_window, Qt); + + /* Now set up the composition region if necessary. */ + + if (!MARKERP (f->conversion.compose_region_start)) + { + f->conversion.compose_region_start = Fmake_marker (); + f->conversion.compose_region_end = Fmake_marker (); + Fset_marker (f->conversion.compose_region_start, + Fpoint (), Qnil); + Fset_marker (f->conversion.compose_region_end, + Fpoint (), Qnil); + Fset_marker_insertion_type (f->conversion.compose_region_end, + Qt); + } + else + { + /* Delete the text between the start of the composition region + and its end. TODO: avoid this widening. */ + record_unwind_protect (save_restriction_restore, + save_restriction_save ()); + Fwiden (); + start = marker_position (f->conversion.compose_region_start); + end = marker_position (f->conversion.compose_region_end); + safe_del_range (start, end); + set_point (start); + } + + /* Insert the new text. */ + Finsert (1, &text); + + /* Now move point to an appropriate location. */ + if (position < 0) + { + wanted = start; + + if (INT_SUBTRACT_WRAPV (wanted, position, &wanted) + || wanted < BEGV) + wanted = BEGV; + + if (wanted > ZV) + wanted = ZV; + } + else + { + end = marker_position (f->conversion.compose_region_end); + wanted = end; + + /* end should be PT after the edit. */ + eassert (end == PT); + + if (INT_ADD_WRAPV (wanted, position - 1, &wanted) + || wanted > ZV) + wanted = ZV; + + if (wanted < BEGV) + wanted = BEGV; + } + + set_point (wanted); + + /* If PT hasn't changed, the conversion region definitely has. + Otherwise, redisplay will update the input method instead. */ + + if (PT == w->last_point + && text_interface + && text_interface->compose_region_changed) + { + if (f->conversion.batch_edit_count > 0) + f->conversion.batch_edit_flags |= PENDING_COMPOSE_CHANGE; + else + text_interface->compose_region_changed (f); + } + + unbind_to (count, Qnil); +} + +/* Set the composing region to START by END. Make it that it is not + already set. */ + +static void +really_set_composing_region (struct frame *f, ptrdiff_t start, + ptrdiff_t end) +{ + specpdl_ref count; + + /* If F's old selected window is no longer live, fail. */ + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return; + + /* If MAX (0, start) == end, then this should behave the same as + really_finish_composing_text. */ + + if (max (0, start) == max (0, end)) + { + really_finish_composing_text (f); + return; + } + + count = SPECPDL_INDEX (); + record_unwind_protect (restore_selected_window, + selected_window); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window (f->old_selected_window, Qt); + + /* Now set up the composition region if necessary. */ + + if (!MARKERP (f->conversion.compose_region_start)) + { + f->conversion.compose_region_start = Fmake_marker (); + f->conversion.compose_region_end = Fmake_marker (); + Fset_marker_insertion_type (f->conversion.compose_region_end, + Qt); + } + + Fset_marker (f->conversion.compose_region_start, + make_fixnum (start), Qnil); + Fset_marker (f->conversion.compose_region_end, + make_fixnum (end), Qnil); + + unbind_to (count, Qnil); +} + +/* Delete LEFT and RIGHT chars around point. */ + +static void +really_delete_surrounding_text (struct frame *f, ptrdiff_t left, + ptrdiff_t right) +{ + specpdl_ref count; + ptrdiff_t start, end; + + /* If F's old selected window is no longer live, fail. */ + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return; + + count = SPECPDL_INDEX (); + record_unwind_protect (restore_selected_window, + selected_window); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window (f->old_selected_window, Qt); + + start = max (BEGV, PT - left); + end = min (ZV, PT + right); + + safe_del_range (start, end); + unbind_to (count, Qnil); +} + +/* Set point in F to POSITION. + + If it has not changed, signal an update through the text input + interface, which is necessary for the IME to acknowledge that the + change has completed. */ + +static void +really_set_point (struct frame *f, ptrdiff_t point) +{ + specpdl_ref count; + + /* If F's old selected window is no longer live, fail. */ + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return; + + count = SPECPDL_INDEX (); + record_unwind_protect (restore_selected_window, + selected_window); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window (f->old_selected_window, Qt); + + if (point == PT) + { + if (f->conversion.batch_edit_count > 0) + f->conversion.batch_edit_flags |= PENDING_POINT_CHANGE; + else + text_interface->point_changed (f, + XWINDOW (f->old_selected_window), + current_buffer); + } + else + /* Set the point. */ + Fgoto_char (make_fixnum (point)); + + unbind_to (count, Qnil); +} + +/* Complete the edit specified by the counter value inside *TOKEN. */ + +static void +complete_edit (void *token) +{ + if (text_interface && text_interface->notify_conversion) + text_interface->notify_conversion (*(unsigned long *) token); +} + +/* Process and free the text conversion ACTION. F must be the frame + on which ACTION will be performed. */ + +static void +handle_pending_conversion_events_1 (struct frame *f, + struct text_conversion_action *action) +{ + Lisp_Object data; + enum text_conversion_operation operation; + struct buffer *buffer; + struct window *w; + specpdl_ref count; + unsigned long token; + + /* Next, process this action and free it. */ + + data = action->data; + operation = action->operation; + token = action->counter; + xfree (action); + + /* Make sure completion is signalled. */ + count = SPECPDL_INDEX (); + record_unwind_protect_ptr (complete_edit, &token); + + switch (operation) + { + case TEXTCONV_START_BATCH_EDIT: + f->conversion.batch_edit_count++; + break; + + case TEXTCONV_END_BATCH_EDIT: + if (f->conversion.batch_edit_count > 0) + f->conversion.batch_edit_count--; + + if (!WINDOW_LIVE_P (f->old_selected_window)) + break; + + if (f->conversion.batch_edit_flags & PENDING_POINT_CHANGE) + { + w = XWINDOW (f->old_selected_window); + buffer = XBUFFER (WINDOW_BUFFER (w)); + + text_interface->point_changed (f, w, buffer); + } + + if (f->conversion.batch_edit_flags & PENDING_COMPOSE_CHANGE) + text_interface->compose_region_changed (f); + + f->conversion.batch_edit_flags = 0; + break; + + case TEXTCONV_COMMIT_TEXT: + really_commit_text (f, XFIXNUM (XCAR (data)), XCDR (data)); + break; + + case TEXTCONV_FINISH_COMPOSING_TEXT: + really_finish_composing_text (f); + break; + + case TEXTCONV_SET_COMPOSING_TEXT: + really_set_composing_text (f, XFIXNUM (XCAR (data)), + XCDR (data)); + break; + + case TEXTCONV_SET_COMPOSING_REGION: + really_set_composing_region (f, XFIXNUM (XCAR (data)), + XFIXNUM (XCDR (data))); + break; + + case TEXTCONV_SET_POINT: + really_set_point (f, XFIXNUM (data)); + break; + + case TEXTCONV_DELETE_SURROUNDING_TEXT: + really_delete_surrounding_text (f, XFIXNUM (XCAR (data)), + XFIXNUM (XCDR (data))); + break; + } + + unbind_to (count, Qnil); +} + +/* Process any outstanding text conversion events. + This may run Lisp or signal. */ + +void +handle_pending_conversion_events (void) +{ + struct frame *f; + Lisp_Object tail, frame; + struct text_conversion_action *action, *next; + bool handled; + + handled = false; + + FOR_EACH_FRAME (tail, frame) + { + f = XFRAME (frame); + + /* Test if F has any outstanding conversion events. Then + process them in bottom to up order. */ + for (action = f->conversion.actions; action; action = next) + { + /* Redisplay in between if there is more than one + action. */ + + if (handled) + redisplay (); + + /* Unlink this action. */ + next = action->next; + f->conversion.actions = next; + + /* Handle and free the action. */ + handle_pending_conversion_events_1 (f, action); + handled = true; + } + } +} + +/* Start a ``batch edit'' in F. During a batch edit, point_changed + will not be called until the batch edit ends. + + Process the actual operation in the event loop in keyboard.c; then, + call `notify_conversion' in the text conversion interface with + COUNTER. */ + +void +start_batch_edit (struct frame *f, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_START_BATCH_EDIT; + action->data = Qnil; + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* End a ``batch edit''. It is ok to call this function even if a + batch edit has not yet started, in which case it does nothing. + + COUNTER means the same as in `start_batch_edit'. */ + +void +end_batch_edit (struct frame *f, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_END_BATCH_EDIT; + action->data = Qnil; + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Insert the specified STRING into F's current buffer's composition + region, and set point to POSITION relative to STRING. + + COUNTER means the same as in `start_batch_edit'. */ + +void +commit_text (struct frame *f, Lisp_Object string, + ptrdiff_t position, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_COMMIT_TEXT; + action->data = Fcons (make_fixnum (position), string); + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Remove the composition region and its overlay from F's current + buffer. Leave the text being composed intact. + + COUNTER means the same as in `start_batch_edit'. */ + +void +finish_composing_text (struct frame *f, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_FINISH_COMPOSING_TEXT; + action->data = Qnil; + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Insert the given STRING and make it the currently active + composition. + + If there is currently no composing region, then the new value of + point is used as the composing region. + + Then, the composing region is replaced with the text in the + specified string. + + Finally, move point to new_point, which is relative to either the + start or the end of OBJECT depending on whether or not it is less + than zero. + + COUNTER means the same as in `start_batch_edit'. */ + +void +set_composing_text (struct frame *f, Lisp_Object object, + ptrdiff_t new_point, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_SET_COMPOSING_TEXT; + action->data = Fcons (make_fixnum (new_point), + object); + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Make the region between START and END the currently active + ``composing region''. + + The ``composing region'' is a region of text in the buffer that is + about to undergo editing by the input method. */ + +void +set_composing_region (struct frame *f, ptrdiff_t start, + ptrdiff_t end, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + if (start > MOST_POSITIVE_FIXNUM) + start = MOST_POSITIVE_FIXNUM; + + if (end > MOST_POSITIVE_FIXNUM) + end = MOST_POSITIVE_FIXNUM; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_SET_COMPOSING_REGION; + action->data = Fcons (make_fixnum (start), + make_fixnum (end)); + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Move point in F's selected buffer to POINT. + + COUNTER means the same as in `start_batch_edit'. */ + +void +textconv_set_point (struct frame *f, ptrdiff_t point, + unsigned long counter) +{ + struct text_conversion_action *action, **last; + + if (point > MOST_POSITIVE_FIXNUM) + point = MOST_POSITIVE_FIXNUM; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_SET_POINT; + action->data = make_fixnum (point); + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Delete LEFT and RIGHT characters around point in F's old selected + window. */ + +void +delete_surrounding_text (struct frame *f, ptrdiff_t left, + ptrdiff_t right, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_DELETE_SURROUNDING_TEXT; + action->data = Fcons (make_fixnum (left), + make_fixnum (right)); + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Return N characters of text around point in F's old selected + window. + + Set *N to the actual number of characters returned, *START_RETURN + to the position of the first character returned, *OFFSET to the + offset of point within that text, *LENGTH to the actual number of + characters returned, and *BYTES to the actual number of bytes + returned. + + Value is NULL upon failure, and a malloced string upon success. */ + +char * +get_extracted_text (struct frame *f, ptrdiff_t n, + ptrdiff_t *start_return, + ptrdiff_t *offset, ptrdiff_t *length, + ptrdiff_t *bytes) +{ + specpdl_ref count; + ptrdiff_t start, end, start_byte, end_byte; + char *buffer; + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return NULL; + + /* Save the excursion, as there will be extensive changes to the + selected window. */ + count = SPECPDL_INDEX (); + record_unwind_protect_excursion (); + + /* Inhibit quitting. */ + specbind (Qinhibit_quit, Qt); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window (f->old_selected_window, Qt); + + /* Figure out the bounds of the text to return. */ + start = PT - n / 2; + end = PT + n - n / 2; + start = max (start, BEGV); + end = min (end, ZV); + buffer = NULL; + + /* Detect overflow. */ + + if (!(start <= PT <= end)) + goto finish; + + /* Convert the character positions to byte positions. */ + start_byte = CHAR_TO_BYTE (start); + end_byte = CHAR_TO_BYTE (end); + + /* Extract the text from the buffer. */ + buffer = xmalloc (end_byte - start_byte); + copy_buffer (start, start_byte, end, end_byte, + buffer); + + /* Return the offsets. */ + *start_return = start; + *offset = PT - start; + *length = end - start; + *bytes = end_byte - start_byte; + + finish: + unbind_to (count, Qnil); + return buffer; +} + /* Window system interface. These are called from the rest of @@ -298,16 +1168,40 @@ textconv_query (struct frame *f, struct textconv_callback_struct *query) void report_selected_window_change (struct frame *f) { + reset_frame_state (f); + if (!text_interface) return; text_interface->reset (f); } +/* Notice that the point in F's selected window's current buffer has + changed. + + F is the frame whose selected window was changed, W is the window + in question, and BUFFER is that window's current buffer. + + Tell the text conversion interface about the change; it will likely + pass the information on to the system input method. */ + +void +report_point_change (struct frame *f, struct window *window, + struct buffer *buffer) +{ + if (!text_interface || !text_interface->point_changed) + return; + + if (f->conversion.batch_edit_count > 0) + f->conversion.batch_edit_flags |= PENDING_POINT_CHANGE; + else + text_interface->point_changed (f, window, buffer); +} + /* Register INTERFACE as the text conversion interface. */ void -register_texconv_interface (struct textconv_interface *interface) +register_textconv_interface (struct textconv_interface *interface) { text_interface = interface; } diff --git a/src/textconv.h b/src/textconv.h index f6e7eb7925f..ce003847637 100644 --- a/src/textconv.h +++ b/src/textconv.h @@ -34,6 +34,20 @@ struct textconv_interface happen if the window is deleted or switches buffers, or an unexpected buffer change occurs.) */ void (*reset) (struct frame *); + + /* Notice that point has moved in the specified frame's selected + window's selected buffer. The second argument is the window + whose point changed, and the third argument is the buffer. */ + void (*point_changed) (struct frame *, struct window *, + struct buffer *); + + /* Notice that the preconversion region has changed without point + being moved. */ + void (*compose_region_changed) (struct frame *); + + /* Notice that an asynch conversion identified by COUNTER has + completed. */ + void (*notify_conversion) (unsigned long); }; @@ -103,7 +117,27 @@ struct textconv_callback_struct struct textconv_conversion_text text; }; -extern int textconv_query (struct frame *, struct textconv_callback_struct *); -extern void register_texconv_interface (struct textconv_interface *); +#define TEXTCONV_SKIP_CONVERSION_REGION (1 << 0) + +extern int textconv_query (struct frame *, struct textconv_callback_struct *, + int); +extern bool detect_conversion_events (void); +extern void handle_pending_conversion_events (void); +extern void start_batch_edit (struct frame *, unsigned long); +extern void end_batch_edit (struct frame *, unsigned long); +extern void commit_text (struct frame *, Lisp_Object, ptrdiff_t, + unsigned long); +extern void finish_composing_text (struct frame *, unsigned long); +extern void set_composing_text (struct frame *, Lisp_Object, + ptrdiff_t, unsigned long); +extern void set_composing_region (struct frame *, ptrdiff_t, ptrdiff_t, + unsigned long); +extern void textconv_set_point (struct frame *, ptrdiff_t, unsigned long); +extern void delete_surrounding_text (struct frame *, ptrdiff_t, + ptrdiff_t, unsigned long); +extern char *get_extracted_text (struct frame *, ptrdiff_t, ptrdiff_t *, + ptrdiff_t *, ptrdiff_t *, ptrdiff_t *); + +extern void register_textconv_interface (struct textconv_interface *); #endif /* _TEXTCONV_H_ */ diff --git a/src/xdisp.c b/src/xdisp.c index 8bbb80b1c8c..4b91fcf31ca 100644 --- a/src/xdisp.c +++ b/src/xdisp.c @@ -17267,6 +17267,9 @@ static void mark_window_display_accurate_1 (struct window *w, bool accurate_p) { struct buffer *b = XBUFFER (w->contents); +#ifdef HAVE_TEXT_CONVERSION + ptrdiff_t prev_point; +#endif w->last_modified = accurate_p ? BUF_MODIFF (b) : 0; w->last_overlay_modified = accurate_p ? BUF_OVERLAY_MODIFF (b) : 0; @@ -17296,11 +17299,36 @@ mark_window_display_accurate_1 (struct window *w, bool accurate_p) w->last_cursor_vpos = w->cursor.vpos; w->last_cursor_off_p = w->cursor_off_p; +#ifdef HAVE_TEXT_CONVERSION + prev_point = w->last_point; +#endif + if (w == XWINDOW (selected_window)) w->last_point = BUF_PT (b); else w->last_point = marker_position (w->pointm); +#ifdef HAVE_TEXT_CONVERSION + /* Point motion is only propagated to the input method for use + in text conversion during a redisplay. While this can lead + to inconsistencies when point has moved but the change has + not yet been displayed, it leads to better results most of + the time, as point often changes within calls to + `save-excursion', and the only way to detect such calls is to + observe that the next redisplay never ends with those changes + applied. + + Changes to buffer text are immediately propagated to the + input method, and the position of point is also updated + during such a change, so the consequences are not that + severe. */ + + if (prev_point != w->last_point + && FRAME_WINDOW_P (WINDOW_XFRAME (w)) + && w == XWINDOW (WINDOW_XFRAME (w)->selected_window)) + report_point_change (WINDOW_XFRAME (w), w, b); +#endif + w->window_end_valid = true; w->update_mode_line = false; w->preserve_vscroll_p = false; diff --git a/src/xfns.c b/src/xfns.c index 9e004f6a678..0e4a25de04a 100644 --- a/src/xfns.c +++ b/src/xfns.c @@ -3861,7 +3861,7 @@ xic_string_conversion_callback (XIC ic, XPointer client_data, request.operation = TEXTCONV_RETRIEVAL; /* Now perform the string conversion. */ - rc = textconv_query (f, &request); + rc = textconv_query (f, &request, 0); if (rc) { diff --git a/src/xterm.c b/src/xterm.c index ccaacec2c57..49b88de2759 100644 --- a/src/xterm.c +++ b/src/xterm.c @@ -31615,7 +31615,7 @@ init_xterm (void) #endif #ifdef HAVE_X_I18N - register_texconv_interface (&text_conversion_interface); + register_textconv_interface (&text_conversion_interface); #endif } -- cgit v1.3 From cf24b61985c26cbf2e5a24cb0b64a8528aa3a9cc Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 15 Feb 2023 22:51:44 +0800 Subject: Update Android port * doc/emacs/input.texi (On-Screen Keyboards): * doc/lispref/commands.texi (Misc Events): Improve documentation of text conversion stuff. * java/org/gnu/emacs/EmacsInputConnection.java (beginBatchEdit) (endBatchEdit, commitCompletion, commitText, deleteSurroundingText) (finishComposingText, getSelectedText, getTextAfterCursor) (EmacsInputConnection, setComposingRegion, performEditorAction) (getExtractedText): Condition debug code on DEBUG_IC. * java/org/gnu/emacs/EmacsService.java (EmacsService, updateIC): Likewise. * lisp/bindings.el (global-map): * lisp/electric.el (global-map): Make `text-conversion' `analyze-text-conversion'. * lisp/progmodes/prog-mode.el (prog-mode): Enable text conversion in input methods. * lisp/simple.el (analyze-text-conversion): New function. * lisp/textmodes/text-mode.el (text-conversion-style) (text-mode): Likewise. * src/androidterm.c (android_handle_ime_event): Handle set_point_and_mark. (android_sync_edit): Give Emacs 100 ms instead. (android_perform_conversion_query): Skip the active region, not the conversion region. (getSelectedText): Implement properly. (android_update_selection): Expose mark to input methods. (android_reset_conversion): Handle `text-conversion-style'. * src/buffer.c (init_buffer_once, syms_of_buffer): Add buffer local variable `text-conversion-style'. * src/buffer.h (struct buffer, bset_text_conversion_style): New fields. * src/emacs.c (android_emacs_init): Call syms_of_textconv. * src/frame.h (enum text_conversion_operation): Rename TEXTCONV_SET_POINT. * src/lisp.h: Export syms_of_textconv. * src/marker.c (set_marker_internal): Force redisplay when the mark is set and the buffer is visible on builds that use text conversion. Explain why. * src/textconv.c (copy_buffer): Fix copying past gap. (get_mark): New function. (textconv_query): Implement new flag. (sync_overlay): New function. Display conversion text in an overlay. (record_buffer_change, really_commit_text) (really_set_composing_text, really_set_composing_region) (really_delete_surrounding_text, really_set_point) (handle_pending_conversion_events_1, decrement_inside) (handle_pending_conversion_events, textconv_set_point) (get_extracted_text, register_textconv_interface): Various fixes and improvements. * src/textconv.h (struct textconv_interface): Update documentation. * src/window.h (GCALIGNED_STRUCT): New field `prev_mark'. * src/xdisp.c (mark_window_display_accurate_1): Handle prev_mark. --- doc/emacs/input.texi | 3 + doc/lispref/commands.texi | 48 +++- java/org/gnu/emacs/EmacsInputConnection.java | 68 ++++-- java/org/gnu/emacs/EmacsService.java | 14 +- lisp/bindings.el | 2 +- lisp/electric.el | 1 + lisp/progmodes/prog-mode.el | 2 + lisp/simple.el | 52 +++++ lisp/textmodes/text-mode.el | 6 + src/androidterm.c | 88 ++++++-- src/buffer.c | 20 ++ src/buffer.h | 11 + src/emacs.c | 3 + src/frame.h | 2 +- src/lisp.h | 1 + src/marker.c | 26 +++ src/textconv.c | 315 ++++++++++++++++++++++++--- src/textconv.h | 11 +- src/window.h | 4 + src/xdisp.c | 11 +- 20 files changed, 607 insertions(+), 81 deletions(-) (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') diff --git a/doc/emacs/input.texi b/doc/emacs/input.texi index 2463a75edcd..154b7025ff4 100644 --- a/doc/emacs/input.texi +++ b/doc/emacs/input.texi @@ -121,6 +121,9 @@ screen, and send the appropriate key events to Emacs after completion. However, on screen keyboard input methods directly perform edits to the selected window of each frame; this is known as ``text conversion'', or ``string conversion'' under the X Window System. +Emacs enables these input methods whenever the buffer local value of +@code{text-conversion-style} is non-@code{nil}, normally inside +derivatives of @code{text-mode} and @code{prog-mode}. Text conversion is performed asynchronously whenever Emacs receives a request to perform the conversion from the input method. After the diff --git a/doc/lispref/commands.texi b/doc/lispref/commands.texi index 2807d3d61b2..5fb2217a03b 100644 --- a/doc/lispref/commands.texi +++ b/doc/lispref/commands.texi @@ -2205,9 +2205,51 @@ A few other event types represent occurrences within the system. This kind of event is sent @strong{after} a system-wide input method performs an edit to one or more buffers. -Once the event is sent, the input method may already have made changes -to multiple frames. @c TODO: allow querying which frames to which -@c changes have been made. +@vindex text-conversion-edits +Once the event is sent, the input method may already have made +changes to multiple buffers inside many different frames. To +determine which buffers have been changed, and what edits have +been made to them, use the variable +@code{text-conversion-edits}, which is set prior to each +@code{text-conversion} event being sent; it is a list of the +form: + +@indentedblock +@w{@code{(@var{buffer} @var{beg} @var{end} @var{ephemeral})}} +@end indentedblock + +Where @var{ephemeral} is the buffer which was modified, +@var{beg} and @var{end} are the positions of the edit at the +time it was completed, and @var{ephemeral} is either a string, +containing any text which was inserted, @code{t}, meaning that +the edit is a temporary edit made by the input method, and +@code{nil}, meaning that some text was deleted. + +@vindex text-conversion-style +Whether or not this event is sent depends on the value of the +buffer-local variable @code{text-conversion-style}, which determines +how an input method that wishes to make edits to buffer contents will +behave. + +This variable can have one three values: + +@table @code +@item nil +This means that the input method will be disabled entirely, and key +events will be sent instead of text conversion events. + +@item action +This means that the input method will be enabled, but @key{RET} will +be sent wherever the input method wanted to insert a new line. + +@item t +This, or any other value, means that the input method will be enabled +and make edits terminated by @code{text-conversion} events. +@end itemize + +Changes to the value of this variable will only take effect upon +the next redisplay after the buffer becomes the selected buffer +of a frame. @cindex @code{delete-frame} event @item (delete-frame (@var{frame})) diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 3cf4419838b..5eb56d5aa71 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -55,7 +55,9 @@ public class EmacsInputConnection extends BaseInputConnection public boolean beginBatchEdit () { - Log.d (TAG, "beginBatchEdit"); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "beginBatchEdit"); + EmacsNative.beginBatchEdit (windowHandle); return true; } @@ -64,7 +66,9 @@ public class EmacsInputConnection extends BaseInputConnection public boolean endBatchEdit () { - Log.d (TAG, "endBatchEdit"); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "endBatchEdit"); + EmacsNative.endBatchEdit (windowHandle); return true; } @@ -73,7 +77,9 @@ public class EmacsInputConnection extends BaseInputConnection public boolean commitCompletion (CompletionInfo info) { - Log.d (TAG, "commitCompletion: " + info); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "commitCompletion: " + info); + EmacsNative.commitCompletion (windowHandle, info.getText ().toString (), info.getPosition ()); @@ -84,7 +90,9 @@ public class EmacsInputConnection extends BaseInputConnection public boolean commitText (CharSequence text, int newCursorPosition) { - Log.d (TAG, "commitText: " + text + " " + newCursorPosition); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "commitText: " + text + " " + newCursorPosition); + EmacsNative.commitText (windowHandle, text.toString (), newCursorPosition); return true; @@ -94,8 +102,10 @@ public class EmacsInputConnection extends BaseInputConnection public boolean deleteSurroundingText (int leftLength, int rightLength) { - Log.d (TAG, ("deleteSurroundingText: " - + leftLength + " " + rightLength)); + if (EmacsService.DEBUG_IC) + Log.d (TAG, ("deleteSurroundingText: " + + leftLength + " " + rightLength)); + EmacsNative.deleteSurroundingText (windowHandle, leftLength, rightLength); return true; @@ -105,7 +115,8 @@ public class EmacsInputConnection extends BaseInputConnection public boolean finishComposingText () { - Log.d (TAG, "finishComposingText"); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "finishComposingText"); EmacsNative.finishComposingText (windowHandle); return true; @@ -115,7 +126,8 @@ public class EmacsInputConnection extends BaseInputConnection public String getSelectedText (int flags) { - Log.d (TAG, "getSelectedText: " + flags); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "getSelectedText: " + flags); return EmacsNative.getSelectedText (windowHandle, flags); } @@ -124,27 +136,44 @@ public class EmacsInputConnection extends BaseInputConnection public String getTextAfterCursor (int length, int flags) { - Log.d (TAG, "getTextAfterCursor: " + length + " " + flags); + String string; + + if (EmacsService.DEBUG_IC) + Log.d (TAG, "getTextAfterCursor: " + length + " " + flags); - return EmacsNative.getTextAfterCursor (windowHandle, length, - flags); + string = EmacsNative.getTextAfterCursor (windowHandle, length, + flags); + + if (EmacsService.DEBUG_IC) + Log.d (TAG, " --> " + string); + + return string; } @Override public String getTextBeforeCursor (int length, int flags) { - Log.d (TAG, "getTextBeforeCursor: " + length + " " + flags); + String string; + + if (EmacsService.DEBUG_IC) + Log.d (TAG, "getTextBeforeCursor: " + length + " " + flags); + + string = EmacsNative.getTextBeforeCursor (windowHandle, length, + flags); + + if (EmacsService.DEBUG_IC) + Log.d (TAG, " --> " + string); - return EmacsNative.getTextBeforeCursor (windowHandle, length, - flags); + return string; } @Override public boolean setComposingText (CharSequence text, int newCursorPosition) { - Log.d (TAG, "setComposingText: " + newCursorPosition); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "setComposingText: " + newCursorPosition); EmacsNative.setComposingText (windowHandle, text.toString (), newCursorPosition); @@ -155,7 +184,8 @@ public class EmacsInputConnection extends BaseInputConnection public boolean setComposingRegion (int start, int end) { - Log.d (TAG, "setComposingRegion: " + start + " " + end); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "setComposingRegion: " + start + " " + end); EmacsNative.setComposingRegion (windowHandle, start, end); return true; @@ -165,7 +195,8 @@ public class EmacsInputConnection extends BaseInputConnection public boolean performEditorAction (int editorAction) { - Log.d (TAG, "performEditorAction: " + editorAction); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "performEditorAction: " + editorAction); EmacsNative.performEditorAction (windowHandle, editorAction); return true; @@ -175,7 +206,8 @@ public class EmacsInputConnection extends BaseInputConnection public ExtractedText getExtractedText (ExtractedTextRequest request, int flags) { - Log.d (TAG, "getExtractedText: " + request + " " + flags); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "getExtractedText: " + request + " " + flags); return EmacsNative.getExtractedText (windowHandle, request, flags); diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 855a738a30f..2acb3ead086 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -88,6 +88,8 @@ public class EmacsService extends Service /* Display metrics used by font backends. */ public DisplayMetrics metrics; + public static final boolean DEBUG_IC = false; + @Override public int onStartCommand (Intent intent, int flags, int startId) @@ -612,10 +614,11 @@ public class EmacsService extends Service int newSelectionEnd, int composingRegionStart, int composingRegionEnd) { - Log.d (TAG, ("updateIC: " + window + " " + newSelectionStart - + " " + newSelectionEnd + " " - + composingRegionStart + " " - + composingRegionEnd)); + if (DEBUG_IC) + Log.d (TAG, ("updateIC: " + window + " " + newSelectionStart + + " " + newSelectionEnd + " " + + composingRegionStart + " " + + composingRegionEnd)); window.view.imManager.updateSelection (window.view, newSelectionStart, newSelectionEnd, @@ -626,7 +629,8 @@ public class EmacsService extends Service public void resetIC (EmacsWindow window, int icMode) { - Log.d (TAG, "resetIC: " + window); + if (DEBUG_IC) + Log.d (TAG, "resetIC: " + window); window.view.setICMode (icMode); window.view.imManager.restartInput (window.view); diff --git a/lisp/bindings.el b/lisp/bindings.el index 057c870958d..eec51a4e413 100644 --- a/lisp/bindings.el +++ b/lisp/bindings.el @@ -1522,7 +1522,7 @@ if `inhibit-field-text-motion' is non-nil." (define-key special-event-map [sigusr2] 'ignore) ;; Text conversion -(define-key global-map [text-conversion] 'ignore) +(define-key global-map [text-conversion] 'analyze-text-conversion) ;; Don't look for autoload cookies in this file. ;; Local Variables: diff --git a/lisp/electric.el b/lisp/electric.el index bac3f5a2b3c..3865d1d5234 100644 --- a/lisp/electric.el +++ b/lisp/electric.el @@ -294,6 +294,7 @@ or comment." ;;;###autoload (define-key global-map "\C-j" 'electric-newline-and-maybe-indent) + ;;;###autoload (defun electric-newline-and-maybe-indent () "Insert a newline. diff --git a/lisp/progmodes/prog-mode.el b/lisp/progmodes/prog-mode.el index 04071703184..7a53399ad14 100644 --- a/lisp/progmodes/prog-mode.el +++ b/lisp/progmodes/prog-mode.el @@ -336,6 +336,8 @@ support it." (setq-local require-final-newline mode-require-final-newline) (setq-local parse-sexp-ignore-comments t) (add-hook 'context-menu-functions 'prog-context-menu 10 t) + ;; Enable text conversion in this buffer. + (setq-local text-conversion-style t) ;; Any programming language is always written left to right. (setq bidi-paragraph-direction 'left-to-right)) diff --git a/lisp/simple.el b/lisp/simple.el index bed6dfb8292..6a12585a55d 100644 --- a/lisp/simple.el +++ b/lisp/simple.el @@ -10864,6 +10864,58 @@ If the buffer doesn't exist, create it first." "Change value in PLIST of PROP to VAL, comparing with `equal'." (declare (obsolete plist-put "29.1")) (plist-put plist prop val #'equal)) + + + +;; Text conversion support. See textconv.c for more details about +;; what this is. + + +;; Actually in textconv.c. +(defvar text-conversion-edits) + +(defun analyze-text-conversion () + "Analyze the results of the previous text conversion event. + +For each insertion: + + - Look for the insertion of a string starting or ending with a + character inside `auto-fill-chars', and fill the text around + it if `auto-fill-mode' is enabled. + + - Look for the insertion of a new line, and cause automatic + line breaking of the previous line when `auto-fill-mode' is + enabled. + + - Look for the insertion of a new line, and indent this new + line if `electric-indent-mode' is enabled." + (interactive) + (dolist (edit text-conversion-edits) + ;; Filter out ephemeral edits and deletions. + (when (and (not (eq (nth 1 edit) (nth 2 edit))) + (stringp (nth 3 edit))) + (with-current-buffer (car edit) + (let* ((inserted (nth 3 edit)) + ;; Get the first and last characters. + (start (aref inserted 0)) + (end (aref inserted (1- (length inserted)))) + ;; Figure out whether or not to auto-fill. + (auto-fill-p (or (aref auto-fill-chars start) + (aref auto-fill-chars end))) + ;; Figure out whether or not a newline was inserted. + (newline-p (string-search "\n" inserted))) + (save-excursion + (if (and auto-fill-function newline-p) + (progn (goto-char (nth 2 edit)) + (previous-logical-line) + (funcall auto-fill-function)) + (when (and auto-fill-function auto-fill-p) + (progn (goto-char (nth 2 edit)) + (funcall auto-fill-function))))) + (when (and electric-indent-mode newline-p) + (goto-char (nth 2 edit)) + (indent-according-to-mode))))))) + (provide 'simple) diff --git a/lisp/textmodes/text-mode.el b/lisp/textmodes/text-mode.el index 48cefc74d06..ccba1b063ab 100644 --- a/lisp/textmodes/text-mode.el +++ b/lisp/textmodes/text-mode.el @@ -41,6 +41,9 @@ "Non-nil if this buffer's major mode is a variant of Text mode.") (make-obsolete-variable 'text-mode-variant 'derived-mode-p "27.1") +;; Actually defined in textconv.c. +(defvar text-conversion-style) + (defvar text-mode-syntax-table (let ((st (make-syntax-table))) (modify-syntax-entry ?\" ". " st) @@ -125,6 +128,9 @@ You can thus get the full benefit of adaptive filling Turning on Text mode runs the normal hook `text-mode-hook'." (setq-local text-mode-variant t) (setq-local require-final-newline mode-require-final-newline) + + ;; Enable text conversion in this buffer. + (setq-local text-conversion-style t) (add-hook 'context-menu-functions 'text-mode-context-menu 10 t)) (define-derived-mode paragraph-indent-text-mode text-mode "Parindent" diff --git a/src/androidterm.c b/src/androidterm.c index 767b7d8240c..0c990d3d2d2 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -621,8 +621,9 @@ android_handle_ime_event (union android_event *event, struct frame *f) break; case ANDROID_IME_SET_POINT: - textconv_set_point (f, event->ime.position, - event->ime.counter); + textconv_set_point_and_mark (f, event->ime.start, + event->ime.end, + event->ime.counter); break; case ANDROID_IME_START_BATCH_EDIT: @@ -4305,7 +4306,7 @@ static sem_t edit_sem; Every time one of the text retrieval functions is called and an editing request is made, Emacs gives the main thread approximately - 50 ms to process it, in order to mostly keep the input method in + 100 ms to process it, in order to mostly keep the input method in sync with the buffer contents. */ static void @@ -4319,7 +4320,7 @@ android_sync_edit (void) return; start = current_timespec (); - end = timespec_add (start, make_timespec (0, 50000000)); + end = timespec_add (start, make_timespec (0, 100000000)); while (true) { @@ -4550,8 +4551,7 @@ android_perform_conversion_query (void *data) if (!f) return; - textconv_query (f, &context->query, - TEXTCONV_SKIP_CONVERSION_REGION); + textconv_query (f, &context->query, TEXTCONV_SKIP_ACTIVE_REGION); /* context->query.text will have been set even if textconv_query returns 1. */ @@ -4648,13 +4648,6 @@ android_text_to_string (JNIEnv *env, char *buffer, ptrdiff_t n, return string; } -JNIEXPORT jstring JNICALL -NATIVE_NAME (getSelectedText) (JNIEnv *env, jobject object, - jshort window) -{ - return NULL; -} - JNIEXPORT jstring JNICALL NATIVE_NAME (getTextAfterCursor) (JNIEnv *env, jobject object, jshort window, jint length, jint flags) @@ -4805,8 +4798,8 @@ NATIVE_NAME (setSelection) (JNIEnv *env, jobject object, jshort window, event.ime.serial = ++event_serial; event.ime.window = window; event.ime.operation = ANDROID_IME_SET_POINT; - event.ime.start = 0; - event.ime.end = 0; + event.ime.start = start; + event.ime.end = end; event.ime.length = 0; event.ime.position = start; event.ime.text = NULL; @@ -5068,6 +5061,34 @@ NATIVE_NAME (getExtractedText) (JNIEnv *env, jobject ignored_object, return object; } +JNIEXPORT jstring JNICALL +NATIVE_NAME (getSelectedText) (JNIEnv *env, jobject object, + jshort window) +{ + struct android_get_extracted_text_context context; + jstring string; + + context.hint_max_chars = -1; + context.token = 0; + context.text = NULL; + context.window = window; + + android_sync_edit (); + if (android_run_in_emacs_thread (android_get_extracted_text, + &context)) + return NULL; + + if (!context.text) + return NULL; + + /* Encode the returned text. */ + string = android_text_to_string (env, context.text, context.length, + context.bytes); + free (context.text); + + return string; +} + #ifdef __clang__ #pragma clang diagnostic pop #else @@ -5083,7 +5104,8 @@ NATIVE_NAME (getExtractedText) (JNIEnv *env, jobject ignored_object, static void android_update_selection (struct frame *f, struct window *w) { - ptrdiff_t start, end, point; + ptrdiff_t start, end, point, mark; + struct buffer *b; if (MARKERP (f->conversion.compose_region_start)) { @@ -5103,12 +5125,20 @@ android_update_selection (struct frame *f, struct window *w) if (!w) w = XWINDOW (f->selected_window); - /* Figure out where the point is. */ + /* Figure out where the point and mark are. If the mark is not + active, then point is set to equal mark. */ + b = XBUFFER (w->contents); point = min (w->last_point, TYPE_MAXIMUM (jint)); + mark = ((!NILP (BVAR (b, mark_active)) + && w->last_mark != -1) + ? min (w->last_mark, TYPE_MAXIMUM (jint)) + : point); - /* Send the update. */ - android_update_ic (FRAME_ANDROID_WINDOW (f), point, point, - start, end); + /* Send the update. Android doesn't have a concept of ``point'' and + ``mark''; instead, it only has a selection, where the start of + the selection is less than or equal to the end. */ + android_update_ic (FRAME_ANDROID_WINDOW (f), min (point, mark), + max (point, mark), start, end); } /* Notice that the input method connection to F should be reset as a @@ -5117,16 +5147,32 @@ android_update_selection (struct frame *f, struct window *w) static void android_reset_conversion (struct frame *f) { + enum android_ic_mode mode; + struct window *w; + struct buffer *buffer; + /* Reset the input method. Pick an appropriate ``input mode'' based on whether or not the minibuffer window is selected; this controls whether or not ``RET'' inserts a newline or sends an actual key event. */ + + w = XWINDOW (f->selected_window); + buffer = XBUFFER (WINDOW_BUFFER (w)); + + if (NILP (BVAR (buffer, text_conversion_style))) + mode = ANDROID_IC_MODE_NULL; + else if (EQ (BVAR (buffer, text_conversion_style), + Qaction)) + mode = ANDROID_IC_MODE_ACTION; + else + mode = ANDROID_IC_MODE_TEXT; + android_reset_ic (FRAME_ANDROID_WINDOW (f), (EQ (f->selected_window, f->minibuffer_window) ? ANDROID_IC_MODE_ACTION - : ANDROID_IC_MODE_TEXT)); + : mode)); /* Move its selection to the specified position. */ android_update_selection (f, NULL); diff --git a/src/buffer.c b/src/buffer.c index 38648519ba0..af4aa583c96 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -4710,6 +4710,7 @@ init_buffer_once (void) #ifdef HAVE_TREE_SITTER XSETFASTINT (BVAR (&buffer_local_flags, ts_parser_list), idx); ++idx; #endif + XSETFASTINT (BVAR (&buffer_local_flags, text_conversion_style), idx); ++idx; XSETFASTINT (BVAR (&buffer_local_flags, cursor_in_non_selected_windows), idx); ++idx; /* buffer_local_flags contains no pointers, so it's safe to treat it @@ -4780,6 +4781,9 @@ init_buffer_once (void) bset_extra_line_spacing (&buffer_defaults, Qnil); #ifdef HAVE_TREE_SITTER bset_ts_parser_list (&buffer_defaults, Qnil); +#endif +#ifdef HAVE_TEXT_CONVERSION + bset_text_conversion_style (&buffer_defaults, Qnil); #endif bset_cursor_in_non_selected_windows (&buffer_defaults, Qt); @@ -5852,6 +5856,22 @@ If t, displays a cursor related to the usual cursor type You can also specify the cursor type as in the `cursor-type' variable. Use Custom to set this variable and update the display. */); + /* While this is defined here, each *term.c module must implement + the logic itself. */ + + DEFVAR_PER_BUFFER ("text-conversion-style", &BVAR (current_buffer, + text_conversion_style), + Qnil, + "How the on screen keyboard's input method should insert in this buffer.\n\ +When nil, the input method will be disabled and an ordinary keyboard\n\ +will be displayed in its place.\n\ +When the symbol `action', the input method will insert text directly, but\n\ +will send `return' key events instead of inserting new line characters.\n\ +Any other value means that the input method will insert text directly.\n\ +\n\ +This variable does not take immediate effect when set; rather, it takes\n\ +effect upon the next redisplay after the selected window or buffer changes."); + DEFVAR_LISP ("kill-buffer-query-functions", Vkill_buffer_query_functions, doc: /* List of functions called with no args to query before killing a buffer. The buffer being killed will be current while the functions are running. diff --git a/src/buffer.h b/src/buffer.h index e700297a264..e71ffe28045 100644 --- a/src/buffer.h +++ b/src/buffer.h @@ -566,6 +566,11 @@ struct buffer /* A list of tree-sitter parsers for this buffer. */ Lisp_Object ts_parser_list_; #endif + + /* What type of text conversion the input method should apply to + this buffer. */ + Lisp_Object text_conversion_style_; + /* Cursor type to display in non-selected windows. t means to use hollow box cursor. See `cursor-type' for other values. */ @@ -842,6 +847,12 @@ bset_width_table (struct buffer *b, Lisp_Object val) b->width_table_ = val; } +INLINE void +bset_text_conversion_style (struct buffer *b, Lisp_Object val) +{ + b->text_conversion_style_ = val; +} + /* BUFFER_CEILING_OF (resp. BUFFER_FLOOR_OF), when applied to n, return the max (resp. min) p such that diff --git a/src/emacs.c b/src/emacs.c index d7de3c85bbe..2f953510a3d 100644 --- a/src/emacs.c +++ b/src/emacs.c @@ -2000,6 +2000,9 @@ Using an Emacs configured with --with-x-toolkit=lucid does not have this problem #ifdef HAVE_WINDOW_SYSTEM init_fringe_once (); /* Swap bitmaps if necessary. */ #endif /* HAVE_WINDOW_SYSTEM */ +#ifdef HAVE_TEXT_CONVERSION + syms_of_textconv (); +#endif } init_alloc (); diff --git a/src/frame.h b/src/frame.h index 27484936ff1..ca4cca17d74 100644 --- a/src/frame.h +++ b/src/frame.h @@ -86,7 +86,7 @@ enum text_conversion_operation TEXTCONV_FINISH_COMPOSING_TEXT, TEXTCONV_SET_COMPOSING_TEXT, TEXTCONV_SET_COMPOSING_REGION, - TEXTCONV_SET_POINT, + TEXTCONV_SET_POINT_AND_MARK, TEXTCONV_DELETE_SURROUNDING_TEXT, }; diff --git a/src/lisp.h b/src/lisp.h index 2dc51d32481..a39ca8cc541 100644 --- a/src/lisp.h +++ b/src/lisp.h @@ -5234,6 +5234,7 @@ extern void reset_frame_state (struct frame *); extern void report_selected_window_change (struct frame *); extern void report_point_change (struct frame *, struct window *, struct buffer *); +extern void syms_of_textconv (void); #endif #ifdef HAVE_NATIVE_COMP diff --git a/src/marker.c b/src/marker.c index e42c49a5434..7b15cd62f1e 100644 --- a/src/marker.c +++ b/src/marker.c @@ -23,6 +23,7 @@ along with GNU Emacs. If not, see . */ #include "lisp.h" #include "character.h" #include "buffer.h" +#include "window.h" /* Record one cached position found recently by buf_charpos_to_bytepos or buf_bytepos_to_charpos. */ @@ -566,6 +567,31 @@ set_marker_internal (Lisp_Object marker, Lisp_Object position, attach_marker (m, b, charpos, bytepos); } + +#ifdef HAVE_TEXT_CONVERSION + + /* If B is the buffer's mark and there is a window displaying B, and + text conversion is enabled while the mark is active, redisplay + the buffer. + + propagate_window_redisplay will propagate this redisplay to the + window, which will eventually reach + mark_window_display_accurate_1. At that point, + report_point_change will be told to update the mark as seen by + the input method. + + This is done all the way in (the seemingly irrelevant) redisplay + because the selection reported to the input method is actually what + is visible on screen, namely w->last_point. */ + + if (m->buffer + && EQ (marker, BVAR (m->buffer, mark)) + && !NILP (BVAR (m->buffer, mark_active)) + && buffer_window_count (m->buffer)) + bset_redisplay (m->buffer); + +#endif + return marker; } diff --git a/src/textconv.c b/src/textconv.c index a39748457d3..835d03f3037 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -89,13 +89,27 @@ copy_buffer (ptrdiff_t beg, ptrdiff_t beg_byte, size = end0 - beg0; memcpy (buffer, BYTE_POS_ADDR (beg0), size); if (beg1 != -1) - memcpy (buffer, BEG_ADDR + beg1, end1 - beg1); + memcpy (buffer + size, BEG_ADDR + beg1, end1 - beg1); } /* Conversion query. */ +/* Return the position of the active mark, or -1 if there is no mark + or it is not active. */ + +static ptrdiff_t +get_mark (void) +{ + if (!NILP (BVAR (current_buffer, mark_active)) + && XMARKER (BVAR (current_buffer, mark))->buffer) + return marker_position (BVAR (current_buffer, + mark)); + + return -1; +} + /* Perform the text conversion operation specified in QUERY and return the results. @@ -109,6 +123,9 @@ copy_buffer (ptrdiff_t beg, ptrdiff_t beg_byte, If FLAGS & TEXTCONV_SKIP_CONVERSION_REGION, then first move PT past the conversion region in the specified direction if it is inside. + If FLAGS & TEXTCONV_SKIP_ACTIVE_REGION, then also move PT past the + region if the mark is active. + Value is 0 if QUERY->operation was not TEXTCONV_SUBSTITUTION or if deleting the text was successful, and 1 otherwise. */ @@ -169,6 +186,39 @@ textconv_query (struct frame *f, struct textconv_callback_struct *query, } } + /* Likewise for the region if the mark is active. */ + + if (flags & TEXTCONV_SKIP_ACTIVE_REGION) + { + temp = get_mark (); + + if (temp == -1) + goto escape; + + start = min (temp, PT); + end = max (temp, PT); + + if (pos >= start && pos < end) + { + switch (query->direction) + { + case TEXTCONV_FORWARD_CHAR: + case TEXTCONV_FORWARD_WORD: + case TEXTCONV_CARET_DOWN: + case TEXTCONV_NEXT_LINE: + case TEXTCONV_LINE_START: + pos = end; + break; + + default: + pos = max (BEGV, start); + break; + } + } + } + + escape: + /* If pos is outside the accessible part of the buffer or if it overflows, move back to point or to the extremes of the accessible region. */ @@ -335,6 +385,55 @@ textconv_query (struct frame *f, struct textconv_callback_struct *query, return 0; } +/* Update the overlay displaying the conversion area on F after a + change to the conversion region. */ + +static void +sync_overlay (struct frame *f) +{ + if (MARKERP (f->conversion.compose_region_start)) + { + if (NILP (f->conversion.compose_region_overlay)) + { + f->conversion.compose_region_overlay + = Fmake_overlay (f->conversion.compose_region_start, + f->conversion.compose_region_end, Qnil, + Qt, Qnil); + Foverlay_put (f->conversion.compose_region_overlay, + Qface, Qunderline); + } + + Fmove_overlay (f->conversion.compose_region_overlay, + f->conversion.compose_region_start, + f->conversion.compose_region_end, Qnil); + } + else if (!NILP (f->conversion.compose_region_overlay)) + { + Fdelete_overlay (f->conversion.compose_region_overlay); + f->conversion.compose_region_overlay = Qnil; + } +} + +/* Record a change to the current buffer as a result of an + asynchronous text conversion operation on F. + + Consult the doc string of `text-conversion-edits' for the meaning + of BEG, END, and EPHEMERAL. */ + +static void +record_buffer_change (ptrdiff_t beg, ptrdiff_t end, + Lisp_Object ephemeral) +{ + Lisp_Object buffer; + + XSETBUFFER (buffer, current_buffer); + + Vtext_conversion_edits + = Fcons (list4 (buffer, make_fixnum (beg), + make_fixnum (end), ephemeral), + Vtext_conversion_edits); +} + /* Reset F's text conversion state. Delete any overlays or markers inside. */ @@ -438,8 +537,10 @@ really_commit_text (struct frame *f, EMACS_INT position, /* Replace its contents. */ start = marker_position (f->conversion.compose_region_start); end = marker_position (f->conversion.compose_region_end); - safe_del_range (start, end); + del_range (start, end); + record_buffer_change (start, start, Qnil); Finsert (1, &text); + record_buffer_change (start, PT, text); /* Move to a the position specified in POSITION. */ @@ -493,6 +594,7 @@ really_commit_text (struct frame *f, EMACS_INT position, location. */ wanted = PT; Finsert (1, &text); + record_buffer_change (wanted, PT, text); if (position < 0) { @@ -520,6 +622,8 @@ really_commit_text (struct frame *f, EMACS_INT position, } } + /* This should deactivate the mark. */ + call0 (Qdeactivate_mark); unbind_to (count, Qnil); } @@ -575,12 +679,11 @@ really_set_composing_text (struct frame *f, ptrdiff_t position, if (!MARKERP (f->conversion.compose_region_start)) { - f->conversion.compose_region_start = Fmake_marker (); - f->conversion.compose_region_end = Fmake_marker (); - Fset_marker (f->conversion.compose_region_start, - Fpoint (), Qnil); - Fset_marker (f->conversion.compose_region_end, - Fpoint (), Qnil); + f->conversion.compose_region_start + = build_marker (current_buffer, PT, PT_BYTE); + f->conversion.compose_region_end + = build_marker (current_buffer, PT, PT_BYTE); + Fset_marker_insertion_type (f->conversion.compose_region_end, Qt); @@ -595,13 +698,19 @@ really_set_composing_text (struct frame *f, ptrdiff_t position, Fwiden (); start = marker_position (f->conversion.compose_region_start); end = marker_position (f->conversion.compose_region_end); - safe_del_range (start, end); + del_range (start, end); set_point (start); + + if (start != end) + record_buffer_change (start, start, Qnil); } /* Insert the new text. */ Finsert (1, &text); + if (start != PT) + record_buffer_change (start, PT, Qnil); + /* Now move point to an appropriate location. */ if (position < 0) { @@ -632,6 +741,12 @@ really_set_composing_text (struct frame *f, ptrdiff_t position, set_point (wanted); + /* This should deactivate the mark. */ + call0 (Qdeactivate_mark); + + /* Move the composition overlay. */ + sync_overlay (f); + /* If PT hasn't changed, the conversion region definitely has. Otherwise, redisplay will update the input method instead. */ @@ -693,18 +808,21 @@ really_set_composing_region (struct frame *f, ptrdiff_t start, make_fixnum (start), Qnil); Fset_marker (f->conversion.compose_region_end, make_fixnum (end), Qnil); + sync_overlay (f); unbind_to (count, Qnil); } -/* Delete LEFT and RIGHT chars around point. */ +/* Delete LEFT and RIGHT chars around point or the active mark, + whichever is larger, avoiding the composing region if + necessary. */ static void really_delete_surrounding_text (struct frame *f, ptrdiff_t left, ptrdiff_t right) { specpdl_ref count; - ptrdiff_t start, end; + ptrdiff_t start, end, a, b, a1, b1, lstart, rstart; /* If F's old selected window is no longer live, fail. */ @@ -719,21 +837,71 @@ really_delete_surrounding_text (struct frame *f, ptrdiff_t left, redisplay. */ Fselect_window (f->old_selected_window, Qt); - start = max (BEGV, PT - left); - end = min (ZV, PT + right); + /* Figure out where to start deleting from. */ + + a = get_mark (); + + if (a != -1 && a != PT) + lstart = rstart = max (a, PT); + else + lstart = rstart = PT; + + /* Avoid the composing text. This behavior is identical to how + Android's BaseInputConnection actually implements avoiding the + composing span. */ + + if (MARKERP (f->conversion.compose_region_start)) + { + a = marker_position (f->conversion.compose_region_start); + b = marker_position (f->conversion.compose_region_end); + + a1 = min (a, b); + b1 = max (a, b); + + lstart = min (lstart, min (PT, a1)); + rstart = max (rstart, max (PT, b1)); + } + + if (lstart == rstart) + { + start = max (BEGV, lstart - left); + end = min (ZV, rstart + right); + + del_range (start, end); + record_buffer_change (start, start, Qnil); + } + else + { + start = max (BEGV, lstart - left); + end = lstart; + + del_range (start, end); + record_buffer_change (start, start, Qnil); + + start = rstart; + end = min (ZV, rstart + right); + del_range (start, end); + record_buffer_change (start, start, Qnil); + } + + /* if the mark is now equal to start, deactivate it. */ + + if (get_mark () == PT) + call0 (Qdeactivate_mark); - safe_del_range (start, end); unbind_to (count, Qnil); } -/* Set point in F to POSITION. +/* Set point in F to POSITION. If MARK is not POSITION, activate the + mark and set MARK to that as well. If it has not changed, signal an update through the text input interface, which is necessary for the IME to acknowledge that the change has completed. */ static void -really_set_point (struct frame *f, ptrdiff_t point) +really_set_point_and_mark (struct frame *f, ptrdiff_t point, + ptrdiff_t mark) { specpdl_ref count; @@ -763,6 +931,11 @@ really_set_point (struct frame *f, ptrdiff_t point) /* Set the point. */ Fgoto_char (make_fixnum (point)); + if (mark == point && BVAR (current_buffer, mark_active)) + call0 (Qdeactivate_mark); + else + call1 (Qpush_mark, make_fixnum (mark)); + unbind_to (count, Qnil); } @@ -845,8 +1018,9 @@ handle_pending_conversion_events_1 (struct frame *f, XFIXNUM (XCDR (data))); break; - case TEXTCONV_SET_POINT: - really_set_point (f, XFIXNUM (data)); + case TEXTCONV_SET_POINT_AND_MARK: + really_set_point_and_mark (f, XFIXNUM (XCAR (data)), + XFIXNUM (XCDR (data))); break; case TEXTCONV_DELETE_SURROUNDING_TEXT: @@ -858,6 +1032,17 @@ handle_pending_conversion_events_1 (struct frame *f, unbind_to (count, Qnil); } +/* Decrement the variable pointed to by *PTR. */ + +static void +decrement_inside (void *ptr) +{ + int *i; + + i = ptr; + (*i)--; +} + /* Process any outstanding text conversion events. This may run Lisp or signal. */ @@ -868,9 +1053,22 @@ handle_pending_conversion_events (void) Lisp_Object tail, frame; struct text_conversion_action *action, *next; bool handled; + static int inside; + specpdl_ref count; handled = false; + /* Reset Vtext_conversion_edits. Do not do this if called + reentrantly. */ + + if (!inside) + Vtext_conversion_edits = Qnil; + + inside++; + + count = SPECPDL_INDEX (); + record_unwind_protect_ptr (decrement_inside, &inside); + FOR_EACH_FRAME (tail, frame) { f = XFRAME (frame); @@ -905,6 +1103,8 @@ handle_pending_conversion_events (void) handled = true; } } + + unbind_to (count, Qnil); } /* Start a ``batch edit'' in F. During a batch edit, point_changed @@ -1057,13 +1257,13 @@ set_composing_region (struct frame *f, ptrdiff_t start, input_pending = true; } -/* Move point in F's selected buffer to POINT. +/* Move point in F's selected buffer to POINT and maybe push MARK. COUNTER means the same as in `start_batch_edit'. */ void -textconv_set_point (struct frame *f, ptrdiff_t point, - unsigned long counter) +textconv_set_point_and_mark (struct frame *f, ptrdiff_t point, + ptrdiff_t mark, unsigned long counter) { struct text_conversion_action *action, **last; @@ -1071,8 +1271,9 @@ textconv_set_point (struct frame *f, ptrdiff_t point, point = MOST_POSITIVE_FIXNUM; action = xmalloc (sizeof *action); - action->operation = TEXTCONV_SET_POINT; - action->data = make_fixnum (point); + action->operation = TEXTCONV_SET_POINT_AND_MARK; + action->data = Fcons (make_fixnum (point), + make_fixnum (mark)); action->next = NULL; action->counter = counter; for (last = &f->conversion.actions; *last; last = &(*last)->next) @@ -1105,6 +1306,9 @@ delete_surrounding_text (struct frame *f, ptrdiff_t left, /* Return N characters of text around point in F's old selected window. + If N is -1, return the text between point and mark instead, given + that the mark is active. + Set *N to the actual number of characters returned, *START_RETURN to the position of the first character returned, *OFFSET to the offset of point within that text, *LENGTH to the actual number of @@ -1137,13 +1341,38 @@ get_extracted_text (struct frame *f, ptrdiff_t n, /* Temporarily switch to F's selected window at the time of the last redisplay. */ Fselect_window (f->old_selected_window, Qt); + buffer = NULL; /* Figure out the bounds of the text to return. */ - start = PT - n / 2; - end = PT + n - n / 2; + if (n != -1) + { + start = PT - n / 2; + end = PT + n - n / 2; + } + else + { + if (!NILP (BVAR (current_buffer, mark_active)) + && XMARKER (BVAR (current_buffer, mark))->buffer) + { + start = marker_position (BVAR (current_buffer, mark)); + end = PT; + + /* Sort start and end. start_byte is used to hold a + temporary value. */ + + if (start > end) + { + start_byte = end; + end = start; + start = start_byte; + } + } + else + goto finish; + } + start = max (start, BEGV); end = min (end, ZV); - buffer = NULL; /* Detect overflow. */ @@ -1218,3 +1447,37 @@ register_textconv_interface (struct textconv_interface *interface) { text_interface = interface; } + + + +void +syms_of_textconv (void) +{ + DEFSYM (Qaction, "action"); + DEFSYM (Qtext_conversion, "text-conversion"); + DEFSYM (Qpush_mark, "push-mark"); + DEFSYM (Qunderline, "underline"); + + DEFVAR_LISP ("text-conversion-edits", Vtext_conversion_edits, + doc: /* List of buffers that were last edited as a result of text conversion. + +This list can be used while handling a `text-conversion' event to +determine the changes which have taken place. + +Each element of the list describes a single edit in a buffer, of the +form: + + (BUFFER BEG END EPHEMERAL) + +If an insertion or a change occured, then BEG and END are buffer +positions denote the bounds of the text that was changed or inserted. +If EPHEMERAL is t, then the input method will shortly make more +changes to the text, so any actions that would otherwise be taken +(such as indenting or automatically filling text) should not take +place; otherwise, it is a string describing the text which was +inserted. + +If a deletion occured, then BEG and END are the same, and EPHEMERAL is +nil. */); + Vtext_conversion_edits = Qnil; +} diff --git a/src/textconv.h b/src/textconv.h index ce003847637..034c663521a 100644 --- a/src/textconv.h +++ b/src/textconv.h @@ -35,9 +35,10 @@ struct textconv_interface unexpected buffer change occurs.) */ void (*reset) (struct frame *); - /* Notice that point has moved in the specified frame's selected - window's selected buffer. The second argument is the window - whose point changed, and the third argument is the buffer. */ + /* Notice that point or mark has moved in the specified frame's + selected window's selected buffer. The second argument is the + window whose point changed, and the third argument is the + buffer. */ void (*point_changed) (struct frame *, struct window *, struct buffer *); @@ -118,6 +119,7 @@ struct textconv_callback_struct }; #define TEXTCONV_SKIP_CONVERSION_REGION (1 << 0) +#define TEXTCONV_SKIP_ACTIVE_REGION (1 << 1) extern int textconv_query (struct frame *, struct textconv_callback_struct *, int); @@ -132,7 +134,8 @@ extern void set_composing_text (struct frame *, Lisp_Object, ptrdiff_t, unsigned long); extern void set_composing_region (struct frame *, ptrdiff_t, ptrdiff_t, unsigned long); -extern void textconv_set_point (struct frame *, ptrdiff_t, unsigned long); +extern void textconv_set_point_and_mark (struct frame *, ptrdiff_t, + ptrdiff_t, unsigned long); extern void delete_surrounding_text (struct frame *, ptrdiff_t, ptrdiff_t, unsigned long); extern char *get_extracted_text (struct frame *, ptrdiff_t, ptrdiff_t *, diff --git a/src/window.h b/src/window.h index 32b5fe14f4f..463c7f89b9b 100644 --- a/src/window.h +++ b/src/window.h @@ -286,6 +286,10 @@ struct window it should be positive. */ ptrdiff_t last_point; + /* Value of mark in the selected window at the time of the last + redisplay. */ + ptrdiff_t last_mark; + /* Line number and position of a line somewhere above the top of the screen. If this field is zero, it means we don't have a base line. */ ptrdiff_t base_line_number; diff --git a/src/xdisp.c b/src/xdisp.c index 4b91fcf31ca..525eaa64b3a 100644 --- a/src/xdisp.c +++ b/src/xdisp.c @@ -17268,7 +17268,7 @@ mark_window_display_accurate_1 (struct window *w, bool accurate_p) { struct buffer *b = XBUFFER (w->contents); #ifdef HAVE_TEXT_CONVERSION - ptrdiff_t prev_point; + ptrdiff_t prev_point, prev_mark; #endif w->last_modified = accurate_p ? BUF_MODIFF (b) : 0; @@ -17301,6 +17301,7 @@ mark_window_display_accurate_1 (struct window *w, bool accurate_p) #ifdef HAVE_TEXT_CONVERSION prev_point = w->last_point; + prev_mark = w->last_mark; #endif if (w == XWINDOW (selected_window)) @@ -17308,6 +17309,11 @@ mark_window_display_accurate_1 (struct window *w, bool accurate_p) else w->last_point = marker_position (w->pointm); + if (XMARKER (BVAR (b, mark))->buffer == b) + w->last_mark = marker_position (BVAR (b, mark)); + else + w->last_mark = -1; + #ifdef HAVE_TEXT_CONVERSION /* Point motion is only propagated to the input method for use in text conversion during a redisplay. While this can lead @@ -17323,7 +17329,8 @@ mark_window_display_accurate_1 (struct window *w, bool accurate_p) during such a change, so the consequences are not that severe. */ - if (prev_point != w->last_point + if ((prev_point != w->last_point + || prev_mark != w->last_mark) && FRAME_WINDOW_P (WINDOW_XFRAME (w)) && w == XWINDOW (WINDOW_XFRAME (w)->selected_window)) report_point_change (WINDOW_XFRAME (w), w, b); -- cgit v1.3 From 2dcce30290dc7782e9de3b4adf59f38b42408c98 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 16 Feb 2023 23:57:01 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Fonts): * doc/emacs/input.texi (On-Screen Keyboards): * doc/lispref/commands.texi (Misc Events): Update documentation. * java/org/gnu/emacs/EmacsInputConnection.java (setSelection): New function. * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView) (reconfigureFrontBuffer): Make bitmap references weak references. * java/org/gnu/emacs/EmacsView.java (handleDirtyBitmap): Don't clear surfaceView bitmap. * lisp/comint.el (comint-mode): * lisp/international/fontset.el (script-representative-chars) (setup-default-fontset): Improve detection of CJK fonts. * lisp/isearch.el (set-text-conversion-style): New variable. (isearch-mode, isearch-done): Save and restore the text conversion style. * lisp/minibuffer.el (minibuffer-mode): Set an appropriate text conversion style. * lisp/simple.el (analyze-text-conversion): Run post-self-insert-hook properly. * lisp/subr.el (read-char-from-minibuffer): Disable text conversion when reading character. * src/androidterm.c (show_back_buffer): Don't check that F is not garbaged. (android_update_selection, android_reset_conversion): Use the ephemeral last point and handle text conversion being disabled. * src/buffer.c (syms_of_buffer): Convert old style DEFVAR. * src/keyboard.c (kbd_buffer_get_event): Handle text conversion first. * src/lisp.h: Update prototypes. * src/lread.c (read_filtered_event): Temporarily disable text conversion. * src/sfnt.c (sfnt_decompose_glyph_1, sfnt_decompose_glyph_2): New functions. (sfnt_decompose_glyph, sfnt_decompose_instructed_outline): Refactor contour decomposition to those two functions. (main): Update tests. * src/sfntfont-android.c (system_font_directories): Add empty field. (Fandroid_enumerate_fonts, init_sfntfont_android): Enumerate fonts in a user fonts directory. * src/sfntfont.c (struct sfnt_font_desc): New field `num_glyphs'. (sfnt_enum_font_1): Set num_glyphs and avoid duplicate fonts. (sfntfont_glyph_valid): New function. (sfntfont_lookup_char, sfntfont_list_1): Make sure glyphs found are valid. * src/textconv.c (sync_overlay, really_commit_text) (really_set_composing_text, really_set_composing_region) (really_delete_surrounding_text, really_set_point_and_mark) (handle_pending_conversion_events_1) (handle_pending_conversion_events, conversion_disabled_p) (disable_text_conversion, resume_text_conversion) (Fset_text_conversion_style, syms_of_textconv): Update to respect new options. * src/textconv.h: * src/window.h (GCALIGNED_STRUCT): New field `ephemeral_last_point'. * src/xdisp.c (mark_window_display_accurate_1): Set it. --- doc/emacs/android.texi | 8 +- doc/emacs/input.texi | 7 + doc/lispref/commands.texi | 11 +- java/org/gnu/emacs/EmacsInputConnection.java | 14 +- java/org/gnu/emacs/EmacsSurfaceView.java | 17 +- java/org/gnu/emacs/EmacsView.java | 8 +- lisp/comint.el | 3 + lisp/international/fontset.el | 14 +- lisp/isearch.el | 17 + lisp/minibuffer.el | 5 +- lisp/simple.el | 11 +- lisp/subr.el | 25 +- src/androidterm.c | 25 +- src/buffer.c | 22 +- src/keyboard.c | 39 +- src/lisp.h | 2 + src/lread.c | 26 +- src/sfnt.c | 625 ++++++++++++++------------- src/sfntfont-android.c | 13 +- src/sfntfont.c | 87 +++- src/textconv.c | 234 +++++++++- src/textconv.h | 1 + src/window.h | 15 + src/xdisp.c | 3 + 24 files changed, 804 insertions(+), 428 deletions(-) (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index 308bd70c59c..35f0ba55cf4 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -446,10 +446,10 @@ application via cut-and-paste. named @code{sfnt-android} and @code{android}. Upon startup, Emacs enumerates all the TrueType format fonts in the -directory @file{/system/fonts}; this is where the Android system -places fonts. Emacs assumes there will always be a font named ``Droid -Sans Mono'', and then defaults to using this font. These fonts are -then rendered by the @code{sfnt-android} font driver. +directory @file{/system/fonts}, and the @file{fonts} directory inside +the Emacs home directory. Emacs assumes there will always be a font +named ``Droid Sans Mono'', and then defaults to using this font. +These fonts are then displayed by the @code{sfnt-android} font driver. When running on Android, Emacs currently lacks support for OpenType fonts. This means that only a subset of the fonts installed on the diff --git a/doc/emacs/input.texi b/doc/emacs/input.texi index 154b7025ff4..87ed1f4f8a9 100644 --- a/doc/emacs/input.texi +++ b/doc/emacs/input.texi @@ -129,3 +129,10 @@ derivatives of @code{text-mode} and @code{prog-mode}. a request to perform the conversion from the input method. After the conversion completes, a @code{text-conversion} event is sent. @xref{Misc Events,,, elisp, the Emacs Reference Manual}. + +@vindex text-conversion-face + If the input method needs to work on a region of the buffer, then +the region becomes known as the ``composing region'' (or +``preconversion region''.) The variable @code{text-conversion-face} +describes whether or not to display the composing region in a specific +face. diff --git a/doc/lispref/commands.texi b/doc/lispref/commands.texi index 5fb2217a03b..898cc9a944b 100644 --- a/doc/lispref/commands.texi +++ b/doc/lispref/commands.texi @@ -2231,7 +2231,7 @@ buffer-local variable @code{text-conversion-style}, which determines how an input method that wishes to make edits to buffer contents will behave. -This variable can have one three values: +This variable can have one of three values: @table @code @item nil @@ -2245,11 +2245,16 @@ be sent wherever the input method wanted to insert a new line. @item t This, or any other value, means that the input method will be enabled and make edits terminated by @code{text-conversion} events. -@end itemize +@end table +@findex disable-text-conversion Changes to the value of this variable will only take effect upon the next redisplay after the buffer becomes the selected buffer -of a frame. +of a frame. If you need to disable text conversion in a way +that takes immediate effect, call the function +@code{set-text-conversion-style} instead. This can potentially +lock up the input method for a significant amount of time, so do +not do this lightly! @cindex @code{delete-frame} event @item (delete-frame (@var{frame})) diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 5eb56d5aa71..e2a15894695 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -173,7 +173,8 @@ public class EmacsInputConnection extends BaseInputConnection setComposingText (CharSequence text, int newCursorPosition) { if (EmacsService.DEBUG_IC) - Log.d (TAG, "setComposingText: " + newCursorPosition); + Log.d (TAG, ("setComposingText: " + + text + " ## " + newCursorPosition)); EmacsNative.setComposingText (windowHandle, text.toString (), newCursorPosition); @@ -213,6 +214,17 @@ public class EmacsInputConnection extends BaseInputConnection flags); } + @Override + public boolean + setSelection (int start, int end) + { + if (EmacsService.DEBUG_IC) + Log.d (TAG, "setSelection: " + start + " " + end); + + EmacsNative.setSelection (windowHandle, start, end); + return true; + } + /* Override functions which are not implemented. */ diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index 2d80be0881a..62e927094e4 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -28,6 +28,8 @@ import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.Paint; +import java.lang.ref.WeakReference; + /* This originally extended SurfaceView. However, doing so proved to be too slow, and Android's surface view keeps up to three of its own back buffers, which use too much memory (up to 96 MB for a @@ -39,7 +41,7 @@ public class EmacsSurfaceView extends View private EmacsView view; private Bitmap frontBuffer; private Canvas bitmapCanvas; - private Bitmap bitmap; + private WeakReference bitmap; private Paint bitmapPaint; public @@ -49,10 +51,11 @@ public class EmacsSurfaceView extends View this.view = view; this.bitmapPaint = new Paint (); + this.bitmap = new WeakReference (null); } private void - copyToFrontBuffer (Rect damageRect) + copyToFrontBuffer (Bitmap bitmap, Rect damageRect) { if (damageRect != null) bitmapCanvas.drawBitmap (bitmap, damageRect, damageRect, @@ -73,7 +76,7 @@ public class EmacsSurfaceView extends View bitmapCanvas = null; } - this.bitmap = bitmap; + this.bitmap = new WeakReference (bitmap); /* Next, create the new front buffer if necessary. */ @@ -92,20 +95,20 @@ public class EmacsSurfaceView extends View bitmapCanvas = new Canvas (frontBuffer); /* And copy over the bitmap contents. */ - copyToFrontBuffer (null); + copyToFrontBuffer (bitmap, null); } else if (bitmap != null) /* Just copy over the bitmap contents. */ - copyToFrontBuffer (null); + copyToFrontBuffer (bitmap, null); } public synchronized void setBitmap (Bitmap bitmap, Rect damageRect) { - if (bitmap != this.bitmap) + if (bitmap != this.bitmap.get ()) reconfigureFrontBuffer (bitmap); else if (bitmap != null) - copyToFrontBuffer (damageRect); + copyToFrontBuffer (bitmap, damageRect); if (bitmap != null) { diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 52da6d41f7d..5ea8b0dcc0e 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -186,13 +186,7 @@ public class EmacsView extends ViewGroup /* Explicitly free the old bitmap's memory. */ if (oldBitmap != null) - { - oldBitmap.recycle (); - - /* Make sure to set the view's bitmap to the new bitmap, or - ugly flicker can result. */ - surfaceView.setBitmap (bitmap, null); - } + oldBitmap.recycle (); /* Some Android versions still don't free the bitmap until the next GC. */ diff --git a/lisp/comint.el b/lisp/comint.el index 9d2c245247f..e1786e7e670 100644 --- a/lisp/comint.el +++ b/lisp/comint.el @@ -694,6 +694,9 @@ Entry to this mode runs the hooks on `comint-mode-hook'." (setq-local comint-last-input-start (point-min-marker)) (setq-local comint-last-input-end (point-min-marker)) (setq-local comint-last-output-start (make-marker)) + ;; It is ok to let the input method edit prompt text, but RET must + ;; be processed by Emacs. + (setq text-conversion-style 'action) (make-local-variable 'comint-last-prompt) (make-local-variable 'comint-prompt-regexp) ; Don't set; default (make-local-variable 'comint-input-ring-size) ; ...to global val. diff --git a/lisp/international/fontset.el b/lisp/international/fontset.el index eb1c7f53d36..8fa67d99477 100644 --- a/lisp/international/fontset.el +++ b/lisp/international/fontset.el @@ -200,7 +200,7 @@ (symbol . [#x201C #x2200 #x2500]) (braille #x2800) (ideographic-description #x2FF0) - (cjk-misc #x300E) + (cjk-misc #x300E #xff0c) (kana #x304B) (bopomofo #x3105) (kanbun #x319D) @@ -683,7 +683,11 @@ (nil . "JISX0213.2000-2") (nil . "JISX0213.2004-1") ,(font-spec :registry "iso10646-1" :lang 'ja) - ,(font-spec :registry "iso10646-1" :lang 'zh)) + ,(font-spec :registry "iso10646-1" :lang 'zh) + ;; This is required, as otherwise many TrueType fonts with + ;; CJK characters but no corresponding ``design language'' + ;; declaration can't be found. + ,(font-spec :registry "iso10646-1" :script 'han)) (cjk-misc (nil . "GB2312.1980-0") (nil . "JISX0208*") @@ -702,7 +706,11 @@ (nil . "JISX0213.2000-1") (nil . "JISX0213.2000-2") ,(font-spec :registry "iso10646-1" :lang 'ja) - ,(font-spec :registry "iso10646-1" :lang 'zh)) + ,(font-spec :registry "iso10646-1" :lang 'zh) + ;; This is required, as otherwise many TrueType fonts + ;; with CJK characters but no corresponding ``design + ;; language'' declaration can't be found. + ,(font-spec :registry "iso10646-1" :script 'cjk-misc)) (hangul (nil . "KSC5601.1987-0") ,(font-spec :registry "iso10646-1" :lang 'ko)) diff --git a/lisp/isearch.el b/lisp/isearch.el index 80c7a3b3d3f..a17b22fd627 100644 --- a/lisp/isearch.el +++ b/lisp/isearch.el @@ -244,6 +244,10 @@ If you use `add-function' to modify this variable, you can use the `isearch-message-prefix' advice property to specify the prefix string displayed in the search message.") +(defvar isearch-text-conversion-style nil + "Value of `text-conversion-style' before Isearch mode +was enabled in this buffer.") + ;; Search ring. (defvar search-ring nil @@ -1221,6 +1225,8 @@ active region is added to the search string." ;; isearch-forward-regexp isearch-backward-regexp) ;; "List of commands for which isearch-mode does not recursive-edit.") +(declare-function set-text-conversion-style "textconv.c") + (defun isearch-mode (forward &optional regexp op-fun recursive-edit regexp-function) "Start Isearch minor mode. It is called by the function `isearch-forward' and other related functions. @@ -1342,6 +1348,13 @@ used to set the value of `isearch-regexp-function'." 'keyboard))) (frame-toggle-on-screen-keyboard (selected-frame) nil)) + ;; Disable text conversion so that isearch can behave correctly. + + (when (fboundp 'set-text-conversion-style) + (setq isearch-text-conversion-style + text-conversion-style) + (set-text-conversion-style nil)) + ;; isearch-mode can be made modal (in the sense of not returning to ;; the calling function until searching is completed) by entering ;; a recursive-edit and exiting it when done isearching. @@ -1475,6 +1488,10 @@ NOPUSH is t and EDIT is t." (setq isearch-tool-bar-old-map nil)) (kill-local-variable 'tool-bar-map)) + ;; Restore the previous text conversion style. + (when (fboundp 'set-text-conversion-style) + (set-text-conversion-style isearch-text-conversion-style)) + (force-mode-line-update) ;; If we ended in the middle of some intangible text, diff --git a/lisp/minibuffer.el b/lisp/minibuffer.el index 8c1a0d4b21c..ef0eb1ca108 100644 --- a/lisp/minibuffer.el +++ b/lisp/minibuffer.el @@ -2918,7 +2918,10 @@ For customizing this mode, it is better to use `minibuffer-setup-hook' and `minibuffer-exit-hook' rather than the mode hook of this mode." :syntax-table nil - :interactive nil) + :interactive nil + ;; Enable text conversion, but always make sure `RET' does + ;; something. + (setq text-conversion-style 'action)) ;;; Completion tables. diff --git a/lisp/simple.el b/lisp/simple.el index 6a12585a55d..e5bf90a8055 100644 --- a/lisp/simple.el +++ b/lisp/simple.el @@ -10887,8 +10887,9 @@ For each insertion: line breaking of the previous line when `auto-fill-mode' is enabled. - - Look for the insertion of a new line, and indent this new - line if `electric-indent-mode' is enabled." + - Run `post-self-insert-functions' for the last character of + any inserted text so that modes such as `electric-pair-mode' + can work." (interactive) (dolist (edit text-conversion-edits) ;; Filter out ephemeral edits and deletions. @@ -10912,9 +10913,9 @@ For each insertion: (when (and auto-fill-function auto-fill-p) (progn (goto-char (nth 2 edit)) (funcall auto-fill-function))))) - (when (and electric-indent-mode newline-p) - (goto-char (nth 2 edit)) - (indent-according-to-mode))))))) + (goto-char (nth 2 edit)) + (let ((last-command-event end)) + (run-hooks 'post-self-insert-hook))))))) diff --git a/lisp/subr.el b/lisp/subr.el index f4f041ff32d..8e594fafa8c 100644 --- a/lisp/subr.el +++ b/lisp/subr.el @@ -3424,6 +3424,9 @@ an error message." (minibuffer-message "Wrong answer") (sit-for 2))) +;; Defined in textconv.c. +(defvar overriding-text-conversion-style) + (defun read-char-from-minibuffer (prompt &optional chars history) "Read a character from the minibuffer, prompting for it with PROMPT. Like `read-char', but uses the minibuffer to read and return a character. @@ -3438,7 +3441,15 @@ while calling this function, then pressing `help-char' causes it to evaluate `help-form' and display the result. There is no need to explicitly add `help-char' to CHARS; `help-char' is bound automatically to `help-form-show'." - (let* ((map (if (consp chars) + + ;; If text conversion is enabled in this buffer, then it will only + ;; be disabled the next time `force-mode-line-update' happens. + (when (and overriding-text-conversion-style + text-conversion-style) + (force-mode-line-update)) + + (let* ((overriding-text-conversion-style nil) + (map (if (consp chars) (or (gethash (list help-form (cons help-char chars)) read-char-from-minibuffer-map-hash) (let ((map (make-sparse-keymap)) @@ -3450,15 +3461,15 @@ There is no need to explicitly add `help-char' to CHARS; ;; being a command char. (when help-form (define-key map (vector help-char) - (lambda () - (interactive) - (let ((help-form msg)) ; lexically bound msg - (help-form-show))))) + (lambda () + (interactive) + (let ((help-form msg)) ; lexically bound msg + (help-form-show))))) (dolist (char chars) (define-key map (vector char) - #'read-char-from-minibuffer-insert-char)) + #'read-char-from-minibuffer-insert-char)) (define-key map [remap self-insert-command] - #'read-char-from-minibuffer-insert-other) + #'read-char-from-minibuffer-insert-other) (puthash (list help-form (cons help-char chars)) map read-char-from-minibuffer-map-hash) map)) diff --git a/src/androidterm.c b/src/androidterm.c index 0c990d3d2d2..c6f75ec9219 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -258,10 +258,6 @@ show_back_buffer (struct frame *f) { struct android_swap_info swap_info; - /* Somehow Android frames can be swapped while garbaged. */ - if (FRAME_GARBAGED_P (f)) - return; - memset (&swap_info, 0, sizeof (swap_info)); swap_info.swap_window = FRAME_ANDROID_WINDOW (f); swap_info.swap_action = ANDROID_COPIED; @@ -5128,7 +5124,8 @@ android_update_selection (struct frame *f, struct window *w) /* Figure out where the point and mark are. If the mark is not active, then point is set to equal mark. */ b = XBUFFER (w->contents); - point = min (w->last_point, TYPE_MAXIMUM (jint)); + point = min (w->ephemeral_last_point, + TYPE_MAXIMUM (jint)); mark = ((!NILP (BVAR (b, mark_active)) && w->last_mark != -1) ? min (w->last_mark, TYPE_MAXIMUM (jint)) @@ -5150,6 +5147,7 @@ android_reset_conversion (struct frame *f) enum android_ic_mode mode; struct window *w; struct buffer *buffer; + Lisp_Object style; /* Reset the input method. @@ -5160,19 +5158,20 @@ android_reset_conversion (struct frame *f) w = XWINDOW (f->selected_window); buffer = XBUFFER (WINDOW_BUFFER (w)); - if (NILP (BVAR (buffer, text_conversion_style))) + style = (EQ (find_symbol_value (Qoverriding_text_conversion_style), + Qlambda) + ? BVAR (buffer, text_conversion_style) + : find_symbol_value (Qoverriding_text_conversion_style)); + + if (NILP (style) || conversion_disabled_p ()) mode = ANDROID_IC_MODE_NULL; - else if (EQ (BVAR (buffer, text_conversion_style), - Qaction)) + else if (EQ (style, Qaction) || EQ (f->selected_window, + f->minibuffer_window)) mode = ANDROID_IC_MODE_ACTION; else mode = ANDROID_IC_MODE_TEXT; - android_reset_ic (FRAME_ANDROID_WINDOW (f), - (EQ (f->selected_window, - f->minibuffer_window) - ? ANDROID_IC_MODE_ACTION - : mode)); + android_reset_ic (FRAME_ANDROID_WINDOW (f), mode); /* Move its selection to the specified position. */ android_update_selection (f, NULL); diff --git a/src/buffer.c b/src/buffer.c index af4aa583c96..393b8c5340a 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -5862,15 +5862,19 @@ Use Custom to set this variable and update the display. */); DEFVAR_PER_BUFFER ("text-conversion-style", &BVAR (current_buffer, text_conversion_style), Qnil, - "How the on screen keyboard's input method should insert in this buffer.\n\ -When nil, the input method will be disabled and an ordinary keyboard\n\ -will be displayed in its place.\n\ -When the symbol `action', the input method will insert text directly, but\n\ -will send `return' key events instead of inserting new line characters.\n\ -Any other value means that the input method will insert text directly.\n\ -\n\ -This variable does not take immediate effect when set; rather, it takes\n\ -effect upon the next redisplay after the selected window or buffer changes."); + doc: /* How the on screen keyboard's input method should insert in this buffer. +When nil, the input method will be disabled and an ordinary keyboard +will be displayed in its place. +When the symbol `action', the input method will insert text directly, but +will send `return' key events instead of inserting new line characters. +Any other value means that the input method will insert text directly. + +If you need to make non-buffer local changes to this variable, use +`overriding-text-conversion-style', which see. + +This variable does not take immediate effect when set; rather, it +takes effect upon the next redisplay after the selected window or +buffer changes. */); DEFVAR_LISP ("kill-buffer-query-functions", Vkill_buffer_query_functions, doc: /* List of functions called with no args to query before killing a buffer. diff --git a/src/keyboard.c b/src/keyboard.c index 538fdffc199..a8062adc468 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -4050,14 +4050,6 @@ kbd_buffer_get_event (KBOARD **kbp, x_handle_pending_selection_requests (); #endif -#ifdef HAVE_TEXT_CONVERSION - /* Handle pending ``text conversion'' requests from an input - method. */ - - if (had_pending_conversion_events) - handle_pending_conversion_events (); -#endif - if (CONSP (Vunread_command_events)) { Lisp_Object first; @@ -4067,6 +4059,24 @@ kbd_buffer_get_event (KBOARD **kbp, return first; } +#ifdef HAVE_TEXT_CONVERSION + /* There are pending text conversion operations. Text conversion + events should be generated before processing any other keyboard + input. */ + if (had_pending_conversion_events) + { + handle_pending_conversion_events (); + obj = Qtext_conversion; + + /* See the comment in handle_pending_conversion_events_1. + Note that in addition, text conversion events are not + generated if no edits were actually made. */ + if (conversion_disabled_p () + || NILP (Vtext_conversion_edits)) + obj = Qnil; + } + else +#endif /* At this point, we know that there is a readable event available somewhere. If the event queue is empty, then there must be a mouse movement enabled and available. */ @@ -4414,25 +4424,12 @@ kbd_buffer_get_event (KBOARD **kbp, #ifdef HAVE_X_WINDOWS else if (had_pending_selection_requests) obj = Qnil; -#endif -#ifdef HAVE_TEXT_CONVERSION - /* This is an internal event used to prevent Emacs from becoming - idle immediately after a text conversion operation. */ - else if (had_pending_conversion_events) - obj = Qtext_conversion; #endif else /* We were promised by the above while loop that there was something for us to read! */ emacs_abort (); -#ifdef HAVE_TEXT_CONVERSION - /* While not implemented as keyboard commands, changes made by the - input method still mean that Emacs is no longer idle. */ - if (had_pending_conversion_events) - timer_stop_idle (); -#endif - input_pending = readable_events (0); Vlast_event_frame = internal_last_event_frame; diff --git a/src/lisp.h b/src/lisp.h index a39ca8cc541..56ef338a5b1 100644 --- a/src/lisp.h +++ b/src/lisp.h @@ -5234,6 +5234,8 @@ extern void reset_frame_state (struct frame *); extern void report_selected_window_change (struct frame *); extern void report_point_change (struct frame *, struct window *, struct buffer *); +extern void disable_text_conversion (void); +extern void resume_text_conversion (void); extern void syms_of_textconv (void); #endif diff --git a/src/lread.c b/src/lread.c index 6abcea556bf..150d8a01e10 100644 --- a/src/lread.c +++ b/src/lread.c @@ -672,7 +672,11 @@ static void substitute_in_interval (INTERVAL, void *); if the character warrants that. If SECONDS is a number, wait that many seconds for input, and - return Qnil if no input arrives within that time. */ + return Qnil if no input arrives within that time. + + If text conversion is enabled and ASCII_REQUIRED && ERROR_NONASCII, + temporarily disable any input method which wants to perform + edits. */ static Lisp_Object read_filtered_event (bool no_switch_frame, bool ascii_required, @@ -680,12 +684,28 @@ read_filtered_event (bool no_switch_frame, bool ascii_required, { Lisp_Object val, delayed_switch_frame; struct timespec end_time; +#ifdef HAVE_TEXT_CONVERSION + specpdl_ref count; +#endif #ifdef HAVE_WINDOW_SYSTEM if (display_hourglass_p) cancel_hourglass (); #endif +#ifdef HAVE_TEXT_CONVERSION + count = SPECPDL_INDEX (); + + /* Don't use text conversion when trying to just read a + character. */ + + if (ascii_required && error_nonascii) + { + disable_text_conversion (); + record_unwind_protect_void (resume_text_conversion); + } +#endif + delayed_switch_frame = Qnil; /* Compute timeout. */ @@ -761,7 +781,11 @@ read_filtered_event (bool no_switch_frame, bool ascii_required, #endif +#ifdef HAVE_TEXT_CONVERSION + return unbind_to (count, val); +#else return val; +#endif } DEFUN ("read-char", Fread_char, Sread_char, 0, 3, 0, diff --git a/src/sfnt.c b/src/sfnt.c index 38f6984b93c..b6f4a48ea8b 100644 --- a/src/sfnt.c +++ b/src/sfnt.c @@ -2708,6 +2708,281 @@ sfnt_lerp_half (struct sfnt_point *control1, struct sfnt_point *control2, result->y = control1->y + ((control2->y - control1->y) >> 1); } +/* Decompose contour data inside X, Y and FLAGS, between the indices + HERE and LAST. Call LINE_TO, CURVE_TO and MOVE_TO as appropriate, + with DCONTEXT as an argument. Apply SCALE to each point; SCALE + should be the factor necessary to turn points into 16.16 fixed + point. + + Value is 1 upon failure, else 0. */ + +static int +sfnt_decompose_glyph_1 (size_t here, size_t last, + sfnt_move_to_proc move_to, + sfnt_line_to_proc line_to, + sfnt_curve_to_proc curve_to, + void *dcontext, + sfnt_fword *x, + sfnt_fword *y, unsigned char *flags, + int scale) +{ + struct sfnt_point control1, control2, start, mid; + size_t i; + + /* The contour is empty. */ + + if (here == last) + return 1; + + /* Move the pen to the start of the contour. Apparently some fonts + have off the curve points as the start of a contour, so when that + happens lerp between the first and last points. */ + + if (flags[here] & 01) /* On Curve */ + { + control1.x = x[here] * scale; + control1.y = y[here] * scale; + start = control1; + } + else if (flags[last] & 01) + { + /* Start at the last point if it is on the curve. Here, the + start really becomes the middle of a spline. */ + control1.x = x[last] * scale; + control1.y = y[last] * scale; + start = control1; + + /* Curve back one point early. */ + last -= 1; + here -= 1; + } + else + { + /* Lerp between the start and the end. */ + control1.x = x[here] * scale; + control1.y = y[here] * scale; + control2.x = x[last] * scale; + control2.y = y[last] * scale; + sfnt_lerp_half (&control1, &control2, &start); + + /* In either of these cases, start iterating from just here as + opposed to here + 1, since logically the contour now starts + from the last curve. */ + here -= 1; + } + + /* Move to the start. */ + move_to (start, dcontext); + + /* Now handle each point between here + 1 and last. */ + + i = here; + while (++i <= last) + { + /* If the point is on the curve, then draw a line here from the + last control point. */ + + if (flags[i] & 01) + { + control1.x = x[i] * scale; + control1.y = y[i] * scale; + + line_to (control1, dcontext); + + /* Move to the next point. */ + continue; + } + + /* Off the curve points are more interesting. They are handled + one by one, with points in between being interpolated, until + either the last point is reached or an on-curve point is + processed. First, load the initial control points. */ + + control1.x = x[i] * scale; + control1.y = y[i] * scale; + + while (++i <= last) + { + /* Load this point. */ + control2.x = x[i] * scale; + control2.y = y[i] * scale; + + /* If this point is on the curve, curve directly to this + point. */ + + if (flags[i] & 01) + { + curve_to (control1, control2, dcontext); + goto continue_loop; + } + + /* Calculate the point between here and the previous + point. */ + sfnt_lerp_half (&control1, &control2, &mid); + + /* Curve over there. */ + curve_to (control1, mid, dcontext); + + /* Reload the control point. */ + control1 = control2; + } + + /* Close the contour by curving back to start. */ + curve_to (control1, start, dcontext); + + /* Don't close the contour twice. */ + goto exit; + + continue_loop: + continue; + } + + /* Close the contour with a line back to start. */ + line_to (start, dcontext); + + exit: + return 0; +} + +/* Decompose contour data inside X, Y and FLAGS, between the indices + HERE and LAST. Call LINE_TO, CURVE_TO and MOVE_TO as appropriate, + with DCONTEXT as an argument. Apply SCALE to each point; SCALE + should be the factor necessary to turn points into 16.16 fixed + point. + + This is the version of sfnt_decompose_glyph_1 which takes + sfnt_fixed (or sfnt_f26dot6) as opposed to sfnt_fword. + + Value is 1 upon failure, else 0. */ + +static int +sfnt_decompose_glyph_2 (size_t here, size_t last, + sfnt_move_to_proc move_to, + sfnt_line_to_proc line_to, + sfnt_curve_to_proc curve_to, + void *dcontext, + sfnt_fixed *x, + sfnt_fixed *y, unsigned char *flags, + int scale) +{ + struct sfnt_point control1, control2, start, mid; + size_t i; + + /* The contour is empty. */ + + if (here == last) + return 1; + + /* Move the pen to the start of the contour. Apparently some fonts + have off the curve points as the start of a contour, so when that + happens lerp between the first and last points. */ + + if (flags[here] & 01) /* On Curve */ + { + control1.x = x[here] * scale; + control1.y = y[here] * scale; + start = control1; + } + else if (flags[last] & 01) + { + /* Start at the last point if it is on the curve. Here, the + start really becomes the middle of a spline. */ + control1.x = x[last] * scale; + control1.y = y[last] * scale; + start = control1; + + /* Curve back one point early. */ + last -= 1; + here -= 1; + } + else + { + /* Lerp between the start and the end. */ + control1.x = x[here] * scale; + control1.y = y[here] * scale; + control2.x = x[last] * scale; + control2.y = y[last] * scale; + sfnt_lerp_half (&control1, &control2, &start); + + /* In either of these cases, start iterating from just here as + opposed to here + 1, since logically the contour now starts + from the last curve. */ + here -= 1; + } + + /* Move to the start. */ + move_to (start, dcontext); + + /* Now handle each point between here + 1 and last. */ + + i = here; + while (++i <= last) + { + /* If the point is on the curve, then draw a line here from the + last control point. */ + + if (flags[i] & 01) + { + control1.x = x[i] * scale; + control1.y = y[i] * scale; + + line_to (control1, dcontext); + + /* Move to the next point. */ + continue; + } + + /* Off the curve points are more interesting. They are handled + one by one, with points in between being interpolated, until + either the last point is reached or an on-curve point is + processed. First, load the initial control points. */ + + control1.x = x[i] * scale; + control1.y = y[i] * scale; + + while (++i <= last) + { + /* Load this point. */ + control2.x = x[i] * scale; + control2.y = y[i] * scale; + + /* If this point is on the curve, curve directly to this + point. */ + + if (flags[i] & 01) + { + curve_to (control1, control2, dcontext); + goto continue_loop; + } + + /* Calculate the point between here and the previous + point. */ + sfnt_lerp_half (&control1, &control2, &mid); + + /* Curve over there. */ + curve_to (control1, mid, dcontext); + + /* Reload the control point. */ + control1 = control2; + } + + /* Close the contour by curving back to start. */ + curve_to (control1, start, dcontext); + + /* Don't close the contour twice. */ + goto exit; + + continue_loop: + continue; + } + + /* Close the contour with a line back to start. */ + line_to (start, dcontext); + + exit: + return 0; +} + /* Decompose GLYPH into its individual components. Call MOVE_TO to move to a specific location. For each line encountered, call LINE_TO to draw a line to that location. For each spline @@ -2736,10 +3011,8 @@ sfnt_decompose_glyph (struct sfnt_glyph *glyph, sfnt_free_glyph_proc free_glyph, void *dcontext) { - size_t here, start, last; - struct sfnt_point pen, control1, control2; + size_t here, last, n; struct sfnt_compound_glyph_context context; - size_t n; if (glyph->simple) { @@ -2756,113 +3029,23 @@ sfnt_decompose_glyph (struct sfnt_glyph *glyph, of the last point in the contour. */ last = glyph->simple->end_pts_of_contours[n]; - /* Move to the start. */ - pen.x = glyph->simple->x_coordinates[here] * 65536; - pen.y = glyph->simple->y_coordinates[here] * 65536; - move_to (pen, dcontext); - - /* Record start so the contour can be closed. */ - start = here; + /* Make sure here and last make sense. */ - /* If there is only one point in a contour, draw a one pixel - wide line. */ - if (last == here) - { - line_to (pen, dcontext); - here++; - - continue; - } - - if (here > last) - /* Indices moved backwards. */ + if (here > last || last >= glyph->simple->number_of_points) return 1; - /* Now start reading points. If the next point is on the - curve, then it is actually a line. */ - for (++here; here <= last; ++here) - { - /* Make sure here is within bounds. */ - if (here >= glyph->simple->number_of_points) - return 1; - - if (glyph->simple->flags[here] & 01) /* On Curve */ - { - pen.x = glyph->simple->x_coordinates[here] * 65536; - pen.y = glyph->simple->y_coordinates[here] * 65536; - - /* See if the last point was on the curve. If it - wasn't, then curve from there to here. */ - if (!(glyph->simple->flags[here - 1] & 01)) - { - control1.x - = glyph->simple->x_coordinates[here - 1] * 65536; - control1.y - = glyph->simple->y_coordinates[here - 1] * 65536; - curve_to (control1, pen, dcontext); - } - else - /* Otherwise, this is an ordinary line from there - to here. */ - line_to (pen, dcontext); - - continue; - } - - /* If the last point was on the curve, then there's - nothing extraordinary to do yet. */ - if (glyph->simple->flags[here - 1] & 01) - ; - else - { - /* Otherwise, interpolate the point halfway between - the last and current points and make that point - the pen. */ - control1.x = glyph->simple->x_coordinates[here - 1] * 65536; - control1.y = glyph->simple->y_coordinates[here - 1] * 65536; - control2.x = glyph->simple->x_coordinates[here] * 65536; - control2.y = glyph->simple->y_coordinates[here] * 65536; - sfnt_lerp_half (&control1, &control2, &pen); - curve_to (control1, pen, dcontext); - } - } - - /* Now close the contour if there is more than one point - inside it. */ - if (start != here - 1) - { - /* Restore here after the for loop increased it. */ - here --; - - /* Previously, this would check whether or not start is - an ``on curve'' point, but that is not necessary. - - If a contour is not closed and the edge building - process skips the second to last vertex, then the - outline can end up with missing edges. */ - - pen.x = glyph->simple->x_coordinates[start] * 65536; - pen.y = glyph->simple->y_coordinates[start] * 65536; - - /* See if the last point (in this case, `here') was - on the curve. If it wasn't, then curve from - there to here. */ - if (!(glyph->simple->flags[here] & 01)) - { - control1.x - = glyph->simple->x_coordinates[here] * 65536; - control1.y - = glyph->simple->y_coordinates[here] * 65536; - curve_to (control1, pen, dcontext); - } - else - /* Otherwise, this is an ordinary line from there - to here. */ - line_to (pen, dcontext); + /* Now perform the decomposition. */ + if (sfnt_decompose_glyph_1 (here, last, move_to, + line_to, curve_to, + dcontext, + glyph->simple->x_coordinates, + glyph->simple->y_coordinates, + glyph->simple->flags, + 65536)) + return 1; - /* Restore here to where it was earlier. */ - here++; - } + /* Move forward to the start of the next contour. */ + here = last + 1; } return 0; @@ -2898,108 +3081,22 @@ sfnt_decompose_glyph (struct sfnt_glyph *glyph, of the last point in the contour. */ last = context.contour_end_points[n]; - /* Move to the start. */ - pen.x = context.x_coordinates[here]; - pen.y = context.y_coordinates[here]; - move_to (pen, dcontext); - - /* Record start so the contour can be closed. */ - start = here; - - /* If there is only one point in a contour, draw a one pixel - wide line. */ - if (last == here) - { - line_to (pen, dcontext); - here++; - - continue; - } + /* Make sure here and last make sense. */ - if (here > last) - /* Indices moved backwards. */ + if (here > last || last >= context.num_points) goto fail; - /* Now start reading points. If the next point is on the - curve, then it is actually a line. */ - for (++here; here <= last; ++here) - { - /* Make sure here is within bounds. */ - if (here >= context.num_points) - return 1; - - if (context.flags[here] & 01) /* On Curve */ - { - pen.x = context.x_coordinates[here]; - pen.y = context.y_coordinates[here]; - - /* See if the last point was on the curve. If it - wasn't, then curve from there to here. */ - if (!(context.flags[here - 1] & 01)) - { - control1.x = context.x_coordinates[here - 1]; - control1.y = context.y_coordinates[here - 1]; - curve_to (control1, pen, dcontext); - } - else - /* Otherwise, this is an ordinary line from there - to here. */ - line_to (pen, dcontext); - - continue; - } - - /* If the last point was on the curve, then there's - nothing extraordinary to do yet. */ - if (context.flags[here - 1] & 01) - ; - else - { - /* Otherwise, interpolate the point halfway between - the last and current points and make that point - the pen. */ - control1.x = context.x_coordinates[here - 1]; - control1.y = context.y_coordinates[here - 1]; - control2.x = context.x_coordinates[here]; - control2.y = context.y_coordinates[here]; - sfnt_lerp_half (&control1, &control2, &pen); - curve_to (control1, pen, dcontext); - } - } - - /* Now close the contour if there is more than one point - inside it. */ - if (start != here - 1) - { - /* Restore here after the for loop increased it. */ - here --; - - /* Previously, this would check whether or not start is an - ``on curve'' point, but that is not necessary. - - If a contour is not closed and the edge building process - skips the second to last vertex, then the outline can end - up with missing edges. */ - - pen.x = context.x_coordinates[start]; - pen.y = context.y_coordinates[start]; - - /* See if the last point (in this case, `here') was on the - curve. If it wasn't, then curve from there to here. */ - if (!(context.flags[here] & 01)) - { - control1.x = context.x_coordinates[here]; - control1.y = context.y_coordinates[here]; - curve_to (control1, pen, dcontext); - } - else - /* Otherwise, this is an ordinary line from there - to here. */ - line_to (pen, dcontext); + /* Now perform the decomposition. */ + if (sfnt_decompose_glyph_2 (here, last, move_to, + line_to, curve_to, + dcontext, + context.x_coordinates, + context.y_coordinates, + context.flags, 1)) + goto fail; - /* Restore here to where it was earlier. */ - here++; - } + /* Move forward. */ + here = last + 1; } early: @@ -10499,9 +10596,7 @@ sfnt_decompose_instructed_outline (struct sfnt_instructed_outline *outline, sfnt_curve_to_proc curve_to, void *dcontext) { - size_t here, start, last; - struct sfnt_point pen, control1, control2; - size_t n; + size_t here, last, n; if (!outline->num_contours) return 0; @@ -10515,109 +10610,20 @@ sfnt_decompose_instructed_outline (struct sfnt_instructed_outline *outline, of the last point in the contour. */ last = outline->contour_end_points[n]; - /* Move to the start. */ - pen.x = outline->x_points[here] * 1024; - pen.y = outline->y_points[here] * 1024; - move_to (pen, dcontext); - - /* Record start so the contour can be closed. */ - start = here; - - /* If there is only one point in a contour, draw a one pixel - wide line. */ - if (last == here) - { - line_to (pen, dcontext); - here++; - - continue; - } + /* Make sure here and last make sense. */ - if (here > last) - /* Indices moved backwards. */ + if (here > last || last >= outline->num_points) goto fail; - /* Now start reading points. If the next point is on the - curve, then it is actually a line. */ - for (++here; here <= last; ++here) - { - /* Make sure here is within bounds. */ - if (here >= outline->num_points) - return 1; - - if (outline->flags[here] & 01) /* On Curve */ - { - pen.x = outline->x_points[here] * 1024; - pen.y = outline->y_points[here] * 1024; - - /* See if the last point was on the curve. If it - wasn't, then curve from there to here. */ - if (!(outline->flags[here - 1] & 01)) - { - control1.x = outline->x_points[here - 1] * 1024; - control1.y = outline->y_points[here - 1] * 1024; - curve_to (control1, pen, dcontext); - } - else - /* Otherwise, this is an ordinary line from there - to here. */ - line_to (pen, dcontext); - - continue; - } - - /* If the last point was on the curve, then there's - nothing extraordinary to do yet. */ - if (outline->flags[here - 1] & 01) - ; - else - { - /* Otherwise, interpolate the point halfway between - the last and current points and make that point - the pen. */ - control1.x = outline->x_points[here - 1] * 1024; - control1.y = outline->y_points[here - 1] * 1024; - control2.x = outline->x_points[here] * 1024; - control2.y = outline->y_points[here] * 1024; - sfnt_lerp_half (&control1, &control2, &pen); - curve_to (control1, pen, dcontext); - } - } - - /* Now close the contour if there is more than one point - inside it. */ - if (start != here - 1) - { - /* Restore here after the for loop increased it. */ - here --; - - /* Previously, this would check whether or not start is an - ``on curve'' point, but that is not necessary. - - If a contour is not closed and the edge building process - skips the second to last vertex, then the outline can end - up with missing edges. */ - - pen.x = outline->x_points[start] * 1024; - pen.y = outline->y_points[start] * 1024; - - /* See if the last point (in this case, `here') was - on the curve. If it wasn't, then curve from - there to here. */ - if (!(outline->flags[here] & 01)) - { - control1.x = outline->x_points[here] * 1024; - control1.y = outline->y_points[here] * 1024; - curve_to (control1, pen, dcontext); - } - else - /* Otherwise, this is an ordinary line from there - to here. */ - line_to (pen, dcontext); + if (sfnt_decompose_glyph_2 (here, last, move_to, + line_to, curve_to, dcontext, + outline->x_points, + outline->y_points, + outline->flags, 1024)) + goto fail; - /* Restore here to where it was earlier. */ - here++; - } + /* Move forward to the start of the next contour. */ + here = last + 1; /* here may be a phantom point when outlining a compound glyph, as they can have phantom points mixed in with contours. @@ -15530,6 +15536,9 @@ main (int argc, char **argv) return 1; } + fprintf (stderr, "number of subtables: %"PRIu16"\n", + table->num_subtables); + for (i = 0; i < table->num_subtables; ++i) { fprintf (stderr, "Found cmap table %"PRIu32": %p\n", @@ -15540,8 +15549,8 @@ main (int argc, char **argv) data[i]->format); } -#define FANCY_PPEM 12 -#define EASY_PPEM 12 +#define FANCY_PPEM 40 +#define EASY_PPEM 40 interpreter = NULL; head = sfnt_read_head_table (fd, font); diff --git a/src/sfntfont-android.c b/src/sfntfont-android.c index feea92827d9..c28a911bfba 100644 --- a/src/sfntfont-android.c +++ b/src/sfntfont-android.c @@ -49,9 +49,11 @@ struct sfntfont_android_scanline_buffer }; /* Array of directories to search for system fonts. */ -const char *system_font_directories[] = +static char *system_font_directories[] = { "/system/fonts", + /* This should be filled in by init_sfntfont_android. */ + (char[PATH_MAX]) { }, }; /* The font cache. */ @@ -691,6 +693,10 @@ loaded before character sets are made available. */) { dir = opendir (system_font_directories[i]); + __android_log_print (ANDROID_LOG_VERBOSE, __func__, + "Loading fonts from: %s", + system_font_directories[i]); + if (!dir) continue; @@ -752,6 +758,11 @@ init_sfntfont_android (void) build_string ("Droid Sans Mono")), Fcons (build_string ("Sans Serif"), build_string ("Droid Sans"))); + + /* Set up the user fonts directory. This directory is ``fonts'' in + the Emacs files directory. */ + snprintf (system_font_directories[1], PATH_MAX, "%s/fonts", + android_get_home_directory ()); } void diff --git a/src/sfntfont.c b/src/sfntfont.c index a5ed54394a2..f9344067f1a 100644 --- a/src/sfntfont.c +++ b/src/sfntfont.c @@ -87,6 +87,10 @@ struct sfnt_font_desc /* The offset of the table directory within PATH. */ off_t offset; + + /* The number of glyphs in this font. Used to catch invalid cmap + tables. This is actually the number of glyphs - 1. */ + int num_glyphs; }; /* List of fonts. */ @@ -517,10 +521,11 @@ sfnt_enum_font_1 (int fd, const char *file, struct sfnt_offset_subtable *subtables, off_t offset) { - struct sfnt_font_desc *desc; + struct sfnt_font_desc *desc, **next, *prev; struct sfnt_head_table *head; struct sfnt_name_table *name; struct sfnt_meta_table *meta; + struct sfnt_maxp_table *maxp; Lisp_Object family, style; /* Create the font desc and copy in the file name. */ @@ -543,12 +548,16 @@ sfnt_enum_font_1 (int fd, const char *file, if (!name) goto bail2; + maxp = sfnt_read_maxp_table (fd, subtables); + if (!maxp) + goto bail3; + /* meta is not required, nor present on many non-Apple fonts. */ meta = sfnt_read_meta_table (fd, subtables); /* Decode the family and style from the name table. */ if (sfnt_decode_family_style (name, &family, &style)) - goto bail3; + goto bail4; /* Set the family. */ desc->family = family; @@ -556,6 +565,9 @@ sfnt_enum_font_1 (int fd, const char *file, desc->char_cache = Qnil; desc->subtable.platform_id = 500; + /* Set the largest glyph identifier. */ + desc->num_glyphs = maxp->num_glyphs; + /* Parse the style. */ sfnt_parse_style (style, desc); @@ -584,13 +596,32 @@ sfnt_enum_font_1 (int fd, const char *file, desc->next = system_fonts; system_fonts = desc; + /* Remove any fonts which have the same style as this one. */ + + next = &system_fonts->next; + prev = *next; + for (; *next; prev = *next) + { + if (!NILP (Fstring_equal (prev->style, desc->style)) + && !NILP (Fstring_equal (prev->family, desc->family))) + { + *next = prev->next; + xfree (prev); + } + else + next = &prev->next; + } + xfree (meta); + xfree (maxp); xfree (name); xfree (head); return 0; - bail3: + bail4: xfree (meta); + xfree (maxp); + bail3: xfree (name); bail2: xfree (head); @@ -602,6 +633,8 @@ sfnt_enum_font_1 (int fd, const char *file, /* Enumerate the font FILE into the list of system fonts. Return 1 if it could not be enumerated, 0 otherwise. + Remove any font whose family and style is a duplicate of this one. + FILE can either be a TrueType collection file containing TrueType fonts, or a TrueType font itself. */ @@ -960,6 +993,25 @@ sfntfont_read_cmap (struct sfnt_font_desc *desc, emacs_close (fd); } +/* Return whether or not CHARACTER has an associated mapping in CMAP, + and the mapping points to a valid glyph. DESC is the font + descriptor associated with the font. */ + +static bool +sfntfont_glyph_valid (struct sfnt_font_desc *desc, + sfnt_char font_character, + struct sfnt_cmap_encoding_subtable_data *cmap) +{ + sfnt_glyph glyph; + + glyph = sfnt_lookup_glyph (font_character, cmap); + + if (!glyph) + return false; + + return glyph <= desc->num_glyphs; +} + /* Look up a character CHARACTER in the font description DESC. Cache the results. Return true if the character exists, false otherwise. @@ -1013,8 +1065,10 @@ sfntfont_lookup_char (struct sfnt_font_desc *desc, Lisp_Object character, if (font_character == CHARSET_INVALID_CODE (charset)) return false; - /* Now return whether or not the glyph is present. */ - present = sfnt_lookup_glyph (font_character, *cmap) != 0; + /* Now return whether or not the glyph is present. Noto Sans + Georgian comes with a corrupt format 4 cmap table that somehow + tries to express glyphs greater than 65565. */ + present = sfntfont_glyph_valid (desc, font_character, *cmap); /* Cache the result. Store Qlambda when not present, Qt otherwise. */ @@ -1133,22 +1187,23 @@ sfntfont_list_1 (struct sfnt_font_desc *desc, Lisp_Object spec) { tem = XCDR (tem); - /* tem is a list of each characters, one of which must be + /* tem is a list of each characters, all of which must be present in the font. */ FOR_EACH_TAIL_SAFE (tem) { - if (FIXNUMP (XCAR (tem))) - { - if (!sfntfont_lookup_char (desc, XCAR (tem), &cmap, - &subtable)) - goto fail; - - /* One character is enough to pass a font. Don't - look at too many. */ - break; - } + if (FIXNUMP (XCAR (tem)) + && !sfntfont_lookup_char (desc, XCAR (tem), &cmap, + &subtable)) + goto fail; } + + /* One or more characters are missing. */ + if (!NILP (tem)) + goto fail; } + /* Fail if there are no matching fonts at all. */ + else if (NILP (tem)) + goto fail; } /* Now check that the language is supported. */ diff --git a/src/textconv.c b/src/textconv.c index 835d03f3037..5090b0a33b6 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -35,6 +35,7 @@ along with GNU Emacs. If not, see . */ #include "textconv.h" #include "buffer.h" #include "syntax.h" +#include "blockinput.h" @@ -47,6 +48,10 @@ along with GNU Emacs. If not, see . */ static struct textconv_interface *text_interface; +/* How many times text conversion has been disabled. */ + +static int suppress_conversion_count; + /* Flags used to determine what must be sent after a batch edit ends. */ @@ -391,7 +396,8 @@ textconv_query (struct frame *f, struct textconv_callback_struct *query, static void sync_overlay (struct frame *f) { - if (MARKERP (f->conversion.compose_region_start)) + if (MARKERP (f->conversion.compose_region_start) + && !NILP (Vtext_conversion_face)) { if (NILP (f->conversion.compose_region_overlay)) { @@ -400,7 +406,7 @@ sync_overlay (struct frame *f) f->conversion.compose_region_end, Qnil, Qt, Qnil); Foverlay_put (f->conversion.compose_region_overlay, - Qface, Qunderline); + Qface, Vtext_conversion_face); } Fmove_overlay (f->conversion.compose_region_overlay, @@ -514,6 +520,7 @@ really_commit_text (struct frame *f, EMACS_INT position, { specpdl_ref count; ptrdiff_t wanted, start, end; + struct window *w; /* If F's old selected window is no longer live, fail. */ @@ -624,6 +631,10 @@ really_commit_text (struct frame *f, EMACS_INT position, /* This should deactivate the mark. */ call0 (Qdeactivate_mark); + + /* Update the ephemeral last point. */ + w = XWINDOW (selected_window); + w->ephemeral_last_point = PT; unbind_to (count, Qnil); } @@ -760,6 +771,10 @@ really_set_composing_text (struct frame *f, ptrdiff_t position, text_interface->compose_region_changed (f); } + /* Update the ephemeral last point. */ + w = XWINDOW (selected_window); + w->ephemeral_last_point = PT; + unbind_to (count, Qnil); } @@ -771,6 +786,7 @@ really_set_composing_region (struct frame *f, ptrdiff_t start, ptrdiff_t end) { specpdl_ref count; + struct window *w; /* If F's old selected window is no longer live, fail. */ @@ -810,6 +826,10 @@ really_set_composing_region (struct frame *f, ptrdiff_t start, make_fixnum (end), Qnil); sync_overlay (f); + /* Update the ephemeral last point. */ + w = XWINDOW (selected_window); + w->ephemeral_last_point = PT; + unbind_to (count, Qnil); } @@ -823,6 +843,7 @@ really_delete_surrounding_text (struct frame *f, ptrdiff_t left, { specpdl_ref count; ptrdiff_t start, end, a, b, a1, b1, lstart, rstart; + struct window *w; /* If F's old selected window is no longer live, fail. */ @@ -889,6 +910,10 @@ really_delete_surrounding_text (struct frame *f, ptrdiff_t left, if (get_mark () == PT) call0 (Qdeactivate_mark); + /* Update the ephemeral last point. */ + w = XWINDOW (selected_window); + w->ephemeral_last_point = PT; + unbind_to (count, Qnil); } @@ -904,6 +929,7 @@ really_set_point_and_mark (struct frame *f, ptrdiff_t point, ptrdiff_t mark) { specpdl_ref count; + struct window *w; /* If F's old selected window is no longer live, fail. */ @@ -922,7 +948,7 @@ really_set_point_and_mark (struct frame *f, ptrdiff_t point, { if (f->conversion.batch_edit_count > 0) f->conversion.batch_edit_flags |= PENDING_POINT_CHANGE; - else + else if (text_interface && text_interface->point_changed) text_interface->point_changed (f, XWINDOW (f->old_selected_window), current_buffer); @@ -936,6 +962,10 @@ really_set_point_and_mark (struct frame *f, ptrdiff_t point, else call1 (Qpush_mark, make_fixnum (mark)); + /* Update the ephemeral last point. */ + w = XWINDOW (selected_window); + w->ephemeral_last_point = PT; + unbind_to (count, Qnil); } @@ -949,9 +979,11 @@ complete_edit (void *token) } /* Process and free the text conversion ACTION. F must be the frame - on which ACTION will be performed. */ + on which ACTION will be performed. -static void + Value is the window which was used, or NULL. */ + +static struct window * handle_pending_conversion_events_1 (struct frame *f, struct text_conversion_action *action) { @@ -969,9 +1001,23 @@ handle_pending_conversion_events_1 (struct frame *f, token = action->counter; xfree (action); + /* Text conversion events can still arrive immediately after + `conversion_disabled_p' becomes true. In that case, process all + events, but don't perform any associated actions. */ + + if (conversion_disabled_p ()) + return NULL; + /* Make sure completion is signalled. */ count = SPECPDL_INDEX (); record_unwind_protect_ptr (complete_edit, &token); + w = NULL; + + if (WINDOW_LIVE_P (f->old_selected_window)) + { + w = XWINDOW (f->old_selected_window); + buffer = XBUFFER (WINDOW_BUFFER (w)); + } switch (operation) { @@ -987,12 +1033,7 @@ handle_pending_conversion_events_1 (struct frame *f, break; if (f->conversion.batch_edit_flags & PENDING_POINT_CHANGE) - { - w = XWINDOW (f->old_selected_window); - buffer = XBUFFER (WINDOW_BUFFER (w)); - - text_interface->point_changed (f, w, buffer); - } + text_interface->point_changed (f, w, buffer); if (f->conversion.batch_edit_flags & PENDING_COMPOSE_CHANGE) text_interface->compose_region_changed (f); @@ -1030,6 +1071,8 @@ handle_pending_conversion_events_1 (struct frame *f, } unbind_to (count, Qnil); + + return w; } /* Decrement the variable pointed to by *PTR. */ @@ -1055,6 +1098,8 @@ handle_pending_conversion_events (void) bool handled; static int inside; specpdl_ref count; + ptrdiff_t last_point; + struct window *w; handled = false; @@ -1065,6 +1110,8 @@ handle_pending_conversion_events (void) Vtext_conversion_edits = Qnil; inside++; + last_point = -1; + w = NULL; count = SPECPDL_INDEX (); record_unwind_protect_ptr (decrement_inside, &inside); @@ -1077,16 +1124,26 @@ handle_pending_conversion_events (void) process them in bottom to up order. */ while (true) { - /* Redisplay in between if there is more than one - action. - - This can read input. This function must be reentrant - here. */ - - if (handled) - redisplay (); + /* Update the input method if handled && + w->ephemeral_last_point != last_point. */ + if (w && (last_point != w->ephemeral_last_point)) + { + if (handled + && last_point != -1 + && text_interface + && text_interface->point_changed) + { + if (f->conversion.batch_edit_count > 0) + f->conversion.batch_edit_flags |= PENDING_POINT_CHANGE; + else + text_interface->point_changed (f, NULL, NULL); + } + + last_point = w->ephemeral_last_point; + } - /* Reload action. */ + /* Reload action. This needs to be reentrant as buffer + modification functions can call `read-char'. */ action = f->conversion.actions; /* If there are no more actions, break. */ @@ -1099,7 +1156,7 @@ handle_pending_conversion_events (void) f->conversion.actions = next; /* Handle and free the action. */ - handle_pending_conversion_events_1 (f, action); + w = handle_pending_conversion_events_1 (f, action); handled = true; } } @@ -1399,6 +1456,16 @@ get_extracted_text (struct frame *f, ptrdiff_t n, return buffer; } +/* Return whether or not text conversion is temporarily disabled. + `reset' should always call this to determine whether or not to + disable the input method. */ + +bool +conversion_disabled_p (void) +{ + return suppress_conversion_count > 0; +} + /* Window system interface. These are called from the rest of @@ -1440,6 +1507,60 @@ report_point_change (struct frame *f, struct window *window, text_interface->point_changed (f, window, buffer); } +/* Temporarily disable text conversion. Must be paired with a + corresponding call to resume_text_conversion. */ + +void +disable_text_conversion (void) +{ + Lisp_Object tail, frame; + struct frame *f; + + suppress_conversion_count++; + + if (!text_interface || suppress_conversion_count > 1) + return; + + /* Loop through and reset the input method on each window system + frame. It should call conversion_disabled_p and then DTRT. */ + + FOR_EACH_FRAME (tail, frame) + { + f = XFRAME (frame); + reset_frame_state (f); + + if (FRAME_WINDOW_P (f) && FRAME_VISIBLE_P (f)) + text_interface->reset (f); + } +} + +/* Undo the effect of the last call to `disable_text_conversion'. */ + +void +resume_text_conversion (void) +{ + Lisp_Object tail, frame; + struct frame *f; + + suppress_conversion_count--; + eassert (suppress_conversion_count >= 0); + + if (!text_interface || suppress_conversion_count) + return; + + /* Loop through and reset the input method on each window system + frame. It should call conversion_disabled_p and then DTRT. */ + + FOR_EACH_FRAME (tail, frame) + { + f = XFRAME (frame); + reset_frame_state (f); + + if (FRAME_WINDOW_P (f) && FRAME_VISIBLE_P (f)) + text_interface->reset (f); + } +} + /* Register INTERFACE as the text conversion interface. */ void @@ -1450,6 +1571,59 @@ register_textconv_interface (struct textconv_interface *interface) +/* Lisp interface. */ + +DEFUN ("set-text-conversion-style", Fset_text_conversion_style, + Sset_text_conversion_style, 1, 1, 0, + doc: /* Set the text conversion style in the current buffer. + +Set `text-conversion-mode' to VALUE, then force any input method +editing frame displaying this buffer to stop itself. + +This can lead to a significant amount of time being taken by the input +method resetting itself, so you should not use this function lightly; +instead, set `text-conversion-mode' before your buffer is displayed, +and let redisplay manage the input method appropriately. */) + (Lisp_Object value) +{ + Lisp_Object tail, frame; + struct frame *f; + Lisp_Object buffer; + + bset_text_conversion_style (current_buffer, value); + + if (!text_interface) + return Qnil; + + /* If there are any seleted windows displaying this buffer, reset + text conversion on their associated frames. */ + + if (buffer_window_count (current_buffer)) + { + buffer = Fcurrent_buffer (); + + FOR_EACH_FRAME (tail, frame) + { + f = XFRAME (frame); + + if (WINDOW_LIVE_P (f->old_selected_window) + && FRAME_WINDOW_P (f) + && EQ (XWINDOW (f->old_selected_window)->contents, + buffer)) + { + block_input (); + reset_frame_state (f); + text_interface->reset (f); + unblock_input (); + } + } + } + + return Qnil; +} + + + void syms_of_textconv (void) { @@ -1457,6 +1631,7 @@ syms_of_textconv (void) DEFSYM (Qtext_conversion, "text-conversion"); DEFSYM (Qpush_mark, "push-mark"); DEFSYM (Qunderline, "underline"); + DEFSYM (Qoverriding_text_conversion_style, "overriding-text-conversion-style"); DEFVAR_LISP ("text-conversion-edits", Vtext_conversion_edits, doc: /* List of buffers that were last edited as a result of text conversion. @@ -1480,4 +1655,21 @@ inserted. If a deletion occured, then BEG and END are the same, and EPHEMERAL is nil. */); Vtext_conversion_edits = Qnil; + + DEFVAR_LISP ("overriding-text-conversion-style", + Voverriding_text_conversion_style, + doc: /* Non-buffer local version of `text-conversion-style'. + +If this variable is the symbol `lambda', it means to consult the +buffer local variable `text-conversion-style' to determine whether or +not to activate the input method. Otherwise, its value is used in +preference to any buffer local value of `text-conversion-style'. */); + Voverriding_text_conversion_style = Qlambda; + + DEFVAR_LISP ("text-conversion-face", Vtext_conversion_face, + doc: /* Face in which to display temporary edits by an input method. +nil means to display no indication of a temporary edit. */); + Vtext_conversion_face = Qunderline; + + defsubr (&Sset_text_conversion_style); } diff --git a/src/textconv.h b/src/textconv.h index 034c663521a..16d13deb092 100644 --- a/src/textconv.h +++ b/src/textconv.h @@ -140,6 +140,7 @@ extern void delete_surrounding_text (struct frame *, ptrdiff_t, ptrdiff_t, unsigned long); extern char *get_extracted_text (struct frame *, ptrdiff_t, ptrdiff_t *, ptrdiff_t *, ptrdiff_t *, ptrdiff_t *); +extern bool conversion_disabled_p (void); extern void register_textconv_interface (struct textconv_interface *); diff --git a/src/window.h b/src/window.h index 463c7f89b9b..36b1bfb7283 100644 --- a/src/window.h +++ b/src/window.h @@ -286,6 +286,21 @@ struct window it should be positive. */ ptrdiff_t last_point; +#ifdef HAVE_TEXT_CONVERSION + /* ``ephemeral'' last point position. This is used while + processing text conversion events. + + `last_point' is normally used during redisplay to indicate the + position of point as seem by the input method. However, it is + not updated if consequtive conversions are processed at the + same time. + + This `ephemeral_last_point' field is either the last point as + set in redisplay or the last point after a text editing + operation. */ + ptrdiff_t ephemeral_last_point; +#endif + /* Value of mark in the selected window at the time of the last redisplay. */ ptrdiff_t last_mark; diff --git a/src/xdisp.c b/src/xdisp.c index 525eaa64b3a..a17fa97ee20 100644 --- a/src/xdisp.c +++ b/src/xdisp.c @@ -17315,6 +17315,9 @@ mark_window_display_accurate_1 (struct window *w, bool accurate_p) w->last_mark = -1; #ifdef HAVE_TEXT_CONVERSION + /* See the description of this field in struct window. */ + w->ephemeral_last_point = w->last_point; + /* Point motion is only propagated to the input method for use in text conversion during a redisplay. While this can lead to inconsistencies when point has moved but the change has -- cgit v1.3 From 1e6f957c0dbb7e4a5e04c20fcb797be1d98df3d8 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 22 Feb 2023 14:59:27 +0800 Subject: Update Android port * doc/emacs/input.texi (On-Screen Keyboards): Document changes to text conversion. * java/org/gnu/emacs/EmacsInputConnection.java (getExtractedText) (EmacsInputConnection): * src/keyboard.c (read_key_sequence): Disable text conversion after reading prefix key. * src/textconv.c (get_extracted_text): Fix returned value when request length is zero. --- doc/emacs/input.texi | 8 +++++--- java/org/gnu/emacs/EmacsInputConnection.java | 20 +++++++++++++++++++- src/keyboard.c | 23 +++++++++++++++++++++++ src/textconv.c | 3 +++ 4 files changed, 50 insertions(+), 4 deletions(-) (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') diff --git a/doc/emacs/input.texi b/doc/emacs/input.texi index 37167f5a9e0..dc1acaedfcb 100644 --- a/doc/emacs/input.texi +++ b/doc/emacs/input.texi @@ -132,9 +132,11 @@ Emacs enables these input methods whenever the buffer local value of derivatives of @code{text-mode} and @code{prog-mode}. Text conversion is performed asynchronously whenever Emacs receives -a request to perform the conversion from the input method. After the -conversion completes, a @code{text-conversion} event is sent. -@xref{Misc Events,,, elisp, the Emacs Reference Manual}. +a request to perform the conversion from the input method, and Emacs +is not currently reading a key sequence for which one prefix key has +already been read (@pxref{Keys}.) After the conversion completes, a +@code{text-conversion} event is sent. @xref{Misc Events,,, elisp, the +Emacs Reference Manual}. @vindex text-conversion-face If the input method needs to work on a region of the buffer, then diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index e2a15894695..834c2226c82 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -207,11 +207,19 @@ public class EmacsInputConnection extends BaseInputConnection public ExtractedText getExtractedText (ExtractedTextRequest request, int flags) { + ExtractedText text; + if (EmacsService.DEBUG_IC) Log.d (TAG, "getExtractedText: " + request + " " + flags); - return EmacsNative.getExtractedText (windowHandle, request, + text = EmacsNative.getExtractedText (windowHandle, request, flags); + + if (EmacsService.DEBUG_IC) + Log.d (TAG, "getExtractedText: " + text.text + " @" + + text.startOffset + ":" + text.selectionStart); + + return text; } @Override @@ -225,6 +233,16 @@ public class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public boolean + sendKeyEvent (KeyEvent key) + { + if (EmacsService.DEBUG_IC) + Log.d (TAG, "sendKeyEvent: " + key); + + return super.sendKeyEvent (key); + } + /* Override functions which are not implemented. */ diff --git a/src/keyboard.c b/src/keyboard.c index 9532eb70f06..69fb8ae2797 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -10053,6 +10053,13 @@ read_key_sequence (Lisp_Object *keybuf, Lisp_Object prompt, /* Gets around Microsoft compiler limitations. */ bool dummyflag = false; +#ifdef HAVE_TEXT_CONVERSION + bool disabled_conversion; + + /* Whether or not text conversion has already been disabled. */ + disabled_conversion = false; +#endif + struct buffer *starting_buffer; /* List of events for which a fake prefix key has been generated. */ @@ -10202,6 +10209,22 @@ read_key_sequence (Lisp_Object *keybuf, Lisp_Object prompt, echo_local_start = echo_length (); keys_local_start = this_command_key_count; +#ifdef HAVE_TEXT_CONVERSION + /* When reading a key sequence while text conversion is in + effect, turn it off after the first character read. This + makes input methods send actual key events instead. + + Make sure only to do this once. */ + + if (!disabled_conversion && t) + { + disable_text_conversion (); + record_unwind_protect_void (resume_text_conversion); + + disabled_conversion = true; + } +#endif + replay_key: /* These are no-ops, unless we throw away a keystroke below and jumped back up to replay_key; in that case, these restore the diff --git a/src/textconv.c b/src/textconv.c index 1ca5f0488f8..4b5f9e01162 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -1462,6 +1462,9 @@ get_extracted_text (struct frame *f, ptrdiff_t n, /* Figure out the bounds of the text to return. */ if (n != -1) { + /* Make sure n is at least 2. */ + n = max (2, n); + start = PT - n / 2; end = PT + n - n / 2; } -- cgit v1.3 From 15bcb446be2f2f5b85a1b9585ec3abaabcbf04d9 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 1 Mar 2023 12:00:46 +0800 Subject: Update Android port * java/Makefile.in (ETAGS, clean): New rules to generate tags. * java/org/gnu/emacs/EmacsActivity.java (EmacsActivity): * java/org/gnu/emacs/EmacsApplication.java (EmacsApplication): * java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu): * java/org/gnu/emacs/EmacsCopyArea.java (EmacsCopyArea): * java/org/gnu/emacs/EmacsDialog.java (EmacsDialog)::(dialog. Then): * java/org/gnu/emacs/EmacsDocumentsProvider.java (EmacsDocumentsProvider): * java/org/gnu/emacs/EmacsDrawLine.java (EmacsDrawLine): * java/org/gnu/emacs/EmacsDrawPoint.java (EmacsDrawPoint): * java/org/gnu/emacs/EmacsDrawRectangle.java (EmacsDrawRectangle): * java/org/gnu/emacs/EmacsFillPolygon.java (EmacsFillPolygon): * java/org/gnu/emacs/EmacsFillRectangle.java (EmacsFillRectangle): * java/org/gnu/emacs/EmacsGC.java (EmacsGC): * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection): * java/org/gnu/emacs/EmacsNative.java (EmacsNative): * java/org/gnu/emacs/EmacsNoninteractive.java (EmacsNoninteractive): * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity): * java/org/gnu/emacs/EmacsPixmap.java (EmacsPixmap): * java/org/gnu/emacs/EmacsPreferencesActivity.java (EmacsPreferencesActivity): * java/org/gnu/emacs/EmacsSdk11Clipboard.java (EmacsSdk11Clipboard): * java/org/gnu/emacs/EmacsSdk23FontDriver.java (EmacsSdk23FontDriver): * java/org/gnu/emacs/EmacsSdk8Clipboard.java (EmacsSdk8Clipboard): * java/org/gnu/emacs/EmacsService.java (EmacsService): * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView) (buffers): * java/org/gnu/emacs/EmacsView.java (EmacsView, ViewGroup): * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow, drawables): * java/org/gnu/emacs/EmacsWindowAttachmentManager.java (EmacsWindowAttachmentManager): Make classes final where appropriate. --- java/Makefile.in | 16 +++++++++++++++- java/org/gnu/emacs/EmacsActivity.java | 22 +++++++++++----------- java/org/gnu/emacs/EmacsApplication.java | 2 +- java/org/gnu/emacs/EmacsContextMenu.java | 2 +- java/org/gnu/emacs/EmacsCopyArea.java | 2 +- java/org/gnu/emacs/EmacsDialog.java | 2 +- java/org/gnu/emacs/EmacsDocumentsProvider.java | 2 +- java/org/gnu/emacs/EmacsDrawLine.java | 2 +- java/org/gnu/emacs/EmacsDrawPoint.java | 2 +- java/org/gnu/emacs/EmacsDrawRectangle.java | 2 +- java/org/gnu/emacs/EmacsFillPolygon.java | 2 +- java/org/gnu/emacs/EmacsFillRectangle.java | 2 +- java/org/gnu/emacs/EmacsGC.java | 2 +- java/org/gnu/emacs/EmacsInputConnection.java | 11 ++++++++++- java/org/gnu/emacs/EmacsNative.java | 2 +- java/org/gnu/emacs/EmacsNoninteractive.java | 2 +- java/org/gnu/emacs/EmacsOpenActivity.java | 2 +- java/org/gnu/emacs/EmacsPixmap.java | 2 +- java/org/gnu/emacs/EmacsPreferencesActivity.java | 2 +- java/org/gnu/emacs/EmacsSdk11Clipboard.java | 2 +- java/org/gnu/emacs/EmacsSdk23FontDriver.java | 2 +- java/org/gnu/emacs/EmacsSdk8Clipboard.java | 2 +- java/org/gnu/emacs/EmacsService.java | 2 +- java/org/gnu/emacs/EmacsSurfaceView.java | 2 +- java/org/gnu/emacs/EmacsView.java | 2 +- java/org/gnu/emacs/EmacsWindow.java | 2 +- .../gnu/emacs/EmacsWindowAttachmentManager.java | 2 +- 27 files changed, 60 insertions(+), 37 deletions(-) (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') diff --git a/java/Makefile.in b/java/Makefile.in index 7ba05f6c9a3..bff021752c7 100644 --- a/java/Makefile.in +++ b/java/Makefile.in @@ -281,9 +281,23 @@ $(APK_NAME): classes.dex emacs.apk-in emacs.keystore $(AM_V_SILENT) $(APKSIGNER) $(SIGN_EMACS_V2) $@ $(AM_V_SILENT) rm -f $@.unaligned *.idsig +# TAGS generation. + +ETAGS = $(top_builddir)/lib-src/etags + +$(ETAGS): FORCE + $(MAKE) -C ../lib-src $(notdir $@) + +tagsfiles = $(JAVA_FILES) $(RESOURCE_FILE) + +.PHONY: tags FORCE +tags: TAGS +TAGS: $(ETAGS) $(tagsfiles) + $(AM_V_GEN) $(ETAGS) $(tagsfiles) + clean: rm -f *.apk emacs.apk-in *.dex *.unaligned *.class *.idsig - rm -rf install-temp $(RESOURCE_FILE) + rm -rf install-temp $(RESOURCE_FILE) TAGS find . -name '*.class' -delete maintainer-clean distclean bootstrap-clean: clean diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 7e09e608984..0ee8c239899 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -108,7 +108,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void detachWindow () { syncFullscreenWith (null); @@ -131,7 +131,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void attachWindow (EmacsWindow child) { Log.d (TAG, "attachWindow: " + child); @@ -161,21 +161,21 @@ public class EmacsActivity extends Activity } @Override - public void + public final void destroy () { finish (); } @Override - public EmacsWindow + public final EmacsWindow getAttachedWindow () { return window; } @Override - public void + public final void onCreate (Bundle savedInstanceState) { FrameLayout.LayoutParams params; @@ -238,7 +238,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void onWindowFocusChanged (boolean isFocused) { Log.d (TAG, ("onWindowFocusChanged: " @@ -256,7 +256,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void onPause () { isPaused = true; @@ -266,7 +266,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void onResume () { isPaused = false; @@ -279,7 +279,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void onContextMenuClosed (Menu menu) { Log.d (TAG, "onContextMenuClosed: " + menu); @@ -298,7 +298,7 @@ public class EmacsActivity extends Activity } @SuppressWarnings ("deprecation") - public void + public final void syncFullscreenWith (EmacsWindow emacsWindow) { WindowInsetsController controller; @@ -372,7 +372,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void onAttachedToWindow () { super.onAttachedToWindow (); diff --git a/java/org/gnu/emacs/EmacsApplication.java b/java/org/gnu/emacs/EmacsApplication.java index 6a065165eb1..10099721744 100644 --- a/java/org/gnu/emacs/EmacsApplication.java +++ b/java/org/gnu/emacs/EmacsApplication.java @@ -27,7 +27,7 @@ import android.content.Context; import android.app.Application; import android.util.Log; -public class EmacsApplication extends Application +public final class EmacsApplication extends Application { private static final String TAG = "EmacsApplication"; diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 6b3ae0c6de9..0de292af21a 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -42,7 +42,7 @@ import android.widget.PopupMenu; Android menu, which can be turned into a popup (or other kind of) menu. */ -public class EmacsContextMenu +public final class EmacsContextMenu { private static final String TAG = "EmacsContextMenu"; diff --git a/java/org/gnu/emacs/EmacsCopyArea.java b/java/org/gnu/emacs/EmacsCopyArea.java index 1daa2190542..f69b0cde866 100644 --- a/java/org/gnu/emacs/EmacsCopyArea.java +++ b/java/org/gnu/emacs/EmacsCopyArea.java @@ -27,7 +27,7 @@ import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Xfermode; -public class EmacsCopyArea +public final class EmacsCopyArea { private static Xfermode overAlu; diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index 9f9124ce99c..80a5e5f7369 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -38,7 +38,7 @@ import android.view.ViewGroup; describes a single alert dialog. Then, `inflate' turns it into AlertDialog. */ -public class EmacsDialog implements DialogInterface.OnDismissListener +public final class EmacsDialog implements DialogInterface.OnDismissListener { private static final String TAG = "EmacsDialog"; diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java index 3c3c7ead3c5..901c3b909e0 100644 --- a/java/org/gnu/emacs/EmacsDocumentsProvider.java +++ b/java/org/gnu/emacs/EmacsDocumentsProvider.java @@ -48,7 +48,7 @@ import java.io.IOException; This functionality is only available on Android 19 and later. */ -public class EmacsDocumentsProvider extends DocumentsProvider +public final class EmacsDocumentsProvider extends DocumentsProvider { /* Home directory. This is the directory whose contents are initially returned to requesting applications. */ diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java index c6e5123bfca..0b23138a36c 100644 --- a/java/org/gnu/emacs/EmacsDrawLine.java +++ b/java/org/gnu/emacs/EmacsDrawLine.java @@ -29,7 +29,7 @@ import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Xfermode; -public class EmacsDrawLine +public final class EmacsDrawLine { public static void perform (EmacsDrawable drawable, EmacsGC gc, diff --git a/java/org/gnu/emacs/EmacsDrawPoint.java b/java/org/gnu/emacs/EmacsDrawPoint.java index 3bc7be17961..de8ddf09cc4 100644 --- a/java/org/gnu/emacs/EmacsDrawPoint.java +++ b/java/org/gnu/emacs/EmacsDrawPoint.java @@ -19,7 +19,7 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -public class EmacsDrawPoint +public final class EmacsDrawPoint { public static void perform (EmacsDrawable drawable, diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index 695a8c6ea44..ce5e94e4a76 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -27,7 +27,7 @@ import android.graphics.RectF; import android.util.Log; -public class EmacsDrawRectangle +public final class EmacsDrawRectangle { public static void perform (EmacsDrawable drawable, EmacsGC gc, diff --git a/java/org/gnu/emacs/EmacsFillPolygon.java b/java/org/gnu/emacs/EmacsFillPolygon.java index 22e2dd0d8a9..d55a0b3aca8 100644 --- a/java/org/gnu/emacs/EmacsFillPolygon.java +++ b/java/org/gnu/emacs/EmacsFillPolygon.java @@ -29,7 +29,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; -public class EmacsFillPolygon +public final class EmacsFillPolygon { public static void perform (EmacsDrawable drawable, EmacsGC gc, Point points[]) diff --git a/java/org/gnu/emacs/EmacsFillRectangle.java b/java/org/gnu/emacs/EmacsFillRectangle.java index aed0a540c8f..4a0478b446f 100644 --- a/java/org/gnu/emacs/EmacsFillRectangle.java +++ b/java/org/gnu/emacs/EmacsFillRectangle.java @@ -26,7 +26,7 @@ import android.graphics.Rect; import android.util.Log; -public class EmacsFillRectangle +public final class EmacsFillRectangle { public static void perform (EmacsDrawable drawable, EmacsGC gc, diff --git a/java/org/gnu/emacs/EmacsGC.java b/java/org/gnu/emacs/EmacsGC.java index bdc27a1ca5b..a7467cb9bd0 100644 --- a/java/org/gnu/emacs/EmacsGC.java +++ b/java/org/gnu/emacs/EmacsGC.java @@ -29,7 +29,7 @@ import android.graphics.Xfermode; /* X like graphics context structures. Keep the enums in synch with androidgui.h! */ -public class EmacsGC extends EmacsHandleObject +public final class EmacsGC extends EmacsHandleObject { public static final int GC_COPY = 0; public static final int GC_XOR = 1; diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 834c2226c82..ed64c368857 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -36,7 +36,7 @@ import android.util.Log; See EmacsEditable for more details. */ -public class EmacsInputConnection extends BaseInputConnection +public final class EmacsInputConnection extends BaseInputConnection { private static final String TAG = "EmacsInputConnection"; private EmacsView view; @@ -243,6 +243,15 @@ public class EmacsInputConnection extends BaseInputConnection return super.sendKeyEvent (key); } + @Override + public boolean + deleteSurroundingTextInCodePoints (int beforeLength, int afterLength) + { + /* This can be implemented the same way as + deleteSurroundingText. */ + return this.deleteSurroundingText (beforeLength, afterLength); + } + /* Override functions which are not implemented. */ diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index f0917a68120..b1205353090 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -25,7 +25,7 @@ import android.content.res.AssetManager; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; -public class EmacsNative +public final class EmacsNative { /* List of native libraries that must be loaded during class initialization. */ diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java index 30901edb75f..f365037b311 100644 --- a/java/org/gnu/emacs/EmacsNoninteractive.java +++ b/java/org/gnu/emacs/EmacsNoninteractive.java @@ -44,7 +44,7 @@ import java.lang.reflect.Method; initializes Emacs. */ @SuppressWarnings ("unchecked") -public class EmacsNoninteractive +public final class EmacsNoninteractive { private static String getLibraryDirectory (Context context) diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index 87ce454a816..fddd5331d2f 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -68,7 +68,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; -public class EmacsOpenActivity extends Activity +public final class EmacsOpenActivity extends Activity implements DialogInterface.OnClickListener { private static final String TAG = "EmacsOpenActivity"; diff --git a/java/org/gnu/emacs/EmacsPixmap.java b/java/org/gnu/emacs/EmacsPixmap.java index a83d8f25542..eb011bc5e65 100644 --- a/java/org/gnu/emacs/EmacsPixmap.java +++ b/java/org/gnu/emacs/EmacsPixmap.java @@ -29,7 +29,7 @@ import android.os.Build; /* Drawable backed by bitmap. */ -public class EmacsPixmap extends EmacsHandleObject +public final class EmacsPixmap extends EmacsHandleObject implements EmacsDrawable { /* The depth of the bitmap. This is not actually used, just defined diff --git a/java/org/gnu/emacs/EmacsPreferencesActivity.java b/java/org/gnu/emacs/EmacsPreferencesActivity.java index 85639fe9236..70934fa4bd4 100644 --- a/java/org/gnu/emacs/EmacsPreferencesActivity.java +++ b/java/org/gnu/emacs/EmacsPreferencesActivity.java @@ -42,7 +42,7 @@ import android.preference.*; Unfortunately, there is no alternative that looks the same way. */ @SuppressWarnings ("deprecation") -public class EmacsPreferencesActivity extends PreferenceActivity +public final class EmacsPreferencesActivity extends PreferenceActivity { /* Restart Emacs with -Q. Call EmacsThread.exit to kill Emacs now, and tell the system to EmacsActivity with some parameters later. */ diff --git a/java/org/gnu/emacs/EmacsSdk11Clipboard.java b/java/org/gnu/emacs/EmacsSdk11Clipboard.java index ea35a463299..a05184513cd 100644 --- a/java/org/gnu/emacs/EmacsSdk11Clipboard.java +++ b/java/org/gnu/emacs/EmacsSdk11Clipboard.java @@ -32,7 +32,7 @@ import java.io.UnsupportedEncodingException; /* This class implements EmacsClipboard for Android 3.0 and later systems. */ -public class EmacsSdk11Clipboard extends EmacsClipboard +public final class EmacsSdk11Clipboard extends EmacsClipboard implements ClipboardManager.OnPrimaryClipChangedListener { private static final String TAG = "EmacsSdk11Clipboard"; diff --git a/java/org/gnu/emacs/EmacsSdk23FontDriver.java b/java/org/gnu/emacs/EmacsSdk23FontDriver.java index 11e128d5769..aaba8dbd166 100644 --- a/java/org/gnu/emacs/EmacsSdk23FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk23FontDriver.java @@ -22,7 +22,7 @@ package org.gnu.emacs; import android.graphics.Paint; import android.graphics.Rect; -public class EmacsSdk23FontDriver extends EmacsSdk7FontDriver +public final class EmacsSdk23FontDriver extends EmacsSdk7FontDriver { private void textExtents1 (Sdk7FontObject font, int code, FontMetrics metrics, diff --git a/java/org/gnu/emacs/EmacsSdk8Clipboard.java b/java/org/gnu/emacs/EmacsSdk8Clipboard.java index 34e66912562..818a722a908 100644 --- a/java/org/gnu/emacs/EmacsSdk8Clipboard.java +++ b/java/org/gnu/emacs/EmacsSdk8Clipboard.java @@ -31,7 +31,7 @@ import java.io.UnsupportedEncodingException; similarly old systems. */ @SuppressWarnings ("deprecation") -public class EmacsSdk8Clipboard extends EmacsClipboard +public final class EmacsSdk8Clipboard extends EmacsClipboard { private static final String TAG = "EmacsSdk8Clipboard"; private ClipboardManager manager; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 7f4f75b5147..e61d9487375 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -82,7 +82,7 @@ class Holder /* EmacsService is the service that starts the thread running Emacs and handles requests by that Emacs instance. */ -public class EmacsService extends Service +public final class EmacsService extends Service { public static final String TAG = "EmacsService"; public static final int MAX_PENDING_REQUESTS = 256; diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index 62e927094e4..e0411f7f8b3 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -35,7 +35,7 @@ import java.lang.ref.WeakReference; own back buffers, which use too much memory (up to 96 MB for a single frame.) */ -public class EmacsSurfaceView extends View +public final class EmacsSurfaceView extends View { private static final String TAG = "EmacsSurfaceView"; private EmacsView view; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 89f526853b2..d2330494bc7 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -51,7 +51,7 @@ import android.util.Log; It is also a ViewGroup, as it also lays out children. */ -public class EmacsView extends ViewGroup +public final class EmacsView extends ViewGroup { public static final String TAG = "EmacsView"; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 90fc4c44198..007a2a86e68 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -59,7 +59,7 @@ import android.os.Build; Views are also drawables, meaning they can accept drawing requests. */ -public class EmacsWindow extends EmacsHandleObject +public final class EmacsWindow extends EmacsHandleObject implements EmacsDrawable { private static final String TAG = "EmacsWindow"; diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java index 510300571b8..1548bf28087 100644 --- a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -50,7 +50,7 @@ import android.util.Log; Finally, every time a window is removed, the consumer is destroyed. */ -public class EmacsWindowAttachmentManager +public final class EmacsWindowAttachmentManager { public static EmacsWindowAttachmentManager MANAGER; private final static String TAG = "EmacsWindowAttachmentManager"; -- cgit v1.3 From 1cae464859341bfd5fc7d7ed4b503ef959af81dd Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 5 Mar 2023 19:58:28 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsActivity.java (onCreate): * java/org/gnu/emacs/EmacsContextMenu.java: * java/org/gnu/emacs/EmacsDocumentsProvider.java (getMimeType): * java/org/gnu/emacs/EmacsDrawLine.java (perform): * java/org/gnu/emacs/EmacsDrawRectangle.java (perform): * java/org/gnu/emacs/EmacsFillPolygon.java: * java/org/gnu/emacs/EmacsFontDriver.java: * java/org/gnu/emacs/EmacsHandleObject.java: * java/org/gnu/emacs/EmacsInputConnection.java: * java/org/gnu/emacs/EmacsMultitaskActivity.java (EmacsMultitaskActivity): * java/org/gnu/emacs/EmacsNative.java: * java/org/gnu/emacs/EmacsNoninteractive.java (EmacsNoninteractive, main): * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity) (startEmacsClient): * java/org/gnu/emacs/EmacsSdk7FontDriver.java: * java/org/gnu/emacs/EmacsSdk8Clipboard.java: * java/org/gnu/emacs/EmacsService.java (EmacsService, onCreate): * java/org/gnu/emacs/EmacsView.java (EmacsView, onLayout): * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow): * java/org/gnu/emacs/EmacsWindowAttachmentManager.java (EmacsWindowAttachmentManager): Remove redundant includes. Reorganize some functions around, remove duplicate `getLibDir' functions, and remove unused local variables. --- java/org/gnu/emacs/EmacsActivity.java | 13 ++++-- java/org/gnu/emacs/EmacsContextMenu.java | 3 -- java/org/gnu/emacs/EmacsDocumentsProvider.java | 4 +- java/org/gnu/emacs/EmacsDrawLine.java | 5 --- java/org/gnu/emacs/EmacsDrawRectangle.java | 1 - java/org/gnu/emacs/EmacsFillPolygon.java | 1 - java/org/gnu/emacs/EmacsFontDriver.java | 2 - java/org/gnu/emacs/EmacsHandleObject.java | 3 -- java/org/gnu/emacs/EmacsInputConnection.java | 9 +--- java/org/gnu/emacs/EmacsMultitaskActivity.java | 6 ++- java/org/gnu/emacs/EmacsNative.java | 2 - java/org/gnu/emacs/EmacsNoninteractive.java | 18 +------- java/org/gnu/emacs/EmacsOpenActivity.java | 20 +-------- java/org/gnu/emacs/EmacsSdk7FontDriver.java | 3 -- java/org/gnu/emacs/EmacsSdk8Clipboard.java | 4 +- java/org/gnu/emacs/EmacsService.java | 49 ++++++++-------------- java/org/gnu/emacs/EmacsView.java | 7 ++-- java/org/gnu/emacs/EmacsWindow.java | 15 +++---- .../gnu/emacs/EmacsWindowAttachmentManager.java | 5 +-- 19 files changed, 53 insertions(+), 117 deletions(-) (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 13002d5d0e5..692d8a14e22 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -24,18 +24,22 @@ import java.util.List; import java.util.ArrayList; import android.app.Activity; + import android.content.Context; import android.content.Intent; + import android.os.Build; import android.os.Bundle; + import android.util.Log; + import android.view.Menu; import android.view.View; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowInsets; import android.view.WindowInsetsController; -import android.widget.FrameLayout.LayoutParams; + import android.widget.FrameLayout; public class EmacsActivity extends Activity @@ -187,6 +191,7 @@ public class EmacsActivity extends Activity Intent intent; View decorView; ViewTreeObserver observer; + int matchParent; /* See if Emacs should be started with -Q. */ intent = getIntent (); @@ -194,8 +199,10 @@ public class EmacsActivity extends Activity = intent.getBooleanExtra ("org.gnu.emacs.START_DASH_Q", false); - params = new FrameLayout.LayoutParams (LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT); + matchParent = FrameLayout.LayoutParams.MATCH_PARENT; + params + = new FrameLayout.LayoutParams (matchParent, + matchParent); /* Make the frame layout. */ layout = new FrameLayout (this); diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index d1a624e68d9..dec5e148a8e 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -25,7 +25,6 @@ import java.util.ArrayList; import android.content.Context; import android.content.Intent; -import android.os.Bundle; import android.os.Build; import android.view.Menu; @@ -35,8 +34,6 @@ import android.view.SubMenu; import android.util.Log; -import android.widget.PopupMenu; - /* Context menu implementation. This object is built from JNI and describes a menu hiearchy. Then, `inflate' can turn it into an Android menu, which can be turned into a popup (or other kind of) diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java index 901c3b909e0..f70da6040ff 100644 --- a/java/org/gnu/emacs/EmacsDocumentsProvider.java +++ b/java/org/gnu/emacs/EmacsDocumentsProvider.java @@ -160,6 +160,7 @@ public final class EmacsDocumentsProvider extends DocumentsProvider { String name, extension, mime; int extensionSeparator; + MimeTypeMap singleton; if (file.isDirectory ()) return Document.MIME_TYPE_DIR; @@ -170,8 +171,9 @@ public final class EmacsDocumentsProvider extends DocumentsProvider if (extensionSeparator > 0) { + singleton = MimeTypeMap.getSingleton (); extension = name.substring (extensionSeparator + 1); - mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension (extension); + mime = singleton.getMimeTypeFromExtension (extension); if (mime != null) return mime; diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java index 0b23138a36c..92e03c48e26 100644 --- a/java/org/gnu/emacs/EmacsDrawLine.java +++ b/java/org/gnu/emacs/EmacsDrawLine.java @@ -21,13 +21,9 @@ package org.gnu.emacs; import java.lang.Math; -import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.PorterDuff.Mode; -import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.graphics.Xfermode; public final class EmacsDrawLine { @@ -38,7 +34,6 @@ public final class EmacsDrawLine Rect rect; Canvas canvas; Paint paint; - int i; /* TODO implement stippling. */ if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index ce5e94e4a76..3bd5779c54e 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -33,7 +33,6 @@ public final class EmacsDrawRectangle perform (EmacsDrawable drawable, EmacsGC gc, int x, int y, int width, int height) { - int i; Paint maskPaint, paint; Canvas maskCanvas; Bitmap maskBitmap; diff --git a/java/org/gnu/emacs/EmacsFillPolygon.java b/java/org/gnu/emacs/EmacsFillPolygon.java index d55a0b3aca8..ea8324c543c 100644 --- a/java/org/gnu/emacs/EmacsFillPolygon.java +++ b/java/org/gnu/emacs/EmacsFillPolygon.java @@ -21,7 +21,6 @@ package org.gnu.emacs; import java.lang.Math; -import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; diff --git a/java/org/gnu/emacs/EmacsFontDriver.java b/java/org/gnu/emacs/EmacsFontDriver.java index e142a3121d3..ff52899a897 100644 --- a/java/org/gnu/emacs/EmacsFontDriver.java +++ b/java/org/gnu/emacs/EmacsFontDriver.java @@ -19,8 +19,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -import java.util.List; - import android.os.Build; /* This code is mostly unused. See sfntfont-android.c for the code diff --git a/java/org/gnu/emacs/EmacsHandleObject.java b/java/org/gnu/emacs/EmacsHandleObject.java index a57a3bbdfa9..5b889895337 100644 --- a/java/org/gnu/emacs/EmacsHandleObject.java +++ b/java/org/gnu/emacs/EmacsHandleObject.java @@ -19,9 +19,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -import java.util.List; -import java.util.ArrayList; -import java.lang.Object; import java.lang.IllegalStateException; /* This defines something that is a so-called ``handle''. Handles diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index ed64c368857..7b40fcff99e 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -23,18 +23,13 @@ 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.inputmethod.TextSnapshot; import android.view.KeyEvent; -import android.text.Editable; - import android.util.Log; -/* Android input methods, take number six. - - See EmacsEditable for more details. */ +/* Android input methods, take number six. See textconv.c for more + details; this is more-or-less a thin wrapper around that file. */ public final class EmacsInputConnection extends BaseInputConnection { diff --git a/java/org/gnu/emacs/EmacsMultitaskActivity.java b/java/org/gnu/emacs/EmacsMultitaskActivity.java index dbdc99a3559..b1c48f03fba 100644 --- a/java/org/gnu/emacs/EmacsMultitaskActivity.java +++ b/java/org/gnu/emacs/EmacsMultitaskActivity.java @@ -19,7 +19,11 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -public class EmacsMultitaskActivity extends EmacsActivity +/* This class only exists because EmacsActivity is already defined as + an activity, and the system wants a new class in order to define a + new activity. */ + +public final class EmacsMultitaskActivity extends EmacsActivity { } diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index b1205353090..38370d60727 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -19,8 +19,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -import java.lang.System; - import android.content.res.AssetManager; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java index f365037b311..aaba74d877c 100644 --- a/java/org/gnu/emacs/EmacsNoninteractive.java +++ b/java/org/gnu/emacs/EmacsNoninteractive.java @@ -46,21 +46,6 @@ import java.lang.reflect.Method; @SuppressWarnings ("unchecked") public final class EmacsNoninteractive { - private static String - getLibraryDirectory (Context context) - { - int apiLevel; - - apiLevel = Build.VERSION.SDK_INT; - - if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) - return context.getApplicationInfo().nativeLibraryDir; - else if (apiLevel >= Build.VERSION_CODES.DONUT) - return context.getApplicationInfo().dataDir + "/lib"; - - return "/data/data/" + context.getPackageName() + "/lib"; - } - public static void main (String[] args) { @@ -188,7 +173,7 @@ public final class EmacsNoninteractive /* Now configure Emacs. The class path should already be set. */ filesDir = context.getFilesDir ().getCanonicalPath (); - libDir = getLibraryDirectory (context); + libDir = EmacsService.getLibraryDirectory (context); cacheDir = context.getCacheDir ().getCanonicalPath (); } catch (Exception e) @@ -198,6 +183,7 @@ public final class EmacsNoninteractive System.err.println ("and that Emacs needs adjustments in order to"); System.err.println ("obtain required system internal resources."); System.err.println ("Please report this bug to bug-gnu-emacs@gnu.org."); + e.printStackTrace (); System.exit (1); } diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index ac643ae8a13..51335ddb2dd 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -46,7 +46,6 @@ package org.gnu.emacs; import android.app.AlertDialog; import android.app.Activity; -import android.content.Context; import android.content.ContentResolver; import android.content.DialogInterface; import android.content.Intent; @@ -301,23 +300,6 @@ public final class EmacsOpenActivity extends Activity }); } - public String - getLibraryDirectory () - { - int apiLevel; - Context context; - - context = getApplicationContext (); - apiLevel = Build.VERSION.SDK_INT; - - if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) - return context.getApplicationInfo().nativeLibraryDir; - else if (apiLevel >= Build.VERSION_CODES.DONUT) - return context.getApplicationInfo().dataDir + "/lib"; - - return "/data/data/" + context.getPackageName() + "/lib"; - } - public void startEmacsClient (String fileName) { @@ -327,7 +309,7 @@ public final class EmacsOpenActivity extends Activity EmacsClientThread thread; File file; - libDir = getLibraryDirectory (); + libDir = EmacsService.getLibraryDirectory (this); builder = new ProcessBuilder (libDir + "/libemacsclient.so", fileName, "--reuse-frame", "--timeout=10", "--no-wait"); diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java index ae91c299de8..6df102f18a2 100644 --- a/java/org/gnu/emacs/EmacsSdk7FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java @@ -20,7 +20,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; import java.io.File; -import java.io.IOException; import java.util.LinkedList; import java.util.List; @@ -32,8 +31,6 @@ import android.graphics.Canvas; import android.util.Log; -import android.os.Build; - public class EmacsSdk7FontDriver extends EmacsFontDriver { private static final String TOFU_STRING = "\uDB3F\uDFFD"; diff --git a/java/org/gnu/emacs/EmacsSdk8Clipboard.java b/java/org/gnu/emacs/EmacsSdk8Clipboard.java index 818a722a908..5a40128b0ac 100644 --- a/java/org/gnu/emacs/EmacsSdk8Clipboard.java +++ b/java/org/gnu/emacs/EmacsSdk8Clipboard.java @@ -19,7 +19,9 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -/* Importing the entire package avoids the deprecation warning. */ +/* Importing the entire package instead of just the legacy + ClipboardManager class avoids the deprecation warning. */ + import android.text.*; import android.content.Context; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index d9cb25f3e9c..d05ebce75dc 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -24,22 +24,15 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.List; -import java.util.ArrayList; -import android.graphics.Canvas; -import android.graphics.Bitmap; import android.graphics.Point; -import android.view.View; import android.view.InputDevice; import android.view.KeyEvent; -import android.annotation.TargetApi; - import android.app.Notification; import android.app.NotificationManager; import android.app.NotificationChannel; -import android.app.PendingIntent; import android.app.Service; import android.content.ClipboardManager; @@ -51,9 +44,6 @@ import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager; import android.content.res.AssetManager; -import android.database.Cursor; -import android.database.MatrixCursor; - import android.hardware.input.InputManager; import android.net.Uri; @@ -68,9 +58,6 @@ import android.os.Vibrator; import android.os.VibratorManager; import android.os.VibrationEffect; -import android.provider.DocumentsContract; -import android.provider.DocumentsContract.Document; - import android.util.Log; import android.util.DisplayMetrics; @@ -87,7 +74,6 @@ class Holder public final class EmacsService extends Service { public static final String TAG = "EmacsService"; - public static final int MAX_PENDING_REQUESTS = 256; public static volatile EmacsService SERVICE; public static boolean needDashQ; @@ -107,6 +93,22 @@ public final class EmacsService extends Service information. */ public static final boolean DEBUG_IC = false; + /* Return the directory leading to the directory in which native + library files are stored on behalf of CONTEXT. */ + + public static String + getLibraryDirectory (Context context) + { + int apiLevel; + + apiLevel = Build.VERSION.SDK_INT; + + if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) + return context.getApplicationInfo ().nativeLibraryDir; + + return context.getApplicationInfo ().dataDir + "/lib"; + } + @Override public int onStartCommand (Intent intent, int flags, int startId) @@ -178,23 +180,6 @@ public final class EmacsService extends Service } } - private String - getLibraryDirectory () - { - int apiLevel; - Context context; - - context = getApplicationContext (); - apiLevel = Build.VERSION.SDK_INT; - - if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) - return context.getApplicationInfo().nativeLibraryDir; - else if (apiLevel >= Build.VERSION_CODES.DONUT) - return context.getApplicationInfo().dataDir + "/lib"; - - return "/data/data/" + context.getPackageName() + "/lib"; - } - @Override public void onCreate () @@ -219,7 +204,7 @@ public final class EmacsService extends Service /* Configure Emacs with the asset manager and other necessary parameters. */ filesDir = app_context.getFilesDir ().getCanonicalPath (); - libDir = getLibraryDirectory (); + libDir = getLibraryDirectory (this); cacheDir = app_context.getCacheDir ().getCanonicalPath (); /* Now provide this application's apk file, so a recursive diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 617836d8811..aefc79c4fb7 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -20,7 +20,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; import android.content.Context; -import android.content.res.ColorStateList; import android.text.InputType; @@ -116,6 +115,7 @@ public final class EmacsView extends ViewGroup super (EmacsService.SERVICE); Object tem; + Context context; this.window = window; this.damageRegion = new Region (); @@ -133,7 +133,8 @@ public final class EmacsView extends ViewGroup setDefaultFocusHighlightEnabled (false); /* Obtain the input method manager. */ - tem = getContext ().getSystemService (Context.INPUT_METHOD_SERVICE); + context = getContext (); + tem = context.getSystemService (Context.INPUT_METHOD_SERVICE); imManager = (InputMethodManager) tem; } @@ -281,7 +282,6 @@ public final class EmacsView extends ViewGroup int count, i; View child; Rect windowRect; - int wantedWidth, wantedHeight; count = getChildCount (); @@ -422,7 +422,6 @@ public final class EmacsView extends ViewGroup } } - /* The following two functions must not be called if the view has no parent, or is parented to an activity. */ diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index ea4cf48090d..ffc7476f010 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -31,19 +31,16 @@ import android.content.Context; import android.graphics.Rect; import android.graphics.Canvas; import android.graphics.Bitmap; -import android.graphics.Point; import android.graphics.PixelFormat; import android.view.View; import android.view.ViewManager; -import android.view.ViewGroup; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.InputDevice; import android.view.WindowManager; -import android.content.Intent; import android.util.Log; import android.os.Build; @@ -87,7 +84,7 @@ public final class EmacsWindow extends EmacsHandleObject public EmacsWindow parent; /* List of all children in stacking order. This must be kept - consistent! */ + consistent with their Z order! */ public ArrayList children; /* Map between pointer identifiers and last known position. Used to @@ -105,9 +102,8 @@ public final class EmacsWindow extends EmacsHandleObject last button press or release event. */ public int lastButtonState, lastModifiers; - /* Whether or not the window is mapped, and whether or not it is - deiconified. */ - private boolean isMapped, isIconified; + /* Whether or not the window is mapped. */ + private boolean isMapped; /* Whether or not to ask for focus upon being mapped, and whether or not the window should be focusable. */ @@ -122,7 +118,8 @@ public final class EmacsWindow extends EmacsHandleObject private WindowManager windowManager; /* The time of the last KEYCODE_VOLUME_DOWN release. This is used - to quit Emacs. */ + to quit Emacs upon two rapid clicks of the volume down + button. */ private long lastVolumeButtonRelease; /* Linked list of character strings which were recently sent as @@ -1103,14 +1100,12 @@ public final class EmacsWindow extends EmacsHandleObject public void noticeIconified () { - isIconified = true; EmacsNative.sendIconified (this.handle); } public void noticeDeiconified () { - isIconified = false; EmacsNative.sendDeiconified (this.handle); } diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java index 1548bf28087..30f29250970 100644 --- a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -20,7 +20,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; import android.content.Intent; @@ -74,8 +73,8 @@ public final class EmacsWindowAttachmentManager public EmacsWindowAttachmentManager () { - consumers = new LinkedList (); - windows = new LinkedList (); + consumers = new ArrayList (); + windows = new ArrayList (); } public void -- cgit v1.3 From 3198b7dc565e0e41d40b6c23509c285e96044a83 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 6 May 2023 11:32:56 +0800 Subject: Update Android port * cross/verbose.mk.android: Get rid of badly aligned ANDROID_CC messages. * java/org/gnu/emacs/EmacsInputConnection.java (syncAfterCommit) (extractAbsoluteOffsets): Add workarounds for several kinds of machines. (commitText, getExtractedText): Likewise. * src/textconv.c (really_commit_text): Improve definition of POSITION. (get_extracted_text): Default to providing at least 4 characters. --- cross/verbose.mk.android | 22 ++----- java/org/gnu/emacs/EmacsInputConnection.java | 87 ++++++++++++++++++++++++++-- src/textconv.c | 26 ++++++--- 3 files changed, 106 insertions(+), 29 deletions(-) (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') diff --git a/cross/verbose.mk.android b/cross/verbose.mk.android index 7d07b978de2..998f9843c7d 100644 --- a/cross/verbose.mk.android +++ b/cross/verbose.mk.android @@ -27,16 +27,7 @@ AM_V_CC = AM_V_CXX = AM_V_CCLD = AM_V_CXXLD = -AM_V_ELC = -AM_V_ELN = AM_V_GEN = -AM_V_GLOBALS = -AM_V_NO_PD = -AM_V_RC = -AM_V_JAVAC = -AM_V_DX = -AM_V_AAPT = -AM_V_ZIPALIGN = else # Whether $(info ...) works. This is to work around a bug in GNU Make @@ -53,13 +44,12 @@ have_working_info = $(filter notintermediate,$(value .FEATURES)) # The workaround is done only for AM_V_ELC and AM_V_ELN, # since the bug is not annoying elsewhere. -AM_V_AR = @$(info $ AR $@) +AM_V_AR = @$(info $ AR $@) AM_V_at = @ -AM_V_CC = @$(info $ ANDROID_CC $@) -AM_V_CXX = @$(info $ ANDROID_CXX $@) -AM_V_CCLD = @$(info $ CCLD $@) -AM_V_CXXLD = @$(info $ CXXLD $@) - -AM_V_GEN = @$(info $ GEN $@) +AM_V_CC = @$(info $ CC $@) +AM_V_CXX = @$(info $ CXX $@) +AM_V_CCLD = @$(info $ CCLD $@) +AM_V_CXXLD = @$(info $ CXXLD $@) +AM_V_GEN = @$(info $ GEN $@) AM_V_NO_PD = --no-print-directory endif diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 7b40fcff99e..d13b48288ce 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -26,6 +26,8 @@ import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.TextSnapshot; import android.view.KeyEvent; +import android.os.Build; + import android.util.Log; /* Android input methods, take number six. See textconv.c for more @@ -37,6 +39,34 @@ public final class EmacsInputConnection extends BaseInputConnection private EmacsView view; private short windowHandle; + /* Whether or not to synchronize and call `updateIC' with the + selection position after committing text. + + This helps with on screen keyboard programs found in some vendor + versions of Android, which rely on immediate updates to the point + position after text is commited in order to place the cursor + within that text. */ + + private static boolean syncAfterCommit; + + /* Whether or not to return empty text with the offset set to zero + if a request arrives that has no flags set and has requested no + characters at all. + + This is necessary with on screen keyboard programs found in some + vendor versions of Android which don't rely on the documented + meaning of `ExtractedText.startOffset', and instead take the + selection offset inside at face value. */ + + private static boolean extractAbsoluteOffsets; + + static + { + if (Build.MANUFACTURER.equalsIgnoreCase ("Huawei") + || Build.MANUFACTURER.equalsIgnoreCase ("Honor")) + extractAbsoluteOffsets = syncAfterCommit = true; + }; + public EmacsInputConnection (EmacsView view) { @@ -85,11 +115,32 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean commitText (CharSequence text, int newCursorPosition) { + int[] selection; + if (EmacsService.DEBUG_IC) Log.d (TAG, "commitText: " + text + " " + newCursorPosition); EmacsNative.commitText (windowHandle, text.toString (), newCursorPosition); + + if (syncAfterCommit) + { + /* Synchronize with the Emacs thread, obtain the new + selection, and report it immediately. */ + + selection = EmacsNative.getSelection (windowHandle); + + if (EmacsService.DEBUG_IC && selection != null) + Log.d (TAG, "commitText: new selection is " + selection[0] + + ", by " + selection[1]); + + if (selection != null) + /* N.B. that the composing region is removed after text is + committed. */ + view.imManager.updateSelection (view, selection[0], + selection[1], -1, -1); + } + return true; } @@ -203,16 +254,42 @@ public final class EmacsInputConnection extends BaseInputConnection getExtractedText (ExtractedTextRequest request, int flags) { ExtractedText text; + int[] selection; if (EmacsService.DEBUG_IC) - Log.d (TAG, "getExtractedText: " + request + " " + flags); - - text = EmacsNative.getExtractedText (windowHandle, request, - flags); + Log.d (TAG, "getExtractedText: " + request.hintMaxChars + ", " + + request.hintMaxLines + " " + flags); + + /* If a request arrives with hintMaxChars, hintMaxLines and flags + set to 0, and the system is known to be buggy, return an empty + extracted text object with the absolute selection positions. */ + + if (extractAbsoluteOffsets + && request.hintMaxChars == 0 + && request.hintMaxLines == 0 + && flags == 0) + { + /* Obtain the selection. */ + selection = EmacsNative.getSelection (windowHandle); + if (selection == null) + return null; + + /* Create the workaround extracted text. */ + text = new ExtractedText (); + text.partialStartOffset = -1; + text.partialEndOffset = -1; + text.text = ""; + text.selectionStart = selection[0]; + text.selectionEnd = selection[1]; + } + else + text = EmacsNative.getExtractedText (windowHandle, request, + flags); if (EmacsService.DEBUG_IC) Log.d (TAG, "getExtractedText: " + text.text + " @" - + text.startOffset + ":" + text.selectionStart); + + text.startOffset + ":" + text.selectionStart + + ", " + text.selectionEnd); return text; } diff --git a/src/textconv.c b/src/textconv.c index 4fa92f43ecd..e1a73e91397 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -540,7 +540,11 @@ restore_selected_window (Lisp_Object window) /* Commit the given text in the composing region. If there is no composing region, then insert the text after F's selected window's - last point instead. Finally, remove the composing region. */ + last point instead. Finally, remove the composing region. + + Then, move point to POSITION relative to TEXT. If POSITION is + greater than zero, it is relative to the character at the end of + TEXT; otherwise, it is relative to the start of TEXT. */ static void really_commit_text (struct frame *f, EMACS_INT position, @@ -577,14 +581,16 @@ really_commit_text (struct frame *f, EMACS_INT position, Finsert (1, &text); record_buffer_change (start, PT, text); - /* Move to a the position specified in POSITION. */ + /* Move to a the position specified in POSITION. If POSITION is + less than zero, it is relative to the start of the text that + was inserted. */ - if (position < 0) + if (position <= 0) { wanted = marker_position (f->conversion.compose_region_start); - if (INT_SUBTRACT_WRAPV (wanted, position, &wanted) + if (INT_ADD_WRAPV (wanted, position, &wanted) || wanted < BEGV) wanted = BEGV; @@ -595,6 +601,9 @@ really_commit_text (struct frame *f, EMACS_INT position, } else { + /* Otherwise, it is relative to the last character in + TEXT. */ + wanted = marker_position (f->conversion.compose_region_end); @@ -631,9 +640,9 @@ really_commit_text (struct frame *f, EMACS_INT position, Finsert (1, &text); record_buffer_change (wanted, PT, text); - if (position < 0) + if (position <= 0) { - if (INT_SUBTRACT_WRAPV (wanted, position, &wanted) + if (INT_ADD_WRAPV (wanted, position, &wanted) || wanted < BEGV) wanted = BEGV; @@ -1533,8 +1542,9 @@ get_extracted_text (struct frame *f, ptrdiff_t n, /* Figure out the bounds of the text to return. */ if (n != -1) { - /* Make sure n is at least 2. */ - n = max (2, n); + /* Make sure n is at least 4, leaving two characters around + PT. */ + n = max (4, n); start = PT - n / 2; end = PT + n - n / 2; -- cgit v1.3 From 889b61b99918d1c6313d4f884de2e2cb3ab466c9 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 7 May 2023 11:09:56 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsInputConnection.java (requestCursorUpdates): * java/org/gnu/emacs/EmacsNative.java (requestCursorUpdates): * java/org/gnu/emacs/EmacsService.java (updateCursorAnchorInfo): New functions. * src/android.c (struct android_emacs_service) (android_init_emacs_service): Add new method. (android_update_cursor_anchor_info): New function. * src/androidfns.c (android_set_preeditarea): New function. * src/androidgui.h (enum android_ime_operation): New operation `REQUEST_CURSOR_UPDATES'. (struct android_ime_event): Document new meaning of `length'. * src/androidterm.c (android_request_cursor_updates): New function. (android_handle_ime_event): Handle new operations. (handle_one_android_event, android_draw_window_cursor): Update the preedit area if needed, like on X. (requestCursorUpdates): New function. * src/androidterm.h (struct android_output): New field `need_cursor_updates'. --- java/org/gnu/emacs/EmacsInputConnection.java | 11 ++++ java/org/gnu/emacs/EmacsNative.java | 1 + java/org/gnu/emacs/EmacsService.java | 32 ++++++++++ src/android.c | 34 +++++++++++ src/androidfns.c | 28 +++++++++ src/androidgui.h | 15 ++++- src/androidterm.c | 88 ++++++++++++++++++++++++++++ src/androidterm.h | 5 ++ 8 files changed, 213 insertions(+), 1 deletion(-) (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index d13b48288ce..21bbaca5d07 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -324,6 +324,17 @@ public final class EmacsInputConnection extends BaseInputConnection return this.deleteSurroundingText (beforeLength, afterLength); } + @Override + public boolean + requestCursorUpdates (int cursorUpdateMode) + { + if (EmacsService.DEBUG_IC) + Log.d (TAG, "requestCursorUpdates: " + cursorUpdateMode); + + EmacsNative.requestCursorUpdates (windowHandle, cursorUpdateMode); + return true; + } + /* Override functions which are not implemented. */ diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 7d13ff99abb..e699dda9ad4 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -209,6 +209,7 @@ public final class EmacsNative ExtractedTextRequest req, int flags); public static native void requestSelectionUpdate (short window); + public static native void requestCursorUpdates (short window, int mode); /* Return the current value of the selection, or -1 upon diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 33436892caa..30ef71540a9 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -25,10 +25,12 @@ import java.io.UnsupportedEncodingException; import java.util.List; +import android.graphics.Matrix; import android.graphics.Point; import android.view.InputDevice; import android.view.KeyEvent; +import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.ExtractedText; import android.app.Notification; @@ -635,6 +637,36 @@ public final class EmacsService extends Service window.view.imManager.restartInput (window.view); } + public void + updateCursorAnchorInfo (EmacsWindow window, float x, + float y, float yBaseline, + float yBottom) + { + CursorAnchorInfo info; + CursorAnchorInfo.Builder builder; + Matrix matrix; + int[] offsets; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + return; + + offsets = new int[2]; + builder = new CursorAnchorInfo.Builder (); + matrix = new Matrix (window.view.getMatrix ()); + window.view.getLocationOnScreen (offsets); + matrix.postTranslate (offsets[0], offsets[1]); + builder.setMatrix (matrix); + builder.setInsertionMarkerLocation (x, y, yBaseline, yBottom, + 0); + info = builder.build (); + + if (DEBUG_IC) + Log.d (TAG, ("updateCursorAnchorInfo: " + x + " " + y + + " " + yBaseline + "-" + yBottom)); + + window.view.imManager.updateCursorAnchorInfo (window.view, info); + } + /* Open a content URI described by the bytes BYTES, a non-terminated string; make it writable if WRITABLE, and readable if READABLE. Truncate the file if TRUNCATE. diff --git a/src/android.c b/src/android.c index 129ad6b5767..8a41a7cdec5 100644 --- a/src/android.c +++ b/src/android.c @@ -113,6 +113,7 @@ struct android_emacs_service jmethodID query_battery; jmethodID display_toast; jmethodID update_extracted_text; + jmethodID update_cursor_anchor_info; }; struct android_emacs_pixmap @@ -2209,6 +2210,8 @@ android_init_emacs_service (void) FIND_METHOD (update_extracted_text, "updateExtractedText", "(Lorg/gnu/emacs/EmacsWindow;" "Landroid/view/inputmethod/ExtractedText;I)V"); + FIND_METHOD (update_cursor_anchor_info, "updateCursorAnchorInfo", + "(Lorg/gnu/emacs/EmacsWindow;FFFF)V"); #undef FIND_METHOD } @@ -6277,6 +6280,37 @@ android_update_extracted_text (android_window window, void *text, android_exception_check_1 (text); } +/* Report the position of the cursor to the input method connection on + WINDOW. + + X is the horizontal position of the end of the insertion marker. Y + is the top of the insertion marker. Y_BASELINE is the baseline of + the row containing the insertion marker, and Y_BOTTOM is the bottom + of the insertion marker. */ + +void +android_update_cursor_anchor_info (android_window window, float x, + float y, float y_baseline, + float y_bottom) +{ + jobject object; + jmethodID method; + + object = android_resolve_handle (window, ANDROID_HANDLE_WINDOW); + method = service_class.update_cursor_anchor_info; + + (*android_java_env)->CallNonvirtualVoidMethod (android_java_env, + emacs_service, + service_class.class, + method, + object, + (jfloat) x, + (jfloat) y, + (jfloat) y_baseline, + (jfloat) y_bottom); + android_exception_check (); +} + /* Window decoration management functions. */ diff --git a/src/androidfns.c b/src/androidfns.c index 3bd34edd5b9..60b0549e7d1 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -3000,6 +3000,34 @@ for more details about these values. */) make_fixnum (state.temperature)); } + + +/* Miscellaneous input method related stuff. */ + +/* Report X, Y, by the phys cursor width and height as the cursor + anchor rectangle for W's frame. */ + +void +android_set_preeditarea (struct window *w, int x, int y) +{ + struct frame *f; + + f = WINDOW_XFRAME (w); + + /* Convert the window coordinates to the frame's coordinate + space. */ + x = (WINDOW_TO_FRAME_PIXEL_X (w, x) + + WINDOW_LEFT_FRINGE_WIDTH (w) + + WINDOW_LEFT_MARGIN_WIDTH (w)); + y = WINDOW_TO_FRAME_PIXEL_Y (w, y); + + /* Note that calculating the baseline is too hard, so the bottom of + the cursor is used instead. */ + android_update_cursor_anchor_info (FRAME_ANDROID_WINDOW (f), x, + y, y + w->phys_cursor_height, + y + w->phys_cursor_height); +} + #endif diff --git a/src/androidgui.h b/src/androidgui.h index ddd8e9fcf72..6db25098398 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -432,6 +432,13 @@ enum android_ime_operation ANDROID_IME_START_BATCH_EDIT, ANDROID_IME_END_BATCH_EDIT, ANDROID_IME_REQUEST_SELECTION_UPDATE, + ANDROID_IME_REQUEST_CURSOR_UPDATES, + }; + +enum + { + ANDROID_CURSOR_UPDATE_IMMEDIATE = 1, + ANDROID_CURSOR_UPDATE_MONITOR = (1 << 1), }; struct android_ime_event @@ -452,7 +459,11 @@ struct android_ime_event indices, and may actually mean ``left'' and ``right''. */ ptrdiff_t start, end, position; - /* The number of characters in TEXT. */ + /* The number of characters in TEXT. + + If OPERATION is ANDROID_IME_REQUEST_CURSOR_UPDATES, then this is + actually the cursor update mode associated with that + operation. */ size_t length; /* TEXT is either NULL, or a pointer to LENGTH bytes of malloced @@ -620,6 +631,8 @@ extern void android_update_ic (android_window, ptrdiff_t, ptrdiff_t, extern void android_reset_ic (android_window, enum android_ic_mode); extern void android_update_extracted_text (android_window, void *, int); +extern void android_update_cursor_anchor_info (android_window, float, + float, float, float); extern int android_set_fullscreen (android_window, bool); enum android_cursor_shape diff --git a/src/androidterm.c b/src/androidterm.c index 8ba7fb6a798..6f7c06875ca 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -632,6 +632,40 @@ android_decode_utf16 (unsigned short *utf16, size_t n) return coding.dst_object; } +/* Handle a cursor update request for F from the input method. + MODE specifies whether or not an update should be sent immediately, + and whether or not they are needed in the future. + + If MODE & ANDROID_CURSOR_UPDATE_IMMEDIATE, report the position of + F's old selected window's phys cursor now. + + If MODE & ANDROID_CURSOR_UPDATE_MONITOR, set + `need_cursor_updates'. */ + +static void +android_request_cursor_updates (struct frame *f, int mode) +{ + struct window *w; + + if (mode & ANDROID_CURSOR_UPDATE_IMMEDIATE + && WINDOWP (WINDOW_LIVE_P (f->old_selected_window) + ? f->old_selected_window + : f->selected_window)) + { + /* Prefer the old selected window, as its selection is what was + reported to the IME previously. */ + + w = XWINDOW (WINDOW_LIVE_P (f->old_selected_window) + ? f->old_selected_window + : f->selected_window); + android_set_preeditarea (w, w->cursor.x, w->cursor.y); + } + + /* Now say whether or not updates are needed in the future. */ + FRAME_OUTPUT_DATA (f)->need_cursor_updates + = (mode & ANDROID_CURSOR_UPDATE_MONITOR); +} + /* Handle a single input method event EVENT, delivered to the frame F. @@ -705,6 +739,10 @@ android_handle_ime_event (union android_event *event, struct frame *f) case ANDROID_IME_REQUEST_SELECTION_UPDATE: request_point_update (f, event->ime.counter); break; + + case ANDROID_IME_REQUEST_CURSOR_UPDATES: + android_request_cursor_updates (f, event->ime.length); + break; } } @@ -724,6 +762,7 @@ handle_one_android_event (struct android_display_info *dpyinfo, double scroll_unit; int keysym; ptrdiff_t nchars, i; + struct window *w; /* It is okay for this to not resemble handle_one_xevent so much. Differences in event handling code are much less nasty than @@ -812,6 +851,12 @@ handle_one_android_event (struct android_display_info *dpyinfo, inev.ie.kind = MOVE_FRAME_EVENT; XSETFRAME (inev.ie.frame_or_window, f); } + + if (f && FRAME_OUTPUT_DATA (f)->need_cursor_updates) + { + w = XWINDOW (f->selected_window); + android_set_preeditarea (w, w->cursor.x, w->cursor.y); + } } goto OTHER; @@ -954,6 +999,16 @@ handle_one_android_event (struct android_display_info *dpyinfo, goto done_keysym; done_keysym: + + /* Now proceed to tell the input method the current position of + the cursor, if required. */ + + if (f && FRAME_OUTPUT_DATA (f)->need_cursor_updates) + { + w = XWINDOW (f->selected_window); + android_set_preeditarea (w, w->cursor.x, w->cursor.y); + } + goto OTHER; case ANDROID_FOCUS_IN: @@ -4321,6 +4376,10 @@ android_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, int x, int y, enum text_cursor_kinds cursor_type, int cursor_width, bool on_p, bool active_p) { + struct frame *f; + + f = WINDOW_XFRAME (w); + if (on_p) { w->phys_cursor_type = cursor_type; @@ -4362,6 +4421,13 @@ android_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, emacs_abort (); } } + + /* Now proceed to tell the input method the current position of + the cursor, if required. */ + + if (FRAME_OUTPUT_DATA (f)->need_cursor_updates + && w == XWINDOW (f->selected_window)) + android_set_preeditarea (w, x, y); } } @@ -5404,6 +5470,28 @@ NATIVE_NAME (requestSelectionUpdate) (JNIEnv *env, jobject object, android_write_event (&event); } +JNIEXPORT void JNICALL +NATIVE_NAME (requestCursorUpdates) (JNIEnv *env, jobject object, + jshort window, jint mode) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_REQUEST_CURSOR_UPDATES; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = mode; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + #ifdef __clang__ #pragma clang diagnostic pop #else diff --git a/src/androidterm.h b/src/androidterm.h index 9396d5fe315..e3738fb2192 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -231,6 +231,10 @@ struct android_output because the frame contents have been dirtied. */ bool_bf need_buffer_flip : 1; + /* Whether or not the input method should be notified every time the + position of this frame's selected window changes. */ + bool_bf need_cursor_updates : 1; + /* Relief GCs, colors etc. */ struct relief { struct android_gc *gc; @@ -383,6 +387,7 @@ extern struct android_display_info *x_display_list; extern void android_free_gcs (struct frame *); extern void android_default_font_parameter (struct frame *, Lisp_Object); +extern void android_set_preeditarea (struct window *, int, int); /* Defined in androidterm.c. */ -- cgit v1.3 From 57903519eb61632c4a85fbaf420109892955079a Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 31 May 2023 10:13:04 +0800 Subject: Update Android port * java/debug.sh (is_root): Go back to using unix sockets; allow adb to forward them correctly. * java/org/gnu/emacs/EmacsInputConnection.java (getExtractedText): Don't print text if NULL. * java/org/gnu/emacs/EmacsService.java (EmacsService): New field `imSyncInProgress'. (updateIC): If an IM sync might be in progress, avoid deadlocks. * java/org/gnu/emacs/EmacsView.java (onCreateInputConnection): Set `imSyncInProgress' across synchronization point. * src/android.c (android_check_query): Use __atomic_store_n. (android_answer_query): New function. (android_begin_query): Set `android_servicing_query' to 2. Check once, and don't spin waiting for query to complete. (android_end_query): Use __atomic_store_n. (android_run_in_emacs_thread): Compare-and-exchange flag. If originally 1, fail. * src/textconv.c (really_set_composing_text): Clear conversion region if text is empty. --- java/debug.sh | 17 +++--- java/org/gnu/emacs/EmacsInputConnection.java | 8 +++ java/org/gnu/emacs/EmacsService.java | 28 ++++++++++ java/org/gnu/emacs/EmacsView.java | 8 ++- src/android.c | 81 ++++++++++++++++++++++------ src/textconv.c | 6 +++ 6 files changed, 126 insertions(+), 22 deletions(-) (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') diff --git a/java/debug.sh b/java/debug.sh index 339b3604810..0458003fe72 100755 --- a/java/debug.sh +++ b/java/debug.sh @@ -19,6 +19,7 @@ ## along with GNU Emacs. If not, see . set -m +set -x oldpwd=`pwd` cd `dirname $0` @@ -310,22 +311,26 @@ rm -f /tmp/file-descriptor-stamp if [ -z "$gdbserver" ]; then if [ "$is_root" = "yes" ]; then - adb -s $device shell $gdbserver_bin --once \ + adb -s $device shell $gdbserver_bin --multi \ "+/data/local/tmp/debug.$package.socket" --attach $pid >&5 & gdb_socket="localfilesystem:/data/local/tmp/debug.$package.socket" - else - adb -s $device shell run-as $package $gdbserver_bin --once \ + else + adb -s $device shell run-as $package $gdbserver_bin --multi \ "+debug.$package.socket" --attach $pid >&5 & gdb_socket="localfilesystem:$app_data_dir/debug.$package.socket" fi else # Normally the program cannot access $gdbserver_bin when it is # placed in /data/local/tmp. - adb -s $device shell run-as $package $gdbserver_cmd --once \ - "0.0.0.0:7654" --attach $pid >&5 & - gdb_socket="tcp:7654" + adb -s $device shell run-as $package $gdbserver_cmd --multi \ + "+debug.$package.socket" --attach $pid >&5 & + gdb_socket="localfilesystem:$app_data_dir/debug.$package.socket" fi +# In order to allow adb to forward to the gdbserver socket, make the +# app data directory a+x. +adb -s $device shell run-as $package chmod a+x $app_data_dir + # Wait until gdbserver successfully runs. line= while read -u 5 line; do diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 21bbaca5d07..420da58c0f8 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -286,6 +286,14 @@ public final class EmacsInputConnection extends BaseInputConnection text = EmacsNative.getExtractedText (windowHandle, request, flags); + if (text == null) + { + if (EmacsService.DEBUG_IC) + Log.d (TAG, "getExtractedText: text is NULL"); + + return null; + } + if (EmacsService.DEBUG_IC) Log.d (TAG, "getExtractedText: " + text.text + " @" + text.startOffset + ":" + text.selectionStart diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 546d22627c5..2f35933a7d1 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -104,6 +104,9 @@ public final class EmacsService extends Service performing drawing calls. */ private static final boolean DEBUG_THREADS = false; + /* Whether or not onCreateInputMethod is calling getSelection. */ + public static volatile boolean imSyncInProgress; + /* Return the directory leading to the directory in which native library files are stored on behalf of CONTEXT. */ @@ -636,16 +639,41 @@ public final class EmacsService extends Service int newSelectionEnd, int composingRegionStart, int composingRegionEnd) { + boolean wasSynchronous; + if (DEBUG_IC) Log.d (TAG, ("updateIC: " + window + " " + newSelectionStart + " " + newSelectionEnd + " " + composingRegionStart + " " + composingRegionEnd)); + + /* `updateSelection' holds an internal lock that is also taken + before `onCreateInputConnection' (in EmacsView.java) is called; + when that then asks the UI thread for the current selection, a + dead lock results. To remedy this, reply to any synchronous + queries now -- and prohibit more queries for the duration of + `updateSelection' -- if EmacsView may have been asking for the + value of the region. */ + + wasSynchronous = false; + if (EmacsService.imSyncInProgress) + { + /* `beginSynchronous' will answer any outstanding queries and + signal that one is now in progress, thereby preventing + `getSelection' from blocking. */ + + EmacsNative.beginSynchronous (); + wasSynchronous = true; + } + window.view.imManager.updateSelection (window.view, newSelectionStart, newSelectionEnd, composingRegionStart, composingRegionEnd); + + if (wasSynchronous) + EmacsNative.endSynchronous (); } public void diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 09bc9d719d3..bb450bb8e6b 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -628,8 +628,14 @@ public final class EmacsView extends ViewGroup } /* Obtain the current position of point and set it as the - selection. */ + selection. Don't do this under one specific situation: if + `android_update_ic' is being called in the main thread, trying + to synchronize with it can cause a dead lock in the IM + manager. */ + + EmacsService.imSyncInProgress = true; selection = EmacsNative.getSelection (window.handle); + EmacsService.imSyncInProgress = false; if (selection != null) Log.d (TAG, "onCreateInputConnection: current selection is: " diff --git a/src/android.c b/src/android.c index 8cc18787358..9d1399f3fc2 100644 --- a/src/android.c +++ b/src/android.c @@ -6959,8 +6959,11 @@ android_display_toast (const char *text) -/* Whether or not a query is currently being made. */ -static bool android_servicing_query; +/* The thread from which a query against a thread is currently being + made, if any. Value is 0 if no query is in progress, 1 if a query + is being made from the UI thread to the main thread, and 2 if a + query is being made the other way around. */ +static char android_servicing_query; /* Function that is waiting to be run in the Emacs thread. */ static void (*android_query_function) (void *); @@ -7010,7 +7013,37 @@ android_check_query (void) /* Finish the query. */ __atomic_store_n (&android_query_context, NULL, __ATOMIC_SEQ_CST); __atomic_store_n (&android_query_function, NULL, __ATOMIC_SEQ_CST); - __atomic_clear (&android_servicing_query, __ATOMIC_SEQ_CST); + __atomic_store_n (&android_servicing_query, 0, __ATOMIC_SEQ_CST); + + /* Signal completion. */ + sem_post (&android_query_sem); +} + +/* Run the function that the UI thread has asked to run, and then + signal its completion. Do not change `android_servicing_query' + after it completes. */ + +static void +android_answer_query (void) +{ + void (*proc) (void *); + void *closure; + + eassert (__atomic_load_n (&android_servicing_query, __ATOMIC_SEQ_CST) + == 1); + + /* First, load the procedure and closure. */ + __atomic_load (&android_query_context, &closure, __ATOMIC_SEQ_CST); + __atomic_load (&android_query_function, &proc, __ATOMIC_SEQ_CST); + + if (!proc) + return; + + proc (closure); + + /* Finish the query. */ + __atomic_store_n (&android_query_context, NULL, __ATOMIC_SEQ_CST); + __atomic_store_n (&android_query_function, NULL, __ATOMIC_SEQ_CST); /* Signal completion. */ sem_post (&android_query_sem); @@ -7025,18 +7058,23 @@ android_check_query (void) static void android_begin_query (void) { - if (__atomic_test_and_set (&android_servicing_query, - __ATOMIC_SEQ_CST)) + char old; + + /* Load the previous value of `android_servicing_query' and upgrade + it to 2. */ + + old = __atomic_exchange_n (&android_servicing_query, + 2, __ATOMIC_SEQ_CST); + + /* See if a query was previously in progress. */ + if (old == 1) { /* Answer the query that is currently being made. */ assert (android_query_function != NULL); - android_check_query (); - - /* Wait for that query to complete. */ - while (__atomic_load_n (&android_servicing_query, - __ATOMIC_SEQ_CST)) - ;; + android_answer_query (); } + + /* `android_servicing_query' is now 2. */ } /* Notice that a query has stopped. This function may be called from @@ -7045,7 +7083,7 @@ android_begin_query (void) static void android_end_query (void) { - __atomic_clear (&android_servicing_query, __ATOMIC_SEQ_CST); + __atomic_store_n (&android_servicing_query, 0, __ATOMIC_SEQ_CST); } /* Synchronously ask the Emacs thread to run the specified PROC with @@ -7063,6 +7101,7 @@ int android_run_in_emacs_thread (void (*proc) (void *), void *closure) { union android_event event; + char old; event.xaction.type = ANDROID_WINDOW_ACTION; event.xaction.serial = ++event_serial; @@ -7074,10 +7113,13 @@ android_run_in_emacs_thread (void (*proc) (void *), void *closure) __atomic_store_n (&android_query_function, proc, __ATOMIC_SEQ_CST); /* Don't allow deadlocks to happen; make sure the Emacs thread is - not waiting for something to be done. */ + not waiting for something to be done (in that case, + `android_query_context' is 2.) */ - if (__atomic_test_and_set (&android_servicing_query, - __ATOMIC_SEQ_CST)) + old = 0; + if (!__atomic_compare_exchange_n (&android_servicing_query, &old, + 1, false, __ATOMIC_SEQ_CST, + __ATOMIC_SEQ_CST)) { __atomic_store_n (&android_query_context, NULL, __ATOMIC_SEQ_CST); @@ -7098,6 +7140,15 @@ android_run_in_emacs_thread (void (*proc) (void *), void *closure) while (sem_wait (&android_query_sem) < 0) ;; + /* At this point, `android_servicing_query' should either be zero if + the query was answered or two if the main thread has started a + query. */ + + eassert (!__atomic_load_n (&android_servicing_query, + __ATOMIC_SEQ_CST) + || (__atomic_load_n (&android_servicing_query, + __ATOMIC_SEQ_CST) == 2)); + return 0; } diff --git a/src/textconv.c b/src/textconv.c index 26f351dc729..1530cc0ce32 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -792,6 +792,12 @@ really_set_composing_text (struct frame *f, ptrdiff_t position, /* Move the composition overlay. */ sync_overlay (f); + /* If TEXT is empty, remove the composing region. This goes against + the documentation, but is ultimately what programs expect. */ + + if (!SCHARS (text)) + really_finish_composing_text (f); + /* If PT hasn't changed, the conversion region definitely has. Otherwise, redisplay will update the input method instead. */ -- cgit v1.3 From 9a958c59a2ce546e6ec99c58ca181dafeac8dd6b Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 1 Jun 2023 10:05:42 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection): Add compatibility adjustments for Samsung devices. --- java/org/gnu/emacs/EmacsInputConnection.java | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 420da58c0f8..54c98d950aa 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -65,6 +65,13 @@ public final class EmacsInputConnection extends BaseInputConnection if (Build.MANUFACTURER.equalsIgnoreCase ("Huawei") || Build.MANUFACTURER.equalsIgnoreCase ("Honor")) extractAbsoluteOffsets = syncAfterCommit = true; + + /* The Samsung keyboard takes `selectionStart' at face value if + some text is returned, and also searches for words solely + within that text. However, when no text is returned, it falls + back to getTextAfterCursor and getTextBeforeCursor. */ + if (Build.MANUFACTURER.equalsIgnoreCase ("Samsung")) + extractAbsoluteOffsets = true; }; public -- cgit v1.3 From aed0a11147e29fc73405f1815fef91ecf6cca7fb Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 1 Jun 2023 15:16:02 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection, performContextMenuAction): New function. * java/org/gnu/emacs/EmacsNative.java (EmacsNative) (performContextMenuAction): New function. * src/android.c (android_get_gc_values): Implement more efficiently. * src/androidterm.c (android_handle_ime_event): Pass through `update' argument to `finish_composing_text'. Fix thinko. * src/textconv.c (really_finish_composing_text) (really_set_composing_text, really_set_composing_region) (handle_pending_conversion_events_1, finish_composing_text): New argument `update'. Notify IME of conversion region changes if set. * src/textconv.h: Update structs and prototypes. --- java/org/gnu/emacs/EmacsInputConnection.java | 46 ++++++++++++++++++ java/org/gnu/emacs/EmacsNative.java | 2 + src/android.c | 15 ++---- src/androidterm.c | 70 ++++++++++++++++++++++++++-- src/textconv.c | 23 ++++++--- src/textconv.h | 3 +- 6 files changed, 136 insertions(+), 23 deletions(-) (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 54c98d950aa..eb6fd5f2763 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -256,6 +256,52 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public boolean + performContextMenuAction (int contextMenuAction) + { + int action; + + if (EmacsService.DEBUG_IC) + Log.d (TAG, "performContextMenuAction: " + contextMenuAction); + + /* Translate the action in Java code. That way, a great deal of + JNI boilerplate can be avoided. */ + + switch (contextMenuAction) + { + case android.R.id.selectAll: + action = 0; + break; + + case android.R.id.startSelectingText: + action = 1; + break; + + case android.R.id.stopSelectingText: + action = 2; + break; + + case android.R.id.cut: + action = 3; + break; + + case android.R.id.copy: + action = 4; + break; + + case android.R.id.paste: + action = 5; + break; + + default: + return true; + } + + EmacsNative.performContextMenuAction (windowHandle, action); + return true; + } + @Override public ExtractedText getExtractedText (ExtractedTextRequest request, int flags) diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 56c03ee38dc..eb75201088b 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -208,6 +208,8 @@ public final class EmacsNative public static native void setSelection (short window, int start, int end); public static native void performEditorAction (short window, int editorAction); + public static native void performContextMenuAction (short window, + int contextMenuAction); public static native ExtractedText getExtractedText (short window, ExtractedTextRequest req, int flags); diff --git a/src/android.c b/src/android.c index 67590ae373d..94587344eb5 100644 --- a/src/android.c +++ b/src/android.c @@ -3863,22 +3863,13 @@ android_get_gc_values (struct android_gc *gc, values->clip_y_origin = gc->clip_y_origin; if (mask & ANDROID_GC_FILL_STYLE) - values->fill_style - = (*android_java_env)->GetIntField (android_java_env, - gcontext, - emacs_gc_fill_style); + values->fill_style = gc->fill_style; if (mask & ANDROID_GC_TILE_STIP_X_ORIGIN) - values->ts_x_origin - = (*android_java_env)->GetIntField (android_java_env, - gcontext, - emacs_gc_ts_origin_x); + values->ts_x_origin = gc->ts_x_origin; if (mask & ANDROID_GC_TILE_STIP_Y_ORIGIN) - values->ts_y_origin - = (*android_java_env)->GetIntField (android_java_env, - gcontext, - emacs_gc_ts_origin_y); + values->ts_y_origin = gc->ts_y_origin; /* Fields involving handles are not used by Emacs, and thus not implemented */ diff --git a/src/androidterm.c b/src/androidterm.c index a9b5834c08f..c302e3f2877 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -681,7 +681,6 @@ android_handle_ime_event (union android_event *event, struct frame *f) switch (event->ime.operation) { case ANDROID_IME_COMMIT_TEXT: - case ANDROID_IME_FINISH_COMPOSING_TEXT: case ANDROID_IME_SET_COMPOSING_TEXT: text = android_decode_utf16 (event->ime.text, event->ime.length); @@ -708,7 +707,8 @@ android_handle_ime_event (union android_event *event, struct frame *f) break; case ANDROID_IME_FINISH_COMPOSING_TEXT: - finish_composing_text (f, event->ime.counter); + finish_composing_text (f, event->ime.counter, + event->ime.length); break; case ANDROID_IME_SET_COMPOSING_TEXT: @@ -5161,7 +5161,71 @@ NATIVE_NAME (performEditorAction) (JNIEnv *env, jobject object, /* Undocumented behavior: performEditorAction is apparently expected to finish composing any text. */ - NATIVE_NAME (finishComposingText) (env, object, window); + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_FINISH_COMPOSING_TEXT; + event.ime.start = 0; + event.ime.end = 0; + + /* This value of `length' means that the input method should receive + an update containing the new conversion region. */ + + event.ime.length = 1; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); + + /* Finally, send the return key press. */ + + event.xkey.type = ANDROID_KEY_PRESS; + event.xkey.serial = ++event_serial; + event.xkey.window = window; + event.xkey.time = 0; + event.xkey.state = 0; + event.xkey.keycode = 66; + event.xkey.unicode_char = 0; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (performContextMenuAction) (JNIEnv *env, jobject object, + jshort window, int action) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + union android_event event; + int key; + + /* Note that ACTION is determined in EmacsInputConnection, and as + such they are not actual resource IDs. */ + + switch (action) + { + case 0: /* android.R.id.selectAll */ + case 1: /* android.R.id.startSelectingText */ + case 2: /* android.R.id.stopSelectingText */ + /* These actions are not implemented. */ + return; + + case 3: /* android.R.id.cut */ + key = 277; + break; + + case 4: /* android.R.id.copy */ + key = 278; + break; + + case 5: /* android.R.id.paste */ + key = 279; + break; + + default: + emacs_abort (); + } event.xkey.type = ANDROID_KEY_PRESS; event.xkey.serial = ++event_serial; diff --git a/src/textconv.c b/src/textconv.c index dcf016104fe..d8166bcfd03 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -676,10 +676,11 @@ really_commit_text (struct frame *f, EMACS_INT position, } /* Remove the composition region on the frame F, while leaving its - contents intact. */ + contents intact. If UPDATE, also notify the input method of the + change. */ static void -really_finish_composing_text (struct frame *f) +really_finish_composing_text (struct frame *f, bool update) { if (!NILP (f->conversion.compose_region_start)) { @@ -687,6 +688,10 @@ really_finish_composing_text (struct frame *f) Fset_marker (f->conversion.compose_region_end, Qnil, Qnil); f->conversion.compose_region_start = Qnil; f->conversion.compose_region_end = Qnil; + + if (update && text_interface + && text_interface->compose_region_changed) + (*text_interface->compose_region_changed) (f); } /* Delete the composition region overlay. */ @@ -796,7 +801,7 @@ really_set_composing_text (struct frame *f, ptrdiff_t position, the documentation, but is ultimately what programs expect. */ if (!SCHARS (text)) - really_finish_composing_text (f); + really_finish_composing_text (f, false); /* If PT hasn't changed, the conversion region definitely has. Otherwise, redisplay will update the input method instead. */ @@ -838,7 +843,7 @@ really_set_composing_region (struct frame *f, ptrdiff_t start, if (max (0, start) == max (0, end)) { - really_finish_composing_text (f); + really_finish_composing_text (f, false); return; } @@ -1167,7 +1172,7 @@ handle_pending_conversion_events_1 (struct frame *f, break; case TEXTCONV_FINISH_COMPOSING_TEXT: - really_finish_composing_text (f); + really_finish_composing_text (f, !NILP (data)); break; case TEXTCONV_SET_COMPOSING_TEXT: @@ -1360,16 +1365,20 @@ commit_text (struct frame *f, Lisp_Object string, /* Remove the composition region and its overlay from F's current buffer. Leave the text being composed intact. + If UPDATE, call `compose_region_changed' after the region is + removed. + COUNTER means the same as in `start_batch_edit'. */ void -finish_composing_text (struct frame *f, unsigned long counter) +finish_composing_text (struct frame *f, unsigned long counter, + bool update) { struct text_conversion_action *action, **last; action = xmalloc (sizeof *action); action->operation = TEXTCONV_FINISH_COMPOSING_TEXT; - action->data = Qnil; + action->data = update ? Qt : Qnil; action->next = NULL; action->counter = counter; for (last = &f->conversion.actions; *last; last = &(*last)->next) diff --git a/src/textconv.h b/src/textconv.h index 055bf251651..e632a9dddcf 100644 --- a/src/textconv.h +++ b/src/textconv.h @@ -128,7 +128,8 @@ extern void start_batch_edit (struct frame *, unsigned long); extern void end_batch_edit (struct frame *, unsigned long); extern void commit_text (struct frame *, Lisp_Object, ptrdiff_t, unsigned long); -extern void finish_composing_text (struct frame *, unsigned long); +extern void finish_composing_text (struct frame *, unsigned long, + bool); extern void set_composing_text (struct frame *, Lisp_Object, ptrdiff_t, unsigned long); extern void set_composing_region (struct frame *, ptrdiff_t, ptrdiff_t, -- cgit v1.3 From 189a91bfb699babd936dae48b96d71a332cac8d2 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 2 Jun 2023 13:31:40 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection): Apply workarounds on Vivo devices as well. * src/android.c (sendKeyPress, sendKeyRelease): Clear counter. * src/androidgui.h (struct android_key_event): New field `counter'. * src/androidterm.c (handle_one_android_event): Generate barriers as appropriate. (JNICALL): Set `counter'. * src/frame.h (enum text_conversion_operation): * src/textconv.c (detect_conversion_events) (really_set_composing_text, handle_pending_conversion_events_1) (handle_pending_conversion_events, textconv_barrier): * src/textconv.h: Implement text conversion barriers and fix various typos. --- java/org/gnu/emacs/EmacsInputConnection.java | 11 ++--- src/android.c | 2 + src/androidgui.h | 4 ++ src/androidterm.c | 12 +++++- src/frame.h | 1 + src/textconv.c | 60 +++++++++++++++++++++++++--- src/textconv.h | 1 + 7 files changed, 79 insertions(+), 12 deletions(-) (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index eb6fd5f2763..9ced7cb7aaf 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -66,11 +66,12 @@ public final class EmacsInputConnection extends BaseInputConnection || Build.MANUFACTURER.equalsIgnoreCase ("Honor")) extractAbsoluteOffsets = syncAfterCommit = true; - /* The Samsung keyboard takes `selectionStart' at face value if - some text is returned, and also searches for words solely - within that text. However, when no text is returned, it falls - back to getTextAfterCursor and getTextBeforeCursor. */ - if (Build.MANUFACTURER.equalsIgnoreCase ("Samsung")) + /* The Samsung and Vivo keyboards take `selectionStart' at face + value if some text is returned, and also searches for words + solely within that text. However, when no text is returned, it + falls back to getTextAfterCursor and getTextBeforeCursor. */ + if (Build.MANUFACTURER.equalsIgnoreCase ("Samsung") + || Build.MANUFACTURER.equalsIgnoreCase ("Vivo")) extractAbsoluteOffsets = true; }; diff --git a/src/android.c b/src/android.c index 94587344eb5..e74d40a0cdb 100644 --- a/src/android.c +++ b/src/android.c @@ -2543,6 +2543,7 @@ NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, event.xkey.state = state; event.xkey.keycode = keycode; event.xkey.unicode_char = unicode_char; + event.xkey.counter = 0; android_write_event (&event); return event_serial; @@ -2565,6 +2566,7 @@ NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, event.xkey.state = state; event.xkey.keycode = keycode; event.xkey.unicode_char = unicode_char; + event.xkey.counter = 0; android_write_event (&event); return event_serial; diff --git a/src/androidgui.h b/src/androidgui.h index 02cc73809b9..9e604cdcb8c 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -277,6 +277,10 @@ struct android_key_event /* If this field is -1, then android_lookup_string should be called to retrieve the associated individual characters. */ unsigned int unicode_char; + + /* If this field is non-zero, a text conversion barrier should be + generated with its value as the counter. */ + unsigned long counter; }; typedef struct android_key_event android_key_pressed_event; diff --git a/src/androidterm.c b/src/androidterm.c index c302e3f2877..211faabf5c2 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -885,6 +885,11 @@ handle_one_android_event (struct android_display_info *dpyinfo, if (!f) goto OTHER; + if (event->xkey.counter) + /* This event was generated by `performEditorAction'. Make + sure it is processed before any subsequent edits. */ + textconv_barrier (f, event->xkey.counter); + wchar_t copy_buffer[129]; wchar_t *copy_bufptr = copy_buffer; int copy_bufsiz = 128 * sizeof (wchar_t); @@ -5178,7 +5183,10 @@ NATIVE_NAME (performEditorAction) (JNIEnv *env, jobject object, android_write_event (&event); - /* Finally, send the return key press. */ + /* Finally, send the return key press. `counter' is set; this means + that a text conversion barrier will be generated once the event + is read, which will cause subsequent edits to wait until the + edits associated with this key press complete. */ event.xkey.type = ANDROID_KEY_PRESS; event.xkey.serial = ++event_serial; @@ -5187,6 +5195,7 @@ NATIVE_NAME (performEditorAction) (JNIEnv *env, jobject object, event.xkey.state = 0; event.xkey.keycode = 66; event.xkey.unicode_char = 0; + event.xkey.counter = ++edit_counter; android_write_event (&event); } @@ -5234,6 +5243,7 @@ NATIVE_NAME (performContextMenuAction) (JNIEnv *env, jobject object, event.xkey.state = 0; event.xkey.keycode = 66; event.xkey.unicode_char = 0; + event.xkey.counter = ++edit_counter; android_write_event (&event); } diff --git a/src/frame.h b/src/frame.h index e2900d1c15b..41b4cd444f6 100644 --- a/src/frame.h +++ b/src/frame.h @@ -89,6 +89,7 @@ enum text_conversion_operation TEXTCONV_SET_POINT_AND_MARK, TEXTCONV_DELETE_SURROUNDING_TEXT, TEXTCONV_REQUEST_POINT_UPDATE, + TEXTCONV_BARRIER, }; /* Structure describing a single edit being performed by the input diff --git a/src/textconv.c b/src/textconv.c index d8166bcfd03..9003816e191 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -36,6 +36,7 @@ along with GNU Emacs. If not, see . */ #include "buffer.h" #include "syntax.h" #include "blockinput.h" +#include "keyboard.h" @@ -522,7 +523,11 @@ detect_conversion_events (void) FOR_EACH_FRAME (tail, frame) { - if (XFRAME (frame)->conversion.actions) + /* See if there's a pending edit on this frame. */ + if (XFRAME (frame)->conversion.actions + && ((XFRAME (frame)->conversion.actions->operation + != TEXTCONV_BARRIER) + || (kbd_fetch_ptr == kbd_store_ptr))) return true; } @@ -740,7 +745,7 @@ really_set_composing_text (struct frame *f, ptrdiff_t position, Fset_marker_insertion_type (f->conversion.compose_region_end, Qt); - start = position; + start = PT; } else { @@ -762,7 +767,7 @@ really_set_composing_text (struct frame *f, ptrdiff_t position, record_buffer_change (start, PT, Qnil); /* Now move point to an appropriate location. */ - if (position < 0) + if (position <= 0) { wanted = start; @@ -1198,6 +1203,19 @@ handle_pending_conversion_events_1 (struct frame *f, case TEXTCONV_REQUEST_POINT_UPDATE: really_request_point_update (f); break; + + case TEXTCONV_BARRIER: + if (kbd_fetch_ptr != kbd_store_ptr) + emacs_abort (); + + /* Once a barrier is hit, synchronize F's selected window's + `ephemeral_last_point' with its current point. The reason + for this is because otherwise a previous keyboard event may + have taken place without redisplay happening in between. */ + + if (w) + w->ephemeral_last_point = window_point (w); + break; } /* Signal success. */ @@ -1231,7 +1249,7 @@ handle_pending_conversion_events (void) static int inside; specpdl_ref count; ptrdiff_t last_point; - struct window *w; + struct window *w, *w1; handled = false; @@ -1242,8 +1260,6 @@ handle_pending_conversion_events (void) Vtext_conversion_edits = Qnil; inside++; - last_point = -1; - w = NULL; count = SPECPDL_INDEX (); record_unwind_protect_ptr (decrement_inside, &inside); @@ -1251,6 +1267,8 @@ handle_pending_conversion_events (void) FOR_EACH_FRAME (tail, frame) { f = XFRAME (frame); + last_point = -1; + w = NULL; /* Test if F has any outstanding conversion events. Then process them in bottom to up order. */ @@ -1283,6 +1301,13 @@ handle_pending_conversion_events (void) if (!action) break; + /* If action is a barrier event and the keyboard buffer is + not yet empty, break out of the loop. */ + + if (action->operation == TEXTCONV_BARRIER + && kbd_store_ptr != kbd_fetch_ptr) + break; + /* Unlink this action. */ next = action->next; f->conversion.actions = next; @@ -1515,6 +1540,29 @@ request_point_update (struct frame *f, unsigned long counter) input_pending = true; } +/* Request that text conversion on F pause until the keyboard buffer + becomes empty. + + Use this function to ensure that edits associated with a keyboard + event complete before the text conversion edits after the barrier + take place. */ + +void +textconv_barrier (struct frame *f, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_BARRIER; + action->data = Qnil; + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + /* Return N characters of text around point in F's old selected window. diff --git a/src/textconv.h b/src/textconv.h index e632a9dddcf..d4d0e9d7227 100644 --- a/src/textconv.h +++ b/src/textconv.h @@ -139,6 +139,7 @@ extern void textconv_set_point_and_mark (struct frame *, ptrdiff_t, extern void delete_surrounding_text (struct frame *, ptrdiff_t, ptrdiff_t, unsigned long); extern void request_point_update (struct frame *, unsigned long); +extern void textconv_barrier (struct frame *, unsigned long); extern char *get_extracted_text (struct frame *, ptrdiff_t, ptrdiff_t *, ptrdiff_t *, ptrdiff_t *, ptrdiff_t *, ptrdiff_t *); -- cgit v1.3 From 63339a9577f085074d16fa8c56ddd56864c94dda Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 7 Jun 2023 11:03:56 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsInputConnection.java (beginBatchEdit) (endBatchEdit, commitCompletion, commitText, deleteSurroundingText) (finishComposingText, getSelectedText, getTextAfterCursor) (getTextBeforeCursor, setComposingText, setComposingRegion) (performEditorAction, performContextMenuAction, getExtractedText) (setSelection, sendKeyEvent, deleteSurroundingTextInCodePoints) (requestCursorUpdates): Ensure that the input connection is up to date. (getSurroundingText): New function. * java/org/gnu/emacs/EmacsNative.java (getSurroundingText): Export new C function. * java/org/gnu/emacs/EmacsService.java (resetIC): Invalidate previously created input connections. * java/org/gnu/emacs/EmacsView.java (EmacsView) (onCreateInputConnection): Signify that input connections are now up to date. * src/androidterm.c (struct android_get_surrounding_text_context): New structure. (android_get_surrounding_text, NATIVE_NAME): * src/textconv.c (get_surrounding_text): * src/textconv.h: New functions. --- java/org/gnu/emacs/EmacsInputConnection.java | 105 ++++++++++++++++++++ java/org/gnu/emacs/EmacsNative.java | 4 + java/org/gnu/emacs/EmacsService.java | 1 + java/org/gnu/emacs/EmacsView.java | 13 +++ src/androidterm.c | 143 +++++++++++++++++++++++++++ src/textconv.c | 100 +++++++++++++++++++ src/textconv.h | 4 + 7 files changed, 370 insertions(+) (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 9ced7cb7aaf..73c93c67ac7 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -23,7 +23,9 @@ import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.SurroundingText; import android.view.inputmethod.TextSnapshot; + import android.view.KeyEvent; import android.os.Build; @@ -88,6 +90,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean beginBatchEdit () { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "beginBatchEdit"); @@ -99,6 +105,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean endBatchEdit () { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "endBatchEdit"); @@ -110,6 +120,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean commitCompletion (CompletionInfo info) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "commitCompletion: " + info); @@ -125,6 +139,10 @@ public final class EmacsInputConnection extends BaseInputConnection { int[] selection; + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "commitText: " + text + " " + newCursorPosition); @@ -156,6 +174,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean deleteSurroundingText (int leftLength, int rightLength) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, ("deleteSurroundingText: " + leftLength + " " + rightLength)); @@ -169,6 +191,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean finishComposingText () { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "finishComposingText"); @@ -180,6 +206,10 @@ public final class EmacsInputConnection extends BaseInputConnection public String getSelectedText (int flags) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + if (EmacsService.DEBUG_IC) Log.d (TAG, "getSelectedText: " + flags); @@ -192,6 +222,10 @@ public final class EmacsInputConnection extends BaseInputConnection { String string; + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + if (EmacsService.DEBUG_IC) Log.d (TAG, "getTextAfterCursor: " + length + " " + flags); @@ -210,6 +244,10 @@ public final class EmacsInputConnection extends BaseInputConnection { String string; + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + if (EmacsService.DEBUG_IC) Log.d (TAG, "getTextBeforeCursor: " + length + " " + flags); @@ -226,6 +264,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean setComposingText (CharSequence text, int newCursorPosition) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, ("setComposingText: " + text + " ## " + newCursorPosition)); @@ -239,6 +281,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean setComposingRegion (int start, int end) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "setComposingRegion: " + start + " " + end); @@ -250,6 +296,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean performEditorAction (int editorAction) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "performEditorAction: " + editorAction); @@ -263,6 +313,10 @@ public final class EmacsInputConnection extends BaseInputConnection { int action; + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "performContextMenuAction: " + contextMenuAction); @@ -310,6 +364,10 @@ public final class EmacsInputConnection extends BaseInputConnection ExtractedText text; int[] selection; + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + if (EmacsService.DEBUG_IC) Log.d (TAG, "getExtractedText: " + request.hintMaxChars + ", " + request.hintMaxLines + " " + flags); @@ -360,6 +418,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean setSelection (int start, int end) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "setSelection: " + start + " " + end); @@ -371,6 +433,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean sendKeyEvent (KeyEvent key) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "sendKeyEvent: " + key); @@ -381,6 +447,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean deleteSurroundingTextInCodePoints (int beforeLength, int afterLength) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + /* This can be implemented the same way as deleteSurroundingText. */ return this.deleteSurroundingText (beforeLength, afterLength); @@ -390,6 +460,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean requestCursorUpdates (int cursorUpdateMode) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "requestCursorUpdates: " + cursorUpdateMode); @@ -397,6 +471,37 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public SurroundingText + getSurroundingText (int beforeLength, int afterLength, + int flags) + { + SurroundingText text; + + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + + if (EmacsService.DEBUG_IC) + Log.d (TAG, ("getSurroundingText: " + beforeLength + ", " + + afterLength)); + + text = EmacsNative.getSurroundingText (windowHandle, beforeLength, + afterLength, flags); + + if (text != null) + Log.d (TAG, ("getSurroundingText: " + + text.getSelectionStart () + + "," + + text.getSelectionEnd () + + "+" + + text.getOffset () + + ": " + + text.getText ())); + + return text; + } + /* Override functions which are not implemented. */ diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index cb1c6caa79a..cb89cf6808a 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -25,6 +25,7 @@ import android.graphics.Bitmap; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.SurroundingText; public final class EmacsNative { @@ -222,6 +223,9 @@ public final class EmacsNative public static native void requestSelectionUpdate (short window); public static native void requestCursorUpdates (short window, int mode); public static native void clearInputFlags (short window); + public static native SurroundingText getSurroundingText (short window, + int left, int right, + int flags); /* Return the current value of the selection, or -1 upon diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 2074a5b7c2b..48e39f8b355 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -748,6 +748,7 @@ public final class EmacsService extends Service window.view.setICMode (icMode); icBeginSynchronous (); + window.view.icGeneration++; window.view.imManager.restartInput (window.view); icEndSynchronous (); } diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index a78dec08839..d432162132d 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -111,6 +111,13 @@ public final class EmacsView extends ViewGroup details. */ private int icMode; + /* The number of calls to `resetIC' to have taken place the last + time an InputConnection was created. */ + public long icSerial; + + /* The number of calls to `recetIC' that have taken place. */ + public volatile long icGeneration; + public EmacsView (EmacsWindow window) { @@ -627,6 +634,12 @@ public final class EmacsView extends ViewGroup return null; } + /* Set icSerial. If icSerial < icGeneration, the input connection + has been reset, and future input should be ignored until a new + connection is created. */ + + icSerial = icGeneration; + /* Reset flags set by the previous input method. */ EmacsNative.clearInputFlags (window.handle); diff --git a/src/androidterm.c b/src/androidterm.c index 2a054715d6a..77f2bd1c7a0 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -5636,6 +5636,149 @@ NATIVE_NAME (clearInputFlags) (JNIEnv *env, jobject object, android_write_event (&event); } + + +/* Context for a call to `getSurroundingText'. */ + +struct android_get_surrounding_text_context +{ + /* Number of characters before the region to return. */ + int before_length; + + /* Number of characters after the region to return. */ + int after_length; + + /* The returned text, or NULL. */ + char *text; + + /* The size of that text in characters and bytes. */ + ptrdiff_t length, bytes; + + /* Offsets into that text. */ + ptrdiff_t offset, start, end; + + /* The window. */ + android_window window; +}; + +/* Return the surrounding text in the surrounding text context + specified by DATA. */ + +static void +android_get_surrounding_text (void *data) +{ + struct android_get_surrounding_text_context *request; + struct frame *f; + ptrdiff_t temp; + + request = data; + + /* Find the frame associated with the window. */ + f = android_window_to_frame (NULL, request->window); + + if (!f) + return; + + /* Now get the surrounding text. */ + request->text + = get_surrounding_text (f, request->before_length, + request->after_length, &request->length, + &request->bytes, &request->offset, + &request->start, &request->end); + + /* Sort request->start and request->end for compatibility with some + bad input methods. */ + + if (request->end < request->start) + { + temp = request->start; + request->start = request->end; + request->end = temp; + } +} + +JNIEXPORT jobject JNICALL +NATIVE_NAME (getSurroundingText) (JNIEnv *env, jobject ignored_object, + jshort window, jint before_length, + jint after_length, jint flags) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + static jclass class; + static jmethodID constructor; + + struct android_get_surrounding_text_context context; + jstring string; + jobject object; + + /* Initialize CLASS if it has not yet been initialized. */ + + if (!class) + { + class + = (*env)->FindClass (env, ("android/view/inputmethod" + "/SurroundingText")); + +#if __ANDROID_API__ < 31 + /* If CLASS cannot be found, the version of Android currently + running is too old. */ + + if (!class) + { + (*env)->ExceptionClear (env); + return NULL; + } +#else /* __ANDROID_API__ >= 31 */ + assert (class); +#endif /* __ANDROID_API__ < 31 */ + + class = (*env)->NewGlobalRef (env, class); + if (!class) + return NULL; + + /* Now look for its constructor. */ + constructor = (*env)->GetMethodID (env, class, "", + "(Ljava/lang/CharSequence;III)V"); + assert (constructor); + } + + context.before_length = before_length; + context.after_length = after_length; + context.window = window; + context.text = NULL; + + android_sync_edit (); + if (android_run_in_emacs_thread (android_get_surrounding_text, + &context)) + return NULL; + + if (!context.text) + return NULL; + + /* Encode the returned text. */ + string = android_text_to_string (env, context.text, context.length, + context.bytes); + free (context.text); + + if (!string) + return NULL; + + /* Create an SurroundingText object containing this information. */ + object = (*env)->NewObject (env, class, constructor, string, + (jint) min (context.start, + TYPE_MAXIMUM (jint)), + (jint) min (context.end, + TYPE_MAXIMUM (jint)), + /* Adjust point offsets to fit into + Android's 0-based indexing. */ + (jint) min (context.offset - 1, + TYPE_MAXIMUM (jint))); + if (!object) + return NULL; + + return object; +} + #ifdef __clang__ #pragma clang diagnostic pop #else diff --git a/src/textconv.c b/src/textconv.c index 0dcf5bdcea8..1161b781b6a 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -1732,6 +1732,106 @@ get_extracted_text (struct frame *f, ptrdiff_t n, return buffer; } +/* Return the text between the positions PT - LEFT and PT + RIGHT. If + the mark is active, return the range of text relative to the bounds + of the region instead. + + Set *LENGTH to the number of characters returned, *BYTES to the + number of bytes returned, *OFFSET to the character position of the + returned text, and *START_RETURN and *END_RETURN to the mark and + point relative to that position. */ + +char * +get_surrounding_text (struct frame *f, ptrdiff_t left, + ptrdiff_t right, ptrdiff_t *length, + ptrdiff_t *bytes, ptrdiff_t *offset, + ptrdiff_t *start_return, + ptrdiff_t *end_return) +{ + specpdl_ref count; + ptrdiff_t start, end, start_byte, end_byte, mark, temp; + char *buffer; + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return NULL; + + /* Save the excursion, as there will be extensive changes to the + selected window. */ + count = SPECPDL_INDEX (); + record_unwind_protect_excursion (); + + /* Inhibit quitting. */ + specbind (Qinhibit_quit, Qt); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + select_window (f->old_selected_window, Qt); + buffer = NULL; + + /* Figure out the bounds of the text to return. */ + + /* First, obtain start and end. */ + end = get_mark (); + start = PT; + + /* If the mark is not active, make it start and end. */ + + if (end == -1) + end = start; + + /* Now sort start and end. */ + + if (end < start) + { + temp = start; + start = end; + end = temp; + } + + /* And subtract left and right. */ + + if (INT_SUBTRACT_WRAPV (start, left, &start) + || INT_ADD_WRAPV (end, right, &end)) + goto finish; + + start = max (start, BEGV); + end = min (end, ZV); + + /* Detect overflow. */ + + if (!(start <= PT && PT <= end)) + goto finish; + + /* Convert the character positions to byte positions. */ + start_byte = CHAR_TO_BYTE (start); + end_byte = CHAR_TO_BYTE (end); + + /* Extract the text from the buffer. */ + buffer = xmalloc (end_byte - start_byte); + copy_buffer (start, start_byte, end, end_byte, + buffer); + + /* Get the mark. If it's not active, use PT. */ + + mark = get_mark (); + + if (mark == -1) + mark = PT; + + /* Return the offsets. Unlike `get_extracted_text', this need not + sort mark and point. */ + + *offset = start; + *start_return = mark - start; + *end_return = PT - start; + *length = end - start; + *bytes = end_byte - start_byte; + + finish: + unbind_to (count, Qnil); + return buffer; +} + /* Return whether or not text conversion is temporarily disabled. `reset' should always call this to determine whether or not to disable the input method. */ diff --git a/src/textconv.h b/src/textconv.h index 339cefdba92..7550388a723 100644 --- a/src/textconv.h +++ b/src/textconv.h @@ -143,6 +143,10 @@ extern void textconv_barrier (struct frame *, unsigned long); extern char *get_extracted_text (struct frame *, ptrdiff_t, ptrdiff_t *, ptrdiff_t *, ptrdiff_t *, ptrdiff_t *, ptrdiff_t *, bool *); +extern char *get_surrounding_text (struct frame *, ptrdiff_t, + ptrdiff_t, ptrdiff_t *, + ptrdiff_t *, ptrdiff_t *, + ptrdiff_t *, ptrdiff_t *); extern bool conversion_disabled_p (void); extern void register_textconv_interface (struct textconv_interface *); -- cgit v1.3 From 90ae3cc387530229e5aca32c00d35495ab680e21 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 14 Jun 2023 15:37:47 +0800 Subject: Improve IM synchronization on Android * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection): Reimplement as an InputConnection, not BaseInputConnection. * src/androidterm.c (performEditorAction): Sync prior to sending keyboard events. --- java/org/gnu/emacs/EmacsInputConnection.java | 189 +++++++++++++++++++++++---- src/androidterm.c | 7 + 2 files changed, 174 insertions(+), 22 deletions(-) (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 73c93c67ac7..1bcc9a62a81 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -19,26 +19,35 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -import android.view.inputmethod.BaseInputConnection; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; + +import android.view.KeyEvent; + import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.SurroundingText; +import android.view.inputmethod.TextAttribute; import android.view.inputmethod.TextSnapshot; -import android.view.KeyEvent; - -import android.os.Build; - import android.util.Log; /* Android input methods, take number six. See textconv.c for more details; this is more-or-less a thin wrapper around that file. */ -public final class EmacsInputConnection extends BaseInputConnection +public final class EmacsInputConnection implements InputConnection { private static final String TAG = "EmacsInputConnection"; + + /* View associated with this input connection. */ private EmacsView view; + + /* The handle ID associated with that view's window. */ private short windowHandle; /* Whether or not to synchronize and call `updateIC' with the @@ -77,15 +86,18 @@ public final class EmacsInputConnection extends BaseInputConnection extractAbsoluteOffsets = true; }; + public EmacsInputConnection (EmacsView view) { - super (view, true); - this.view = view; this.windowHandle = view.window.handle; } + + /* The functions below are called by input methods whenever they + need to perform an edit. */ + @Override public boolean beginBatchEdit () @@ -116,7 +128,6 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } - @Override public boolean commitCompletion (CompletionInfo info) { @@ -133,6 +144,19 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public boolean + commitCorrection (CorrectionInfo info) + { + /* The input method calls this function not to commit text, but to + indicate that a subsequent edit will consist of a correction. + Emacs has no use for this information. + + Of course this completely contradicts the provided + documentation, but this is how Android actually behaves. */ + return false; + } + @Override public boolean commitText (CharSequence text, int newCursorPosition) @@ -170,6 +194,14 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public boolean + commitText (CharSequence text, int newCursorPosition, + TextAttribute textAttribute) + { + return commitText (text, newCursorPosition); + } + @Override public boolean deleteSurroundingText (int leftLength, int rightLength) @@ -187,6 +219,16 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public boolean + deleteSurroundingTextInCodePoints (int leftLength, int rightLength) + { + /* Emacs returns characters which cannot be represented in a Java + `char' as NULL characters, so code points always reflect + characters themselves. */ + return deleteSurroundingText (leftLength, rightLength); + } + @Override public boolean finishComposingText () @@ -277,6 +319,14 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public boolean + setComposingText (CharSequence text, int newCursorPosition, + TextAttribute textAttribute) + { + return setComposingText (text, newCursorPosition); + } + @Override public boolean setComposingRegion (int start, int end) @@ -292,6 +342,13 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public boolean + setComposingRegion (int start, int end, TextAttribute textAttribute) + { + return setComposingRegion (start, end); + } + @Override public boolean performEditorAction (int editorAction) @@ -430,6 +487,8 @@ public final class EmacsInputConnection extends BaseInputConnection } @Override + /* ACTION_MULTIPLE is apparently obsolete. */ + @SuppressWarnings ("deprecation") public boolean sendKeyEvent (KeyEvent key) { @@ -440,20 +499,33 @@ public final class EmacsInputConnection extends BaseInputConnection if (EmacsService.DEBUG_IC) Log.d (TAG, "sendKeyEvent: " + key); - return super.sendKeyEvent (key); - } + /* Use the standard API if possible. */ - @Override - public boolean - deleteSurroundingTextInCodePoints (int beforeLength, int afterLength) - { - /* Return if the input connection is out of date. */ - if (view.icSerial < view.icGeneration) - return false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + view.imManager.dispatchKeyEventFromInputMethod (view, key); + else + { + /* Fall back to dispatching the event manually if not. */ + + switch (key.getAction ()) + { + case KeyEvent.ACTION_DOWN: + view.onKeyDown (key.getKeyCode (), key); + break; + + case KeyEvent.ACTION_UP: + view.onKeyUp (key.getKeyCode (), key); + break; + + case KeyEvent.ACTION_MULTIPLE: + view.onKeyMultiple (key.getKeyCode (), + key.getRepeatCount (), + key); + break; + } + } - /* This can be implemented the same way as - deleteSurroundingText. */ - return this.deleteSurroundingText (beforeLength, afterLength); + return true; } @Override @@ -471,6 +543,16 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public boolean + requestCursorUpdates (int cursorUpdateMode, int filter) + { + if (filter != 0) + return false; + + return requestCursorUpdates (cursorUpdateMode); + } + @Override public SurroundingText getSurroundingText (int beforeLength, int afterLength, @@ -505,11 +587,74 @@ public final class EmacsInputConnection extends BaseInputConnection /* Override functions which are not implemented. */ + @Override + public Handler + getHandler () + { + return null; + } + + @Override + public void + closeConnection () + { + + } + + @Override + public boolean + commitContent (InputContentInfo inputContentInfo, int flags, + Bundle opts) + { + return false; + } + + @Override + public boolean + setImeConsumesInput (boolean imeConsumesInput) + { + return false; + } + @Override public TextSnapshot takeSnapshot () { - Log.d (TAG, "takeSnapshot"); return null; } + + @Override + public boolean + clearMetaKeyStates (int states) + { + return false; + } + + @Override + public boolean + reportFullscreenMode (boolean enabled) + { + return false; + } + + @Override + public boolean + performSpellCheck () + { + return false; + } + + @Override + public boolean + performPrivateCommand (String action, Bundle data) + { + return false; + } + + @Override + public int + getCursorCapsMode (int reqModes) + { + return 0; + } } diff --git a/src/androidterm.c b/src/androidterm.c index f08536c02ab..191ff65199b 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -5193,6 +5193,13 @@ NATIVE_NAME (performEditorAction) (JNIEnv *env, jobject object, union android_event event; + /* It's a good idea to call `android_sync_edit' before sending the + key event. Otherwise, if RET causes the current window to be + changed, any text previously committed might end up in the newly + selected window. */ + + android_sync_edit (); + /* Undocumented behavior: performEditorAction is apparently expected to finish composing any text. */ -- cgit v1.3 From 363e293cc919ab02c40bd9a8fa4875c2e5644b2d Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 15 Jun 2023 12:36:50 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection, beginBatchEdit, reset, endBatchEdit): Keep track of the number of batch edits and return an appropriate value. (takeSnapshot): Implement function. * java/org/gnu/emacs/EmacsNative.java (takeSnapshot): New function. * java/org/gnu/emacs/EmacsService.java (resetIC): Improve debugging output. * java/org/gnu/emacs/EmacsView.java (onCreateInputConnection): Call `reset' to clear the UI side batch edit count. * src/androidterm.c (struct android_get_surrounding_text_context): New fields `conversion_start' and `conversion_end'. (android_get_surrounding_text): Return the conversion region. (android_get_surrounding_text_internal, NATIVE_NAME): Factor out `getSurroundingText'. (takeSnapshot): New function. --- java/org/gnu/emacs/EmacsInputConnection.java | 66 +++++++++++--- java/org/gnu/emacs/EmacsNative.java | 2 + java/org/gnu/emacs/EmacsService.java | 25 +++++- java/org/gnu/emacs/EmacsView.java | 3 + src/androidterm.c | 129 +++++++++++++++++++++++++-- 5 files changed, 201 insertions(+), 24 deletions(-) (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 1bcc9a62a81..f8dce5dfa79 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -50,6 +50,11 @@ public final class EmacsInputConnection implements InputConnection /* The handle ID associated with that view's window. */ private short windowHandle; + /* Number of batch edits currently underway. Used to avoid + synchronizing with the Emacs thread after each + `endBatchEdit'. */ + private int batchEditCount; + /* Whether or not to synchronize and call `updateIC' with the selection position after committing text. @@ -110,6 +115,10 @@ public final class EmacsInputConnection implements InputConnection Log.d (TAG, "beginBatchEdit"); EmacsNative.beginBatchEdit (windowHandle); + + /* Keep a record of the number of outstanding batch edits here as + well. */ + batchEditCount++; return true; } @@ -125,7 +134,14 @@ public final class EmacsInputConnection implements InputConnection Log.d (TAG, "endBatchEdit"); EmacsNative.endBatchEdit (windowHandle); - return true; + + /* Subtract one from the UI thread record of the number of batch + edits currently under way. */ + + if (batchEditCount > 0) + batchEditCount -= 1; + + return batchEditCount > 0; } public boolean @@ -584,21 +600,50 @@ public final class EmacsInputConnection implements InputConnection return text; } - - /* Override functions which are not implemented. */ - @Override - public Handler - getHandler () + public TextSnapshot + takeSnapshot () { - return null; + TextSnapshot snapshot; + + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + + snapshot = EmacsNative.takeSnapshot (windowHandle); + + if (EmacsService.DEBUG_IC) + Log.d (TAG, ("takeSnapshot: " + + snapshot.getSurroundingText ().getText () + + " @ " + snapshot.getCompositionEnd () + + ", " + snapshot.getCompositionStart ())); + + return snapshot; } @Override public void closeConnection () { + batchEditCount = 0; + } + + + public void + reset () + { + batchEditCount = 0; + } + + + /* Override functions which are not implemented. */ + + @Override + public Handler + getHandler () + { + return null; } @Override @@ -616,13 +661,6 @@ public final class EmacsInputConnection implements InputConnection return false; } - @Override - public TextSnapshot - takeSnapshot () - { - return null; - } - @Override public boolean clearMetaKeyStates (int states) diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 2fcbf8b94ef..9e87c419f95 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -26,6 +26,7 @@ import android.graphics.Bitmap; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.SurroundingText; +import android.view.inputmethod.TextSnapshot; public final class EmacsNative { @@ -230,6 +231,7 @@ public final class EmacsNative public static native SurroundingText getSurroundingText (short window, int left, int right, int flags); + public static native TextSnapshot takeSnapshot (short window); /* Return the current value of the selection, or -1 upon diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 96216e51cf4..2fe4e8c4146 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -767,8 +767,31 @@ public final class EmacsService extends Service public void resetIC (EmacsWindow window, int icMode) { + int oldMode; + if (DEBUG_IC) - Log.d (TAG, "resetIC: " + window); + Log.d (TAG, "resetIC: " + window + ", " + icMode); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + && (oldMode = window.view.getICMode ()) == icMode + /* Don't do this if there is currently no input + connection. */ + && oldMode != IC_MODE_NULL) + { + if (DEBUG_IC) + Log.d (TAG, "resetIC: calling invalidateInput"); + + /* Android 33 and later allow the IM reset to be optimized out + and replaced by a call to `invalidateInput', which is much + faster, as it does not involve resetting the input + connection. */ + + icBeginSynchronous (); + window.view.imManager.invalidateInput (window.view); + icEndSynchronous (); + + return; + } window.view.setICMode (icMode); diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 278c6025902..aba1184b0c2 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -681,6 +681,9 @@ public final class EmacsView extends ViewGroup if (inputConnection == null) inputConnection = new EmacsInputConnection (this); + else + /* Clear several pieces of state in the input connection. */ + inputConnection.reset (); /* Return the input connection. */ return inputConnection; diff --git a/src/androidterm.c b/src/androidterm.c index 191ff65199b..29076981a47 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -5668,6 +5668,10 @@ struct android_get_surrounding_text_context /* Offsets into that text. */ ptrdiff_t offset, start, end; + /* The start and end indices of the conversion region. + -1 if it does not exist. */ + ptrdiff_t conversion_start, conversion_end; + /* The window. */ android_window window; }; @@ -5706,22 +5710,47 @@ android_get_surrounding_text (void *data) request->start = request->end; request->end = temp; } + + /* Retrieve the conversion region. */ + + request->conversion_start = -1; + request->conversion_end = -1; + + if (MARKERP (f->conversion.compose_region_start)) + { + request->conversion_start + = marker_position (f->conversion.compose_region_start) - 1; + request->conversion_end + = marker_position (f->conversion.compose_region_end) - 1; + } } -JNIEXPORT jobject JNICALL -NATIVE_NAME (getSurroundingText) (JNIEnv *env, jobject ignored_object, - jshort window, jint before_length, - jint after_length, jint flags) -{ - JNI_STACK_ALIGNMENT_PROLOGUE; +/* Return a local reference to a `SurroundingText' object describing + WINDOW's surrounding text. ENV should be a valid JNI environment + for the current thread. - static jclass class; - static jmethodID constructor; + BEFORE_LENGTH and AFTER_LENGTH specify the number of characters + around point and mark to return. + Return the conversion region (or -1) in *CONVERSION_START and + *CONVERSION_END if non-NULL. + + Value is the object upon success, else NULL. */ + +static jobject +android_get_surrounding_text_internal (JNIEnv *env, jshort window, + jint before_length, + jint after_length, + ptrdiff_t *conversion_start, + ptrdiff_t *conversion_end) +{ struct android_get_surrounding_text_context context; jstring string; jobject object; + static jclass class; + static jmethodID constructor; + /* Initialize CLASS if it has not yet been initialized. */ if (!class) @@ -5745,7 +5774,9 @@ NATIVE_NAME (getSurroundingText) (JNIEnv *env, jobject ignored_object, class = (*env)->NewGlobalRef (env, class); if (!class) - return NULL; + /* Clear class to prevent a local reference from remaining in + `class'. */ + return (class = NULL); /* Now look for its constructor. */ constructor = (*env)->GetMethodID (env, class, "", @@ -5787,6 +5818,86 @@ NATIVE_NAME (getSurroundingText) (JNIEnv *env, jobject ignored_object, if (!object) return NULL; + /* Now return the conversion region if that was requested. */ + + if (conversion_start) + { + *conversion_start = context.conversion_start; + *conversion_end = context.conversion_start; + } + + return object; +} + +JNIEXPORT jobject JNICALL +NATIVE_NAME (getSurroundingText) (JNIEnv *env, jobject object, + jshort window, jint before_length, + jint after_length, jint flags) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + return android_get_surrounding_text_internal (env, window, before_length, + after_length, NULL, NULL); +} + +JNIEXPORT jobject JNICALL +NATIVE_NAME (takeSnapshot) (JNIEnv *env, jobject object, jshort window) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + jobject text; + ptrdiff_t start, end; + + static jclass class; + static jmethodID constructor; + + /* First, obtain the surrounding text and conversion region. */ + text = android_get_surrounding_text_internal (env, window, 600, 600, + &start, &end); + + /* If that fails, return NULL. */ + + if (!text) + return NULL; + + /* Next, initialize the TextSnapshot class. */ + + if (!class) + { + class + = (*env)->FindClass (env, ("android/view/inputmethod" + "/TextSnapshot")); +#if __ANDROID_API__ < 33 + /* If CLASS cannot be found, the version of Android currently + running is too old. */ + + if (!class) + { + (*env)->ExceptionClear (env); + return NULL; + } +#else /* __ANDROID_API__ >= 33 */ + assert (class); +#endif /* __ANDROID_API__ < 33 */ + + class = (*env)->NewGlobalRef (env, class); + if (!class) + /* Clear class to prevent a local reference from remaining in + `class'. */ + return (class = NULL); + + constructor = (*env)->GetMethodID (env, class, "", + "(Landroid/view/inputmethod" + "/SurroundingText;III)V"); + assert (constructor); + } + + /* Try to create a TextSnapshot object. */ + eassert (start <= end); + object = (*env)->NewObject (env, class, constructor, text, + (jint) min (start, TYPE_MAXIMUM (jint)), + (jint) min (end, TYPE_MAXIMUM (jint)), + (jint) 0); return object; } -- cgit v1.3 From 4c390f14f4f344ce63c04fface5c8a2a11061412 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 14 Jul 2023 08:51:07 +0800 Subject: Clean up Android debug code * java/org/gnu/emacs/EmacsInputConnection.java (getSurroundingText): Don't print debug information if DEBUG_IC is off. --- java/org/gnu/emacs/EmacsInputConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'java/org/gnu/emacs/EmacsInputConnection.java') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index f8dce5dfa79..c3764a7b29f 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -587,7 +587,7 @@ public final class EmacsInputConnection implements InputConnection text = EmacsNative.getSurroundingText (windowHandle, beforeLength, afterLength, flags); - if (text != null) + if (EmacsService.DEBUG_IC && text != null) Log.d (TAG, ("getSurroundingText: " + text.getSelectionStart () + "," -- cgit v1.3