michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.toolbar; michael@0: michael@0: import org.mozilla.gecko.GeckoAppShell; michael@0: import org.mozilla.gecko.GeckoEvent; michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.gfx.BitmapUtils; michael@0: import org.mozilla.gecko.util.GeckoEventListener; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: import org.mozilla.gecko.widget.GeckoPopupMenu; michael@0: michael@0: import org.json.JSONArray; michael@0: import org.json.JSONException; michael@0: import org.json.JSONObject; michael@0: michael@0: import android.content.Context; michael@0: import android.content.res.Resources; michael@0: import android.graphics.Bitmap; michael@0: import android.graphics.drawable.BitmapDrawable; michael@0: import android.graphics.drawable.Drawable; michael@0: import android.util.AttributeSet; michael@0: import android.util.Log; michael@0: import android.view.ContextMenu; michael@0: import android.view.Menu; michael@0: import android.view.MenuItem; michael@0: import android.view.View; michael@0: import android.widget.Button; michael@0: import android.widget.ImageButton; michael@0: import android.widget.ImageView; michael@0: import android.widget.LinearLayout; michael@0: michael@0: import java.util.UUID; michael@0: import java.util.ArrayList; michael@0: michael@0: public class PageActionLayout extends LinearLayout implements GeckoEventListener, michael@0: View.OnClickListener, michael@0: View.OnLongClickListener { michael@0: private final String LOGTAG = "GeckoPageActionLayout"; michael@0: private final String MENU_BUTTON_KEY = "MENU_BUTTON_KEY"; michael@0: private final int DEFAULT_PAGE_ACTIONS_SHOWN = 2; michael@0: michael@0: private ArrayList mPageActionList; michael@0: private GeckoPopupMenu mPageActionsMenu; michael@0: private Context mContext; michael@0: private LinearLayout mLayout; michael@0: michael@0: // By default it's two, can be changed by calling setNumberShown(int) michael@0: private int mMaxVisiblePageActions; michael@0: michael@0: public PageActionLayout(Context context, AttributeSet attrs) { michael@0: super(context, attrs); michael@0: mContext = context; michael@0: mLayout = this; michael@0: michael@0: mPageActionList = new ArrayList(); michael@0: setNumberShown(DEFAULT_PAGE_ACTIONS_SHOWN); michael@0: refreshPageActionIcons(); michael@0: michael@0: registerEventListener("PageActions:Add"); michael@0: registerEventListener("PageActions:Remove"); michael@0: } michael@0: michael@0: private void setNumberShown(int count) { michael@0: mMaxVisiblePageActions = count; michael@0: michael@0: for(int index = 0; index < count; index++) { michael@0: if ((this.getChildCount() - 1) < index) { michael@0: mLayout.addView(createImageButton()); michael@0: } michael@0: } michael@0: } michael@0: michael@0: public void onDestroy() { michael@0: unregisterEventListener("PageActions:Add"); michael@0: unregisterEventListener("PageActions:Remove"); michael@0: } michael@0: michael@0: protected void registerEventListener(String event) { michael@0: GeckoAppShell.getEventDispatcher().registerEventListener(event, this); michael@0: } michael@0: michael@0: protected void unregisterEventListener(String event) { michael@0: GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this); michael@0: } michael@0: michael@0: @Override michael@0: public void handleMessage(String event, JSONObject message) { michael@0: try { michael@0: if (event.equals("PageActions:Add")) { michael@0: final String id = message.getString("id"); michael@0: final String title = message.getString("title"); michael@0: final String imageURL = message.optString("icon"); michael@0: final boolean mImportant = message.getBoolean("important"); michael@0: michael@0: addPageAction(id, title, imageURL, new OnPageActionClickListeners() { michael@0: @Override michael@0: public void onClick(String id) { michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PageActions:Clicked", id)); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onLongClick(String id) { michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PageActions:LongClicked", id)); michael@0: return true; michael@0: } michael@0: }, mImportant); michael@0: } else if (event.equals("PageActions:Remove")) { michael@0: final String id = message.getString("id"); michael@0: michael@0: removePageAction(id); michael@0: } michael@0: } catch(JSONException ex) { michael@0: Log.e(LOGTAG, "Error deocding", ex); michael@0: } michael@0: } michael@0: michael@0: private void addPageAction(final String id, final String title, final String imageData, final OnPageActionClickListeners mOnPageActionClickListeners, boolean mImportant) { michael@0: final PageAction pageAction = new PageAction(id, title, null, mOnPageActionClickListeners, mImportant); michael@0: michael@0: int insertAt = mPageActionList.size(); michael@0: while(insertAt > 0 && mPageActionList.get(insertAt-1).isImportant()) { michael@0: insertAt--; michael@0: } michael@0: mPageActionList.add(insertAt, pageAction); michael@0: michael@0: BitmapUtils.getDrawable(mContext, imageData, new BitmapUtils.BitmapLoader() { michael@0: @Override michael@0: public void onBitmapFound(final Drawable d) { michael@0: if (mPageActionList.contains(pageAction)) { michael@0: pageAction.setDrawable(d); michael@0: refreshPageActionIcons(); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private void removePageAction(String id) { michael@0: for(int i = 0; i < mPageActionList.size(); i++) { michael@0: if (mPageActionList.get(i).getID().equals(id)) { michael@0: mPageActionList.remove(i); michael@0: refreshPageActionIcons(); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: michael@0: private ImageButton createImageButton() { michael@0: ImageButton imageButton = new ImageButton(mContext, null, R.style.UrlBar_ImageButton_Icon); michael@0: imageButton.setLayoutParams(new LayoutParams(mContext.getResources().getDimensionPixelSize(R.dimen.page_action_button_width), LayoutParams.MATCH_PARENT)); michael@0: imageButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE); michael@0: imageButton.setOnClickListener(this); michael@0: imageButton.setOnLongClickListener(this); michael@0: return imageButton; michael@0: } michael@0: michael@0: @Override michael@0: public void onClick(View v) { michael@0: String buttonClickedId = (String)v.getTag(); michael@0: if (buttonClickedId != null) { michael@0: if (buttonClickedId.equals(MENU_BUTTON_KEY)) { michael@0: showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1); michael@0: } else { michael@0: getPageActionWithId(buttonClickedId).onClick(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public boolean onLongClick(View v) { michael@0: String buttonClickedId = (String)v.getTag(); michael@0: if (buttonClickedId.equals(MENU_BUTTON_KEY)) { michael@0: showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1); michael@0: return true; michael@0: } else { michael@0: return getPageActionWithId(buttonClickedId).onLongClick(); michael@0: } michael@0: } michael@0: michael@0: private void setActionForView(final ImageButton view, final PageAction pageAction) { michael@0: if (pageAction == null) { michael@0: view.setTag(null); michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run () { michael@0: view.setImageDrawable(null); michael@0: view.setVisibility(View.GONE); michael@0: view.setContentDescription(null); michael@0: } michael@0: }); michael@0: return; michael@0: } michael@0: michael@0: view.setTag(pageAction.getID()); michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run () { michael@0: view.setImageDrawable(pageAction.getDrawable()); michael@0: view.setVisibility(View.VISIBLE); michael@0: view.setContentDescription(pageAction.getTitle()); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private void refreshPageActionIcons() { michael@0: final Resources resources = mContext.getResources(); michael@0: for(int index = 0; index < this.getChildCount(); index++) { michael@0: final ImageButton v = (ImageButton)this.getChildAt(index); michael@0: final PageAction pageAction = getPageActionForViewAt(index); michael@0: michael@0: // If there are more pageactions then buttons, set the menu icon. Otherwise set the page action's icon if there is a page action. michael@0: if (index == (this.getChildCount() - 1) && mPageActionList.size() > mMaxVisiblePageActions) { michael@0: v.setTag(MENU_BUTTON_KEY); michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run () { michael@0: v.setImageDrawable(resources.getDrawable(R.drawable.icon_pageaction)); michael@0: v.setVisibility((pageAction != null) ? View.VISIBLE : View.GONE); michael@0: v.setContentDescription(resources.getString(R.string.page_action_dropmarker_description)); michael@0: } michael@0: }); michael@0: } else { michael@0: setActionForView(v, pageAction); michael@0: } michael@0: } michael@0: } michael@0: michael@0: private PageAction getPageActionForViewAt(int index) { michael@0: /** michael@0: * We show the user the most recent pageaction added since this keeps the user aware of any new page actions being added michael@0: * Also, the order of the pageAction is important i.e. if a page action is added, instead of shifting the pagactions to the michael@0: * left to make space for the new one, it would be more visually appealing to have the pageaction appear in the blank space. michael@0: * michael@0: * buttonIndex is needed for this reason because every new View added to PageActionLayout gets added to the right of its neighbouring View. michael@0: * Hence the button on the very leftmost has the index 0. We want our pageactions to start from the rightmost michael@0: * and hence we maintain the insertion order of the child Views which is essentially the reverse of their index michael@0: */ michael@0: michael@0: int buttonIndex = (this.getChildCount() - 1) - index; michael@0: int totalVisibleButtons = ((mPageActionList.size() < this.getChildCount()) ? mPageActionList.size() : this.getChildCount()); michael@0: michael@0: if (mPageActionList.size() > buttonIndex) { michael@0: // Return the pageactions starting from the end of the list for the number of visible pageactions. michael@0: return mPageActionList.get((mPageActionList.size() - totalVisibleButtons) + buttonIndex); michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: private PageAction getPageActionWithId(String id) { michael@0: for(int i = 0; i < mPageActionList.size(); i++) { michael@0: PageAction pageAction = mPageActionList.get(i); michael@0: if (pageAction.getID().equals(id)) { michael@0: return pageAction; michael@0: } michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: private void showMenu(View mPageActionButton, int toShow) { michael@0: if (mPageActionsMenu == null) { michael@0: mPageActionsMenu = new GeckoPopupMenu(mPageActionButton.getContext(), mPageActionButton); michael@0: mPageActionsMenu.inflate(0); michael@0: mPageActionsMenu.setOnMenuItemClickListener(new GeckoPopupMenu.OnMenuItemClickListener() { michael@0: @Override michael@0: public boolean onMenuItemClick(MenuItem item) { michael@0: int id = item.getItemId(); michael@0: for(int i = 0; i < mPageActionList.size(); i++) { michael@0: PageAction pageAction = mPageActionList.get(i); michael@0: if (pageAction.key() == id) { michael@0: pageAction.onClick(); michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: } michael@0: }); michael@0: } michael@0: Menu menu = mPageActionsMenu.getMenu(); michael@0: menu.clear(); michael@0: michael@0: for(int i = 0; i < mPageActionList.size(); i++) { michael@0: if (i < toShow) { michael@0: PageAction pageAction = mPageActionList.get(i); michael@0: MenuItem item = menu.add(Menu.NONE, pageAction.key(), Menu.NONE, pageAction.getTitle()); michael@0: item.setIcon(pageAction.getDrawable()); michael@0: } michael@0: } michael@0: mPageActionsMenu.show(); michael@0: } michael@0: michael@0: private static interface OnPageActionClickListeners { michael@0: public void onClick(String id); michael@0: public boolean onLongClick(String id); michael@0: } michael@0: michael@0: private static class PageAction { michael@0: private OnPageActionClickListeners mOnPageActionClickListeners; michael@0: private Drawable mDrawable; michael@0: private String mTitle; michael@0: private String mId; michael@0: private int key; michael@0: private boolean mImportant; michael@0: michael@0: public PageAction(String id, michael@0: String title, michael@0: Drawable image, michael@0: OnPageActionClickListeners onPageActionClickListeners, michael@0: boolean important) { michael@0: mId = id; michael@0: mTitle = title; michael@0: mDrawable = image; michael@0: mOnPageActionClickListeners = onPageActionClickListeners; michael@0: mImportant = important; michael@0: michael@0: key = UUID.fromString(mId.subSequence(1, mId.length() - 2).toString()).hashCode(); michael@0: } michael@0: michael@0: public Drawable getDrawable() { michael@0: return mDrawable; michael@0: } michael@0: michael@0: public void setDrawable(Drawable d) { michael@0: mDrawable = d; michael@0: } michael@0: michael@0: public String getTitle() { michael@0: return mTitle; michael@0: } michael@0: michael@0: public String getID() { michael@0: return mId; michael@0: } michael@0: michael@0: public int key() { michael@0: return key; michael@0: } michael@0: michael@0: public boolean isImportant() { michael@0: return mImportant; michael@0: } michael@0: michael@0: public void onClick() { michael@0: if (mOnPageActionClickListeners != null) { michael@0: mOnPageActionClickListeners.onClick(mId); michael@0: } michael@0: } michael@0: michael@0: public boolean onLongClick() { michael@0: if (mOnPageActionClickListeners != null) { michael@0: return mOnPageActionClickListeners.onLongClick(mId); michael@0: } michael@0: return false; michael@0: } michael@0: } michael@0: }