1 /* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3 Copyright (C) 2023 Free Software Foundation, Inc.
4
5 This file is part of GNU Emacs.
6
7 GNU Emacs is free software: you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation, either version 3 of the License, or (at
10 your option) any later version.
11
12 GNU Emacs is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
16
17 You should have received a copy of the GNU General Public License
18 along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20 package org.gnu.emacs;
21
22 import java.util.List;
23 import java.util.ArrayList;
24
25 import android.content.Context;
26 import android.content.Intent;
27
28 import android.os.Build;
29
30 import android.view.ContextMenu;
31 import android.view.Menu;
32 import android.view.MenuItem;
33 import android.view.View;
34 import android.view.SubMenu;
35
36 import android.util.Log;
37
38 /* Context menu implementation. This object is built from JNI and
39 describes a menu hiearchy. Then, `inflate' can turn it into an
40 Android menu, which can be turned into a popup (or other kind of)
41 menu. */
42
43 public final class EmacsContextMenu
44 {
45 private static final String TAG = "EmacsContextMenu";
46
47 /* Whether or not an item was selected. */
48 public static boolean itemAlreadySelected;
49
50 /* Whether or not a submenu was selected.
51 Value is -1 if no; value is -2 if yes, and a context menu
52 close event will definitely be sent. Any other value is
53 the timestamp when the submenu was selected. */
54 public static long wasSubmenuSelected;
55
56 /* The serial ID of the last context menu to be displayed. */
57 public static int lastMenuEventSerial;
58
59 /* The last group ID used for a menu item. */
60 public int lastGroupId;
61
62 private static final class Item implements MenuItem.OnMenuItemClickListener
63 {
64 public int itemID;
65 public String itemName, tooltip;
66 public EmacsContextMenu subMenu;
67 public boolean isEnabled, isCheckable, isChecked;
68 public EmacsView inflatedView;
69 public boolean isRadio;
70
71 @Override
72 public boolean
73 onMenuItemClick (MenuItem item)
74 {
75 Log.d (TAG, "onMenuItemClick: " + itemName + " (" + itemID + ")");
76
77 if (subMenu != null)
78 {
79 /* Android 6.0 and earlier don't support nested submenus
80 properly, so display the submenu popup by hand. */
81
82 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
83 {
84 Log.d (TAG, "onMenuItemClick: displaying submenu " + subMenu);
85
86 /* Still set wasSubmenuSelected -- if not set, the
87 dismissal of this context menu will result in a
88 context menu event being sent. */
89 wasSubmenuSelected = -2;
90
91 /* Running a popup menu from inside a click handler
92 doesn't work, so make sure it is displayed
93 outside. */
94
95 inflatedView.post (new Runnable () {
96 @Override
97 public void
98 run ()
99 {
100 inflatedView.popupMenu (subMenu, 0, 0, true);
101 }
102 });
103
104 return true;
105 }
106
107 /* After opening a submenu within a submenu, Android will
108 send onContextMenuClosed for a ContextMenuBuilder. This
109 will normally confuse Emacs into thinking that the
110 context menu has been dismissed. Wrong!
111
112 Setting this flag makes EmacsActivity to only handle
113 SubMenuBuilder being closed, which always means the menu
114 has actually been dismissed.
115
116 However, these extraneous events aren't sent on devices
117 where submenus display without dismissing their parents.
118 Thus, only ignore the close event if it happens within
119 300 milliseconds of the submenu being selected. */
120 wasSubmenuSelected = System.currentTimeMillis ();
121 return false;
122 }
123
124 /* Send a context menu event. */
125 EmacsNative.sendContextMenu ((short) 0, itemID,
126 lastMenuEventSerial);
127
128 /* Say that an item has already been selected. */
129 itemAlreadySelected = true;
130 return true;
131 }
132 };
133
134 /* List of menu items contained in this menu. */
135 public List<Item> menuItems;
136
137 /* The parent context menu, or NULL if none. */
138 private EmacsContextMenu parent;
139
140 /* The title of this context menu, or NULL if none. */
141 private String title;
142
143
144
145 /* Create a context menu with no items inside and the title TITLE,
146 which may be NULL. */
147
148 public static EmacsContextMenu
149 createContextMenu (String title)
150 {
151 EmacsContextMenu menu;
152
153 menu = new EmacsContextMenu ();
154 menu.title = title;
155 menu.menuItems = new ArrayList<Item> ();
156
157 return menu;
158 }
159
160 /* Add a normal menu item to the context menu with the id ITEMID and
161 the name ITEMNAME. Enable it if ISENABLED, else keep it
162 disabled.
163
164 If this is not a submenu and ISCHECKABLE is set, make the item
165 checkable. Likewise, if ISCHECKED is set, make the item
166 checked.
167
168 If TOOLTIP is non-NULL, set the menu item tooltip to TOOLTIP.
169
170 If ISRADIO, then display the check mark as a radio button. */
171
172 public void
173 addItem (int itemID, String itemName, boolean isEnabled,
174 boolean isCheckable, boolean isChecked,
175 String tooltip, boolean isRadio)
176 {
177 Item item;
178
179 item = new Item ();
180 item.itemID = itemID;
181 item.itemName = itemName;
182 item.isEnabled = isEnabled;
183 item.isCheckable = isCheckable;
184 item.isChecked = isChecked;
185 item.tooltip = tooltip;
186 item.isRadio = isRadio;
187
188 menuItems.add (item);
189 }
190
191 /* Create a disabled menu item with the name ITEMNAME. */
192
193 public void
194 addPane (String itemName)
195 {
196 Item item;
197
198 item = new Item ();
199 item.itemName = itemName;
200
201 menuItems.add (item);
202 }
203
204 /* Add a submenu to the context menu with the specified title and
205 item name. */
206
207 public EmacsContextMenu
208 addSubmenu (String itemName, String tooltip)
209 {
210 EmacsContextMenu submenu;
211 Item item;
212
213 item = new Item ();
214 item.itemID = 0;
215 item.itemName = itemName;
216 item.tooltip = tooltip;
217 item.subMenu = createContextMenu (itemName);
218 item.subMenu.parent = this;
219
220 menuItems.add (item);
221 return item.subMenu;
222 }
223
224 /* Add the contents of this menu to MENU. Assume MENU will be
225 displayed in INFLATEDVIEW. */
226
227 private void
228 inflateMenuItems (Menu menu, EmacsView inflatedView)
229 {
230 Intent intent;
231 MenuItem menuItem;
232 SubMenu submenu;
233
234 for (Item item : menuItems)
235 {
236 if (item.subMenu != null)
237 {
238 /* This is a submenu. On versions of Android which
239 support doing so, create the submenu and add the
240 contents of the menu to it.
241
242 Note that Android 4.0 and later technically supports
243 having multiple layers of nested submenus, but if they
244 are used, onContextMenuClosed becomes unreliable. */
245
246 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
247 {
248 submenu = menu.addSubMenu (item.itemName);
249 item.subMenu.inflateMenuItems (submenu, inflatedView);
250
251 /* This is still needed to set wasSubmenuSelected. */
252 menuItem = submenu.getItem ();
253 }
254 else
255 menuItem = menu.add (item.itemName);
256
257 item.inflatedView = inflatedView;
258 menuItem.setOnMenuItemClickListener (item);
259 }
260 else
261 {
262 if (item.isRadio)
263 menuItem = menu.add (++lastGroupId, Menu.NONE, Menu.NONE,
264 item.itemName);
265 else
266 menuItem = menu.add (item.itemName);
267 menuItem.setOnMenuItemClickListener (item);
268
269 /* If the item ID is zero, then disable the item. */
270 if (item.itemID == 0 || !item.isEnabled)
271 menuItem.setEnabled (false);
272
273 /* Now make the menu item display a checkmark as
274 appropriate. */
275
276 if (item.isCheckable)
277 menuItem.setCheckable (true);
278
279 if (item.isChecked)
280 menuItem.setChecked (true);
281
282 /* Define an exclusively checkable group if the item is a
283 radio button. */
284
285 if (item.isRadio)
286 menu.setGroupCheckable (lastGroupId, true, true);
287
288 /* If the tooltip text is set and the system is new enough
289 to support menu item tooltips, set it on the item. */
290
291 if (item.tooltip != null
292 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
293 menuItem.setTooltipText (item.tooltip);
294 }
295 }
296 }
297
298 /* Enter the items in this context menu to MENU.
299 Assume that MENU will be displayed in VIEW; this may lead to
300 popupMenu being called on VIEW if a submenu is selected.
301
302 If MENU is a ContextMenu, set its header title to the one
303 contained in this object. */
304
305 public void
306 expandTo (Menu menu, EmacsView view)
307 {
308 inflateMenuItems (menu, view);
309
310 /* See if menu is a ContextMenu and a title is set. */
311 if (title == null || !(menu instanceof ContextMenu))
312 return;
313
314 /* Set its title to this.title. */
315 ((ContextMenu) menu).setHeaderTitle (title);
316 }
317
318 /* Return the parent or NULL. */
319
320 public EmacsContextMenu
321 parent ()
322 {
323 return this.parent;
324 }
325
326 /* Like display, but does the actual work and runs in the main
327 thread. */
328
329 private boolean
330 display1 (EmacsWindow window, int xPosition, int yPosition)
331 {
332 /* Set this flag to false. It is used to decide whether or not to
333 send 0 in response to the context menu being closed. */
334 itemAlreadySelected = false;
335
336 /* No submenu has been selected yet. */
337 wasSubmenuSelected = -1;
338
339 return window.view.popupMenu (this, xPosition, yPosition,
340 false);
341 }
342
343 /* Display this context menu on WINDOW, at xPosition and yPosition.
344 SERIAL is a number that will be returned in any menu event
345 generated to identify this context menu. */
346
347 public boolean
348 display (final EmacsWindow window, final int xPosition,
349 final int yPosition, final int serial)
350 {
351 Runnable runnable;
352 final EmacsHolder<Boolean> rc;
353
354 rc = new EmacsHolder<Boolean> ();
355 rc.thing = false;
356
357 runnable = new Runnable () {
358 @Override
359 public void
360 run ()
361 {
362 synchronized (this)
363 {
364 lastMenuEventSerial = serial;
365 rc.thing = display1 (window, xPosition, yPosition);
366 notify ();
367 }
368 }
369 };
370
371 EmacsService.syncRunnable (runnable);
372 return rc.thing;
373 }
374
375 /* Dismiss this context menu. WINDOW is the window where the
376 context menu is being displayed. */
377
378 public void
379 dismiss (final EmacsWindow window)
380 {
381 Runnable runnable;
382
383 EmacsService.SERVICE.runOnUiThread (new Runnable () {
384 @Override
385 public void
386 run ()
387 {
388 window.view.cancelPopupMenu ();
389 itemAlreadySelected = false;
390 }
391 });
392 }
393 };