Add quick controls

    Bug: http://b/issue?id=3277888
    Added Quick Controls Lab setting
    Implemented Quick Controls UI

Change-Id: I72011daf9140aa5d15c8b785126867c10bbc5501
diff --git a/src/com/android/browser/BaseUi.java b/src/com/android/browser/BaseUi.java
index 95e795c..1e9038d 100644
--- a/src/com/android/browser/BaseUi.java
+++ b/src/com/android/browser/BaseUi.java
@@ -231,22 +231,6 @@
         mActiveTab = tab;
         attachTabToContentView(tab);
         setShouldShowErrorConsole(tab, mUiController.shouldShowErrorConsole());
-        WebView view = tab.getWebView();
-        // TabControl.setCurrentTab has been called before this,
-        // so the tab is guaranteed to have a webview
-        if (view == null) {
-            Log.e(LOGTAG, "active tab with no webview detected");
-            return;
-        }
-        view.setEmbeddedTitleBar(getEmbeddedTitleBar());
-        if (tab.isInVoiceSearchMode()) {
-            showVoiceTitleBar(tab.getVoiceDisplayTitle());
-        } else {
-            revertVoiceTitleBar(tab);
-        }
-        resetTitleIconAndProgress(tab);
-        updateLockIconToLatest(tab);
-        tab.getTopWindow().requestFocus();
     }
 
     Tab getActiveTab() {
diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java
index 1f091e2..1b8acc6 100644
--- a/src/com/android/browser/BrowserSettings.java
+++ b/src/com/android/browser/BrowserSettings.java
@@ -117,6 +117,9 @@
     private boolean navDump = false;
     private boolean hardwareAccelerated = true;
 
+    // Lab settings
+    private boolean quickControls = false;
+
     // By default the error console is shown once the user navigates to about:debug.
     // The setting can be then toggled from the settings menu.
     private boolean showConsole = true;
@@ -167,6 +170,8 @@
     public final static String PREF_HARDWARE_ACCEL = "enable_hardware_accel";
     public final static String PREF_USER_AGENT = "user_agent";
 
+    public final static String PREF_QUICK_CONTROLS = "enable_quick_controls";
+
     private static final String DESKTOP_USERAGENT = "Mozilla/5.0 (Macintosh; " +
             "U; Intel Mac OS X 10_6_3; en-us) AppleWebKit/533.16 (KHTML, " +
             "like Gecko) Version/5.0 Safari/533.16";
@@ -490,6 +495,8 @@
             navDump = p.getBoolean("enable_nav_dump", navDump);
         }
 
+        quickControls = p.getBoolean(PREF_QUICK_CONTROLS, quickControls);
+
         // Only set these on startup if it is a dev build
         if (DEV_BUILD) {
             userAgent = Integer.parseInt(p.getString(PREF_USER_AGENT, "0"));
@@ -573,6 +580,10 @@
         return hardwareAccelerated;
     }
 
+    public boolean useQuickControls() {
+        return quickControls;
+    }
+
     public boolean showDebugSettings() {
         return showDebugSettings;
     }
@@ -836,6 +847,8 @@
         } else if (PREF_USER_AGENT.equals(key)) {
             userAgent = Integer.parseInt(p.getString(PREF_USER_AGENT, "0"));
             update();
+        } else if (PREF_QUICK_CONTROLS.equals(key)) {
+            quickControls = p.getBoolean(PREF_QUICK_CONTROLS, quickControls);
         }
     }
 }
diff --git a/src/com/android/browser/PhoneUi.java b/src/com/android/browser/PhoneUi.java
index 66656cf..e35e624 100644
--- a/src/com/android/browser/PhoneUi.java
+++ b/src/com/android/browser/PhoneUi.java
@@ -19,6 +19,7 @@
 import android.app.Activity;
 import android.content.Context;
 import android.graphics.PixelFormat;
+import android.util.Log;
 import android.view.ActionMode;
 import android.view.Gravity;
 import android.view.Menu;
@@ -118,6 +119,27 @@
     }
 
     @Override
