From a46d91589e2560a105171176c6b35c8d02ef76b3 Mon Sep 17 00:00:00 2001 From: Shilpan Bhagat Date: Mon, 15 Jul 2013 17:19:05 -0700 Subject: [PATCH] Bug 734877: Implement a page actions api. r=wesj --- mobile/android/base/BrowserToolbar.java | 98 +++--- mobile/android/base/Makefile.in | 4 + mobile/android/base/PageActionLayout.java | 315 ++++++++++++++++++ mobile/android/base/menu/MenuPopup.java | 4 +- .../drawable-hdpi/icon_pageaction.png | Bin 0 -> 187 bytes .../drawable-mdpi/icon_pageaction.png | Bin 0 -> 174 bytes .../drawable-xhdpi/icon_pageaction.png | Bin 0 -> 289 bytes .../layout-large-v11/browser_toolbar.xml | 11 +- .../base/resources/layout/browser_toolbar.xml | 11 +- .../android/base/resources/values/dimens.xml | 3 + mobile/android/base/strings.xml.in | 1 - .../android/base/widget/GeckoPopupMenu.java | 9 +- mobile/android/chrome/content/browser.js | 117 ++++++- .../locales/en-US/chrome/browser.properties | 4 + 14 files changed, 506 insertions(+), 71 deletions(-) create mode 100644 mobile/android/base/PageActionLayout.java create mode 100644 mobile/android/base/resources/drawable-hdpi/icon_pageaction.png create mode 100644 mobile/android/base/resources/drawable-mdpi/icon_pageaction.png create mode 100644 mobile/android/base/resources/drawable-xhdpi/icon_pageaction.png diff --git a/mobile/android/base/BrowserToolbar.java b/mobile/android/base/BrowserToolbar.java index 940f0123e21..23d97123b6c 100644 --- a/mobile/android/base/BrowserToolbar.java +++ b/mobile/android/base/BrowserToolbar.java @@ -11,14 +11,14 @@ import org.mozilla.gecko.gfx.ImmutableViewportMetrics; import org.mozilla.gecko.gfx.LayerView; import org.mozilla.gecko.menu.GeckoMenu; import org.mozilla.gecko.menu.MenuPopup; +import org.mozilla.gecko.PageActionLayout; +import org.mozilla.gecko.PrefsHelper; import org.mozilla.gecko.util.Clipboard; import org.mozilla.gecko.util.StringUtils; import org.mozilla.gecko.util.HardwareUtils; - import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.util.UiAsyncTask; - -import org.mozilla.gecko.PrefsHelper; +import org.mozilla.gecko.util.GeckoEventListener; import org.json.JSONObject; @@ -63,14 +63,14 @@ import android.widget.RelativeLayout; import android.widget.RelativeLayout.LayoutParams; import android.widget.ViewSwitcher; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class BrowserToolbar extends GeckoRelativeLayout implements Tabs.OnTabsChangedListener, GeckoMenu.ActionItemBarPresenter, - Animation.AnimationListener { + Animation.AnimationListener, + GeckoEventListener { private static final String LOGTAG = "GeckoToolbar"; public static final String PREF_TITLEBAR_MODE = "browser.chrome.titlebarMode"; private LayoutParams mAwesomeBarParams; @@ -88,7 +88,7 @@ public class BrowserToolbar extends GeckoRelativeLayout public ImageButton mFavicon; public ImageButton mStop; public ImageButton mSiteSecurity; - public ImageButton mReader; + public PageActionLayout mPageActionLayout; private AnimationDrawable mProgressSpinner; private TabCounter mTabsCounter; private ImageView mShadow; @@ -183,6 +183,9 @@ public class BrowserToolbar extends GeckoRelativeLayout mDomainColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_domaintext)); mPrivateDomainColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_domaintext_private)); + registerEventListener("Reader:Click"); + registerEventListener("Reader:LongClick"); + mShowSiteSecurity = false; mShowReader = false; @@ -221,8 +224,8 @@ public class BrowserToolbar extends GeckoRelativeLayout mProgressSpinner = (AnimationDrawable) res.getDrawable(R.drawable.progress_spinner); mStop = (ImageButton) findViewById(R.id.stop); - mReader = (ImageButton) findViewById(R.id.reader); mShadow = (ImageView) findViewById(R.id.shadow); + mPageActionLayout = (PageActionLayout) findViewById(R.id.page_action_layout); if (Build.VERSION.SDK_INT >= 16) { mShadow.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); @@ -237,9 +240,9 @@ public class BrowserToolbar extends GeckoRelativeLayout // order appropriately. if (HardwareUtils.isTablet()) { mFocusOrder = Arrays.asList(mTabs, mBack, mForward, this, - mSiteSecurity, mReader, mStop, mActionItemBar, mMenu); + mSiteSecurity, mPageActionLayout, mStop, mActionItemBar, mMenu); } else { - mFocusOrder = Arrays.asList(this, mSiteSecurity, mReader, mStop, + mFocusOrder = Arrays.asList(this, mSiteSecurity, mPageActionLayout, mStop, mTabs, mMenu); } } @@ -353,28 +356,6 @@ public class BrowserToolbar extends GeckoRelativeLayout } }); - mReader.setOnClickListener(new Button.OnClickListener() { - @Override - public void onClick(View view) { - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) { - tab.toggleReaderMode(); - } - } - }); - - mReader.setOnLongClickListener(new Button.OnLongClickListener() { - public boolean onLongClick(View v) { - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) { - tab.addToReadingList(); - return true; - } - - return false; - } - }); - mShadow.setOnClickListener(new Button.OnClickListener() { @Override public void onClick(View v) { @@ -474,7 +455,7 @@ public class BrowserToolbar extends GeckoRelativeLayout if (showProgress && tab.getState() == Tab.STATE_LOADING) setProgressVisibility(true); setSecurityMode(tab.getSecurityMode()); - setReaderMode(tab.getReaderEnabled()); + setPageActionVisibility(mStop.getVisibility() == View.VISIBLE); } break; case STOP: @@ -519,7 +500,7 @@ public class BrowserToolbar extends GeckoRelativeLayout break; case READER_ENABLED: if (Tabs.getInstance().isSelectedTab(tab)) { - setReaderMode(tab.getReaderEnabled()); + setPageActionVisibility(mStop.getVisibility() == View.VISIBLE); } break; } @@ -537,7 +518,7 @@ public class BrowserToolbar extends GeckoRelativeLayout mFavicon.setNextFocusDownId(nextId); mStop.setNextFocusDownId(nextId); mSiteSecurity.setNextFocusDownId(nextId); - mReader.setNextFocusDownId(nextId); + mPageActionLayout.setNextFocusDownId(nextId); mMenu.setNextFocusDownId(nextId); } @@ -613,7 +594,7 @@ public class BrowserToolbar extends GeckoRelativeLayout ViewHelper.setTranslationX(mMenuIcon, curveTranslation); } - ViewHelper.setAlpha(mReader, 0); + ViewHelper.setAlpha(mPageActionLayout, 0); ViewHelper.setAlpha(mStop, 0); } @@ -661,7 +642,7 @@ public class BrowserToolbar extends GeckoRelativeLayout // Fade toolbar buttons (reader, stop) after the entry // is schrunk back to its original size. - buttonsAnimator.attach(mReader, + buttonsAnimator.attach(mPageActionLayout, PropertyAnimator.Property.ALPHA, 1); buttonsAnimator.attach(mStop, @@ -708,7 +689,7 @@ public class BrowserToolbar extends GeckoRelativeLayout setSelected(true); // Hide stop/reader buttons immediately - ViewHelper.setAlpha(mReader, 0); + ViewHelper.setAlpha(mPageActionLayout, 0); ViewHelper.setAlpha(mStop, 0); // Slide the right side elements of the toolbar @@ -827,22 +808,16 @@ public class BrowserToolbar extends GeckoRelativeLayout // Handle the viewing mode page actions setSiteSecurityVisibility(mShowSiteSecurity && !isLoading); - // Handle the readerMode image and visibility: We show the reader mode button if 1) you can - // enter reader mode for current page or 2) if you're already in reader mode, - // in which case we show the reader mode "close" (reader_active) icon. boolean inReaderMode = false; Tab tab = Tabs.getInstance().getSelectedTab(); if (tab != null) inReaderMode = ReaderModeUtils.isAboutReader(tab.getURL()); - mReader.setImageResource(inReaderMode ? R.drawable.reader_active : R.drawable.reader); - - mReader.setVisibility(!isLoading && (mShowReader || inReaderMode) ? View.VISIBLE : View.GONE); + mPageActionLayout.setVisibility(!isLoading ? View.VISIBLE : View.GONE); // We want title to fill the whole space available for it when there are icons // being shown on the right side of the toolbar as the icons already have some // padding in them. This is just to avoid wasting space when icons are shown. mTitle.setPadding(0, 0, (!isLoading && !(mShowReader || inReaderMode) ? mTitlePadding : 0), 0); - updateFocusOrder(); } @@ -926,7 +901,7 @@ public class BrowserToolbar extends GeckoRelativeLayout String url = tab.getURL(); // Only set shadow to visible when not on about screens except about:blank. - visible &= !(url == null || (url.startsWith("about:") && + visible &= !(url == null || (url.startsWith("about:") && !url.equals("about:blank"))); if ((mShadow.getVisibility() == View.VISIBLE) != visible) { @@ -997,11 +972,6 @@ public class BrowserToolbar extends GeckoRelativeLayout setPageActionVisibility(mStop.getVisibility() == View.VISIBLE); } - private void setReaderMode(boolean showReader) { - mShowReader = showReader; - setPageActionVisibility(mStop.getVisibility() == View.VISIBLE); - } - public void prepareTabsAnimation(PropertyAnimator animator, boolean tabsAreShown) { if (!tabsAreShown) { PropertyAnimator buttonsAnimator = @@ -1162,7 +1132,7 @@ public class BrowserToolbar extends GeckoRelativeLayout setFavicon(tab.getFavicon()); setProgressVisibility(tab.getState() == Tab.STATE_LOADING); setSecurityMode(tab.getSecurityMode()); - setReaderMode(tab.getReaderEnabled()); + setPageActionVisibility(mStop.getVisibility() == View.VISIBLE); setShadowVisibility(true); updateBackButton(tab.canDoBack()); updateForwardButton(tab.canDoForward()); @@ -1189,6 +1159,9 @@ public class BrowserToolbar extends GeckoRelativeLayout mPrefObserverId = null; } Tabs.unregisterOnTabsChangedListener(this); + + unregisterEventListener("Reader:Click"); + unregisterEventListener("Reader:LongClick"); } public boolean openOptionsMenu() { @@ -1225,4 +1198,27 @@ public class BrowserToolbar extends GeckoRelativeLayout return true; } + + protected void registerEventListener(String event) { + GeckoAppShell.getEventDispatcher().registerEventListener(event, this); + } + + protected void unregisterEventListener(String event) { + GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this); + } + + @Override + public void handleMessage(String event, JSONObject message) { + if (event.equals("Reader:Click")) { + Tab tab = Tabs.getInstance().getSelectedTab(); + if (tab != null) { + tab.toggleReaderMode(); + } + } else if (event.equals("Reader:LongClick")) { + Tab tab = Tabs.getInstance().getSelectedTab(); + if (tab != null) { + tab.addToReadingList(); + } + } + } } diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in index 05de17641c7..d575b02da9b 100644 --- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -129,6 +129,7 @@ FENNEC_JAVA_FILES = \ NotificationService.java \ NSSBridge.java \ OrderedBroadcastHelper.java \ + PageActionLayout.java \ PrefsHelper.java \ PrivateDataPreference.java \ PrivateTab.java \ @@ -641,6 +642,7 @@ RES_DRAWABLE_MDPI = \ res/drawable-mdpi/ic_menu_new_tab.png \ res/drawable-mdpi/ic_menu_reload.png \ res/drawable-mdpi/ic_status_logo.png \ + res/drawable-mdpi/icon_pageaction.png \ res/drawable-mdpi/progress_spinner_1.png \ res/drawable-mdpi/progress_spinner_2.png \ res/drawable-mdpi/progress_spinner_3.png \ @@ -763,6 +765,7 @@ RES_DRAWABLE_HDPI = \ res/drawable-hdpi/ic_menu_new_tab.png \ res/drawable-hdpi/ic_menu_reload.png \ res/drawable-hdpi/ic_status_logo.png \ + res/drawable-hdpi/icon_pageaction.png \ res/drawable-hdpi/tab_indicator_divider.9.png \ res/drawable-hdpi/tab_indicator_selected.9.png \ res/drawable-hdpi/tab_indicator_selected_focused.9.png \ @@ -861,6 +864,7 @@ RES_DRAWABLE_XHDPI = \ res/drawable-xhdpi/ic_menu_new_tab.png \ res/drawable-xhdpi/ic_menu_reload.png \ res/drawable-xhdpi/ic_status_logo.png \ + res/drawable-xhdpi/icon_pageaction.png \ res/drawable-xhdpi/spinner_default.9.png \ res/drawable-xhdpi/spinner_focused.9.png \ res/drawable-xhdpi/spinner_pressed.9.png \ diff --git a/mobile/android/base/PageActionLayout.java b/mobile/android/base/PageActionLayout.java new file mode 100644 index 00000000000..d1dff5c9d74 --- /dev/null +++ b/mobile/android/base/PageActionLayout.java @@ -0,0 +1,315 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import org.mozilla.gecko.gfx.BitmapUtils; +import org.mozilla.gecko.util.GeckoEventListener; +import org.mozilla.gecko.util.ThreadUtils; +import org.mozilla.gecko.widget.GeckoPopupMenu; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import java.util.UUID; +import java.util.LinkedHashMap; + +public class PageActionLayout extends LinearLayout implements GeckoEventListener, + View.OnClickListener, + View.OnLongClickListener { + private final String LOGTAG = "GeckoPageActionLayout"; + private final String MENU_BUTTON_KEY = "MENU_BUTTON_KEY"; + private final int DEFAULT_PAGE_ACTIONS_SHOWN = 2; + + private LinkedHashMap mPageActionList; + private GeckoPopupMenu mPageActionsMenu; + private Context mContext; + private LinearLayout mLayout; + + // By default it's two, can be changed by calling setNumberShown(int) + private int mMaxVisiblePageActions; + + public PageActionLayout(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mLayout = this; + + mPageActionList = new LinkedHashMap(); + setNumberShown(DEFAULT_PAGE_ACTIONS_SHOWN); + + registerEventListener("PageActions:Add"); + registerEventListener("PageActions:Remove"); + } + + public void setNumberShown(int count) { + mMaxVisiblePageActions = count; + + for(int index = 0; index < count; index++) { + if ((this.getChildCount() - 1) < index) { + mLayout.addView(createImageButton()); + } + } + } + + public void onDestroy() { + unregisterEventListener("PageActions:Add"); + unregisterEventListener("PageActions:Remove"); + } + + protected void registerEventListener(String event) { + GeckoAppShell.getEventDispatcher().registerEventListener(event, this); + } + + protected void unregisterEventListener(String event) { + GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this); + } + + @Override + public void handleMessage(String event, JSONObject message) { + try { + if (event.equals("PageActions:Add")) { + final String id = message.getString("id"); + final String title = message.getString("title"); + final String imageURL = message.optString("icon"); + + addPageAction(id, title, imageURL, new OnPageActionClickListeners() { + @Override + public void onClick(String id) { + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PageActions:Clicked", id)); + } + + @Override + public boolean onLongClick(String id) { + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PageActions:LongClicked", id)); + return true; + } + }); + } else if (event.equals("PageActions:Remove")) { + final String id = message.getString("id"); + + removePageAction(id); + } + } catch(JSONException ex) { + Log.e(LOGTAG, "Error deocding", ex); + } + } + + public void addPageAction(final String id, final String title, final String imageData, final OnPageActionClickListeners mOnPageActionClickListeners) { + final PageAction pageAction = new PageAction(id, title, null, mOnPageActionClickListeners); + mPageActionList.put(id, pageAction); + + BitmapUtils.getDrawable(mContext, imageData, new BitmapUtils.BitmapLoader() { + @Override + public void onBitmapFound(final Drawable d) { + if (mPageActionList.containsKey(id)) { + pageAction.setDrawable(d); + refreshPageActionIcons(); + } + } + }); + } + + public void removePageAction(String id) { + mPageActionList.remove(id); + refreshPageActionIcons(); + } + + private ImageButton createImageButton() { + ImageButton imageButton = new ImageButton(mContext, null, R.style.AddressBar_ImageButton_Icon); + imageButton.setLayoutParams(new LayoutParams(mContext.getResources().getDimensionPixelSize(R.dimen.page_action_button_width), LayoutParams.MATCH_PARENT)); + imageButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + imageButton.setOnClickListener(this); + imageButton.setOnLongClickListener(this); + return imageButton; + } + + @Override + public void onClick(View v) { + String buttonClickedId = (String)v.getTag(); + if (buttonClickedId != null) { + if (buttonClickedId.equals(MENU_BUTTON_KEY)) { + showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1); + } else { + mPageActionList.get(buttonClickedId).onClick(); + } + } + } + + @Override + public boolean onLongClick(View v) { + String buttonClickedId = (String)v.getTag(); + if (buttonClickedId.equals(MENU_BUTTON_KEY)) { + showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1); + return true; + } else { + return mPageActionList.get(buttonClickedId).onLongClick(); + } + } + + private void refreshPageActionIcons() { + final Resources resources = mContext.getResources(); + for(int index = 0; index < this.getChildCount(); index++) { + final ImageButton v = (ImageButton)this.getChildAt(index); + final PageAction pageAction = getPageActionForViewAt(index); + + if (index == (this.getChildCount() - 1)) { + String id = (pageAction != null) ? pageAction.getID() : null; + v.setTag((mPageActionList.size() > mMaxVisiblePageActions) ? MENU_BUTTON_KEY : id); + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run () { + // If there are more pageactions then buttons, set the menu icon. Otherwise set the page action's icon if there is a page action. + Drawable d = (pageAction != null) ? pageAction.getDrawable() : null; + v.setImageDrawable((mPageActionList.size() > mMaxVisiblePageActions) ? resources.getDrawable(R.drawable.icon_pageaction) : d); + v.setVisibility((pageAction != null) ? View.VISIBLE : View.GONE); + } + }); + } else { + v.setTag((pageAction != null) ? pageAction.getID() : null); + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run () { + v.setImageDrawable((pageAction != null) ? pageAction.getDrawable() : null); + v.setVisibility((pageAction != null) ? View.VISIBLE : View.GONE); + } + }); + } + } + } + + private PageAction getPageActionForViewAt(int index) { + /** + * We show the user the most recent pageaction added since this keeps the user aware of any new page actions being added + * Also, the order of the pageAction is important i.e. if a page action is added, instead of shifting the pagactions to the + * left to make space for the new one, it would be more visually appealing to have the pageaction appear in the blank space. + * + * buttonIndex is needed for this reason because every new View added to PageActionLayout gets added to the right of its neighbouring View. + * Hence the button on the very leftmost has the index 0. We want our pageactions to start from the rightmost + * and hence we maintain the insertion order of the child Views which is essentially the reverse of their index + */ + + int buttonIndex = (this.getChildCount() - 1) - index; + int totalVisibleButtons = ((mPageActionList.size() < this.getChildCount()) ? mPageActionList.size() : this.getChildCount()); + + if (mPageActionList.size() > buttonIndex) { + // Return the pageactions starting from the end of the list for the number of visible pageactions. + return getPageActionAt((mPageActionList.size() - totalVisibleButtons) + buttonIndex); + } + return null; + } + + private PageAction getPageActionAt(int index) { + int count = 0; + for(PageAction pageAction : mPageActionList.values()) { + if (count == index) { + return pageAction; + } + count++; + } + return null; + } + + private void showMenu(View mPageActionButton, int toShow) { + if (mPageActionsMenu == null) { + mPageActionsMenu = new GeckoPopupMenu(mPageActionButton.getContext(), mPageActionButton); + mPageActionsMenu.inflate(0); + mPageActionsMenu.setOnMenuItemClickListener(new GeckoPopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + for(PageAction pageAction : mPageActionList.values()) { + if (pageAction.key() == item.getItemId()) { + pageAction.onClick(); + } + } + return true; + } + }); + } + Menu menu = mPageActionsMenu.getMenu(); + menu.clear(); + + int count = 0; + for(PageAction pageAction : mPageActionList.values()) { + if (count < toShow) { + MenuItem item = menu.add(Menu.NONE, pageAction.key(), Menu.NONE, pageAction.getTitle()); + item.setIcon(pageAction.getDrawable()); + } + count++; + } + mPageActionsMenu.show(); + } + + public static interface OnPageActionClickListeners { + public void onClick(String id); + public boolean onLongClick(String id); + } + + private static class PageAction { + private OnPageActionClickListeners mOnPageActionClickListeners; + private Drawable mDrawable; + private String mTitle; + private String mId; + private int key; + + public PageAction(String id, String title, Drawable image, OnPageActionClickListeners mOnPageActionClickListeners) { + this.mId = id; + this.mTitle = title; + this.mDrawable = image; + this.mOnPageActionClickListeners = mOnPageActionClickListeners; + + this.key = UUID.fromString(mId.subSequence(1, mId.length() - 2).toString()).hashCode(); + } + + public Drawable getDrawable() { + return mDrawable; + } + + public void setDrawable(Drawable d) { + this.mDrawable = d; + } + + public String getTitle() { + return mTitle; + } + + public String getID() { + return mId; + } + + public int key() { + return key; + } + + public void onClick() { + if (mOnPageActionClickListeners != null) { + mOnPageActionClickListeners.onClick(mId); + } + } + + public boolean onLongClick() { + if (mOnPageActionClickListeners != null) { + return mOnPageActionClickListeners.onLongClick(mId); + } + return false; + } + } +} \ No newline at end of file diff --git a/mobile/android/base/menu/MenuPopup.java b/mobile/android/base/menu/MenuPopup.java index dba20007611..eb5ed684f27 100644 --- a/mobile/android/base/menu/MenuPopup.java +++ b/mobile/android/base/menu/MenuPopup.java @@ -98,14 +98,14 @@ public class MenuPopup extends PopupWindow { int screenWidth = mResources.getDisplayMetrics().widthPixels; int arrowWidth = mResources.getDimensionPixelSize(R.dimen.menu_popup_arrow_width); int arrowOffset = (anchor.getWidth() - arrowWidth)/2; - + if (anchorLocation[0] + mPopupWidth <= screenWidth) { // left align ((LayoutParams) mArrowTop.getLayoutParams()).rightMargin = mPopupWidth - anchor.getWidth() + arrowOffset; ((LayoutParams) mArrowBottom.getLayoutParams()).rightMargin = mPopupWidth - anchor.getWidth() + arrowOffset; } else { // right align - ((LayoutParams) mArrowTop.getLayoutParams()).rightMargin = mArrowMargin; + ((LayoutParams) mArrowTop.getLayoutParams()).rightMargin = screenWidth - anchorLocation[0] - anchor.getWidth()/2 - arrowWidth/2; ((LayoutParams) mArrowBottom.getLayoutParams()).rightMargin = mArrowMargin; } diff --git a/mobile/android/base/resources/drawable-hdpi/icon_pageaction.png b/mobile/android/base/resources/drawable-hdpi/icon_pageaction.png new file mode 100644 index 0000000000000000000000000000000000000000..1a28b9e6629fff585c8d51eb30bd83447a0e514f GIT binary patch literal 187 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3O`a}}Ar-gYUa{tCaNu#h7}@&c z_UzW)*+S+^I(q-_Jz}D$(BWg$te}}!+r@sK@qoHy&W3|*?d_{~y)+28=Jmjk;g0tM zrrRPfdQ^pGe2DE>HX|chbDw}#`_4%P@*(`jb$b>_KHIW^f1i*Pd)l8p3oV~LI@IHT l!{LSVx2tT@IUo8TaLbl2wpe&_jvvr944$rjF6*2UngHcPNkjku literal 0 HcmV?d00001 diff --git a/mobile/android/base/resources/drawable-mdpi/icon_pageaction.png b/mobile/android/base/resources/drawable-mdpi/icon_pageaction.png new file mode 100644 index 0000000000000000000000000000000000000000..badb89012f364fc13daf2471eda784a99aa3f1d7 GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE;Asi%u$NX4zUmp5`Y7zngJJmSDL zeanVIKK2W7){1kJmPT7NT%L3OkweoAnH#D{!)5<`=n8iEvRcMRFXC!%o3O?uL9TmN zjvIP8vm}(0Y+2Nencpd!HB4i%+_mS3nuddC)0sp6I?g5l8T*9HA`Vt<|D);2rhj>x W*`C}FCw2fGz~JfX=d#Wzp$PzGTR~)y=KVm7$|Y<<99W# z=95aP0jC}a-#wV{k++5^{y>I-*5aN`n%zbcEk0(EZ;EVQz0eVQSG~^K=5T$D|NeCA zq#n-_79kIXMlf-);cS?-i+ReF-HQU4D;$pnr}HLUSKhFgktg6j^SX8y<{uiZZCyJU zxvn&9Gm+AM;8wQb1!JtFNpHjK8|E(ZFHSA|$-hJLl+s7`6448vxbHAMo%n-&S69Yg z#=KN^&l-nt&bRj;n3Z)L;V%ij@R9Xh!zqt{>?Kz(yzJckKx@)}=2+e>&e0+NKeYXK jWa3nr;K0P`WATAc_9|apzD74Q(9aB>u6{1-oD!M<>`Qbt literal 0 HcmV?d00001 diff --git a/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml b/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml index b0fcd50b31d..dba52d77d56 100644 --- a/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml +++ b/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml @@ -103,11 +103,12 @@ android:layout_gravity="center_vertical" gecko:autoUpdateTheme="false"/> - + - + 6dp -13dip 32dp + + + 32dp 8dp diff --git a/mobile/android/base/strings.xml.in b/mobile/android/base/strings.xml.in index 2bfe15d2bc8..efa1ec131cf 100644 --- a/mobile/android/base/strings.xml.in +++ b/mobile/android/base/strings.xml.in @@ -193,7 +193,6 @@ &reading_list_removed; &reading_list_failed; &reading_list_duplicate; - &reader; &contextmenu_open_new_tab; &contextmenu_open_private_tab; diff --git a/mobile/android/base/widget/GeckoPopupMenu.java b/mobile/android/base/widget/GeckoPopupMenu.java index 9814ec455e8..a6f2e6d8a32 100644 --- a/mobile/android/base/widget/GeckoPopupMenu.java +++ b/mobile/android/base/widget/GeckoPopupMenu.java @@ -17,10 +17,10 @@ import android.view.MenuItem; import android.view.View; /** - * A PopupMenu that uses the custom GeckoMenu. This menu is + * A PopupMenu that uses the custom GeckoMenu. This menu is * usually tied to an anchor, and show as a dropdrown from the anchor. */ -public class GeckoPopupMenu implements GeckoMenu.Callback, +public class GeckoPopupMenu implements GeckoMenu.Callback, GeckoMenu.MenuPresenter { // An interface for listeners for dismissal. @@ -92,8 +92,9 @@ public class GeckoPopupMenu implements GeckoMenu.Callback, * @param menuRes The menu resource to be inflated. */ public void inflate(int menuRes) { - mMenuInflater.inflate(menuRes, mMenu); - + if (menuRes > 0) { + mMenuInflater.inflate(menuRes, mMenu); + } mMenuPanel.addView(mMenu); mMenuPopup.setPanelView(mMenuPanel); } diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 72bc943ff13..626c82cedce 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -731,7 +731,7 @@ var BrowserApp = { if (tabs[i].browser.contentWindow == aWindow) return tabs[i]; } - return null; + return null; }, getBrowserForWindow: function getBrowserForWindow(aWindow) { @@ -1542,6 +1542,8 @@ var BrowserApp = { var NativeWindow = { init: function() { Services.obs.addObserver(this, "Menu:Clicked", false); + Services.obs.addObserver(this, "PageActions:Clicked", false); + Services.obs.addObserver(this, "PageActions:LongClicked", false); Services.obs.addObserver(this, "Doorhanger:Reply", false); Services.obs.addObserver(this, "Toast:Click", false); Services.obs.addObserver(this, "Toast:Hidden", false); @@ -1550,6 +1552,8 @@ var NativeWindow = { uninit: function() { Services.obs.removeObserver(this, "Menu:Clicked"); + Services.obs.removeObserver(this, "PageActions:Clicked"); + Services.obs.removeObserver(this, "PageActions:LongClicked"); Services.obs.removeObserver(this, "Doorhanger:Reply"); Services.obs.removeObserver(this, "Toast:Click", false); Services.obs.removeObserver(this, "Toast:Hidden", false); @@ -1595,6 +1599,35 @@ var NativeWindow = { } }, + pageactions: { + _items: { }, + add: function(aOptions) { + let id = uuidgen.generateUUID().toString(); + sendMessageToJava({ + gecko: { + type: "PageActions:Add", + id: id, + title: aOptions.title, + icon: aOptions.icon, + } + }); + this._items[id] = { + clickCallback: aOptions.clickCallback, + longClickCallback: aOptions.longClickCallback + }; + return id; + }, + remove: function(id) { + sendMessageToJava({ + gecko: { + type: "PageActions:Remove", + id: id + } + }); + delete this._items[id]; + } + }, + menu: { _callbacks: [], _menuId: 1, @@ -1700,6 +1733,12 @@ var NativeWindow = { } else if (aTopic == "Toast:Hidden") { if (this.toast._callbacks[aData]) delete this.toast._callbacks[aData]; + } else if (aTopic == "PageActions:Clicked") { + if (this.pageactions._items[aData].clickCallback) + this.pageactions._items[aData].clickCallback(); + } else if (aTopic == "PageActions:LongClicked") { + if (this.pageactions._items[aData].longClickCallback) + this.pageactions._items[aData].longClickCallback(); } else if (aTopic == "Doorhanger:Reply") { let data = JSON.parse(aData); let reply_id = data["callback"]; @@ -2498,6 +2537,8 @@ function Tab(aURL, aParams) { this._fixedMarginTop = 0; this._fixedMarginRight = 0; this._fixedMarginBottom = 0; + this._readerEnabled = false; + this._readerActive = false; this.userScrollPos = { x: 0, y: 0 }; this.viewportExcludesHorizontalMargins = true; this.viewportExcludesVerticalMargins = true; @@ -2792,6 +2833,7 @@ Tab.prototype = { this.browser.setAttribute("type", "content-primary"); this.browser.focus(); this.browser.docShellIsActive = true; + Reader.updatePageAction(this); } else { this.browser.setAttribute("type", "content-targetable"); this.browser.docShellIsActive = false; @@ -3484,9 +3526,12 @@ Tab.prototype = { if (article == null || (article.url != tabURL)) { // Don't clear the article for about:reader pages since we want to // use the article from the previous page - if (!tabURL.startsWith("about:reader")) + if (!tabURL.startsWith("about:reader")) { this.savedArticle = null; - + this.readerEnabled = false; + } else { + this.readerActive = true; + } return; } @@ -3496,6 +3541,12 @@ Tab.prototype = { type: "Content:ReaderEnabled", tabID: this.id }); + + if(this.readerActive) + this.readerActive = false; + + if(!this.readerEnabled) + this.readerEnabled = true; }.bind(this)); } } @@ -3951,6 +4002,26 @@ Tab.prototype = { } }, + set readerEnabled(isReaderEnabled) { + this._readerEnabled = isReaderEnabled; + if (this.getActive()) + Reader.updatePageAction(this); + }, + + get readerEnabled() { + return this._readerEnabled; + }, + + set readerActive(isReaderActive) { + this._readerActive = isReaderActive; + if (this.getActive()) + Reader.updatePageAction(this); + }, + + get readerActive() { + return this._readerActive; + }, + // nsIBrowserTab get window() { if (!this.browser) @@ -6889,6 +6960,46 @@ let Reader = { Services.prefs.addObserver("reader.parse-on-load.", this, false); }, + pageAction: { + readerModeCallback: function(){ + sendMessageToJava({ + gecko: { + type: "Reader:Click", + } + }); + }, + + readerModeActiveCallback: function(){ + sendMessageToJava({ + gecko: { + type: "Reader:LongClick", + } + }); + }, + }, + + updatePageAction: function(tab) { + if(this.pageAction.id) { + NativeWindow.pageactions.remove(this.pageAction.id); + delete this.pageAction.id; + } + + if (tab.readerActive) { + this.pageAction.id = NativeWindow.pageactions.add({ + title: Strings.browser.GetStringFromName("readerMode.exit"), + icon: "drawable://reader_active", + clickCallback: this.pageAction.readerModeCallback + }); + } else if (tab.readerEnabled) { + this.pageAction.id = NativeWindow.pageactions.add({ + title: Strings.browser.GetStringFromName("readerMode.enter"), + icon: "drawable://reader", + clickCallback:this.pageAction.readerModeCallback, + longClickCallback: this.pageAction.readerModeActiveCallback + }); + } + }, + observe: function(aMessage, aTopic, aData) { switch(aTopic) { case "Reader:Add": { diff --git a/mobile/android/locales/en-US/chrome/browser.properties b/mobile/android/locales/en-US/chrome/browser.properties index d2cd594054a..ad2592b0c46 100644 --- a/mobile/android/locales/en-US/chrome/browser.properties +++ b/mobile/android/locales/en-US/chrome/browser.properties @@ -268,3 +268,7 @@ getUserMedia.audioDevice.prompt = Microphone to use getUserMedia.sharingCamera.message2 = Camera is on getUserMedia.sharingMicrophone.message2 = Microphone is on getUserMedia.sharingCameraAndMicrophone.message2 = Camera and microphone are on + +#Reader mode +readerMode.enter = Enter Reader Mode +readerMode.exit = Exit Reader Mode