Bug 817716 - (Part 1) Add 3-dot menu to tabs panel, including a "close all tabs" option. r=bnicholson

This commit is contained in:
Margaret Leibovic 2014-06-19 08:37:32 -04:00
parent c2214fcc07
commit df30eb4d18
7 changed files with 210 additions and 14 deletions

View File

@ -256,6 +256,7 @@ size. -->
<!ENTITY new_tab "New Tab">
<!ENTITY new_private_tab "New Private Tab">
<!ENTITY close_all_tabs "Close All Tabs">
<!ENTITY close_private_tabs "Close Private Tabs">
<!ENTITY tabs_normal "Tabs">
<!ENTITY tabs_private "Private">
<!ENTITY tabs_synced "Synced">

View File

@ -25,4 +25,13 @@
android:contentDescription="@string/new_tab"
android:background="@drawable/action_bar_button_inverse"/>
<ImageButton android:id="@+id/menu"
style="@style/UrlBar.ImageButton"
android:layout_width="@dimen/browser_toolbar_height"
android:layout_height="@dimen/browser_toolbar_height"
android:padding="@dimen/browser_toolbar_button_padding"
android:src="@drawable/menu"
android:contentDescription="@string/menu"
android:background="@drawable/action_bar_button"/>
</merge>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/new_tab"
android:title="@string/new_tab"/>
<item android:id="@+id/new_private_tab"
android:title="@string/new_private_tab"/>
<item android:id="@+id/close_all_tabs"
android:title="@string/close_all_tabs"/>
<item android:id="@+id/close_private_tabs"
android:title="@string/close_private_tabs"/>
</menu>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/new_tab"
android:title="@string/new_tab"/>
<item android:id="@+id/new_private_tab"
android:title="@string/new_private_tab"/>
<item android:id="@+id/close_all_tabs"
android:title="@string/close_all_tabs"/>
<item android:id="@+id/close_private_tabs"
android:title="@string/close_private_tabs"/>
</menu>

View File

@ -246,6 +246,7 @@
<string name="new_tab">&new_tab;</string>
<string name="new_private_tab">&new_private_tab;</string>
<string name="close_all_tabs">&close_all_tabs;</string>
<string name="close_private_tabs">&close_private_tabs;</string>
<string name="tabs_normal">&tabs_normal;</string>
<string name="tabs_private">&tabs_private;</string>
<string name="tabs_synced">&tabs_synced;</string>

View File