+    public void setActiveTab(Tab tab) {
+        super.setActiveTab(tab);
+        WebView view = tab.getWebView();
+        // TabControl.setCurrentTab has been called before this,
+        // so the tab is guaranteed to have a webview
+        if (view == null) {
+            Log.e(LOGTAG, "active tab with no webview detected");
+            return;
+        }
+        view.setEmbeddedTitleBar(getEmbeddedTitleBar());
+        if (tab.isInVoiceSearchMode()) {
+            showVoiceTitleBar(tab.getVoiceDisplayTitle());
+        } else {
+            revertVoiceTitleBar(tab);
+        }
+        resetTitleIconAndProgress(tab);
+        updateLockIconToLatest(tab);
+        tab.getTopWindow().requestFocus();
+    }
+
+    @Override
     protected void attachFakeTitleBar(WebView mainView) {
         WindowManager manager = (WindowManager)
                 mActivity.getSystemService(Context.WINDOW_SERVICE);
diff --git a/src/com/android/browser/PieControl.java b/src/com/android/browser/PieControl.java
new file mode 100644
index 0000000..210e9ea
--- /dev/null
+++ b/src/com/android/browser/PieControl.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import com.android.browser.view.PieMenu;
+
+import android.app.Activity;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup.LayoutParams;
+import android.webkit.WebView;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * controller for Quick Controls pie menu
+ */
+public class PieControl implements OnClickListener, PieMenu.PieController {
+
+    private Activity mActivity;
+    private UiController mUiController;
+    private XLargeUi mUi;
+    private PieMenu mPie;
+    private ImageView mBack;
+    private ImageView mForward;
+    private ImageView mRefresh;
+    private ImageView mUrl;
+    private ImageView mOptions;
+    private ImageView mBookmarks;
+    private ImageView mNewTab;
+    private ImageView mClose;
+
+    private Map<View,Tab> mTabItems;
+
+    boolean mNewTabMode = true;
+
+    public PieControl(Activity activity, UiController controller, XLargeUi ui) {
+        mActivity = activity;
+        mUiController = controller;
+        mUi = ui;
+        mTabItems = new HashMap<View, Tab>();
+    }
+
+    protected void attachToContainer(FrameLayout container) {
+        if (mPie == null) {
+            mPie = new PieMenu(mActivity);
+            LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
+                    LayoutParams.MATCH_PARENT);
+            mPie.setLayoutParams(lp);
+            mForward = makeMenuView(R.drawable.ic_pie_forward);
+            mPie.addItem(mForward);
+            mRefresh = makeMenuView(R.drawable.ic_pie_refresh);
+            mPie.addItem(mRefresh);
+            mBack = makeMenuView(R.drawable.ic_pie_back);
+            mPie.addItem(mBack);
+            mUrl = makeMenuView(R.drawable.ic_pie_search);
+            mPie.addItem(mUrl);
+            mBookmarks = makeMenuView(R.drawable.ic_pie_bookmarks);
+            mPie.addItem(mBookmarks);
+            mNewTab = makeMenuView(R.drawable.ic_pie_new_tab);
+            mPie.addItem(mNewTab);
+            mOptions = makeMenuView(R.drawable.ic_pie_more);
+            mPie.addItem(mOptions);
+            setClickListener(mBack, mForward, mRefresh, mUrl, mOptions,
+                    mBookmarks, mNewTab);
+            mPie.setController(this);
+        }
+        container.addView(mPie);
+    }
+
+    protected void removeFromContainer(FrameLayout container) {
+        container.removeView(mPie);
+    }
+
+    private ImageView makeMenuView(int image) {
+        ImageView item = new ImageView(mActivity);
+        item.setImageResource(image);
+        LayoutParams lp = new LayoutParams(48, 48);
+        item.setLayoutParams(lp);
+        return item;
+    }
+
+    private void setClickListener(View... views) {
+        for (View view : views) {
+            view.setOnClickListener(this);
+        }
+    }
+
+    protected void forceToTop(FrameLayout container) {
+        if (mPie.getParent() != null) {
+            container.removeView(mPie);
+            container.addView(mPie);
+        }
+    }
+
+    @Override
+    public void onClick(View v) {
+        mPie.show(false);
+        Tab tab = mUiController.getTabControl().getCurrentTab();
+        WebView web = tab.getWebView();
+        if (mBack == v) {
+            web.goBack();
+        } else if (mForward == v) {
+            web.goForward();
+        } else if (mRefresh == v) {
+            if (tab.inPageLoad()) {
+                web.stopLoading();
+            } else {
+                web.reload();
+            }
+        } else if (mUrl == v) {
+            mUi.showFakeTitleBarAndEdit();
+        } else if (mOptions == v) {
+            mActivity.openOptionsMenu();
+        } else if (mBookmarks == v) {
+            mUiController.bookmarksOrHistoryPicker(false);
+        } else if (mNewTab == v) {
+            mUiController.openTabToHomePage();
+        } else if (mClose == v) {
+            mUiController.closeCurrentTab();
+        } else {
+            Tab ntab = mTabItems.get(v);
+            if (ntab != null) {
+                mUiController.switchToTab(mUiController.getTabControl().getTabIndex(ntab));
+            }
+        }
+    }
+
+    @Override
+    public boolean onOpen() {
+        return true;
+    }
+
+}
diff --git a/src/com/android/browser/TabBar.java b/src/com/android/browser/TabBar.java
index 6a139f3..ea734a6 100644
--- a/src/com/android/browser/TabBar.java
+++ b/src/com/android/browser/TabBar.java
@@ -88,6 +88,7 @@
     private int mTabOverlap;
     private int mTabSliceWidth;
     private int mTabPadding;
