Bug 736321 - Support HTML Context menus in Fennec. r=mfinkle

This commit is contained in:
Wes Johnston 2013-01-04 11:18:42 -08:00
parent 14606784d8
commit 2603a6e9a2
2 changed files with 136 additions and 46 deletions

View File

@ -558,6 +558,7 @@ public class PromptService implements OnClickListener, OnCancelListener, OnItemC
public boolean inGroup = false;
public boolean disabled = false;
public int id = 0;
public boolean isParent = false;
// This member can't be accessible from JS, see bug 733749.
public Drawable icon = null;
@ -568,6 +569,7 @@ public class PromptService implements OnClickListener, OnCancelListener, OnItemC
try { inGroup = aObject.getBoolean("inGroup"); } catch(Exception ex) { }
try { disabled = aObject.getBoolean("disabled"); } catch(Exception ex) { }
try { id = aObject.getInt("id"); } catch(Exception ex) { }
try { isParent = aObject.getBoolean("isParent"); } catch(Exception ex) { }
}
public PromptListItem(String aLabel) {
@ -599,53 +601,62 @@ public class PromptService implements OnClickListener, OnCancelListener, OnItemC
}
private void maybeUpdateIcon(PromptListItem item, TextView t) {
if (item.icon == null)
if (item.icon == null && !item.isParent) {
t.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
return;
}
Drawable d = null;
Resources res = GeckoApp.mAppContext.getResources();
// Set padding inside the item.
t.setPadding(item.inGroup ? mLeftRightTextWithIconPadding + mGroupPaddingSize :
mLeftRightTextWithIconPadding,
mTopBottomTextWithIconPadding,
mLeftRightTextWithIconPadding,
mTopBottomTextWithIconPadding);
// Set the padding between the icon and the text.
t.setCompoundDrawablePadding(mIconTextPadding);
if (item.icon != null) {
// Set padding inside the item.
t.setPadding(item.inGroup ? mLeftRightTextWithIconPadding + mGroupPaddingSize :
mLeftRightTextWithIconPadding,
mTopBottomTextWithIconPadding,
mLeftRightTextWithIconPadding,
mTopBottomTextWithIconPadding);
// We want the icon to be of a specific size. Some do not
// follow this rule so we have to resize them.
Bitmap bitmap = ((BitmapDrawable) item.icon).getBitmap();
d = new BitmapDrawable(Bitmap.createScaledBitmap(bitmap, mIconSize, mIconSize, true));
}
// We want the icon to be of a specific size. Some do not
// follow this rule so we have to resize them.
Bitmap bitmap = ((BitmapDrawable) item.icon).getBitmap();
Drawable d = new BitmapDrawable(Bitmap.createScaledBitmap(bitmap, mIconSize, mIconSize, true));
Drawable moreDrawable = null;
if (item.isParent) {
moreDrawable = res.getDrawable(android.R.drawable.ic_menu_more);
}
t.setCompoundDrawablesWithIntrinsicBounds(d, null, null, null);
if (d != null || moreDrawable != null) {
t.setCompoundDrawablesWithIntrinsicBounds(d, null, moreDrawable, null);
}
}
private void maybeUpdateCheckedState(int position, PromptListItem item, ViewHolder viewHolder) {
if (item.isGroup || mSelected == null)
viewHolder.textView.setPadding((item.inGroup ? mGroupPaddingSize : viewHolder.paddingLeft),
viewHolder.paddingTop,
viewHolder.paddingRight,
viewHolder.paddingBottom);
viewHolder.textView.setEnabled(!item.disabled && !item.isGroup);
viewHolder.textView.setClickable(item.isGroup || item.disabled);
if (mSelected == null)
return;
CheckedTextView ct;
try {
ct = (CheckedTextView) viewHolder.textView;
// Apparently just using ct.setChecked(true) doesn't work, so this
// is stolen from the android source code as a way to set the checked
// state of these items
if (listView != null)
listView.setItemChecked(position, mSelected[position]);
} catch (Exception e) {
return;
}
ct.setEnabled(!item.disabled);
ct.setClickable(item.disabled);
// Apparently just using ct.setChecked(true) doesn't work, so this
// is stolen from the android source code as a way to set the checked
// state of these items
if (listView != null)
listView.setItemChecked(position, mSelected[position]);
ct.setPadding((item.inGroup ? mGroupPaddingSize : viewHolder.paddingLeft),
viewHolder.paddingTop,
viewHolder.paddingRight,
viewHolder.paddingBottom);
}
@Override

View File

