|
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- |
|
2 * This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 package org.mozilla.gecko.toolbar; |
|
7 |
|
8 import org.mozilla.gecko.GeckoAppShell; |
|
9 import org.mozilla.gecko.GeckoEvent; |
|
10 import org.mozilla.gecko.R; |
|
11 import org.mozilla.gecko.gfx.BitmapUtils; |
|
12 import org.mozilla.gecko.util.GeckoEventListener; |
|
13 import org.mozilla.gecko.util.ThreadUtils; |
|
14 import org.mozilla.gecko.widget.GeckoPopupMenu; |
|
15 |
|
16 import org.json.JSONArray; |
|
17 import org.json.JSONException; |
|
18 import org.json.JSONObject; |
|
19 |
|
20 import android.content.Context; |
|
21 import android.content.res.Resources; |
|
22 import android.graphics.Bitmap; |
|
23 import android.graphics.drawable.BitmapDrawable; |
|
24 import android.graphics.drawable.Drawable; |
|
25 import android.util.AttributeSet; |
|
26 import android.util.Log; |
|
27 import android.view.ContextMenu; |
|
28 import android.view.Menu; |
|
29 import android.view.MenuItem; |
|
30 import android.view.View; |
|
31 import android.widget.Button; |
|
32 import android.widget.ImageButton; |
|
33 import android.widget.ImageView; |
|
34 import android.widget.LinearLayout; |
|
35 |
|
36 import java.util.UUID; |
|
37 import java.util.ArrayList; |
|
38 |
|
39 public class PageActionLayout extends LinearLayout implements GeckoEventListener, |
|
40 View.OnClickListener, |
|
41 View.OnLongClickListener { |
|
42 private final String LOGTAG = "GeckoPageActionLayout"; |
|
43 private final String MENU_BUTTON_KEY = "MENU_BUTTON_KEY"; |
|
44 private final int DEFAULT_PAGE_ACTIONS_SHOWN = 2; |
|
45 |
|
46 private ArrayList<PageAction> mPageActionList; |
|
47 private GeckoPopupMenu mPageActionsMenu; |
|
48 private Context mContext; |
|
49 private LinearLayout mLayout; |
|
50 |
|
51 // By default it's two, can be changed by calling setNumberShown(int) |
|
52 private int mMaxVisiblePageActions; |
|
53 |
|
54 public PageActionLayout(Context context, AttributeSet attrs) { |
|
55 super(context, attrs); |
|
56 mContext = context; |
|
57 mLayout = this; |
|
58 |
|
59 mPageActionList = new ArrayList<PageAction>(); |
|
60 setNumberShown(DEFAULT_PAGE_ACTIONS_SHOWN); |
|
61 refreshPageActionIcons(); |
|
62 |
|
63 registerEventListener("PageActions:Add"); |
|
64 registerEventListener("PageActions:Remove"); |
|
65 } |
|
66 |
|
67 private void setNumberShown(int count) { |
|
68 mMaxVisiblePageActions = count; |
|
69 |
|
70 for(int index = 0; index < count; index++) { |
|
71 if ((this.getChildCount() - 1) < index) { |
|
72 mLayout.addView(createImageButton()); |
|
73 } |
|
74 } |
|
75 } |
|
76 |
|
77 public void onDestroy() { |
|
78 unregisterEventListener("PageActions:Add"); |
|
79 unregisterEventListener("PageActions:Remove"); |
|
80 } |
|
81 |
|
82 protected void registerEventListener(String event) { |
|
83 GeckoAppShell.getEventDispatcher().registerEventListener(event, this); |
|
84 } |
|
85 |
|
86 protected void unregisterEventListener(String event) { |
|
87 GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this); |
|
88 } |
|
89 |
|
90 @Override |
|
91 public void handleMessage(String event, JSONObject message) { |
|
92 try { |
|
93 if (event.equals("PageActions:Add")) { |
|
94 final String id = message.getString("id"); |
|
95 final String title = message.getString("title"); |
|
96 final String imageURL = message.optString("icon"); |
|
97 final boolean mImportant = message.getBoolean("important"); |
|
98 |
|
99 addPageAction(id, title, imageURL, new OnPageActionClickListeners() { |
|
100 @Override |
|
101 public void onClick(String id) { |
|
102 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PageActions:Clicked", id)); |
|
103 } |
|
104 |
|
105 @Override |
|
106 public boolean onLongClick(String id) { |
|
107 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PageActions:LongClicked", id)); |
|
108 return true; |
|
109 } |
|
110 }, mImportant); |
|
111 } else if (event.equals("PageActions:Remove")) { |
|
112 final String id = message.getString("id"); |
|
113 |
|
114 removePageAction(id); |
|
115 } |
|
116 } catch(JSONException ex) { |
|
117 Log.e(LOGTAG, "Error deocding", ex); |
|
118 } |
|
119 } |
|
120 |
|
121 private void addPageAction(final String id, final String title, final String imageData, final OnPageActionClickListeners mOnPageActionClickListeners, boolean mImportant) { |
|
122 final PageAction pageAction = new PageAction(id, title, null, mOnPageActionClickListeners, mImportant); |
|
123 |
|
124 int insertAt = mPageActionList.size(); |
|
125 while(insertAt > 0 && mPageActionList.get(insertAt-1).isImportant()) { |
|
126 insertAt--; |
|
127 } |
|
128 mPageActionList.add(insertAt, pageAction); |
|
129 |
|
130 BitmapUtils.getDrawable(mContext, imageData, new BitmapUtils.BitmapLoader() { |
|
131 @Override |
|
132 public void onBitmapFound(final Drawable d) { |
|
133 if (mPageActionList.contains(pageAction)) { |
|
134 pageAction.setDrawable(d); |
|
135 refreshPageActionIcons(); |
|
136 } |
|
137 } |
|
138 }); |
|
139 } |
|
140 |
|
141 private void removePageAction(String id) { |
|
142 for(int i = 0; i < mPageActionList.size(); i++) { |
|
143 if (mPageActionList.get(i).getID().equals(id)) { |
|
144 mPageActionList.remove(i); |
|
145 refreshPageActionIcons(); |
|
146 return; |
|
147 } |
|
148 } |
|
149 } |
|
150 |
|
151 private ImageButton createImageButton() { |
|
152 ImageButton imageButton = new ImageButton(mContext, null, R.style.UrlBar_ImageButton_Icon); |
|
153 imageButton.setLayoutParams(new LayoutParams(mContext.getResources().getDimensionPixelSize(R.dimen.page_action_button_width), LayoutParams.MATCH_PARENT)); |
|
154 imageButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE); |
|
155 imageButton.setOnClickListener(this); |
|
156 imageButton.setOnLongClickListener(this); |
|
157 return imageButton; |
|
158 } |
|
159 |
|
160 @Override |
|
161 public void onClick(View v) { |
|
162 String buttonClickedId = (String)v.getTag(); |
|
163 if (buttonClickedId != null) { |
|
164 if (buttonClickedId.equals(MENU_BUTTON_KEY)) { |
|
165 showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1); |
|
166 } else { |
|
167 getPageActionWithId(buttonClickedId).onClick(); |
|
168 } |
|
169 } |
|
170 } |
|
171 |
|
172 @Override |
|
173 public boolean onLongClick(View v) { |
|
174 String buttonClickedId = (String)v.getTag(); |
|
175 if (buttonClickedId.equals(MENU_BUTTON_KEY)) { |
|
176 showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1); |
|
177 return true; |
|
178 } else { |
|
179 return getPageActionWithId(buttonClickedId).onLongClick(); |
|
180 } |
|
181 } |
|
182 |
|
183 private void setActionForView(final ImageButton view, final PageAction pageAction) { |
|
184 if (pageAction == null) { |
|
185 view.setTag(null); |
|
186 ThreadUtils.postToUiThread(new Runnable() { |
|
187 @Override |
|
188 public void run () { |
|
189 view.setImageDrawable(null); |
|
190 view.setVisibility(View.GONE); |
|
191 view.setContentDescription(null); |
|
192 } |
|
193 }); |
|
194 return; |
|
195 } |
|
196 |
|
197 view.setTag(pageAction.getID()); |
|
198 ThreadUtils.postToUiThread(new Runnable() { |
|
199 @Override |
|
200 public void run () { |
|
201 view.setImageDrawable(pageAction.getDrawable()); |
|
202 view.setVisibility(View.VISIBLE); |
|
203 view.setContentDescription(pageAction.getTitle()); |
|
204 } |
|
205 }); |
|
206 } |
|
207 |
|
208 private void refreshPageActionIcons() { |
|
209 final Resources resources = mContext.getResources(); |
|
210 for(int index = 0; index < this.getChildCount(); index++) { |
|
211 final ImageButton v = (ImageButton)this.getChildAt(index); |
|
212 final PageAction pageAction = getPageActionForViewAt(index); |
|
213 |
|
214 // If there are more pageactions then buttons, set the menu icon. Otherwise set the page action's icon if there is a page action. |
|
215 if (index == (this.getChildCount() - 1) && mPageActionList.size() > mMaxVisiblePageActions) { |
|
216 v.setTag(MENU_BUTTON_KEY); |
|
217 ThreadUtils.postToUiThread(new Runnable() { |
|
218 @Override |
|
219 public void run () { |
|
220 v.setImageDrawable(resources.getDrawable(R.drawable.icon_pageaction)); |
|
221 v.setVisibility((pageAction != null) ? View.VISIBLE : View.GONE); |
|
222 v.setContentDescription(resources.getString(R.string.page_action_dropmarker_description)); |
|
223 } |
|
224 }); |
|
225 } else { |
|
226 setActionForView(v, pageAction); |
|
227 } |
|
228 } |
|
229 } |
|
230 |
|
231 private PageAction getPageActionForViewAt(int index) { |
|
232 /** |
|
233 * We show the user the most recent pageaction added since this keeps the user aware of any new page actions being added |
|
234 * Also, the order of the pageAction is important i.e. if a page action is added, instead of shifting the pagactions to the |
|
235 * left to make space for the new one, it would be more visually appealing to have the pageaction appear in the blank space. |
|
236 * |
|
237 * buttonIndex is needed for this reason because every new View added to PageActionLayout gets added to the right of its neighbouring View. |
|
238 * Hence the button on the very leftmost has the index 0. We want our pageactions to start from the rightmost |
|
239 * and hence we maintain the insertion order of the child Views which is essentially the reverse of their index |
|
240 */ |
|
241 |
|
242 int buttonIndex = (this.getChildCount() - 1) - index; |
|
243 int totalVisibleButtons = ((mPageActionList.size() < this.getChildCount()) ? mPageActionList.size() : this.getChildCount()); |
|
244 |
|
245 if (mPageActionList.size() > buttonIndex) { |
|
246 // Return the pageactions starting from the end of the list for the number of visible pageactions. |
|
247 return mPageActionList.get((mPageActionList.size() - totalVisibleButtons) + buttonIndex); |
|
248 } |
|
249 return null; |
|
250 } |
|
251 |
|
252 private PageAction getPageActionWithId(String id) { |
|
253 for(int i = 0; i < mPageActionList.size(); i++) { |
|
254 PageAction pageAction = mPageActionList.get(i); |
|
255 if (pageAction.getID().equals(id)) { |
|
256 return pageAction; |
|
257 } |
|
258 } |
|
259 return null; |
|
260 } |
|
261 |
|
262 private void showMenu(View mPageActionButton, int toShow) { |
|
263 if (mPageActionsMenu == null) { |
|
264 mPageActionsMenu = new GeckoPopupMenu(mPageActionButton.getContext(), mPageActionButton); |
|
265 mPageActionsMenu.inflate(0); |
|
266 mPageActionsMenu.setOnMenuItemClickListener(new GeckoPopupMenu.OnMenuItemClickListener() { |
|
267 @Override |
|
268 public boolean onMenuItemClick(MenuItem item) { |
|
269 int id = item.getItemId(); |
|
270 for(int i = 0; i < mPageActionList.size(); i++) { |
|
271 PageAction pageAction = mPageActionList.get(i); |
|
272 if (pageAction.key() == id) { |
|
273 pageAction.onClick(); |
|
274 return true; |
|
275 } |
|
276 } |
|
277 return false; |
|
278 } |
|
279 }); |
|
280 } |
|
281 Menu menu = mPageActionsMenu.getMenu(); |
|
282 menu.clear(); |
|
283 |
|
284 for(int i = 0; i < mPageActionList.size(); i++) { |
|
285 if (i < toShow) { |
|
286 PageAction pageAction = mPageActionList.get(i); |
|
287 MenuItem item = menu.add(Menu.NONE, pageAction.key(), Menu.NONE, pageAction.getTitle()); |
|
288 item.setIcon(pageAction.getDrawable()); |
|
289 } |
|
290 } |
|
291 mPageActionsMenu.show(); |
|
292 } |
|
293 |
|
294 private static interface OnPageActionClickListeners { |
|
295 public void onClick(String id); |
|
296 public boolean onLongClick(String id); |
|
297 } |
|
298 |
|
299 private static class PageAction { |
|
300 private OnPageActionClickListeners mOnPageActionClickListeners; |
|
301 private Drawable mDrawable; |
|
302 private String mTitle; |
|
303 private String mId; |
|
304 private int key; |
|
305 private boolean mImportant; |
|
306 |
|
307 public PageAction(String id, |
|
308 String title, |
|
309 Drawable image, |
|
310 OnPageActionClickListeners onPageActionClickListeners, |
|
311 boolean important) { |
|
312 mId = id; |
|
313 mTitle = title; |
|
314 mDrawable = image; |
|
315 mOnPageActionClickListeners = onPageActionClickListeners; |
|
316 mImportant = important; |
|
317 |
|
318 key = UUID.fromString(mId.subSequence(1, mId.length() - 2).toString()).hashCode(); |
|
319 } |
|
320 |
|
321 public Drawable getDrawable() { |
|
322 return mDrawable; |
|
323 } |
|
324 |
|
325 public void setDrawable(Drawable d) { |
|
326 mDrawable = d; |
|
327 } |
|
328 |
|
329 public String getTitle() { |
|
330 return mTitle; |
|
331 } |
|
332 |
|
333 public String getID() { |
|
334 return mId; |
|
335 } |
|
336 |
|
337 public int key() { |
|
338 return key; |
|
339 } |
|
340 |
|
341 public boolean isImportant() { |
|
342 return mImportant; |
|
343 } |
|
344 |
|
345 public void onClick() { |
|
346 if (mOnPageActionClickListeners != null) { |
|
347 mOnPageActionClickListeners.onClick(mId); |
|
348 } |
|
349 } |
|
350 |
|
351 public boolean onLongClick() { |
|
352 if (mOnPageActionClickListeners != null) { |
|
353 return mOnPageActionClickListeners.onLongClick(mId); |
|
354 } |
|
355 return false; |
|
356 } |
|
357 } |
|
358 } |