+    private boolean mUseQuickControls;
 
     public TabBar(Activity activity, UiController controller, XLargeUi ui) {
         super(activity);
@@ -139,6 +140,14 @@
         mShaderPaint.setAntiAlias(true);
     }
 
+    void setUseQuickControls(boolean useQuickControls) {
+        mUseQuickControls = useQuickControls;
+    }
+
+    int getTabCount() {
+        return mTabMap.size();
+    }
+
     void updateTabs(List<Tab> tabs) {
         mTabs.clearTabs();
         mTabMap.clear();
@@ -182,6 +191,7 @@
         if (mNewTab == view) {
             mUiController.openTabToHomePage();
         } else if (mTabs.getSelectedTab() == view) {
+            if (mUseQuickControls) return;
             if (mUi.isFakeTitleBarShowing() && !isLoading()) {
                 mUi.hideFakeTitleBar();
             } else {
@@ -202,7 +212,7 @@
         mUserRequestedUrlbar = true;
     }
 
-    private void showTitleBarIndicator(boolean show) {
+    void showTitleBarIndicator(boolean show) {
         Tab tab = mTabControl.getCurrentTab();
         if (tab != null) {
             TabViewData tvd = mTabMap.get(tab);
@@ -229,6 +239,7 @@
 
     @Override
     public void onScroll(int visibleTitleHeight) {
+        if (mUseQuickControls) return;
         // isLoading is using the current tab, which initially might not be set yet
         if (mTabControl.getCurrentTab() != null) {
             if ((mVisibleTitleHeight != 0) && (visibleTitleHeight == 0)
@@ -417,7 +428,7 @@
             int[] pos = new int[2];
             getLocationInWindow(mWindowPos);
             Drawable drawable = mSelected ? mActiveDrawable : mInactiveDrawable;
-            drawable.setBounds(0, 0, mUi.getTitleBarWidth(), getHeight());
+            drawable.setBounds(0, 0, mUi.getContentWidth(), getHeight());
             drawClipped(canvas, drawable, mPath, mWindowPos[0]);
             canvas.restoreToCount(state);
             super.dispatchDraw(canvas);
diff --git a/src/com/android/browser/TitleBarXLarge.java b/src/com/android/browser/TitleBarXLarge.java
index b680512..f1c6c6b 100644
--- a/src/com/android/browser/TitleBarXLarge.java
+++ b/src/com/android/browser/TitleBarXLarge.java
@@ -73,6 +73,7 @@
 
     private boolean mInLoad;
     private boolean mEditable;
+    private boolean mUseQuickControls;
 
     public TitleBarXLarge(Activity activity, UiController controller,
             XLargeUi ui) {
@@ -140,6 +141,29 @@
         }
     }
 
+    void setUseQuickControls(boolean useQuickControls) {
+        mUseQuickControls = useQuickControls;
+        if (mUseQuickControls) {
+            mBackButton.setVisibility(View.GONE);
+            mForwardButton.setVisibility(View.GONE);
+            mStopButton.setVisibility(View.GONE);
+            mAllButton.setVisibility(View.GONE);
+        } else {
+            mBackButton.setVisibility(View.VISIBLE);
+            mForwardButton.setVisibility(View.VISIBLE);
+            mStopButton.setVisibility(View.VISIBLE);
+            mAllButton.setVisibility(View.VISIBLE);
+        }
+    }
+
+    void setShowProgressOnly(boolean progress) {
+        if (progress) {
+            mContainer.setVisibility(View.GONE);
+        } else {
+            mContainer.setVisibility(View.VISIBLE);
+        }
+    }
+
     @Override
     public void onFocusChange(View view, boolean hasFocus) {
         if (!mEditable && hasFocus) {
@@ -291,12 +315,16 @@
             updateSearchMode();
         } else {
             mUrlInput.clearFocus();
-            mSearchButton.setVisibility(View.VISIBLE);
             mGoButton.setVisibility(View.GONE);
             mVoiceSearch.setVisibility(View.GONE);
             mStar.setVisibility(View.VISIBLE);
             mClearButton.setVisibility(View.GONE);
             mVoiceSearchIndicator.setVisibility(View.GONE);
+            if (mUseQuickControls) {
+                mSearchButton.setVisibility(View.GONE);
+            } else {
+                mSearchButton.setVisibility(View.VISIBLE);
+            }
         }
     }
 
diff --git a/src/com/android/browser/XLargeUi.java b/src/com/android/browser/XLargeUi.java
index 69e6724..7f9baa7 100644
--- a/src/com/android/browser/XLargeUi.java
+++ b/src/com/android/browser/XLargeUi.java
@@ -21,8 +21,12 @@
 import android.app.ActionBar;
 import android.app.Activity;
 import android.graphics.Bitmap;
+import android.util.Log;
 import android.view.ActionMode;
+import android.view.Gravity;
 import android.webkit.WebView;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
 
 import java.util.List;
 
@@ -33,11 +37,15 @@
 
     private static final String LOGTAG = "XLargeUi";
 
+    private ActionBar mActionBar;
     private TabBar mTabBar;
 
     private TitleBarXLarge mTitleBar;
     private TitleBarXLarge mFakeTitleBar;
 
+    private boolean mUseQuickControls;
+    private PieControl mPieControl;
+
     /**
      * @param browser
      * @param controller
@@ -49,10 +57,51 @@
         mTitleBar.setEditable(false);
         mFakeTitleBar = new TitleBarXLarge(mActivity, mUiController, this);
         mFakeTitleBar.setEditable(true);
-        ActionBar actionBar = mActivity.getActionBar();
         mTabBar = new TabBar(mActivity, mUiController, this);
-        actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
-        actionBar.setCustomView(mTabBar);
+        mActionBar = mActivity.getActionBar();
+        mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
+        mActionBar.setCustomView(mTabBar);
+        setUseQuickControls(BrowserSettings.getInstance().useQuickControls());
+    }
+
+    private void setUseQuickControls(boolean useQuickControls) {
+        mUseQuickControls = useQuickControls;
+        if (useQuickControls) {
+            checkTabCount();
+            mPieControl = new PieControl(mActivity, mUiController, this);
+            mPieControl.attachToContainer(mContentView);
+            setFakeTitleBarGravity(Gravity.BOTTOM);
+
+            // remove embedded title bar if present
+            WebView web = mTabControl.getCurrentWebView();
+            if ((web != null) && (web.getVisibleTitleHeight() > 0)) {
+                web.setEmbeddedTitleBar(null);
+            }
+        } else {
+            mActivity.getActionBar().show();
+            if (mPieControl != null) {
+                mPieControl.removeFromContainer(mContentView);
+            }
+            setFakeTitleBarGravity(Gravity.TOP);
+            // remove embedded title bar if present
+            WebView web = mTabControl.getCurrentWebView();
+            if ((web != null) && (web.getVisibleTitleHeight() == 0)) {
+                web.setEmbeddedTitleBar(mTitleBar);
+            }
+        }
+        mTabBar.setUseQuickControls(mUseQuickControls);
+        mFakeTitleBar.setUseQuickControls(mUseQuickControls);
+    }
+
+    private void checkTabCount() {
+        if (mUseQuickControls) {
+            int n = mTabBar.getTabCount();
+            if (n >= 2) {
+                mActivity.getActionBar().show();
+            } else if (n == 1) {
+                mActivity.getActionBar().hide();
+            }
+        }
     }
 
     @Override
@@ -114,6 +163,9 @@
     public void onPageFinished(Tab tab, String url) {
         mTabBar.onPageFinished(tab);
         super.onPageFinished(tab, url);
+        if (mUseQuickControls) {
+            mFakeTitleBar.setShowProgressOnly(false);
+        }
     }
 
     @Override
@@ -123,7 +175,17 @@
             mFakeTitleBar.setProgress(progress);
             if (progress == 100) {
                 hideFakeTitleBar();
+                if (mUseQuickControls) {
+                    mFakeTitleBar.setShowProgressOnly(false);
+                    setFakeTitleBarGravity(Gravity.BOTTOM);
+                }
             } else {
+                if (mUseQuickControls) {
+                    mFakeTitleBar.setShowProgressOnly(true);
+                    if (!isFakeTitleBarShowing()) {
+                        setFakeTitleBarGravity(Gravity.TOP);
+                    }
+                }
                 showFakeTitleBar();
             }
         }
@@ -137,28 +199,55 @@
     @Override
     public void addTab(Tab tab) {
         mTabBar.onNewTab(tab);
+        checkTabCount();
     }
 
     @Override
     public void setActiveTab(Tab tab) {
         super.setActiveTab(tab);
+        ScrollWebView view = (ScrollWebView) tab.getWebView();
+        // TabControl.setCurrentTab has been called before this,
+        // so the tab is guaranteed to have a webview
+        if (view == null) {
+            Log.e(LOGTAG, "active tab with no webview detected");
+            return;
+        }
+        // Request focus on the top window.
+        if (mUseQuickControls) {
+            mPieControl.forceToTop(mContentView);
+            view.setScrollListener(null);
+            mTabBar.showTitleBarIndicator(false);
+        } else {
+            view.setEmbeddedTitleBar(mTitleBar);
+            view.setScrollListener(this);
+        }
         mTabBar.onSetActiveTab(tab);
+        if (tab.isInVoiceSearchMode()) {
+            showVoiceTitleBar(tab.getVoiceDisplayTitle());
+        } else {
+            revertVoiceTitleBar(tab);
+        }
+        resetTitleIconAndProgress(tab);
+        updateLockIconToLatest(tab);
+        tab.getTopWindow().requestFocus();
     }
 
     @Override
     public void updateTabs(List<Tab> tabs) {
         mTabBar.updateTabs(tabs);
+        checkTabCount();
     }
 
     @Override
     public void removeTab(Tab tab) {
         super.removeTab(tab);
         mTabBar.onRemoveTab(tab);
+        checkTabCount();
     }
 
-    int getTitleBarWidth() {
-        if (mTitleBar != null) {
-            return mTitleBar.getWidth();
+    int getContentWidth() {
+        if (mContentView != null) {
+            return mContentView.getWidth();
         }
         return 0;
     }
@@ -168,6 +257,22 @@
         mFakeTitleBar.onEditUrl(clearInput);
     }
 
+    void setFakeTitleBarGravity(int gravity) {
+        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams)
+                mFakeTitleBar.getLayoutParams();
+        if (lp == null) {
+            lp = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
+                    LayoutParams.WRAP_CONTENT);
+        }
+        lp.gravity = gravity;
+        mFakeTitleBar.setLayoutParams(lp);
+    }
+
+    void showFakeTitleBarAndEdit() {
+        showFakeTitleBar();
+        mFakeTitleBar.onEditUrl(false);
+    }
+
     @Override
     protected void attachFakeTitleBar(WebView mainView) {
         mContentView.addView(mFakeTitleBar);
@@ -208,6 +313,20 @@
     }
 
     @Override
+    public void onActionModeFinished(boolean inLoad) {
+        checkTabCount();
+        if (inLoad) {
+            // the titlebar was removed when the CAB was shown
+            // if the page is loading, show it again
+            mFakeTitleBar.setShowProgressOnly(true);
+            if (!isFakeTitleBarShowing()) {
+                setFakeTitleBarGravity(Gravity.TOP);
+            }
+            showFakeTitleBar();
+        }
+    }
+
+    @Override
     public void setUrlTitle(Tab tab, String url, String title) {
         super.setUrlTitle(tab, url, title);
         mTabBar.onUrlAndTitle(tab, url, title);
diff --git a/src/com/android/browser/preferences/LabPreferencesFragment.java b/src/com/android/browser/preferences/LabPreferencesFragment.java
new file mode 100644
index 0000000..8a8546f
--- /dev/null
+++ b/src/com/android/browser/preferences/LabPreferencesFragment.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.browser.preferences;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.BrowserSettings;
+import com.android.browser.Controller;
+import com.android.browser.R;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.PreferenceActivity.Header;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager.OnActivityResultListener;
+
+import java.io.IOException;
+import java.io.Serializable;
+
+public class LabPreferencesFragment extends PreferenceFragment
+        implements OnPreferenceChangeListener {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Load the XML preferences file
+        addPreferencesFromResource(R.xml.lab_preferences);
+
+        Preference e = findPreference(BrowserSettings.PREF_QUICK_CONTROLS);
+        e.setOnPreferenceChangeListener(this);
+    }
+
+    @Override
+    public boolean onPreferenceChange(Preference preference, Object newValue) {
+        // Attempt to restart
+        startActivity(new Intent(BrowserActivity.ACTION_RESTART, null,
+                getActivity(), BrowserActivity.class));
+        return true;
+    }
+
+}
diff --git a/src/com/android/browser/view/PieMenu.java b/src/com/android/browser/view/PieMenu.java
new file mode 100644
index 0000000..d838a34
--- /dev/null
+++ b/src/com/android/browser/view/PieMenu.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser.view;
+
+import com.android.browser.R;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class PieMenu extends FrameLayout {
+
+    private static final int RADIUS_GAP = 10;
+
+    public interface PieController {
+        /**
+         * called before menu opens to customize menu
+         * returns if pie state has been changed
+         */
+        public boolean onOpen();
+    }
+    private Point mCenter;
+    private int mRadius;
+    private int mRadiusInc;
+    private int mSlop;
+
+    private boolean mOpen;
+    private Paint mPaint;
+    private Paint mSelectedPaint;
+    private PieController mController;
+
+    private Map<View, List<View>> mMenu;
+    private List<View> mStack;
+
+    private boolean mDirty;
+
+    /**
+     * @param context
+     * @param attrs
+     * @param defStyle
+     */
+    public PieMenu(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    /**
+     * @param context
+     * @param attrs
+     */
+    public PieMenu(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    /**
+     * @param context
+     */
+    public PieMenu(Context context) {
+        super(context);
+        init(context);
+    }
+
+    private void init(Context ctx) {
+        this.setTag(new MenuTag(0));
+        mStack = new ArrayList<View>();
+        mStack.add(this);
+        Resources res = ctx.getResources();
+        mRadius = (int) res.getDimension(R.dimen.qc_radius);
+        mRadiusInc = (int) res.getDimension(R.dimen.qc_radius_inc);
+        mSlop = (int) res.getDimension(R.dimen.qc_slop);
+        mPaint = new Paint();
+        mPaint.setAntiAlias(true);
+        mPaint.setColor(res.getColor(R.color.qc_slice_normal));
+        mSelectedPaint = new Paint();
+        mSelectedPaint.setAntiAlias(true);
+        mSelectedPaint.setColor(res.getColor(R.color.qc_slice_active));
+        mOpen = false;
+        mMenu = new HashMap<View, List<View>>();
+        setWillNotDraw(false);
+        setDrawingCacheEnabled(false);
+        mCenter = new Point(0,0);
+        mDirty = true;
+    }
+
+    public void setController(PieController ctl) {
+        mController = ctl;
+    }
+
+    public void setRadius(int r) {
+        mRadius = r;
+        requestLayout();
+    }
+
+    public void setRadiusIncrement(int ri) {
+        mRadiusInc = ri;
+        requestLayout();
+    }
+
+    /**
+     * add a menu item to another item as a submenu
+     * @param item
+     * @param parent
+     */
+    public void addItem(View item, View parent) {
+        List<View> subs = mMenu.get(parent);
+        if (subs == null) {
+            subs = new ArrayList<View>();
+            mMenu.put(parent, subs);
+        }
+        subs.add(item);
+        MenuTag tag = new MenuTag(((MenuTag) parent.getTag()).level + 1);
+        item.setTag(tag);
+    }
+
+    public void addItem(View view) {
+        // add the item to the pie itself
+        addItem(view, this);
+    }
+
+    public void removeItem(View view) {
+        List<View> subs = mMenu.get(view);
+        mMenu.remove(view);
+        for (View p : mMenu.keySet()) {
+            List<View> sl = mMenu.get(p);
+            if (sl != null) {
+                sl.remove(view);
+            }
+        }
+    }
+
+    public void clearItems(View parent) {
+        List<View> subs = mMenu.remove(parent);
+        if (subs != null) {
+            for (View sub: subs) {
+                clearItems(sub);
+            }
+        }
+    }
+
+    public void clearItems() {
+        mMenu.clear();
+    }
+
+
+    public void show(boolean show) {
+        mOpen = show;
+        if (mOpen) {
+            if (mController != null) {
+                boolean changed = mController.onOpen();
+            }
+            mDirty = true;
+        }
+        if (!show) {
+            // hide sub items
+            mStack.clear();
+            mStack.add(this);
+        }
+        invalidate();
+    }
+
+    private void setCenter(int x, int y) {
+        if (x < mSlop) {
+            mCenter.x = 0;
+        } else {
+            mCenter.x = getWidth();
+        }
+        mCenter.y = y;
+    }
+
+    private boolean onTheLeft() {
+        return mCenter.x < mSlop;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (mOpen) {
+            int radius = mRadius;
+            // start in the center for 0 level menu
+            float anchor = (float) Math.PI / 2;
+            PointF angles = new PointF();
+            int state = canvas.save();
+            if (onTheLeft()) {
+                // left handed
+                canvas.scale(-1, 1);
+            }
+            for (View parent : mStack) {
+                List<View> subs = mMenu.get(parent);
+                if (subs != null) {
+                    setGeometry(anchor, subs.size(), angles);
+                }
+                anchor = drawSlices(canvas, subs, radius, angles.x, angles.y);
+                radius += mRadiusInc + RADIUS_GAP;
+            }
+            canvas.restoreToCount(state);
+            mDirty = false;
+        }
+    }
+
+    /**
+     * draw the set of slices
+     * @param canvas
+     * @param items
+     * @param radius
+     * @param start
+     * @param sweep
+     * @return the angle of the selected slice
+     */
+    private float drawSlices(Canvas canvas, List<View> items, int radius,
+            float start, float sweep) {
+        float angle = start + sweep / 2;
+        // gap between slices in degrees
+        float gap = 1f;
+        float newanchor = 0f;
+        for (View item : items) {
+            if (mDirty) {
+                item.measure(item.getLayoutParams().width,
+                        item.getLayoutParams().height);
+                int w = item.getMeasuredWidth();
+                int h = item.getMeasuredHeight();
+                int x = (int) (radius * Math.sin(angle));
+                int y =  mCenter.y - (int) (radius * Math.cos(angle)) - h / 2;
+                if (onTheLeft()) {
+                    x = mCenter.x + x - w / 2;
+                } else {
+                    x = mCenter.x - x - w / 2;
+                }
+                item.layout(x, y, x + w, y + h);
+            }
+            float itemstart = angle - sweep / 2;
+            int inner = radius - mRadiusInc / 2;
+            int outer = radius + mRadiusInc / 2;
+            Path slice = makeSlice(getDegrees(itemstart) - gap,
+                    getDegrees(itemstart + sweep) + gap,
+                    outer, inner, mCenter);
+            MenuTag tag = (MenuTag) item.getTag();
+            tag.start = itemstart;
+            tag.sweep = sweep;
+            tag.inner = inner;
+            tag.outer = outer;
+
+            Paint p = item.isPressed() ? mSelectedPaint : mPaint;
+            canvas.drawPath(slice, p);
+            int state = canvas.save();
+            if (onTheLeft()) {
+                canvas.scale(-1, 1);
+            }
+            canvas.translate(item.getX(), item.getY());
+            item.draw(canvas);
+            canvas.restoreToCount(state);
+            if (mStack.contains(item)) {
+                // item is anchor for sub menu
+                newanchor = angle;
+            }
+            angle += sweep;
+        }
+        return newanchor;
+    }
+
+    /**
+     * converts a
+     * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock)
+     * @return skia angle
+     */
+    private float getDegrees(double angle) {
+        return (float) (270 - 180 * angle / Math.PI);
+    }
+
+    private Path makeSlice(float startangle, float endangle, int outerradius,
+            int innerradius, Point center) {
+        RectF bb = new RectF(center.x - outerradius, center.y - outerradius,
+                center.x + outerradius, center.y + outerradius);
+        RectF bbi = new RectF(center.x - innerradius, center.y - innerradius,
+                center.x + innerradius, center.y + innerradius);
+        Path path = new Path();
+        path.arcTo(bb, startangle, endangle - startangle, true);
+        path.arcTo(bbi, endangle, startangle - endangle);
+        path.close();
+        return path;
+    }
+
+    /**
+     * all angles are 0 .. MATH.PI where 0 points up, and rotate counterclockwise
+     * set the startangle and slice sweep in result
+     * @param anchorangle : angle at which the menu is anchored
+     * @param nslices
+     * @param result : x : start, y : sweep
+     */
+    private void setGeometry(float anchorangle, int nslices, PointF result) {
+        float span = (float) Math.min(anchorangle, Math.PI - anchorangle);
+        float sweep = 2 * span / (nslices + 1);
+        result.x = anchorangle - span + sweep / 2;
+        result.y = sweep;
+    }
+
+    // touch handling for pie
+
+    View mCurrentView;
+    Rect mHitRect = new Rect();
+
+    @Override
+    public boolean onTouchEvent(MotionEvent evt) {
+        float x = evt.getX();
+        float y = evt.getY();
+        int action = evt.getActionMasked();
+        int edges = evt.getEdgeFlags();
+        if (MotionEvent.ACTION_DOWN == action) {
+            if ((x > getWidth() - mSlop) || (x < mSlop)) {
+                setCenter((int) x, (int) y);
+                show(true);
+                return true;
+            }
+        } else if (MotionEvent.ACTION_UP == action) {
+            if (mOpen) {
+                View v = mCurrentView;
+                deselect();
+                if (v != null) {
+                    v.performClick();
+                }
+                show(false);
+                return true;
+            }
+        } else if (MotionEvent.ACTION_CANCEL == action) {
+            if (mOpen) {
+                show(false);
+            }
+            deselect();
+            return false;
+        } else if (MotionEvent.ACTION_MOVE == action) {
+            View v = findView((int) x, (int) y);
+            if (mCurrentView != v) {
+                onEnter(v);
+                invalidate();
+            }
+        }
+        // always re-dispatch event
+        return false;
+    }
+
+    /**
+     * enter a slice for a view
+     * updates model only
+     * @param view
+     */
+    private void onEnter(View view) {
+        // deselect
+        if (mCurrentView != null) {
+            if (getLevel(mCurrentView) >= getLevel(view)) {
+                mCurrentView.setPressed(false);
+            }
+        }
+        if (view != null) {
+            // clear up stack
+            MenuTag tag = (MenuTag) view.getTag();
+            int i = mStack.size() - 1;
+            while (i > 0) {
+                View v = mStack.get(i);
+                if (((MenuTag) v.getTag()).level >= tag.level) {
+                    v.setPressed(false);
+                    mStack.remove(i);
+                } else {
+                    break;
+                }
+                i--;
+            }
+            List<View> items = mMenu.get(view);
+            if (items != null) {
+                mStack.add(view);
+                mDirty = true;
+            }
+            view.setPressed(true);
+        }
+        mCurrentView = view;
+    }
+
+    private void deselect() {
+        if (mCurrentView != null) {
+            mCurrentView.setPressed(false);
+        }
+        mCurrentView = null;
+    }
+
+    private int getLevel(View v) {
+        if (v == null) return -1;
+        return ((MenuTag) v.getTag()).level;
+    }
+
+    private View findView(int x, int y) {
+        // get angle and radius from x/y
+        float angle = (float) Math.PI / 2;
+        x = mCenter.x - x;
+        if (mCenter.x < mSlop) {
+            x = -x;
+        }
+        y = mCenter.y - y;
+        float dist = (float) Math.sqrt(x * x + y * y);
+        if (y > 0) {
+            angle = (float) Math.asin(x / dist);
+        } else if (y < 0) {
+            angle = (float) (Math.PI - Math.asin(x / dist ));
+        }
+        // find the matching item:
+        for (View parent : mStack) {
+            List<View> subs = mMenu.get(parent);
+            if (subs != null) {
+                for (View item : subs) {
+                    MenuTag tag = (MenuTag) item.getTag();
+                    if ((tag.inner < dist)
+                            && (tag.outer > dist)
+                            && (tag.start < angle)
+                            && (tag.start + tag.sweep > angle)) {
+                        return item;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    class MenuTag {
+
+        int level;
+        float start;
+        float sweep;
+        int inner;
+        int outer;
+
+        public MenuTag(int l) {
+            level = l;
+        }
+
+    }
+
+}