@ -15,6 +15,7 @@ import org.mozilla.gecko.LightweightThemeDrawable;
import org.mozilla.gecko.R;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.widget.GeckoPopupMenu;
import org.mozilla.gecko.widget.IconTabWidget;
import android.content.Context;
@ -23,7 +24,10 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;
@ -32,7 +36,8 @@ import android.widget.LinearLayout;
import android.widget.RelativeLayout;
public class TabsPanel extends LinearLayout
implements LightweightTheme.OnChangeListener,
implements GeckoPopupMenu.OnMenuItemClickListener,
LightweightTheme.OnChangeListener,
IconTabWidget.OnTabChangedListener {
@SuppressWarnings("unused")
private static final String LOGTAG = "Gecko" + TabsPanel.class.getSimpleName();
@ -50,6 +55,10 @@ public class TabsPanel extends LinearLayout
public boolean shouldExpand();
}
public static interface CloseAllPanelView {
public void closeAll();
}
public static interface TabsLayoutChangeListener {
public void onTabsLayoutChange(int width, int height);
}
@ -68,6 +77,7 @@ public class TabsPanel extends LinearLayout
private AppStateListener mAppStateListener;
private IconTabWidget mTabWidget;
private static ImageButton mMenuButton;
private static ImageButton mAddTab;
private Panel mCurrentPanel;
@ -75,6 +85,8 @@ public class TabsPanel extends LinearLayout
private boolean mVisible;
private boolean mHeaderVisible;
private GeckoPopupMenu mPopupMenu;
public TabsPanel(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
@ -91,6 +103,10 @@ public class TabsPanel extends LinearLayout
mIsSideBar = false;
mPopupMenu = new GeckoPopupMenu(context);
mPopupMenu.inflate(R.menu.tabs_menu);
mPopupMenu.setOnMenuItemClickListener(this);
LayoutInflater.from(context).inflate(R.layout.tabs_panel, this);
initialize();
@ -149,6 +165,19 @@ public class TabsPanel extends LinearLayout
}
mTabWidget.setTabSelectionListener(this);
mMenuButton = (ImageButton) findViewById(R.id.menu);
mMenuButton.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View view) {
final Menu menu = mPopupMenu.getMenu();
menu.findItem(R.id.close_all_tabs).setVisible(mCurrentPanel == Panel.NORMAL_TABS);
menu.findItem(R.id.close_private_tabs).setVisible(mCurrentPanel == Panel.PRIVATE_TABS);
mPopupMenu.show();
}
});
mPopupMenu.setAnchor(mMenuButton);
}
public void addTab() {
@ -172,6 +201,37 @@ public class TabsPanel extends LinearLayout
}
}
@Override
public boolean onMenuItemClick(MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.close_all_tabs) {
if (mCurrentPanel == Panel.NORMAL_TABS) {
// Disable the menu button so that the menu won't interfere with the tab close animation.
mMenuButton.setEnabled(false);
((CloseAllPanelView) mPanelNormal).closeAll();
} else {
Log.e(LOGTAG, "Close all tabs menu item should only be visible for normal tabs panel");
}
return true;
}
if (itemId == R.id.close_private_tabs) {
if (mCurrentPanel == Panel.PRIVATE_TABS) {
((CloseAllPanelView) mPanelPrivate).closeAll();
} else {
Log.e(LOGTAG, "Close private tabs menu item should only be visible for private tabs panel");
}
return true;
}
if (itemId == R.id.new_tab || itemId == R.id.new_private_tab) {
hide();
}
return mActivity.onOptionsItemSelected(item);
}
private static int getTabContainerHeight(TabsListContainer listContainer) {
Resources resources = listContainer.getContext().getResources();
@ -350,12 +410,17 @@ public class TabsPanel extends LinearLayout
mFooter.setVisibility(View.GONE);
mAddTab.setVisibility(View.INVISIBLE);
mMenuButton.setVisibility(View.INVISIBLE);
} else {
if (mFooter != null)
mFooter.setVisibility(View.VISIBLE);
mAddTab.setVisibility(View.VISIBLE);
mAddTab.setImageLevel(index);
mMenuButton.setVisibility(View.VISIBLE);
mMenuButton.setEnabled(true);
}
if (isSideBar()) {
@ -374,6 +439,7 @@ public class TabsPanel extends LinearLayout
if (mVisible) {
mVisible = false;
mPopupMenu.dismiss();
dispatchLayoutChange(0, 0);
}
}

View File