@ -1381,6 +1381,7 @@ var NativeWindow = {
},
contextmenus: {
items: {}, // a list of context menu items that we may show
_nativeItemsSeparator: 0, // the index to insert native context menu items at
_contextId: 0, // id to assign to new context menu items if they are added
init: function() {
@ -1552,6 +1553,62 @@ var NativeWindow = {
else this._targetRef = null;
},
_addHTMLContextMenuItems: function cm_addContextMenuItems(aMenu, aParent) {
for (let i = 0; i < aMenu.childNodes.length; i++) {
let item = aMenu.childNodes[i];
if (!item.label || item.hasAttribute("hidden"))
continue;
let id = this._contextId++;
let menuitem = {
id: id,
isGroup: false,
callback: (function(aTarget, aX, aY) {
// If this is a menu item, show a new context menu with the submenu in it
if (item instanceof Ci.nsIDOMHTMLMenuElement) {
this.menuitems = [];
this._nativeItemsSeparator = 0;
this._addHTMLContextMenuItems(item, id);
this._innerShow(aTarget, aX, aY);
} else {
// oltherwise just click the item
item.click();
}
}).bind(this),
getValue: function(aElt) {
return {
icon: item.icon,
label: item.label,
id: id,
isGroup: false,
inGroup: false,
disabled: item.disabled,
isParent: item instanceof Ci.nsIDOMHTMLMenuElement
}
}
};
this.menuitems.splice(this._nativeItemsSeparator, 0, menuitem);
this._nativeItemsSeparator++;
}
},
_getMenuItemForId: function(aId) {
if (!this.menuitems)
return null;
for (let i = 0; i < this.menuitems.length; i++) {
if (this.menuitems[i].id == aId)
return this.menuitems[i];
}
return null;
},
// Checks if there are context menu items to show, and if it finds them
// sends a contextmenu event to content. We also send showing events to
// any html5 context menus we are about to show
_sendToContent: function(aX, aY) {
// find and store the top most element this context menu is being shown for
// use the highlighted element if possible, otherwise look for nearby clickable elements
@ -1566,27 +1623,37 @@ var NativeWindow = {
// store a weakref to the target to be used when the context menu event returns
this._target = target;
this.menuitems = {};
this.menuitems = [];
let menuitemsSet = false;
// now walk up the tree and for each node look for any context menu items that apply
let element = target;
this._nativeItemsSeparator = 0;
while (element) {
for each (let item in this.items) {
if (!this.menuitems[item.id] && item.matches(element, aX, aY)) {
this.menuitems[item.id] = item;
menuitemsSet = true;
// first check for any html5 context menus that might exist
let contextmenu = element.contextMenu;
if (contextmenu) {
// send this before we build the list to make sure the site can update the menu
contextmenu.QueryInterface(Components.interfaces.nsIHTMLMenu);
contextmenu.sendShowEvent();
this._addHTMLContextMenuItems(contextmenu, null);
}
// then check for any context menu items registered in the ui
for each (let item in this.items) {
if (!this._getMenuItemForId(item.id) && item.matches(element, aX, aY)) {
this.menuitems.push(item);
}
}
// if we reach a link or a text node, stop digging up through the node hierarchy
if (this.linkOpenableContext.matches(element) || this.textContext.matches(element))
break;
element = element.parentNode;
}
// only send the contextmenu event to content if we are planning to show a context menu (i.e. not on every long tap)
if (menuitemsSet) {
if (this.menuitems.length > 0) {
let event = target.ownerDocument.createEvent("MouseEvent");
event.initMouseEvent("contextmenu", true, true, content,
0, aX, aY, aX, aY, false, false, false, false,
@ -1602,21 +1669,26 @@ var NativeWindow = {
}
},
// Actually shows the native context menu by passing a list of context menu items to
// show to the Java.
_show: function(aEvent) {
let popupNode = this._target;
this._target = null;
if (aEvent.defaultPrevented || !popupNode) {
return;
}
this._innerShow(popupNode, aEvent.clientX, aEvent.clientY);
},
_innerShow: function(aTarget, aX, aY) {
Haptic.performSimpleAction(Haptic.LongPress);
// spin through the tree looking for a title for this context menu
let node = popupNode;
let node = aTarget;
let title ="";
while(node && !title) {
if (node.hasAttribute && node.hasAttribute("title")) {
title = node.getAttribute("title")
title = node.getAttribute("title");
} else if ((node instanceof Ci.nsIDOMHTMLAnchorElement && node.href) ||
(node instanceof Ci.nsIDOMHTMLAreaElement && node.href)) {
title = this._getLinkURL(node);
@ -1630,8 +1702,8 @@ var NativeWindow = {
// convert this.menuitems object to an array for sending to native code
let itemArray = [];
for each (let item in this.menuitems) {
itemArray.push(item.getValue(popupNode));
for (let i = 0; i < this.menuitems.length; i++) {
itemArray.push(this.menuitems[i].getValue(aTarget));
}
let msg = {
@ -1643,18 +1715,25 @@ var NativeWindow = {
};
let data = JSON.parse(sendMessageToJava(msg));
let selectedId = itemArray[data.button].id;
let selectedItem = this.menuitems[selectedId];
let selectedItem = this._getMenuItemForId(selectedId);
this.menuitems = null;
if (selectedItem && selectedItem.callback) {
while (popupNode) {
if (selectedItem.matches(popupNode, aEvent.clientX, aEvent.clientY)) {
selectedItem.callback.call(selectedItem, popupNode, aEvent.clientX, aEvent.clientY);
break;
if (selectedItem.matches) {
// for menuitems added using the native UI, pass the dom element that matched that item to the callback
while (aTarget) {
if (selectedItem.matches(aTarget, aX, aY)) {
selectedItem.callback.call(selectedItem, aTarget, aX, aY);
foundNode = true;
break;
}
aTarget = aTarget.parentNode;
}
popupNode = popupNode.parentNode;
} else {
// if this was added using the html5 context menu api, just click on the context menu item
selectedItem.callback.call(selectedItem, aTarget, aX, aY);
}
}
this.menuitems = null;
},
handleEvent: function(aEvent) {