diff options
Diffstat (limited to 'src/androidmenu.c')
-rw-r--r-- | src/androidmenu.c | 829 |
1 files changed, 829 insertions, 0 deletions
diff --git a/src/androidmenu.c b/src/androidmenu.c new file mode 100644 index 00000000000..f74e7ca6d99 --- /dev/null +++ b/src/androidmenu.c @@ -0,0 +1,829 @@ +/* Communication module for Android terminals. + +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 <https://www.gnu.org/licenses/>. */ + +#include <config.h> + +#include "lisp.h" +#include "androidterm.h" +#include "android.h" +#include "blockinput.h" +#include "keyboard.h" +#include "menu.h" + +#ifndef ANDROID_STUBIFY + +#include <android/log.h> + +/* Flag indicating whether or not a popup menu has been posted and not + yet popped down. */ + +static int popup_activated_flag; + +/* Serial number used to identify which context menu events are + associated with the context menu currently being displayed. */ + +unsigned int current_menu_serial; + +int +popup_activated (void) +{ + return popup_activated_flag; +} + + + +/* Toolkit menu implementation. */ + +/* Structure describing the EmacsContextMenu class. */ + +struct android_emacs_context_menu +{ + jclass class; + jmethodID create_context_menu; + jmethodID add_item; + jmethodID add_submenu; + jmethodID add_pane; + jmethodID parent; + jmethodID display; + jmethodID dismiss; +}; + +/* Identifiers associated with the EmacsContextMenu class. */ +static struct android_emacs_context_menu menu_class; + +static void +android_init_emacs_context_menu (void) +{ + jclass old; + + menu_class.class + = (*android_java_env)->FindClass (android_java_env, + "org/gnu/emacs/EmacsContextMenu"); + eassert (menu_class.class); + + old = menu_class.class; + menu_class.class + = (jclass) (*android_java_env)->NewGlobalRef (android_java_env, + (jobject) old); + ANDROID_DELETE_LOCAL_REF (old); + + if (!menu_class.class) + emacs_abort (); + +#define FIND_METHOD(c_name, name, signature) \ + menu_class.c_name \ + = (*android_java_env)->GetMethodID (android_java_env, \ + menu_class.class, \ + name, signature); \ + eassert (menu_class.c_name); + +#define FIND_METHOD_STATIC(c_name, name, signature) \ + menu_class.c_name \ + = (*android_java_env)->GetStaticMethodID (android_java_env, \ + menu_class.class, \ + name, signature); \ + eassert (menu_class.c_name); + + FIND_METHOD_STATIC (create_context_menu, "createContextMenu", + "(Ljava/lang/String;)" + "Lorg/gnu/emacs/EmacsContextMenu;"); + + FIND_METHOD (add_item, "addItem", "(ILjava/lang/String;ZZZ" + "Ljava/lang/String;Z)V"); + FIND_METHOD (add_submenu, "addSubmenu", "(Ljava/lang/String;" + "Ljava/lang/String;Ljava/lang/String;)" + "Lorg/gnu/emacs/EmacsContextMenu;"); + FIND_METHOD (add_pane, "addPane", "(Ljava/lang/String;)V"); + FIND_METHOD (parent, "parent", "()Lorg/gnu/emacs/EmacsContextMenu;"); + FIND_METHOD (display, "display", "(Lorg/gnu/emacs/EmacsWindow;III)Z"); + FIND_METHOD (dismiss, "dismiss", "(Lorg/gnu/emacs/EmacsWindow;)V"); + +#undef FIND_METHOD +#undef FIND_METHOD_STATIC +} + +static void +android_unwind_local_frame (void) +{ + (*android_java_env)->PopLocalFrame (android_java_env, NULL); +} + +/* Push a local reference frame to the JVM stack and record it on the + specpdl. Release local references created within that frame when + the specpdl is unwound past where it is after returning. */ + +static void +android_push_local_frame (void) +{ + int rc; + + rc = (*android_java_env)->PushLocalFrame (android_java_env, 30); + + /* This means the JVM ran out of memory. */ + if (rc < 1) + android_exception_check (); + + record_unwind_protect_void (android_unwind_local_frame); +} + +/* Data for android_dismiss_menu. */ + +struct android_dismiss_menu_data +{ + /* The menu object. */ + jobject menu; + + /* The window object. */ + jobject window; +}; + +/* Cancel the context menu passed in POINTER. Also, clear + popup_activated_flag. */ + +static void +android_dismiss_menu (void *pointer) +{ + struct android_dismiss_menu_data *data; + + data = pointer; + (*android_java_env)->CallVoidMethod (android_java_env, + data->menu, + menu_class.dismiss, + data->window); + popup_activated_flag = 0; +} + +/* Recursively process events until a ANDROID_CONTEXT_MENU event + arrives. Then, return the item ID specified in the event in + *ID. */ + +static void +android_process_events_for_menu (int *id) +{ + int blocked; + + /* Set menu_event_id to -1; handle_one_android_event will set it to + the event ID upon receiving a context menu event. This can cause + a non-local exit. */ + x_display_list->menu_event_id = -1; + + /* Unblock input completely. */ + blocked = interrupt_input_blocked; + totally_unblock_input (); + + /* Now wait for the menu event ID to change. */ + while (x_display_list->menu_event_id == -1) + { + /* Wait for events to become available. */ + android_wait_event (); + + /* Process pending signals. */ + process_pending_signals (); + + /* Maybe quit. This is important because the framework (on + Android 4.0.3) can sometimes fail to deliver context menu + closed events if a submenu was opened, and the user still + needs to be able to quit. */ + maybe_quit (); + } + + /* Restore the input block. */ + interrupt_input_blocked = blocked; + + /* Return the ID. */ + *id = x_display_list->menu_event_id; +} + +/* Structure describing a ``subprefix'' in the menu. */ + +struct android_menu_subprefix +{ + /* The subprefix above. */ + struct android_menu_subprefix *last; + + /* The subprefix itself. */ + Lisp_Object subprefix; +}; + +/* Free the subprefixes starting from *DATA. */ + +static void +android_free_subprefixes (void *data) +{ + struct android_menu_subprefix **head, *subprefix; + + head = data; + + while (*head) + { + subprefix = *head; + *head = subprefix->last; + + xfree (subprefix); + } +} + +Lisp_Object +android_menu_show (struct frame *f, int x, int y, int menuflags, + Lisp_Object title, const char **error_name) +{ + jobject context_menu, current_context_menu; + jobject title_string, help_string, temp; + size_t i; + Lisp_Object pane_name, prefix; + const char *pane_string; + specpdl_ref count, count1; + Lisp_Object item_name, enable, def, tem, entry, type, selected; + Lisp_Object help; + jmethodID method; + jobject store; + bool rc; + jobject window; + int id, item_id, submenu_depth; + struct android_dismiss_menu_data data; + struct android_menu_subprefix *subprefix, *temp_subprefix; + struct android_menu_subprefix *subprefix_1; + bool checkmark; + unsigned int serial; + + count = SPECPDL_INDEX (); + serial = ++current_menu_serial; + + block_input (); + + /* Push the first local frame. */ + android_push_local_frame (); + + /* Push the first local frame for the context menu. */ + title_string = (!NILP (title) + ? (jobject) android_build_string (title) + : NULL); + method = menu_class.create_context_menu; + current_context_menu = context_menu + = (*android_java_env)->CallStaticObjectMethod (android_java_env, + menu_class.class, + method, + title_string); + + if (title_string) + ANDROID_DELETE_LOCAL_REF (title_string); + + /* Push the second local frame for temporaries. */ + count1 = SPECPDL_INDEX (); + android_push_local_frame (); + + /* Iterate over the menu. */ + i = 0, submenu_depth = 0; + + while (i < menu_items_used) + { + if (NILP (AREF (menu_items, i))) + { + /* This is the start of a new submenu. However, it can be + ignored here. */ + i += 1; + submenu_depth += 1; + } + else if (EQ (AREF (menu_items, i), Qlambda)) + { + /* This is the end of a submenu. Go back to the previous + context menu. */ + store = current_context_menu; + current_context_menu + = (*android_java_env)->CallObjectMethod (android_java_env, + current_context_menu, + menu_class.parent); + android_exception_check (); + + if (store != context_menu) + ANDROID_DELETE_LOCAL_REF (store); + i += 1; + submenu_depth -= 1; + + if (!current_context_menu || submenu_depth < 0) + { + __android_log_print (ANDROID_LOG_FATAL, __func__, + "unbalanced submenu pop in menu_items"); + emacs_abort (); + } + } + else if (EQ (AREF (menu_items, i), Qt) + && submenu_depth != 0) + i += MENU_ITEMS_PANE_LENGTH; + else if (EQ (AREF (menu_items, i), Qquote)) + i += 1; + else if (EQ (AREF (menu_items, i), Qt)) + { + /* This is a new pane. Switch back to the topmost context + menu. */ + if (current_context_menu != context_menu) + ANDROID_DELETE_LOCAL_REF (current_context_menu); + current_context_menu = context_menu; + + /* Now figure out the title of this pane. */ + pane_name = AREF (menu_items, i + MENU_ITEMS_PANE_NAME); + prefix = AREF (menu_items, i + MENU_ITEMS_PANE_PREFIX); + pane_string = (NILP (pane_name) + ? "" : SSDATA (pane_name)); + if ((menuflags & MENU_KEYMAPS) && !NILP (prefix)) + pane_string++; + + /* Add the pane. */ + temp = (*android_java_env)->NewStringUTF (android_java_env, + pane_string); + android_exception_check (); + + (*android_java_env)->CallVoidMethod (android_java_env, + current_context_menu, + menu_class.add_pane, + temp); + android_exception_check (); + ANDROID_DELETE_LOCAL_REF (temp); + + i += MENU_ITEMS_PANE_LENGTH; + } + else + { + item_name = AREF (menu_items, i + MENU_ITEMS_ITEM_NAME); + enable = AREF (menu_items, i + MENU_ITEMS_ITEM_ENABLE); + def = AREF (menu_items, i + MENU_ITEMS_ITEM_DEFINITION); + type = AREF (menu_items, i + MENU_ITEMS_ITEM_TYPE); + selected = AREF (menu_items, i + MENU_ITEMS_ITEM_SELECTED); + help = AREF (menu_items, i + MENU_ITEMS_ITEM_HELP); + + /* This is an actual menu item (or submenu). Add it to the + menu. */ + + if (i + MENU_ITEMS_ITEM_LENGTH < menu_items_used + && NILP (AREF (menu_items, i + MENU_ITEMS_ITEM_LENGTH))) + { + /* This is a submenu. Add it. */ + title_string = (!NILP (item_name) + ? android_build_string (item_name) + : NULL); + help_string = NULL; + + /* Menu items can have tool tips on Android 26 and + later. In this case, set it to the help string. */ + + if (android_get_current_api_level () >= 26 + && STRINGP (help)) + help_string = android_build_string (help); + + store = current_context_menu; + current_context_menu + = (*android_java_env)->CallObjectMethod (android_java_env, + current_context_menu, + menu_class.add_submenu, + title_string, NULL, + help_string); + android_exception_check (); + + if (store != context_menu) + ANDROID_DELETE_LOCAL_REF (store); + + if (title_string) + ANDROID_DELETE_LOCAL_REF (title_string); + + if (help_string) + ANDROID_DELETE_LOCAL_REF (help_string); + } + else if (NILP (def) && menu_separator_name_p (SSDATA (item_name))) + /* Ignore this separator item. */ + ; + else + { + /* Compute the item ID. This is the index of value. + Make sure it doesn't overflow. */ + + if (!INT_ADD_OK (0, i + MENU_ITEMS_ITEM_VALUE, &item_id)) + memory_full (i + MENU_ITEMS_ITEM_VALUE * sizeof (Lisp_Object)); + + /* Add this menu item with the appropriate state. */ + + title_string = (!NILP (item_name) + ? android_build_string (item_name) + : NULL); + help_string = NULL; + + /* Menu items can have tool tips on Android 26 and + later. In this case, set it to the help string. */ + + if (android_get_current_api_level () >= 26 + && STRINGP (help)) + help_string = android_build_string (help); + + /* Determine whether or not to display a check box. */ + + checkmark = (EQ (type, QCtoggle) + || EQ (type, QCradio)); + + (*android_java_env)->CallVoidMethod (android_java_env, + current_context_menu, + menu_class.add_item, + (jint) item_id, + title_string, + (jboolean) !NILP (enable), + (jboolean) checkmark, + (jboolean) !NILP (selected), + help_string, + (jboolean) (EQ (type, + QCradio))); + android_exception_check (); + + if (title_string) + ANDROID_DELETE_LOCAL_REF (title_string); + + if (help_string) + ANDROID_DELETE_LOCAL_REF (help_string); + } + + i += MENU_ITEMS_ITEM_LENGTH; + } + } + + /* The menu has now been built. Pop the second local frame. */ + unbind_to (count1, Qnil); + + /* Now, display the context menu. */ + window = android_resolve_handle (FRAME_ANDROID_WINDOW (f), + ANDROID_HANDLE_WINDOW); + rc = (*android_java_env)->CallBooleanMethod (android_java_env, + context_menu, + menu_class.display, + window, (jint) x, + (jint) y, + (jint) serial); + android_exception_check (); + + if (!rc) + /* This means displaying the menu failed. */ + goto finish; + + /* Make sure the context menu is always dismissed. */ + data.menu = context_menu; + data.window = window; + record_unwind_protect_ptr (android_dismiss_menu, &data); + + /* Next, process events waiting for something to be selected. */ + popup_activated_flag = 1; + android_process_events_for_menu (&id); + + if (!id) + /* This means no menu item was selected. */ + goto finish; + + /* This means the id is invalid. */ + if (id >= ASIZE (menu_items)) + goto finish; + + /* Now return the menu item at that location. */ + tem = Qnil; + subprefix = NULL; + record_unwind_protect_ptr (android_free_subprefixes, &subprefix); + + /* Find the selected item, and its pane, to return + the proper value. */ + + prefix = entry = Qnil; + i = 0; + while (i < menu_items_used) + { + if (NILP (AREF (menu_items, i))) + { + temp_subprefix = xmalloc (sizeof *temp_subprefix); + temp_subprefix->last = subprefix; + subprefix = temp_subprefix; + subprefix->subprefix = prefix; + + prefix = entry; + i++; + } + else if (EQ (AREF (menu_items, i), Qlambda)) + { + prefix = subprefix->subprefix; + temp_subprefix = subprefix->last; + xfree (subprefix); + subprefix = temp_subprefix; + + i++; + } + else if (EQ (AREF (menu_items, i), Qt)) + { + prefix + = AREF (menu_items, i + MENU_ITEMS_PANE_PREFIX); + i += MENU_ITEMS_PANE_LENGTH; + } + /* Ignore a nil in the item list. + It's meaningful only for dialog boxes. */ + else if (EQ (AREF (menu_items, i), Qquote)) + i += 1; + else + { + entry = AREF (menu_items, i + MENU_ITEMS_ITEM_VALUE); + + if (i + MENU_ITEMS_ITEM_VALUE == id) + { + if (menuflags & MENU_KEYMAPS) + { + entry = list1 (entry); + + if (!NILP (prefix)) + entry = Fcons (prefix, entry); + + for (subprefix_1 = subprefix; subprefix_1; + subprefix_1 = subprefix_1->last) + if (!NILP (subprefix_1->subprefix)) + entry = Fcons (subprefix_1->subprefix, entry); + } + + tem = entry; + } + i += MENU_ITEMS_ITEM_LENGTH; + } + } + + unblock_input (); + return unbind_to (count, tem); + + finish: + unblock_input (); + return unbind_to (count, Qnil); +} + + + +/* Toolkit dialog implementation. */ + +/* Structure describing the EmacsDialog class. */ + +struct android_emacs_dialog +{ + jclass class; + jmethodID create_dialog; + jmethodID add_button; + jmethodID display; +}; + +/* Identifiers associated with the EmacsDialog class. */ +static struct android_emacs_dialog dialog_class; + +static void +android_init_emacs_dialog (void) +{ + jclass old; + + dialog_class.class + = (*android_java_env)->FindClass (android_java_env, + "org/gnu/emacs/EmacsDialog"); + eassert (dialog_class.class); + + old = dialog_class.class; + dialog_class.class + = (jclass) (*android_java_env)->NewGlobalRef (android_java_env, + (jobject) old); + ANDROID_DELETE_LOCAL_REF (old); + + if (!dialog_class.class) + emacs_abort (); + +#define FIND_METHOD(c_name, name, signature) \ + dialog_class.c_name \ + = (*android_java_env)->GetMethodID (android_java_env, \ + dialog_class.class, \ + name, signature); \ + eassert (dialog_class.c_name); + +#define FIND_METHOD_STATIC(c_name, name, signature) \ + dialog_class.c_name \ + = (*android_java_env)->GetStaticMethodID (android_java_env, \ + dialog_class.class, \ + name, signature); \ + + FIND_METHOD_STATIC (create_dialog, "createDialog", "(Ljava/lang/String;" + "Ljava/lang/String;I)Lorg/gnu/emacs/EmacsDialog;"); + FIND_METHOD (add_button, "addButton", "(Ljava/lang/String;IZ)V"); + FIND_METHOD (display, "display", "()Z"); + +#undef FIND_METHOD +#undef FIND_METHOD_STATIC +} + +static Lisp_Object +android_dialog_show (struct frame *f, Lisp_Object title, + Lisp_Object header, const char **error_name) +{ + specpdl_ref count; + jobject dialog, java_header, java_title, temp; + size_t i; + Lisp_Object item_name, enable, entry; + bool rc; + int id; + jmethodID method; + unsigned int serial; + + /* Generate a unique ID for events from this dialog box. */ + serial = ++current_menu_serial; + + if (menu_items_n_panes > 1) + { + *error_name = "Multiple panes in dialog box"; + return Qnil; + } + + /* Do the initial setup. */ + count = SPECPDL_INDEX (); + *error_name = NULL; + + android_push_local_frame (); + + /* Figure out what header to use. */ + java_header = (!NILP (header) + ? android_build_jstring ("Information") + : android_build_jstring ("Question")); + + /* And the title. */ + java_title = android_build_string (title); + + /* Now create the dialog. */ + method = dialog_class.create_dialog; + dialog = (*android_java_env)->CallStaticObjectMethod (android_java_env, + dialog_class.class, + method, java_header, + java_title, + (jint) serial); + android_exception_check (); + + /* Delete now unused local references. */ + if (java_header) + ANDROID_DELETE_LOCAL_REF (java_header); + ANDROID_DELETE_LOCAL_REF (java_title); + + /* Create the buttons. */ + i = MENU_ITEMS_PANE_LENGTH; + while (i < menu_items_used) + { + item_name = AREF (menu_items, i + MENU_ITEMS_ITEM_NAME); + enable = AREF (menu_items, i + MENU_ITEMS_ITEM_ENABLE); + + /* Verify that there is no submenu here. */ + + if (NILP (item_name)) + { + *error_name = "Submenu in dialog items"; + return unbind_to (count, Qnil); + } + + /* Skip past boundaries between buttons on different sides. The + Android toolkit is too silly to understand this + distinction. */ + + if (EQ (item_name, Qquote)) + ++i; + else + { + /* Make sure i is within bounds. */ + if (i > TYPE_MAXIMUM (jint)) + { + *error_name = "Dialog box too big"; + return unbind_to (count, Qnil); + } + + /* Add the button. */ + temp = android_build_string (item_name); + (*android_java_env)->CallVoidMethod (android_java_env, + dialog, + dialog_class.add_button, + temp, (jint) i, + (jboolean) NILP (enable)); + android_exception_check (); + ANDROID_DELETE_LOCAL_REF (temp); + i += MENU_ITEMS_ITEM_LENGTH; + } + } + + /* The dialog is now built. Run it. */ + rc = (*android_java_env)->CallBooleanMethod (android_java_env, + dialog, + dialog_class.display); + android_exception_check (); + + if (!rc) + quit (); + + /* Wait for the menu ID to arrive. */ + android_process_events_for_menu (&id); + + if (!id) + quit (); + + /* Find the selected item, and its pane, to return + the proper value. */ + i = 0; + while (i < menu_items_used) + { + if (EQ (AREF (menu_items, i), Qt)) + i += MENU_ITEMS_PANE_LENGTH; + else if (EQ (AREF (menu_items, i), Qquote)) + /* This is the boundary between left-side elts and right-side + elts. */ + ++i; + else + { + entry = AREF (menu_items, i + MENU_ITEMS_ITEM_VALUE); + + if (id == i) + return entry; + + i += MENU_ITEMS_ITEM_LENGTH; + } + } + + return Qnil; +} + +Lisp_Object +android_popup_dialog (struct frame *f, Lisp_Object header, + Lisp_Object contents) +{ + Lisp_Object title; + const char *error_name; + Lisp_Object selection; + specpdl_ref specpdl_count = SPECPDL_INDEX (); + + check_window_system (f); + + /* Decode the dialog items from what was specified. */ + title = Fcar (contents); + CHECK_STRING (title); + record_unwind_protect_void (unuse_menu_items); + + if (NILP (Fcar (Fcdr (contents)))) + /* No buttons specified, add an "Ok" button so users can pop down + the dialog. */ + contents = list2 (title, Fcons (build_string ("Ok"), Qt)); + + list_of_panes (list1 (contents)); + + /* Display them in a dialog box. */ + block_input (); + selection = android_dialog_show (f, title, header, &error_name); + unblock_input (); + + unbind_to (specpdl_count, Qnil); + discard_menu_items (); + + if (error_name) + error ("%s", error_name); + + return selection; +} + +#else + +int +popup_activated (void) +{ + return 0; +} + +#endif + +DEFUN ("menu-or-popup-active-p", Fmenu_or_popup_active_p, + Smenu_or_popup_active_p, 0, 0, 0, + doc: /* SKIP: real doc in xfns.c. */) + (void) +{ + return (popup_activated ()) ? Qt : Qnil; +} + +void +init_androidmenu (void) +{ +#ifndef ANDROID_STUBIFY + android_init_emacs_context_menu (); + android_init_emacs_dialog (); +#endif +} + +void +syms_of_androidmenu (void) +{ + defsubr (&Smenu_or_popup_active_p); +} |