@ -17,6 +17,7 @@ import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.PropertyAnimator.Property;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.widget.TwoWayView;
import org.mozilla.gecko.widget.TabThumbnailWrapper;
@ -38,38 +39,44 @@ import android.widget.ImageView;
import android.widget.TextView;
class TabsTray extends TwoWayView
implements TabsPanel.PanelView {
implements TabsPanel.PanelView,
TabsPanel.CloseAllPanelView {
private static final String LOGTAG = "Gecko" + TabsTray.class.getSimpleName();
private Context mContext;
private TabsPanel mTabsPanel;
final private boolean mIsPrivate;
private TabsAdapter mTabsAdapter;
private List<View> mPendingClosedTabs;
private int mCloseAnimationCount;
private int mCloseAnimationCount = 0;
private int mCloseAllAnimationCount = 0;
private TabSwipeGestureListener mSwipeListener;
// Time to animate non-flinged tabs of screen, in milliseconds
private static final int ANIMATION_DURATION = 250;
// Time between starting successive tab animations in closeAllTabs.
private static final int ANIMATION_CASCADE_DELAY = 75;
private int mOriginalSize = 0;
public TabsTray(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
mCloseAnimationCount = 0;
mPendingClosedTabs = new ArrayList<View>();
setItemsCanFocus(true);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsTray);
boolean isPrivate = (a.getInt(R.styleable.TabsTray_tabs, 0x0) == 1);
mIsPrivate = (a.getInt(R.styleable.TabsTray_tabs, 0x0) == 1);
a.recycle();
mTabsAdapter = new TabsAdapter(mContext, isPrivate);
mTabsAdapter = new TabsAdapter(mContext);
setAdapter(mTabsAdapter);
mSwipeListener = new TabSwipeGestureListener();
@ -137,15 +144,13 @@ class TabsTray extends TwoWayView
// Adapter to bind tabs into a list
private class TabsAdapter extends BaseAdapter implements Tabs.OnTabsChangedListener {
private Context mContext;
private boolean mIsPrivate;
private ArrayList<Tab> mTabs;
private LayoutInflater mInflater;
private Button.OnClickListener mOnCloseClickListener;
public TabsAdapter(Context context, boolean isPrivate) {
public TabsAdapter(Context context) {
mContext = context;
mInflater = LayoutInflater.from(mContext);
mIsPrivate = isPrivate;
mOnCloseClickListener = new Button.OnClickListener() {
@Override
@ -281,16 +286,21 @@ class TabsTray extends TwoWayView
private void resetTransforms(View view) {
ViewHelper.setAlpha(view, 1);
if (mOriginalSize == 0)
return;
if (isVertical()) {
ViewHelper.setHeight(view, mOriginalSize);
ViewHelper.setTranslationX(view, 0);
} else {
ViewHelper.setWidth(view, mOriginalSize);
ViewHelper.setTranslationY(view, 0);
}
// We only need to reset the height or width after individual tab close animations.
if (mOriginalSize != 0) {
if (isVertical()) {
ViewHelper.setHeight(view, mOriginalSize);
} else {
ViewHelper.setWidth(view, mOriginalSize);
}
}
}
@Override
@ -320,6 +330,75 @@ class TabsTray extends TwoWayView
return (getOrientation().compareTo(TwoWayView.Orientation.VERTICAL) == 0);
}
@Override
public void closeAll() {
final int childCount = getChildCount();
// Just close the panel if there are no tabs to close.
if (childCount == 0) {
autoHidePanel();
return;
}
// Disable the view so that gestures won't interfere wth the tab close animation.
setEnabled(false);
// Delay starting each successive animation to create a cascade effect.
int cascadeDelay = 0;
for (int i = childCount - 1; i >= 0; i--) {
final View view = getChildAt(i);
final PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
animator.attach(view, Property.ALPHA, 0);
if (isVertical()) {
animator.attach(view, Property.TRANSLATION_X, view.getWidth());
} else {
animator.attach(view, Property.TRANSLATION_Y, view.getHeight());
}
mCloseAllAnimationCount++;
animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() { }
@Override
public void onPropertyAnimationEnd() {
mCloseAllAnimationCount--;
if (mCloseAllAnimationCount > 0) {
return;
}
// Hide the panel after the animation is done.
autoHidePanel();
// Re-enable the view after the animation is done.
TabsTray.this.setEnabled(true);
// Then actually close all the tabs.
final Iterable<Tab> tabs = Tabs.getInstance().getTabsInOrder();
for (Tab tab : tabs) {
// In the normal panel we want to close all tabs (both private and normal),
// but in the private panel we only want to close private tabs.
if (!mIsPrivate || tab.isPrivate()) {
Tabs.getInstance().closeTab(tab, false);
}
}
}
});
ThreadUtils.getUiHandler().postDelayed(new Runnable() {
@Override
public void run() {
animator.start();
}
}, cascadeDelay);
cascadeDelay += ANIMATION_CASCADE_DELAY;
}
}
private void animateClose(final View view, int pos) {
PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
animator.attach(view, Property.ALPHA, 0);
@ -565,7 +644,7 @@ class TabsTray extends TwoWayView
}
case MotionEvent.ACTION_MOVE: {
if (mSwipeView == null)
if (mSwipeView == null || mVelocityTracker == null)
break;
mVelocityTracker.addMovement(e);