From dda3063e79ff1d959f7c7376880764792b241516 Mon Sep 17 00:00:00 2001 From: Julian Winkler Date: Wed, 20 Mar 2024 22:09:56 +0100 Subject: [PATCH] PopupMenu: use AOSP MenuInflater and support item visibility --- .../android_widget_PopupMenu.h | 24 +- .../widgets/android_widget_PopupMenu.c | 21 +- .../android/view/InflateException.java | 10 +- src/api-impl/android/view/MenuInflater.java | 448 +++++++++++++++++- src/api-impl/android/widget/PopupMenu.java | 361 +++++++++----- 5 files changed, 740 insertions(+), 124 deletions(-) diff --git a/src/api-impl-jni/generated_headers/android_widget_PopupMenu.h b/src/api-impl-jni/generated_headers/android_widget_PopupMenu.h index 57a7a5b3..60c80ba2 100644 --- a/src/api-impl-jni/generated_headers/android_widget_PopupMenu.h +++ b/src/api-impl-jni/generated_headers/android_widget_PopupMenu.h @@ -17,11 +17,27 @@ JNIEXPORT jlong JNICALL Java_android_widget_PopupMenu_native_1init /* * Class: android_widget_PopupMenu - * Method: native_appendItem - * Signature: (JLjava/lang/String;I)V + * Method: native_insertItem + * Signature: (JILjava/lang/String;I)V */ -JNIEXPORT void JNICALL Java_android_widget_PopupMenu_native_1appendItem - (JNIEnv *, jobject, jlong, jstring, jint); +JNIEXPORT void JNICALL Java_android_widget_PopupMenu_native_1insertItem + (JNIEnv *, jobject, jlong, jint, jstring, jint); + +/* + * Class: android_widget_PopupMenu + * Method: native_insertSubmenu + * Signature: (JILjava/lang/String;J)V + */ +JNIEXPORT void JNICALL Java_android_widget_PopupMenu_native_1insertSubmenu + (JNIEnv *, jobject, jlong, jint, jstring, jlong); + +/* + * Class: android_widget_PopupMenu + * Method: native_removeItem + * Signature: (JI)V + */ +JNIEXPORT void JNICALL Java_android_widget_PopupMenu_native_1removeItem + (JNIEnv *, jobject, jlong, jint); /* * Class: android_widget_PopupMenu diff --git a/src/api-impl-jni/widgets/android_widget_PopupMenu.c b/src/api-impl-jni/widgets/android_widget_PopupMenu.c index e9b707d5..3da05056 100644 --- a/src/api-impl-jni/widgets/android_widget_PopupMenu.c +++ b/src/api-impl-jni/widgets/android_widget_PopupMenu.c @@ -10,13 +10,30 @@ JNIEXPORT jlong JNICALL Java_android_widget_PopupMenu_native_1init(JNIEnv *env, return _INTPTR(g_menu_new()); } -JNIEXPORT void JNICALL Java_android_widget_PopupMenu_native_1appendItem(JNIEnv *env, jobject this, jlong menu_ptr, jstring title_jstr, jint id) +JNIEXPORT void JNICALL Java_android_widget_PopupMenu_native_1insertItem(JNIEnv *env, jobject this, jlong menu_ptr, jint position, jstring title_jstr, jint id) { const gchar *title = (*env)->GetStringUTFChars(env, title_jstr, NULL); + printf("insertItem position: %d title: %s\n", position, title); GMenuItem *item = g_menu_item_new(title, NULL); (*env)->ReleaseStringUTFChars(env, title_jstr, title); g_menu_item_set_action_and_target(item, "popupmenu.clicked", "i", id); - g_menu_append_item(G_MENU(_PTR(menu_ptr)), item); + g_menu_insert_item(G_MENU(_PTR(menu_ptr)), position, item); + g_object_unref(item); +} + +JNIEXPORT void JNICALL Java_android_widget_PopupMenu_native_1insertSubmenu(JNIEnv *env, jobject this, jlong menu_ptr, jint position, jstring title_jstr, jlong submenu_ptr) +{ + const gchar *title = (*env)->GetStringUTFChars(env, title_jstr, NULL); + printf("insertSubmenu position: %d title: %s\n", position, title); + GMenuItem *item = g_menu_item_new_submenu(title, G_MENU_MODEL(_PTR(submenu_ptr))); + (*env)->ReleaseStringUTFChars(env, title_jstr, title); + g_menu_insert_item(G_MENU(_PTR(menu_ptr)), position, item); + g_object_unref(item); +} + +JNIEXPORT void JNICALL Java_android_widget_PopupMenu_native_1removeItem(JNIEnv *env, jobject this, jlong menu_ptr, jint position) +{ + g_menu_remove(G_MENU(_PTR(menu_ptr)), position); } static void popupmenu_activated(GSimpleAction *action, GVariant *parameter, gpointer user_data) { diff --git a/src/api-impl/android/view/InflateException.java b/src/api-impl/android/view/InflateException.java index 11a58c86..fac5b1ef 100644 --- a/src/api-impl/android/view/InflateException.java +++ b/src/api-impl/android/view/InflateException.java @@ -1,5 +1,13 @@ package android.view; -public class InflateException extends Exception { +public class InflateException extends RuntimeException { + + public InflateException(String string, Exception e) { + super(string, e); + } + + public InflateException(String string) { + super(string); + } } diff --git a/src/api-impl/android/view/MenuInflater.java b/src/api-impl/android/view/MenuInflater.java index 9a2796b9..cc3a7f0c 100644 --- a/src/api-impl/android/view/MenuInflater.java +++ b/src/api-impl/android/view/MenuInflater.java @@ -1,11 +1,455 @@ +/* +* Copyright (C) 2006 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + package android.view; +import android.app.Activity; import android.content.Context; +import android.content.ContextWrapper; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +/** + * This class is used to instantiate menu XML files into Menu objects. +*

+* For performance reasons, menu inflation relies heavily on pre-processing of +* XML files that is done at build time. Therefore, it is not currently possible +* to use MenuInflater with an XmlPullParser over a plain XML file at runtime; +* it only works with an XmlPullParser returned from a compiled resource (R. +* something file.) +*/ public class MenuInflater { + private static final String LOG_TAG = "MenuInflater"; - public MenuInflater(Context context) {} + /** Menu tag name in XML. */ + private static final String XML_MENU = "menu"; - public void inflate(int menuRes, Menu menu) {} + /** Group tag name in XML. */ + private static final String XML_GROUP = "group"; + /** Item tag name in XML. */ + private static final String XML_ITEM = "item"; + + private static final int NO_ID = 0; + + private static final Class[] ACTION_VIEW_CONSTRUCTOR_SIGNATURE = new Class[] {Context.class}; + + private final Object[] mActionViewConstructorArguments; + + private Context mContext; + private Object mRealOwner; + + /** + * Constructs a menu inflater. + * + * @see Activity#getMenuInflater() + */ + public MenuInflater(Context context) { + mContext = context; + mActionViewConstructorArguments = new Object[] {context}; + } + + /** + * Constructs a menu inflater. + * + * @see Activity#getMenuInflater() + * @hide + */ + public MenuInflater(Context context, Object realOwner) { + mContext = context; + mRealOwner = realOwner; + mActionViewConstructorArguments = new Object[] {context}; + } + + /** + * Inflate a menu hierarchy from the specified XML resource. Throws + * {@link InflateException} if there is an error. + * + * @param menuRes Resource ID for an XML layout resource to load (e.g., + * R.menu.main_activity) + * @param menu The Menu to inflate into. The items and submenus will be + * added to this Menu. + */ + public void inflate(int menuRes, Menu menu) { + XmlResourceParser parser = null; + try { + parser = mContext.getResources().getLayout(menuRes); + AttributeSet attrs = Xml.asAttributeSet(parser); + + parseMenu(parser, attrs, menu); + } catch (XmlPullParserException e) { + throw new InflateException("Error inflating menu XML", e); + } catch (IOException e) { + throw new InflateException("Error inflating menu XML", e); + } finally { + if (parser != null) parser.close(); + } + } + + /** + * Called internally to fill the given menu. If a sub menu is seen, it will + * call this recursively. + */ + private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu) + throws XmlPullParserException, IOException { + MenuState menuState = new MenuState(menu); + + int eventType = parser.getEventType(); + String tagName; + boolean lookingForEndOfUnknownTag = false; + String unknownTagName = null; + + // This loop will skip to the menu start tag + do { + if (eventType == XmlPullParser.START_TAG) { + tagName = parser.getName(); + if (tagName.equals(XML_MENU)) { + // Go to next tag + eventType = parser.next(); + break; + } + + throw new RuntimeException("Expecting menu, got " + tagName); + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + + boolean reachedEndOfMenu = false; + while (!reachedEndOfMenu) { + switch (eventType) { + case XmlPullParser.START_TAG: + if (lookingForEndOfUnknownTag) { + break; + } + + tagName = parser.getName(); + if (tagName.equals(XML_GROUP)) { + menuState.readGroup(attrs); + } else if (tagName.equals(XML_ITEM)) { + menuState.readItem(attrs); + } else if (tagName.equals(XML_MENU)) { + // A menu start tag denotes a submenu for an item + SubMenu subMenu = menuState.addSubMenuItem(); + registerMenu(subMenu, attrs); + + // Parse the submenu into returned SubMenu + parseMenu(parser, attrs, subMenu); + } else { + lookingForEndOfUnknownTag = true; + unknownTagName = tagName; + } + break; + + case XmlPullParser.END_TAG: + tagName = parser.getName(); + if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) { + lookingForEndOfUnknownTag = false; + unknownTagName = null; + } else if (tagName.equals(XML_GROUP)) { + menuState.resetGroup(); + } else if (tagName.equals(XML_ITEM)) { + // Add the item if it hasn't been added (if the item was + // a submenu, it would have been added already) + if (!menuState.hasAddedItem()) { + registerMenu(menuState.addItem(), attrs); + } + } else if (tagName.equals(XML_MENU)) { + reachedEndOfMenu = true; + } + break; + + case XmlPullParser.END_DOCUMENT: + throw new RuntimeException("Unexpected end of document"); + } + + eventType = parser.next(); + } + } + + /** + * The method is a hook for layoutlib to do its magic. + * Nothing is needed outside of LayoutLib. However, it should not be deleted because it + * appears to do nothing. + */ + private void registerMenu(@SuppressWarnings("unused") MenuItem item, + @SuppressWarnings("unused") AttributeSet set) { + } + + /** + * The method is a hook for layoutlib to do its magic. + * Nothing is needed outside of LayoutLib. However, it should not be deleted because it + * appears to do nothing. + */ + private void registerMenu(@SuppressWarnings("unused") SubMenu subMenu, + @SuppressWarnings("unused") AttributeSet set) { + } + + // Needed by layoutlib. + /*package*/ Context getContext() { + return mContext; + } + + private static class InflatedOnMenuItemClickListener + implements MenuItem.OnMenuItemClickListener { + private static final Class[] PARAM_TYPES = new Class[] { MenuItem.class }; + + private Object mRealOwner; + private Method mMethod; + + public InflatedOnMenuItemClickListener(Object realOwner, String methodName) { + mRealOwner = realOwner; + Class c = realOwner.getClass(); + try { + mMethod = c.getMethod(methodName, PARAM_TYPES); + } catch (Exception e) { + InflateException ex = new InflateException( + "Couldn't resolve menu item onClick handler " + methodName + + " in class " + c.getName()); + ex.initCause(e); + throw ex; + } + } + + public boolean onMenuItemClick(MenuItem item) { + try { + if (mMethod.getReturnType() == Boolean.TYPE) { + return (Boolean) mMethod.invoke(mRealOwner, item); + } else { + mMethod.invoke(mRealOwner, item); + return true; + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + private Object getRealOwner() { + if (mRealOwner == null) { + mRealOwner = findRealOwner(mContext); + } + return mRealOwner; + } + + private Object findRealOwner(Object owner) { + if (owner instanceof Activity) { + return owner; + } + if (owner instanceof ContextWrapper) { + return findRealOwner(((ContextWrapper) owner).getBaseContext()); + } + return owner; + } + + /** + * State for the current menu. + *

+ * Groups can not be nested unless there is another menu (which will have + * its state class). + */ + private class MenuState { + private Menu menu; + + /* + * Group state is set on items as they are added, allowing an item to + * override its group state. (As opposed to set on items at the group end tag.) + */ + private int groupId; + private int groupCheckable; + private boolean groupVisible; + private boolean groupEnabled; + + private boolean itemAdded; + private int itemId; + private int itemCategoryOrder; + private CharSequence itemTitle; + private CharSequence itemTitleCondensed; + private int itemIconResId; + /** + * Sync to attrs.xml enum: + * - 0: none + * - 1: all + * - 2: exclusive + */ + private int itemCheckable; + private boolean itemChecked; + private boolean itemVisible; + private boolean itemEnabled; + + /** + * Sync to attrs.xml enum, values in MenuItem: + * - 0: never + * - 1: ifRoom + * - 2: always + * - -1: Safe sentinel for "no value". + */ + private int itemShowAsAction; + + private int itemActionViewLayout; + private String itemActionViewClassName; + + private String itemListenerMethodName; + + private static final int defaultGroupId = NO_ID; + private static final int defaultItemId = NO_ID; + private static final int defaultItemCheckable = 0; + private static final boolean defaultItemChecked = false; + private static final boolean defaultItemVisible = true; + private static final boolean defaultItemEnabled = true; + + public MenuState(final Menu menu) { + this.menu = menu; + + resetGroup(); + } + + public void resetGroup() { + groupId = defaultGroupId; + groupCheckable = defaultItemCheckable; + groupVisible = defaultItemVisible; + groupEnabled = defaultItemEnabled; + } + + /** + * Called when the parser is pointing to a group tag. + */ + public void readGroup(AttributeSet attrs) { + TypedArray a = mContext.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.MenuGroup); + + groupId = a.getResourceId(com.android.internal.R.styleable.MenuGroup_id, defaultGroupId); + groupCheckable = a.getInt(com.android.internal.R.styleable.MenuGroup_checkableBehavior, defaultItemCheckable); + groupVisible = a.getBoolean(com.android.internal.R.styleable.MenuGroup_visible, defaultItemVisible); + groupEnabled = a.getBoolean(com.android.internal.R.styleable.MenuGroup_enabled, defaultItemEnabled); + + a.recycle(); + } + + /** + * Called when the parser is pointing to an item tag. + */ + public void readItem(AttributeSet attrs) { + TypedArray a = mContext.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.MenuItem); + + // Inherit attributes from the group as default value + itemId = a.getResourceId(com.android.internal.R.styleable.MenuItem_id, defaultItemId); + itemTitle = a.getText(com.android.internal.R.styleable.MenuItem_title); + itemTitleCondensed = a.getText(com.android.internal.R.styleable.MenuItem_titleCondensed); + itemIconResId = a.getResourceId(com.android.internal.R.styleable.MenuItem_icon, 0); + + if (a.hasValue(com.android.internal.R.styleable.MenuItem_checkable)) { + // Item has attribute checkable, use it + itemCheckable = a.getBoolean(com.android.internal.R.styleable.MenuItem_checkable, false) ? 1 : 0; + } else { + // Item does not have attribute, use the group's (group can have one more state + // for checkable that represents the exclusive checkable) + itemCheckable = groupCheckable; + } + itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked); + itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible); + itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled); + itemShowAsAction = a.getInt(com.android.internal.R.styleable.MenuItem_showAsAction, -1); + itemListenerMethodName = a.getString(com.android.internal.R.styleable.MenuItem_onClick); + itemActionViewLayout = a.getResourceId(com.android.internal.R.styleable.MenuItem_actionLayout, 0); + itemActionViewClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionViewClass); + + a.recycle(); + + itemAdded = false; + } + + private void setItem(MenuItem item) { + item.setChecked(itemChecked) + .setVisible(itemVisible) + .setEnabled(itemEnabled) + .setCheckable(itemCheckable >= 1) + .setTitleCondensed(itemTitleCondensed) + .setIcon(itemIconResId); + + if (itemShowAsAction >= 0) { + item.setShowAsAction(itemShowAsAction); + } + + if (itemListenerMethodName != null) { + if (mContext.isRestricted()) { + throw new IllegalStateException("The android:onClick attribute cannot " + + "be used within a restricted context"); + } + item.setOnMenuItemClickListener( + new InflatedOnMenuItemClickListener(getRealOwner(), itemListenerMethodName)); + } + + boolean actionViewSpecified = false; + if (itemActionViewClassName != null) { + View actionView = (View) newInstance(itemActionViewClassName, + ACTION_VIEW_CONSTRUCTOR_SIGNATURE, mActionViewConstructorArguments); + item.setActionView(actionView); + actionViewSpecified = true; + } + if (itemActionViewLayout > 0) { + if (!actionViewSpecified) { + actionViewSpecified = true; + } else { + Log.w(LOG_TAG, "Ignoring attribute 'itemActionViewLayout'." + + " Action view already specified."); + } + } + } + + public MenuItem addItem() { + itemAdded = true; + MenuItem item = menu.add(groupId, itemId, itemCategoryOrder, itemTitle); + setItem(item); + return item; + } + + public SubMenu addSubMenuItem() { + itemAdded = true; + SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle); + setItem(subMenu.getItem()); + return subMenu; + } + + public boolean hasAddedItem() { + return itemAdded; + } + + @SuppressWarnings("unchecked") + private T newInstance(String className, Class[] constructorSignature, + Object[] arguments) { + try { + Class clazz = mContext.getClassLoader().loadClass(className); + Constructor constructor = clazz.getConstructor(constructorSignature); + constructor.setAccessible(true); + return (T) constructor.newInstance(arguments); + } catch (Exception e) { + Log.w(LOG_TAG, "Cannot instantiate class: " + className, e); + } + return null; + } + } } diff --git a/src/api-impl/android/widget/PopupMenu.java b/src/api-impl/android/widget/PopupMenu.java index 4b25c68c..53b60163 100644 --- a/src/api-impl/android/widget/PopupMenu.java +++ b/src/api-impl/android/widget/PopupMenu.java @@ -17,8 +17,6 @@ package android.widget; import android.content.Context; -import android.content.res.TypedArray; -import android.content.res.XmlResourceParser; import android.graphics.drawable.Drawable; import android.view.Gravity; import android.view.Menu; @@ -28,7 +26,8 @@ import android.view.SubMenu; import android.view.View; import android.view.View.OnTouchListener; -import org.xmlpull.v1.XmlPullParser; +import java.util.ArrayList; +import java.util.List; import com.android.internal.R; @@ -47,6 +46,7 @@ public class PopupMenu { private OnDismissListener mOnDismissListener; private OnTouchListener mDragListener; private long popover; + private MenuImpl menu; /** * Constructor to create a new popup menu with an anchor view. @@ -94,6 +94,8 @@ public class PopupMenu { int popupStyleRes) { mContext = context; mAnchor = anchor; + menu = new MenuImpl(); + popover = native_buildPopover(menu.menu); } /** @@ -125,110 +127,16 @@ public class PopupMenu { } protected native long native_init(); - protected native void native_appendItem(long menu, String item, int id); + protected native void native_insertItem(long menu, int position, String item, int id); + protected native void native_insertSubmenu(long menu, int position, String item, long submenu); + protected native void native_removeItem(long menu, int position); protected native long native_buildPopover(long menu); protected native void native_show(long popover, long anchor); // callback from native code protected void menuItemClickCallback(final int id) { if (mMenuItemClickListener != null) { - mMenuItemClickListener.onMenuItemClick(new MenuItem() { - - @Override - public MenuItem setIcon(int iconRes) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setIcon'"); - } - - @Override - public MenuItem setVisible(boolean visible) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setVisible'"); - } - - @Override - public MenuItem setChecked(boolean checked) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setChecked'"); - } - - @Override - public MenuItem setEnabled(boolean enabled) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setEnabled'"); - } - - @Override - public MenuItem setCheckable(boolean checkable) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setCheckable'"); - } - - @Override - public MenuItem setTitleCondensed(CharSequence titleCondensed) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setTitleCondensed'"); - } - - @Override - public MenuItem setTitle(CharSequence title) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setTitle'"); - } - - @Override - public MenuItem setActionView(View actionView) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setActionView'"); - } - - @Override - public void setShowAsAction(int action) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setShowAsAction'"); - } - - @Override - public int getItemId() { - return id; - } - - @Override - public int getGroupId() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getGroupId'"); - } - - @Override - public MenuItem setOnMenuItemClickListener(OnMenuItemClickListener listener) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setOnMenuItemClickListener'"); - } - - @Override - public MenuItem setTitle(int resId) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setTitle'"); - } - - @Override - public boolean isVisible() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'isVisible'"); - } - - @Override - public Drawable getIcon() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getIcon'"); - } - - @Override - public SubMenu getSubMenu() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getSubMenu'"); - } - }); + mMenuItemClickListener.onMenuItemClick(getMenu().findItem(id)); } } @@ -240,20 +148,7 @@ public class PopupMenu { * @throws Exception */ public void inflate(int menuRes) throws Exception { - XmlResourceParser parser = mContext.getResources().getXml(menuRes); - int type; - while ((type=parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT); - if (type == XmlPullParser.START_TAG && "menu".equals(parser.getName())) { - long menu = native_init(); - while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { - if (type == XmlPullParser.START_TAG && "item".equals(parser.getName())) { - final TypedArray a = mContext.getResources().obtainAttributes(parser, R.styleable.MenuItem); - native_appendItem(menu, a.getString(R.styleable.MenuItem_title), a.getResourceId(R.styleable.MenuItem_id, -1)); - a.recycle(); - } - } - popover = native_buildPopover(menu); - } + getMenuInflater().inflate(menuRes, getMenu()); } /** @@ -266,6 +161,14 @@ public class PopupMenu { native_show(popover, mAnchor.widget); } + public Menu getMenu() { + return menu; + } + + public void dismiss() { + System.out.println("PopupMenu.dismiss() called"); + } + /** * Interface responsible for receiving menu item click events if the items * themselves do not have individual item click listeners. @@ -294,4 +197,232 @@ public class PopupMenu { void onDismiss(PopupMenu menu); } + private class MenuImpl implements Menu { + long menu = native_init(); + + List items = new ArrayList<>(); + int numVisibleItems = 0; + + @Override + public MenuItem add(int groupId, int itemId, int order, CharSequence title) { + MenuItemImpl item = new MenuItemImpl(itemId, this, String.valueOf(title), null); + items.add(item); + item.setVisible(true); + return item; + } + + @Override + public MenuItem add(int groupId, int itemId, int order, int titleRes) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'add'"); + } + + @Override + public MenuItem findItem(int id) { + for (MenuItemImpl item : items) { + if (item.id == id) + return item; + if (item.subMenu != null) { + MenuItem found = item.subMenu.findItem(id); + if (found != null) + return found; + } + } + return null; + } + + @Override + public MenuItem getItem(int id) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getItem'"); + } + + @Override + public void clear() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'clear'"); + } + + @Override + public void removeGroup(int groupId) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'removeGroup'"); + } + + @Override + public SubMenu addSubMenu(int id) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'addSubMenu'"); + } + + @Override + public MenuItem add(int id) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'add'"); + } + + @Override + public MenuItem add(CharSequence text) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'add'"); + } + + @Override + public void setGroupCheckable(int group, boolean checkable, boolean exclusive) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'setGroupCheckable'"); + } + + @Override + public SubMenu addSubMenu(int groupId, int itemId, int order, CharSequence title) { + SubMenuImpl submenu = new SubMenuImpl(itemId, this, String.valueOf(title)); + items.add(submenu.item); + submenu.item.setVisible(true); + return submenu; + } + } + + private class SubMenuImpl extends MenuImpl implements SubMenu { + private MenuItemImpl item; + + public SubMenuImpl(int id, MenuImpl parent, String title) { + item = new MenuItemImpl(id, parent, title, this); + } + + @Override + public MenuItem getItem() { + return item; + } + + } + + private class MenuItemImpl implements MenuItem { + private int id; + private MenuImpl parent; + private String title; + SubMenuImpl subMenu; + int position; // position in list of visible items, or -1 if not visible + + private MenuItemImpl(int id, MenuImpl parent, String title, SubMenuImpl subMenu) { + this.id = id; + this.parent = parent; + this.position = -1; + this.title = title; + this.subMenu = subMenu; + } + + @Override + public MenuItem setIcon(int iconRes) { + return this; + } + + @Override + public MenuItem setVisible(boolean visible) { + // GMenu doesn't support invisible items, so we remove them while they're not visible + if (!visible && isVisible()) { + parent.numVisibleItems--; + for (int i = parent.items.size()-1; i >= 0; i--) { + MenuItemImpl item = parent.items.get(i); + if (item != this && item.isVisible()) + item.position--; + else if (item == this) + break; + } + native_removeItem(parent.menu, position); + position = -1; + } else if (visible && !isVisible()) { + position = parent.numVisibleItems++; + for (int i = parent.items.size()-1; i >= 0; i--) { + MenuItemImpl item = parent.items.get(i); + if (item != this && item.isVisible()) + position = item.position++; + else if (item == this) + break; + } + if (subMenu != null) + native_insertSubmenu(parent.menu, position, title, subMenu.menu); + else + native_insertItem(parent.menu, position, title, id); + } + return this; + } + + @Override + public MenuItem setChecked(boolean checked) { + return this; + } + + @Override + public MenuItem setEnabled(boolean enabled) { + return this; + } + + @Override + public MenuItem setCheckable(boolean checkable) { + return this; + } + + @Override + public MenuItem setTitleCondensed(CharSequence titleCondensed) { + return this; + } + + @Override + public MenuItem setTitle(CharSequence title) { + this.title = String.valueOf(title); + return this; + } + + @Override + public MenuItem setActionView(View actionView) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'setActionView'"); + } + + @Override + public void setShowAsAction(int action) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'setShowAsAction'"); + } + + @Override + public int getItemId() { + return id; + } + + @Override + public int getGroupId() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getGroupId'"); + } + + @Override + public MenuItem setOnMenuItemClickListener(OnMenuItemClickListener listener) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'setOnMenuItemClickListener'"); + } + + @Override + public MenuItem setTitle(int resId) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'setTitle'"); + } + + @Override + public boolean isVisible() { + return position >= 0; + } + + @Override + public Drawable getIcon() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getIcon'"); + } + + @Override + public SubMenu getSubMenu() { + return subMenu; + } + } + } \ No newline at end of file