root/java/org/gnu/emacs/EmacsContextMenu.java

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. onMenuItemClick
  2. createContextMenu
  3. addItem
  4. addPane
  5. addSubmenu
  6. inflateMenuItems
  7. expandTo
  8. parent
  9. display1
  10. display
  11. dismiss

     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 };

/* [<][>][^][v][top][bottom][index][help] */