emacs/src/androidmenu.c

861 lines
22 KiB
C
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Communication module for Android terminals.
Copyright (C) 2023-2024 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;)"
"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)->CallNonvirtualVoidMethod (android_java_env,
data->menu,
menu_class.class,
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;
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;
JNIEnv *env;
count = SPECPDL_INDEX ();
serial = ++current_menu_serial;
env = android_java_env;
block_input ();
/* Push the first local frame. */
android_push_local_frame ();
/* Set title_string to a Java string containing TITLE if non-nil.
If the menu consists of more than one pane, replace the title
with the pane header item so that the menu looks consistent. */
title_string = NULL;
if (STRINGP (title) && menu_items_n_panes < 2)
title_string = android_build_string (title, NULL);
/* Push the first local frame for the context menu. */
method = menu_class.create_context_menu;
current_context_menu = context_menu
= (*android_java_env)->CallStaticObjectMethod (android_java_env,
menu_class.class,
method,
title_string);
/* Delete the unused title reference. */
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
= (*env)->CallNonvirtualObjectMethod (env,
current_context_menu,
menu_class.class,
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))
{
/* If the menu contains a single pane, then the pane is
actually TITLE. Don't duplicate the text within the
context menu title. */
if (menu_items_n_panes < 2)
goto next_item;
/* 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_NAME may be nil, in which case it must be set to an
empty string. */
if (NILP (pane_name))
pane_name = empty_unibyte_string;
/* Remove the leading prefix character if need be. */
if ((menuflags & MENU_KEYMAPS) && !NILP (prefix)
&& SCHARS (prefix))
pane_name = Fsubstring (pane_name, make_fixnum (1), Qnil);
/* Add the pane. */
temp = android_build_string (pane_name, NULL);
android_exception_check ();
(*env)->CallNonvirtualVoidMethod (env, current_context_menu,
menu_class.class,
menu_class.add_pane, temp);
android_exception_check ();
ANDROID_DELETE_LOCAL_REF (temp);
next_item:
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)
: 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, NULL);
store = current_context_menu;
current_context_menu
= (*env)->CallNonvirtualObjectMethod (env,
current_context_menu,
menu_class.class,
menu_class.add_submenu,
title_string,
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 (ckd_add (&item_id, i + MENU_ITEMS_ITEM_VALUE, 0))
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)
: 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, NULL);
/* Determine whether or not to display a check box. */
checkmark = (EQ (type, QCtoggle)
|| EQ (type, QCradio));
(*env)->CallNonvirtualVoidMethod (env,
current_context_menu,
menu_class.class,
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));
rc = (*env)->CallNonvirtualBooleanMethod (env, context_menu,
menu_class.class,
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;
JNIEnv *env;
/* 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, NULL);
/* 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);
/* Save the JNI environment pointer prior to constructing the
dialog, as typing (*android_java_env)->... gives rise to very
long lines. */
env = android_java_env;
/* 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, NULL);
(*env)->CallNonvirtualVoidMethod (env, dialog,
dialog_class.class,
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 = (*env)->CallNonvirtualBooleanMethod (env, dialog,
dialog_class.class,
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);
}