Add Top/Bottom Menu Bar and the popup menu.

Change-Id: Ic5aed56ab50ffdc79787ca9d830f8f929c934f7e
diff --git a/new3d/res/drawable-hdpi/icon_details.png b/new3d/res/drawable-hdpi/icon_details.png
new file mode 100644
index 0000000..4a5c276
--- /dev/null
+++ b/new3d/res/drawable-hdpi/icon_details.png
Binary files differ
diff --git a/new3d/res/drawable-hdpi/menu_bar_bg.9.png b/new3d/res/drawable-hdpi/menu_bar_bg.9.png
new file mode 100644
index 0000000..d352d44
--- /dev/null
+++ b/new3d/res/drawable-hdpi/menu_bar_bg.9.png
Binary files differ
diff --git a/new3d/res/drawable-hdpi/menu_highlight.9.png b/new3d/res/drawable-hdpi/menu_highlight.9.png
new file mode 100644
index 0000000..3876e70
--- /dev/null
+++ b/new3d/res/drawable-hdpi/menu_highlight.9.png
Binary files differ
diff --git a/new3d/res/drawable-hdpi/popup_triangle_bottom.png b/new3d/res/drawable-hdpi/popup_triangle_bottom.png
index 9ff36e9..a637fdb 100644
--- a/new3d/res/drawable-hdpi/popup_triangle_bottom.png
+++ b/new3d/res/drawable-hdpi/popup_triangle_bottom.png
Binary files differ
diff --git a/new3d/res/drawable-hdpi/top_menu_bar_bg.9.png b/new3d/res/drawable-hdpi/top_menu_bar_bg.9.png
new file mode 100644
index 0000000..fa38967
--- /dev/null
+++ b/new3d/res/drawable-hdpi/top_menu_bar_bg.9.png
Binary files differ
diff --git a/new3d/res/drawable/popup_triangle_bottom.png b/new3d/res/drawable/popup_triangle_bottom.png
index 42dc82b..bcf34c3 100644
--- a/new3d/res/drawable/popup_triangle_bottom.png
+++ b/new3d/res/drawable/popup_triangle_bottom.png
Binary files differ
diff --git a/new3d/src/com/android/gallery3d/app/Gallery.java b/new3d/src/com/android/gallery3d/app/Gallery.java
index 45c95fd..3fa705d 100644
--- a/new3d/src/com/android/gallery3d/app/Gallery.java
+++ b/new3d/src/com/android/gallery3d/app/Gallery.java
@@ -33,6 +33,7 @@
 import com.android.gallery3d.ui.GLHandler;
 import com.android.gallery3d.ui.GLRootView;
 import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.HeadUpDisplay;
 import com.android.gallery3d.ui.MediaSetSlotAdapter;
 import com.android.gallery3d.ui.OverlayLayout;
 import com.android.gallery3d.ui.SlotView;
@@ -67,6 +68,7 @@
         GLView overlay = new OverlayLayout();
         overlay.addComponent(mBackground);
         overlay.addComponent(mSlotView);
+        overlay.addComponent(new HeadUpDisplay(this));
         mGLRootView.setContentPane(overlay);
 
         mHandler = new GLHandler(mGLRootView) {
diff --git a/new3d/src/com/android/gallery3d/ui/ColorTexture.java b/new3d/src/com/android/gallery3d/ui/ColorTexture.java
index deffc9a..12865a4 100644
--- a/new3d/src/com/android/gallery3d/ui/ColorTexture.java
+++ b/new3d/src/com/android/gallery3d/ui/ColorTexture.java
@@ -28,7 +28,8 @@
     }
 
     public void draw(GLRootView root, int x, int y, int w, int h) {
-        root.drawColor(x, y, w, h, mColor);
+        root.setColor(mColor);
+        root.drawRect(x, y, w, h);
     }
 
     public boolean isOpaque() {
diff --git a/new3d/src/com/android/gallery3d/ui/DisplayItemPanel.java b/new3d/src/com/android/gallery3d/ui/DisplayItemPanel.java
index 2267134..ffa8ad5 100644
--- a/new3d/src/com/android/gallery3d/ui/DisplayItemPanel.java
+++ b/new3d/src/com/android/gallery3d/ui/DisplayItemPanel.java
@@ -96,7 +96,7 @@
         matrix.preTranslate(-mScrollX, 0);
         if (mAnimationStartTime == NO_ANIMATION) {
             for (DisplayItem item: mItems) {
-                renderItem(view, item, matrix);
+                renderItem(view, item);
             }
         } else {
             long now = view.currentAnimationTimeMillis();
@@ -119,10 +119,11 @@
         matrix.preTranslate(mScrollX, 0);
     }
 
-    private void renderItem(GLRootView root, DisplayItem item, Matrix matrix) {
-        item.mCurrent.apply(matrix);
+    private void renderItem(GLRootView root, DisplayItem item) {
+        Transformation transformation = root.pushTransform();
+        item.mCurrent.apply(transformation.getMatrix());
         item.render(root);
-        item.mCurrent.inverse(matrix);
+        root.popTransform();
     }
 
     private void renderItem(
diff --git a/new3d/src/com/android/gallery3d/ui/DrawableTexture.java b/new3d/src/com/android/gallery3d/ui/DrawableTexture.java
new file mode 100644
index 0000000..da13d5a
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/DrawableTexture.java
@@ -0,0 +1,26 @@
+package com.android.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+
+public class DrawableTexture extends CanvasTexture {
+
+    private final Drawable mDrawable;
+
+    public DrawableTexture(Drawable drawable) {
+        super(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
+        mDrawable = drawable;
+    }
+
+    @Override
+    public void setSize(int width, int height) {
+        super.setSize(width, height);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas, Bitmap backing) {
+        mDrawable.setBounds(0, 0, mWidth, mHeight);
+        mDrawable.draw(canvas);
+    }
+}
diff --git a/new3d/src/com/android/gallery3d/ui/GLListView.java b/new3d/src/com/android/gallery3d/ui/GLListView.java
new file mode 100644
index 0000000..6145b92
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/GLListView.java
@@ -0,0 +1,372 @@
+package com.android.gallery3d.ui;
+
+import static android.view.View.MeasureSpec.makeMeasureSpec;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Message;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View.MeasureSpec;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.Transformation;
+import android.widget.Scroller;
+
+import javax.microedition.khronos.opengles.GL11;
+
+public class GLListView extends GLView {
+    @SuppressWarnings("unused")
+    private static final String TAG = "GLListView";
+    private static final int INDEX_NONE = -1;
+    private static final int SCROLL_BAR_TIMEOUT = 2500;
+
+    private static final int HIDE_SCROLL_BAR = 1;
+
+    private Model mModel;
+    private final Handler mHandler;
+
+    private int mHighlightIndex = INDEX_NONE;
+    private GLView mHighlightView;
+
+    private Texture mHighLight;
+    private NinePatchTexture mScrollbar;
+
+    private int mVisibleStart = 0; // inclusive
+    private int mVisibleEnd = 0; // exclusive
+
+    private boolean mHasMeasured = false;
+
+    private boolean mScrollBarVisible = false;
+    private Animation mScrollBarAnimation;
+    private OnItemSelectedListener mOnItemSelectedListener;
+
+    private final GestureDetector mGestureDetector;
+    private final Scroller mScroller;
+    private boolean mScrollable;
+    private boolean mIsPressed = false;
+
+    static public interface Model {
+        public int size();
+        public GLView getView(int index);
+        public boolean isSelectable(int index);
+    }
+
+    static public interface OnItemSelectedListener {
+        public void onItemSelected(GLView view, int position);
+    }
+
+    public GLListView(Context context) {
+        mScroller = new Scroller(context);
+        mHandler = new Handler() {
+            @Override
+            public void handleMessage(Message msg) {
+                GLRootView root = getGLRootView();
+                if (root != null) {
+                    synchronized (root) {
+                        handleMessageLocked(msg);
+                    }
+                } else {
+                    handleMessageLocked(msg);
+                }
+            }
+
+            private void handleMessageLocked(Message msg) {
+                switch(msg.what) {
+                    case HIDE_SCROLL_BAR:
+                        setScrollBarVisible(false);
+                        break;
+                }
+            }
+        };
+        mGestureDetector = new GestureDetector(
+                context, new MyGestureListener(), mHandler);
+    }
+
+    @Override
+    protected void onVisibilityChanged(int visibility) {
+        super.onVisibilityChanged(visibility);
+        if (visibility == GLView.VISIBLE && mScrollHeight > getHeight()) {
+            setScrollBarVisible(true);
+            mHandler.sendEmptyMessageDelayed(
+                    HIDE_SCROLL_BAR, SCROLL_BAR_TIMEOUT);
+        }
+    }
+
+    private void setScrollBarVisible(boolean visible) {
+        if (mScrollBarVisible == visible || mScrollbar == null) return;
+        mScrollBarVisible = visible;
+        if (!visible) {
+            mScrollBarAnimation = new AlphaAnimation(1, 0);
+            mScrollBarAnimation.setDuration(300);
+            mScrollBarAnimation.start();
+        } else {
+            mScrollBarAnimation = null;
+        }
+        invalidate();
+    }
+
+    public void setHighLight(Texture highLight) {
+        mHighLight = highLight;
+    }
+
+    public void setDataModel(Model model) {
+        mModel = model;
+        mScrollY = 0;
+        requestLayout();
+    }
+
+    public void setOnItemSelectedListener(OnItemSelectedListener l) {
+        mOnItemSelectedListener = l;
+    }
+
+    private boolean drawWithAnimation(GLRootView root,
+            Texture texture, int x, int y, int w, int h, Animation anim) {
+        long now = root.currentAnimationTimeMillis();
+        Transformation temp = root.obtainTransformation();
+        boolean more = anim.getTransformation(now, temp);
+        Transformation transformation = root.pushTransform();
+        transformation.compose(temp);
+        texture.draw(root, x, y, w, h);
+        invalidate();
+        root.popTransform();
+        return more;
+    }
+
+    @Override
+    protected void render(GLRootView root, GL11 gl) {
+        root.clipRect(0, 0, getWidth(), getHeight());
+        if (mHighlightIndex != INDEX_NONE) {
+            GLView view = mModel.getView(mHighlightIndex);
+            Rect bounds = view.bounds();
+            if (mHighLight != null) {
+                int width = bounds.width();
+                int height = bounds.height();
+                mHighLight.draw(root,
+                        bounds.left - mScrollX, bounds.top - mScrollY,
+                        width, height);
+            }
+        }
+        super.render(root, gl);
+        root.clearClip();
+
+        if (mScrollBarAnimation != null || mScrollBarVisible) {
+            int width = mScrollbar.getWidth();
+            int height = getHeight() * getHeight() / mScrollHeight;
+            int yoffset = mScrollY * getHeight() / mScrollHeight;
+            if (mScrollBarAnimation != null) {
+                if (!drawWithAnimation(
+                        root, mScrollbar, getWidth() - width, yoffset,
+                        width, height, mScrollBarAnimation)) {
+                    mScrollBarAnimation = null;
+                }
+            } else {
+                mScrollbar.draw(
+                        root, getWidth() - width, yoffset, width, height);
+            }
+        }
+        if (mScroller.computeScrollOffset()) {
+            setScrollPosition(mScroller.getCurrY(), false);
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthSpec, int heightSpec) {
+        // first get the total height
+        int height = 0;
+        int maxWidth = 0;
+        for (int i = 0, n = mModel.size(); i < n; ++i) {
+            GLView view = mModel.getView(i);
+            view.measure(widthSpec, MeasureSpec.UNSPECIFIED);
+            height += view.getMeasuredHeight();
+            maxWidth = Math.max(maxWidth, view.getMeasuredWidth());
+        }
+        mScrollHeight = height;
+        mHasMeasured = true;
+        new MeasureHelper(this)
+                .setPreferredContentSize(maxWidth, height)
+                .measure(widthSpec, heightSpec);
+    }
+
+    @Override
+    public int getComponentCount() {
+        return mVisibleEnd - mVisibleStart;
+    }
+
+    @Override
+    public GLView getComponent(int index) {
+        if (index < 0 || index >= mVisibleEnd - mVisibleStart) {
+            throw new ArrayIndexOutOfBoundsException(index);
+        }
+        return mModel.getView(mVisibleStart + index);
+    }
+
+    @Override
+    public void requestLayout() {
+        mHasMeasured = false;
+        super.requestLayout();
+    }
+
+    @Override
+    protected void onLayout(
+            boolean change, int left, int top, int right, int bottom) {
+
+        if (!mHasMeasured || mMeasuredWidth != (right - left)) {
+            measure(makeMeasureSpec(right - left, MeasureSpec.EXACTLY),
+                    makeMeasureSpec(bottom - top, MeasureSpec.EXACTLY));
+        }
+
+        mScrollable = mScrollHeight > (bottom - top);
+        int width = right - left;
+        int yoffset = 0;
+
+        for (int i = 0, n = mModel.size(); i < n; ++i) {
+            GLView item = mModel.getView(i);
+            item.onAddToParent(this);
+            int nextOffset = yoffset + item.getMeasuredHeight();
+            item.layout(0, yoffset, width, nextOffset);
+            yoffset = nextOffset;
+        }
+        setScrollPosition(mScrollY, true);
+    }
+
+    private void setScrollPosition(int position, boolean force) {
+        int height = getHeight();
+
+        position = Util.clamp(position, 0, mScrollHeight - height);
+
+        if (!force && position == mScrollY) return;
+        mScrollY = position;
+
+        int n = mModel.size();
+
+        int start = 0;
+        int end = 0;
+        for (start = 0; start < n; ++start) {
+            if (position < mModel.getView(start).mBounds.bottom) break;
+        }
+
+        int bottom = position + height;
+        for (end = start; end < n; ++ end) {
+            if (bottom <= mModel.getView(end).mBounds.top) break;
+        }
+        setVisibleRange(start , end);
+        invalidate();
+    }
+
+    private void setVisibleRange(int start, int end) {
+        if (start == mVisibleStart && end == mVisibleEnd) return;
+        mVisibleStart = start;
+        mVisibleEnd = end;
+    }
+
+    @Override
+    protected boolean dispatchTouchEvent(MotionEvent event) {
+        return onTouch(event);
+    }
+
+    @Override @SuppressWarnings("fallthrough")
+    protected boolean onTouch(MotionEvent event) {
+
+        mGestureDetector.onTouchEvent(event);
+
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mIsPressed = true;
+                mHandler.removeMessages(HIDE_SCROLL_BAR);
+                setScrollBarVisible(mScrollHeight > getHeight());
+
+                // fallthrough: we need to highlight the item which is pressed
+            case MotionEvent.ACTION_MOVE:
+                if (!mScrollable) {
+                    findAndSetHighlightItem((int) event.getY());
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                mIsPressed = false;
+                if (mScrollBarVisible) {
+                    mHandler.removeMessages(HIDE_SCROLL_BAR);
+                    mHandler.sendEmptyMessageDelayed(
+                            HIDE_SCROLL_BAR, SCROLL_BAR_TIMEOUT);
+                }
+                if (!mScrollable && mOnItemSelectedListener != null
+                        && mHighlightView != null) {
+                    mOnItemSelectedListener
+                            .onItemSelected(mHighlightView, mHighlightIndex);
+                }
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_OUTSIDE:
+                setHighlightItem(null, INDEX_NONE);
+        }
+        return true;
+    }
+
+    private void findAndSetHighlightItem(int y) {
+        int position = y + mScrollY;
+        for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
+            GLView child = mModel.getView(i);
+            if (child.mBounds.bottom > position) {
+                if (mModel.isSelectable(i)) {
+                    setHighlightItem(child, i);
+                    return;
+                }
+                break;
+            }
+        }
+        setHighlightItem(null, INDEX_NONE);
+    }
+
+    private void setHighlightItem(GLView view, int index) {
+        if (index == mHighlightIndex) return;
+        mHighlightIndex = index;
+        mHighlightView = view;
+        if (mHighLight != null) invalidate();
+    }
+
+    public void setScroller(NinePatchTexture scrollbar) {
+        this.mScrollbar = scrollbar;
+        requestLayout();
+    }
+
+    private class MyGestureListener
+            extends GestureDetector.SimpleOnGestureListener {
+
+        @Override
+        public boolean onFling(MotionEvent e1,
+                MotionEvent e2, float velocityX, float velocityY) {
+            if (!mScrollable) return false;
+            mScroller.fling(0, mScrollY,
+                    0, -(int) velocityY, 0, 0, 0, mScrollHeight - getHeight());
+            invalidate();
+            return true;
+        }
+
+        @Override
+        public boolean onScroll(MotionEvent e1,
+                MotionEvent e2, float distanceX, float distanceY) {
+            if (!mScrollable) return false;
+            setHighlightItem(null, INDEX_NONE);
+            setScrollPosition(mScrollY + (int) distanceY, false);
+            return true;
+        }
+
+        @Override
+        public void onShowPress(MotionEvent e) {
+            if (!mScrollable || !mIsPressed) return;
+            findAndSetHighlightItem((int) e.getY());
+        }
+
+        @Override
+        public boolean onSingleTapUp(MotionEvent e) {
+            if (!mScrollable) return false;
+            findAndSetHighlightItem((int) e.getY());
+            if (mOnItemSelectedListener != null && mHighlightView != null) {
+                mOnItemSelectedListener
+                        .onItemSelected(mHighlightView, mHighlightIndex);
+            }
+            setHighlightItem(null, INDEX_NONE);
+            return true;
+        }
+    }
+
+}
diff --git a/new3d/src/com/android/gallery3d/ui/GLRootView.java b/new3d/src/com/android/gallery3d/ui/GLRootView.java
index b2afc67..5ad25dc 100644
--- a/new3d/src/com/android/gallery3d/ui/GLRootView.java
+++ b/new3d/src/com/android/gallery3d/ui/GLRootView.java
@@ -274,12 +274,6 @@
         matrix.preScale(1, -1);
     }
 
-    private void drawRect(int x, int y, int width, int height) {
-        float matrix[] = mMatrixValues;
-        mTransformation.getMatrix().getValues(matrix);
-        drawRect(x, y, width, height, matrix);
-    }
-
     private static void putRectangle(float x, float y,
             float width, float height, float[] buffer, FloatBuffer pointer) {
         buffer[0] = x;
@@ -293,6 +287,34 @@
         pointer.put(buffer, 0, 8).position(0);
     }
 
+    public void setColor(int color) {
+        float alpha = mTransformation.getAlpha();
+        mGLState.setBlendEnabled(!Util.isOpaque(color) || alpha < OPAQUE_ALPHA);
+        mGLState.setFragmentColor(color, alpha);
+    }
+
+    public void drawLine(int x1, int y1, int x2, int y2) {
+        float matrix[] = mMatrixValues;
+        mTransformation.getMatrix().getValues(matrix);
+        GL11 gl = mGL;
+        gl.glPushMatrix();
+        gl.glMultMatrixf(toGLMatrix(matrix), 0);
+        float buffer[] = mXyBuffer;
+        buffer[0] = x1;
+        buffer[1] = y1;
+        buffer[2] = x2;
+        buffer[3] = y2;
+        mXyPointer.put(buffer, 0, 4).position(0);
+        gl.glDrawArrays(GL11.GL_LINE_STRIP, 0, 2);
+        gl.glPopMatrix();
+    }
+
+    public void drawRect(int x, int y, int width, int height) {
+        float matrix[] = mMatrixValues;
+        mTransformation.getMatrix().getValues(matrix);
+        drawRect(x, y, width, height, matrix);
+    }
+
     private void drawRect(
             int x, int y, int width, int height, float matrix[]) {
         GL11 gl = mGL;
@@ -597,6 +619,7 @@
             throw new RuntimeException("Cannot support alpha value");
         }
         mGLState.setBlendEnabled(!from.isOpaque() || !to.isOpaque());
+        mGLState.setTextureAlpha(1);
         mGLState.setTexture2DEnabled(true);
 
         final GL11 gl = mGL;
@@ -795,15 +818,16 @@
         }
 
         public void setFragmentColor(int color, float alpha) {
-            setTexture2DEnabled(false);
-            alpha /= 256f;
             // Set mTextureAlpha to an invalid value, so that it will reset
             // again in setTextureAlpha(float) later.
             mTextureAlpha = -1.0f;
-            mGL.glColor4f(
-                    ((color >> 16) & 0xFF) * alpha,
-                    ((color >> 8) & 0xFF) * alpha,
-                    (color & 0xFF) * alpha, (color >>> 24) * alpha);
+
+            setTexture2DEnabled(false);
+            int prealpha = (int) ((color >>> 24) * alpha + 0.5);
+            mGL.glColor4x(
+                    ((color >> 16) & 0xFF) * prealpha,
+                    ((color >> 8) & 0xFF) * prealpha,
+                    (color & 0xFF) * prealpha, prealpha << 8);
         }
 
         public void setTexture2DEnabled(boolean enabled) {
diff --git a/new3d/src/com/android/gallery3d/ui/GLView.java b/new3d/src/com/android/gallery3d/ui/GLView.java
index 76fbf59..0a33f1b 100644
--- a/new3d/src/com/android/gallery3d/ui/GLView.java
+++ b/new3d/src/com/android/gallery3d/ui/GLView.java
@@ -42,7 +42,7 @@
     protected final Rect mPaddings = new Rect();
 
     private GLRootView mRootView;
-    private GLView mParent;
+    protected GLView mParent;
     private ArrayList<GLView> mComponents;
     private GLView mMotionTarget;
 
@@ -233,7 +233,7 @@
         return false;
     }
 
-    private boolean dispatchTouchEvent(MotionEvent event,
+    protected boolean dispatchTouchEvent(MotionEvent event,
             int x, int y, GLView component, boolean checkBounds) {
         Rect rect = component.mBounds;
         int left = rect.left;
@@ -268,7 +268,8 @@
             }
         }
         if (action == MotionEvent.ACTION_DOWN) {
-            for (int i = 0, n = getComponentCount(); i < n; ++i) {
+            // in the reverse rendering order
+            for (int i = getComponentCount() - 1; i >= 0; --i) {
                 GLView component = getComponent(i);
                 if (component.getVisibility() != GLView.VISIBLE) continue;
                 if (dispatchTouchEvent(event, x, y, component, true)) {
diff --git a/new3d/src/com/android/gallery3d/ui/HeadUpDisplay.java b/new3d/src/com/android/gallery3d/ui/HeadUpDisplay.java
new file mode 100644
index 0000000..66b1810
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/HeadUpDisplay.java
@@ -0,0 +1,262 @@
+package com.android.gallery3d.ui;
+
+import static com.android.gallery3d.ui.Util.dpToPixel;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.MotionEvent;
+import android.view.View.MeasureSpec;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class HeadUpDisplay extends GLView {
+
+    private static final int POPUP_WINDOW_OVERLAP = 20;
+    private static final int POPUP_TRIANGLE_OFFSET = 15;
+
+    private static final float MAX_HEIGHT_RATIO = 0.8f;
+    private static final float MAX_WIDTH_RATIO = 0.8f;
+
+    private static int sPopupWindowOverlap = -1;
+    private static int sPopupTriangleOffset;
+
+    private final MenuItemBar mBottomBar;
+    private final MenuBar mTopBar;
+    private final Context mContext;
+    private final NinePatchTexture mHighlight;
+
+    private PopupWindow mPopupWindow;
+    private GLListView mListView;
+
+    private GLView mAnchorView;
+    private final HashMap<MenuItem, MenuAdapter> mContentMap =
+            new HashMap<MenuItem, MenuAdapter>();
+
+    private static void initializeStaticVariables(Context context) {
+        if (sPopupWindowOverlap >= 0) return;
+
+        sPopupWindowOverlap = dpToPixel(context, POPUP_WINDOW_OVERLAP);
+        sPopupTriangleOffset = dpToPixel(context, POPUP_TRIANGLE_OFFSET);
+    }
+
+    public HeadUpDisplay(Context context) {
+        initializeStaticVariables(context);
+
+        mContext = context;
+        mBottomBar = new MenuItemBar();
+        mTopBar = new MenuBar();
+        mHighlight = new NinePatchTexture(context, R.drawable.menu_highlight);
+        mTopBar.setBackground(
+                new NinePatchTexture(context, R.drawable.top_menu_bar_bg));
+        mBottomBar.setBackground(
+                new NinePatchTexture(context, R.drawable.menu_bar_bg));
+        mBottomBar.setOnSelectedListener(new MySelectedListener());
+
+        super.addComponent(mTopBar);
+        super.addComponent(mBottomBar);
+
+        initialize();
+    }
+
+    @Override
+    protected void onLayout(
+            boolean changesize, int left, int top, int right, int bottom) {
+        int width = right - left;
+        int height = bottom - top;
+        mTopBar.measure(
+                MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+                MeasureSpec.UNSPECIFIED);
+        mBottomBar.measure(
+                MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+                MeasureSpec.UNSPECIFIED);
+        mTopBar.layout(0, 0, width, mTopBar.getMeasuredHeight());
+        mBottomBar.layout(
+                0, height - mBottomBar.getMeasuredHeight(), width, height);
+
+        if (mPopupWindow != null
+                && mPopupWindow.getVisibility() == GLView.VISIBLE) {
+            this.layoutPopupWindow(mAnchorView);
+        }
+    }
+
+    private MenuItem addBottomMenuItem(int iconId, int stringId) {
+        MenuItem item = new MenuItem(mContext, iconId, stringId);
+        item.setHighlight(mHighlight);
+        mBottomBar.addComponent(item);
+        return item;
+    }
+
+    private void addTopMenuButton(int stringId) {
+        MenuButton button =
+                new MenuButton(mContext, IconLabel.NULL_ID, stringId);
+        button.setHighlight(mHighlight);
+        mTopBar.addComponent(button);
+    }
+
+    private void initialize() {
+        Context context = mContext;
+        addTopMenuButton(R.string.select_all);
+        mTopBar.addComponent(new IconLabel(
+                context, IconLabel.NULL_ID, R.string.items));
+        addTopMenuButton(R.string.deselect_all);
+
+        MenuItem share = addBottomMenuItem(R.drawable.icon_share, R.string.share);
+        MenuItem delete = addBottomMenuItem(R.drawable.icon_delete, R.string.delete);
+        MenuItem more = addBottomMenuItem(R.drawable.icon_more, R.string.more);
+
+        MenuAdapter deleteMenu = new MenuAdapter(context);
+        deleteMenu.addMenu(R.drawable.icon_delete, R.string.confirm_delete);
+        deleteMenu.addMenu(R.drawable.icon_cancel, R.string.cancel);
+
+        MenuAdapter moreMenu = new MenuAdapter(context);
+        moreMenu.addMenu(R.drawable.icon_details, R.string.details);
+
+        mContentMap.put(share, buildShareMenu(context));
+        mContentMap.put(delete, deleteMenu);
+        mContentMap.put(more, moreMenu);
+    }
+
+    @Override
+    protected boolean onTouch(MotionEvent event) {
+        if (mPopupWindow != null &&
+                mPopupWindow.getVisibility() == GLView.VISIBLE) {
+            mBottomBar.setSelectedItem(null);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    protected boolean dispatchTouchEvent(MotionEvent event,
+            int x, int y, GLView component, boolean checkBounds) {
+        if (mPopupWindow != null
+                && mPopupWindow.getVisibility() == GLView.VISIBLE
+                && component != mPopupWindow && component != mBottomBar) {
+            return false;
+        }
+        return super.dispatchTouchEvent(event, x, y, component, checkBounds);
+    }
+
+    private void layoutPopupWindow(GLView anchorView) {
+        mAnchorView = anchorView;
+
+        Rect rect = new Rect();
+        getBoundsOf(anchorView, rect);
+
+        int width = (int) (getWidth() * MAX_WIDTH_RATIO + .5);
+        int height = (int) (getHeight() * MAX_HEIGHT_RATIO + .5);
+
+        mPopupWindow.measure(
+                MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
+                MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
+
+        width = mPopupWindow.getMeasuredWidth();
+        height = mPopupWindow.getMeasuredHeight();
+
+        int anchorX = (rect.left + rect.right) / 2;
+        int anchorY = rect.top + sPopupWindowOverlap;
+
+        int xoffset = Util.clamp(anchorX - width / 2, 0, getWidth() - width);
+        int yoffset = Math.max(0, anchorY - height);
+
+        mPopupWindow.setAnchorPosition(anchorX - xoffset);
+        mPopupWindow.layout(
+                xoffset, yoffset, xoffset + width, yoffset + height);
+    }
+
+    private void initializePopupWindow() {
+        Context context = mContext;
+        mListView = new GLListView(context);
+        mPopupWindow = new PopupWindow();
+
+        mPopupWindow.setBackground(
+                new NinePatchTexture(context, R.drawable.popup));
+        mPopupWindow.setAnchor(new ResourceTexture(
+                context, R.drawable.popup_triangle_bottom),
+                (int) (sPopupTriangleOffset + 0.5));
+        mListView.setHighLight(new NinePatchTexture(
+                context, R.drawable.popup_option_selected));
+        mPopupWindow.setContent(mListView);
+        mPopupWindow.setVisibility(GLView.INVISIBLE);
+        super.addComponent(mPopupWindow);
+    }
+
+    private MenuAdapter buildShareMenu(Context context) {
+        Intent intent = new Intent(Intent.ACTION_SEND);
+        // TODO: the type should match to the selected items
+        intent.setType("image/jpeg");
+        MenuAdapter menu = new MenuAdapter(context);
+        PackageManager packageManager = mContext.getPackageManager();
+        for(ResolveInfo info
+                : packageManager.queryIntentActivities(intent, 0)) {
+            String label = info.loadLabel(packageManager).toString();
+            Drawable icon = info.loadIcon(packageManager);
+            menu.addMenu(icon, label);
+        }
+        return menu;
+    }
+
+    private class MySelectedListener implements OnSelectedListener {
+
+        public void onSelected(GLView source) {
+            if (source == null) {
+                mPopupWindow.popoff();
+            } else {
+                if (mPopupWindow == null) initializePopupWindow();
+                mListView.setDataModel(mContentMap.get(source));
+                layoutPopupWindow(source);
+                if (mPopupWindow.getVisibility() != GLView.VISIBLE) {
+                    mPopupWindow.popup();
+                }
+            }
+        }
+    }
+
+    private static class MenuAdapter implements GLListView.Model {
+
+        private final ArrayList<IconLabel> mContent = new ArrayList<IconLabel>();
+        private final Context mContext;
+
+        public MenuAdapter(Context context) {
+            mContext = context;
+        }
+
+        public void addMenu(int icon, int title) {
+            mContent.add(new IconLabel(mContext, icon, title));
+        }
+
+        public void addMenu(Drawable icon, String title) {
+
+            DrawableTexture iconTexture = null;
+            if (icon != null) {
+                iconTexture = new DrawableTexture(icon);
+                float target = 45;
+                int width = icon.getIntrinsicWidth();
+                int height = icon.getIntrinsicHeight();
+                float scale = target / Math.max(width, height);
+                iconTexture.setSize((int) (width * scale + .5f),
+                        (int) (height * scale + .5f));
+            }
+
+            mContent.add(new IconLabel(mContext, iconTexture, title));
+        }
+
+        public GLView getView(int index) {
+            return mContent.get(index);
+        }
+
+        public boolean isSelectable(int index) {
+            return true;
+        }
+
+        public int size() {
+            return mContent.size();
+        }
+    }
+}
diff --git a/new3d/src/com/android/gallery3d/ui/IconLabel.java b/new3d/src/com/android/gallery3d/ui/IconLabel.java
new file mode 100644
index 0000000..e1fe13e
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/IconLabel.java
@@ -0,0 +1,123 @@
+package com.android.gallery3d.ui;
+
+import static com.android.gallery3d.ui.Util.dpToPixel;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.view.animation.Transformation;
+
+import javax.microedition.khronos.opengles.GL11;
+
+public class IconLabel extends GLView {
+
+    public static final int NULL_ID = 0;
+
+    private static final int FONT_COLOR = Color.WHITE;
+    private static final float FONT_SIZE = 18;
+
+    private static final int MINIMAL_HEIGHT = 32;
+
+    private static final int NO_ICON_LEADING_SPACE = 10;
+    private static final int TEXT_LEFT_PADDING = 6;
+    private static final int TEXT_RIGHT_PADDING = 10;
+
+    private static final float ENABLED_ALPHA = 1f;
+    private static final float DISABLED_ALPHA = 0.3f;
+
+    private static final int HORIZONTAL_PADDINGS = 4;
+    private static final int VERTICAL_PADDINGS = 2;
+
+    private static int sNoIconLeadingSpace;
+    private static int sTextLeftPadding;
+    private static int sTextRightPadding;
+    private static int sMinimalHeight;
+    private static float sFontSize;
+    private static int sHorizontalPaddings = -1;
+    private static int sVerticalPaddings;
+
+    private final BasicTexture mIcon;
+    private final StringTexture mText;
+    private boolean mEnabled = true;
+
+    private static void initializeStaticVariables(Context context) {
+        if (sHorizontalPaddings >= 0) return;
+
+        sNoIconLeadingSpace = dpToPixel(context, NO_ICON_LEADING_SPACE);
+        sTextLeftPadding = dpToPixel(context, TEXT_LEFT_PADDING);
+        sTextRightPadding = dpToPixel(context, TEXT_RIGHT_PADDING);
+        sMinimalHeight = dpToPixel(context, MINIMAL_HEIGHT);
+        sHorizontalPaddings = dpToPixel(context, HORIZONTAL_PADDINGS);
+        sVerticalPaddings = dpToPixel(context, VERTICAL_PADDINGS);
+
+        sFontSize = dpToPixel(context, FONT_SIZE);
+    }
+
+    public IconLabel(Context context, int iconId, int stringId) {
+        this(context, iconId == NULL_ID
+                ? null : new ResourceTexture(context, iconId),
+                context.getString(stringId));
+        initializeStaticVariables(context);
+    }
+
+    public IconLabel(Context context, BasicTexture icon, String label) {
+        initializeStaticVariables(context);
+        mIcon = icon;
+        mText = StringTexture.newInstance(label, sFontSize, FONT_COLOR);
+        setPaddings(sHorizontalPaddings,
+                sVerticalPaddings, sHorizontalPaddings, sVerticalPaddings);
+    }
+
+    @Override
+    protected void onMeasure(int widthSpec, int heightSpec) {
+        int width = mIcon == null ? sNoIconLeadingSpace : mIcon.getWidth();
+        width += mText.getWidth();
+        width += sTextRightPadding + sTextLeftPadding;
+        int height = Math.max(
+                mText.getHeight(), mIcon == null ? 0 : mIcon.getHeight());
+        height = Math.max(sMinimalHeight, height);
+        new MeasureHelper(this)
+                .setPreferredContentSize(width, height)
+                .measure(widthSpec, heightSpec);
+    }
+
+    @Override
+    protected void render(GLRootView root, GL11 gl) {
+        /*
+         * The layout: |--padding--|--mIcon--|--sTextLeftPadding--|--mText--|
+         *     --mTextRightPadding--|--padding--|
+         */
+        Rect p = mPaddings;
+
+        int width = getWidth() - p.left - p.right;
+        int height = getHeight() - p.top - p.bottom;
+
+        int xoffset = p.left;
+
+        Transformation trans = root.getTransformation();
+        float oldAlpha = trans.getAlpha();
+        trans.setAlpha(oldAlpha * (mEnabled ? ENABLED_ALPHA : DISABLED_ALPHA));
+
+        BasicTexture icon = mIcon;
+        if (icon != null) {
+            icon.draw(root, xoffset,
+                    p.top + (height - icon.getHeight()) / 2);
+            xoffset += icon.getWidth();
+        } else {
+            xoffset += sNoIconLeadingSpace;
+        }
+
+        StringTexture title = mText;
+        xoffset += sTextLeftPadding;
+        int yoffset = p.top + (height - title.getHeight()) / 2;
+        //TODO: cut the text if it is too long
+        title.draw(root, xoffset, yoffset);
+
+        trans.setAlpha(oldAlpha);
+    }
+
+    public void setEnabled(boolean enabled) {
+        if (mEnabled == enabled) return;
+        mEnabled = enabled;
+        invalidate();
+    }
+}
diff --git a/new3d/src/com/android/gallery3d/ui/MeasureHelper.java b/new3d/src/com/android/gallery3d/ui/MeasureHelper.java
new file mode 100644
index 0000000..fe03938
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/MeasureHelper.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.gallery3d.ui;
+
+import android.graphics.Rect;
+import android.view.View.MeasureSpec;
+
+class MeasureHelper {
+
+    private final GLView mComponent;
+    private int mPreferredWidth;
+    private int mPreferredHeight;
+
+    public MeasureHelper(GLView component) {
+        mComponent = component;
+    }
+
+    public MeasureHelper setPreferredContentSize(int width, int height) {
+        mPreferredWidth = width;
+        mPreferredHeight = height;
+        return this;
+    }
+
+    public void measure(int widthSpec, int heightSpec) {
+        Rect p = mComponent.getPaddings();
+        setMeasuredSize(
+                getLength(widthSpec, mPreferredWidth + p.left + p.right),
+                getLength(heightSpec, mPreferredHeight + p.top + p.bottom));
+    }
+
+    private static int getLength(int measureSpec, int prefered) {
+        int specLength = MeasureSpec.getSize(measureSpec);
+        switch(MeasureSpec.getMode(measureSpec)) {
+            case MeasureSpec.EXACTLY: return specLength;
+            case MeasureSpec.AT_MOST: return Math.min(prefered, specLength);
+            default: return prefered;
+        }
+    }
+
+    protected void setMeasuredSize(int width, int height) {
+        mComponent.setMeasuredSize(width, height);
+    }
+
+}
diff --git a/new3d/src/com/android/gallery3d/ui/MenuBar.java b/new3d/src/com/android/gallery3d/ui/MenuBar.java
new file mode 100644
index 0000000..118252b
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/MenuBar.java
@@ -0,0 +1,117 @@
+/*
+ * 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.gallery3d.ui;
+
+import android.graphics.Rect;
+import android.view.View.MeasureSpec;
+
+import javax.microedition.khronos.opengles.GL11;
+
+class MenuBar extends GLView {
+
+    private static final int BORDER_SIZE = 1; // 1 pixel on all devices
+    private static final int BORDER_COLOR = 0x33FFFFFF;
+
+    private NinePatchTexture mBackground;
+
+    public MenuBar() {
+    }
+
+    public void setBackground(NinePatchTexture background) {
+        if (mBackground == background) return;
+        mBackground = background;
+        if (background != null) {
+            setPaddings(background.getPaddings());
+        } else {
+            setPaddings(0, 0, 0, 0);
+        }
+        invalidate();
+    }
+
+    @Override
+    protected void onMeasure(int widthSpec, int heightSpec) {
+        int width = 0;
+        int height = 0;
+        int n = getComponentCount();
+        for (int i = 0; i < n; ++i) {
+            GLView component = getComponent(i);
+            component.measure(
+                    MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+            height = Math.max(height, component.getMeasuredHeight());
+            width += component.getMeasuredWidth();
+        }
+        width += (n - 1) * BORDER_SIZE;
+        height += 2 * BORDER_SIZE;
+        new MeasureHelper(this)
+                .setPreferredContentSize(width, height)
+                .measure(widthSpec, heightSpec);
+    }
+
+    @Override
+    protected void onLayout(
+            boolean changed, int left, int top, int right, int bottom) {
+        Rect p = mPaddings;
+
+        int remainings = right - left - p.left - p.right;
+        int n = getComponentCount();
+        for (int i = 0; i < n; ++i) {
+            GLView component = getComponent(i);
+            component.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+            remainings -= component.getMeasuredWidth();
+        }
+        remainings -= (n - 1) * BORDER_SIZE;
+
+        int layoutLeft = p.left;
+        int layoutTop = p.top + BORDER_SIZE;
+        int layoutBottom = bottom - top - p.bottom - BORDER_SIZE;
+        for (int i = 0; i < n; ++i) {
+            GLView component = getComponent(i);
+            int space = remainings / (n - i);
+            remainings -= space;
+            int width = component.getMeasuredWidth() + space;
+            int layoutRight = layoutLeft + width;
+            component.layout(layoutLeft, layoutTop, layoutRight, layoutBottom);
+            layoutLeft = layoutRight + BORDER_SIZE;
+        }
+    }
+
+    @Override
+    protected void renderBackground(GLRootView root, GL11 gl) {
+        int width = getWidth();
+        int height = getHeight();
+        if (mBackground != null) {
+            mBackground.draw(root, 0, 0, width, height);
+        }
+        Rect p = mPaddings;
+
+        width -= p.left + p.right;
+        height -= p.top + p.bottom;
+
+        int top = p.top;
+        int bottom = top + height;
+        int left = p.left;
+        int right = left + width;
+
+        root.setColor(BORDER_COLOR);
+        root.drawLine(left, top, right, top);
+        root.drawLine(left, bottom - 1, right, bottom -1);
+        for (int i = 0, n = getComponentCount() - 1; i < n; ++i) {
+            Rect bounds = getComponent(i).mBounds;
+            root.drawLine(bounds.right, top, bounds.right, bottom);
+        }
+    }
+}
diff --git a/new3d/src/com/android/gallery3d/ui/MenuButton.java b/new3d/src/com/android/gallery3d/ui/MenuButton.java
new file mode 100644
index 0000000..b2d9eb6
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/MenuButton.java
@@ -0,0 +1,59 @@
+package com.android.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+
+import javax.microedition.khronos.opengles.GL11;
+
+public class MenuButton extends IconLabel {
+
+    private boolean mPressed;
+    private Texture mHighlight;
+    private OnClickedListener mOnClickListener;
+
+    public MenuButton(Context context, int icon, int label) {
+        super(context, icon, label);
+    }
+
+    public void setHighlight(Texture texture) {
+        mHighlight = texture;
+    }
+
+    @SuppressWarnings("fallthrough")
+    @Override
+    protected boolean onTouch(MotionEvent event) {
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mPressed = true;
+                invalidate();
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mOnClickListener != null) {
+                    mOnClickListener.onClicked(this);
+                }
+                // fall-through
+            case MotionEvent.ACTION_CANCEL:
+                mPressed = false;
+                invalidate();
+                break;
+        }
+        return true;
+    }
+
+    @Override
+    protected void render(GLRootView root, GL11 gl) {
+        if (mPressed) {
+            int width = getWidth();
+            int height = getHeight();
+            if (mHighlight instanceof NinePatchTexture) {
+                Rect p = ((NinePatchTexture) mHighlight).getPaddings();
+                mHighlight.draw(root, -p.left, -p.top,
+                        width + p.left + p.right, height + p.top + p.bottom);
+            } else {
+                mHighlight.draw(root, 0, 0, width, height);
+            }
+        }
+        super.render(root, gl);
+    }
+}
diff --git a/new3d/src/com/android/gallery3d/ui/MenuItem.java b/new3d/src/com/android/gallery3d/ui/MenuItem.java
new file mode 100644
index 0000000..7ed5765
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/MenuItem.java
@@ -0,0 +1,41 @@
+package com.android.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+import javax.microedition.khronos.opengles.GL11;
+
+public class MenuItem extends IconLabel {
+    private boolean mSelected;
+    private Texture mHighlight;
+
+    public MenuItem(Context context, int icon, int label) {
+        super(context, icon, label);
+    }
+
+    public void setHighlight(Texture texture) {
+        mHighlight = texture;
+    }
+
+    protected void setSelected(boolean selected) {
+        if (selected == mSelected) return;
+        mSelected = selected;
+        invalidate();
+    }
+
+    @Override
+    protected void render(GLRootView root, GL11 gl) {
+        if (mSelected) {
+            int width = getWidth();
+            int height = getHeight();
+            if (mHighlight instanceof NinePatchTexture) {
+                Rect p = ((NinePatchTexture) mHighlight).getPaddings();
+                mHighlight.draw(root, -p.left, -p.top,
+                        width + p.left + p.right, height + p.top + p.bottom);
+            } else {
+                mHighlight.draw(root, 0, 0, width, height);
+            }
+        }
+        super.render(root, gl);
+    }
+}
diff --git a/new3d/src/com/android/gallery3d/ui/MenuItemBar.java b/new3d/src/com/android/gallery3d/ui/MenuItemBar.java
new file mode 100644
index 0000000..e0667f7
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/MenuItemBar.java
@@ -0,0 +1,60 @@
+package com.android.gallery3d.ui;
+
+import android.view.MotionEvent;
+
+public class MenuItemBar extends MenuBar {
+    public static final int INDEX_NONE = -1;
+
+    private OnSelectedListener mOnSelectedListener;
+    private MenuItem mSelectedItem;
+    private boolean mSelectionChanged = false;
+
+    public void setSelectedItem(MenuItem source) {
+        if (mSelectedItem == source) return;
+        mSelectionChanged = true;
+        if (mSelectedItem != null) mSelectedItem.setSelected(false);
+        mSelectedItem = source;
+        if (mSelectedItem != null) mSelectedItem.setSelected(true);
+
+        if (mOnSelectedListener != null) {
+            mOnSelectedListener.onSelected(mSelectedItem);
+        }
+    }
+
+    public void setOnSelectedListener(OnSelectedListener listener) {
+        mOnSelectedListener = listener;
+    }
+
+    @Override
+    protected boolean dispatchTouchEvent(MotionEvent event) {
+        // do not dispatch to children
+        return onTouch(event);
+    }
+
+    @SuppressWarnings("fallthrough")
+    @Override
+    protected boolean onTouch(MotionEvent event) {
+        int x = (int) event.getX();
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mSelectionChanged = false;
+                // fall-through
+            case MotionEvent.ACTION_MOVE:
+                for (int i = 0, n = getComponentCount(); i < n; ++i) {
+                    GLView component = getComponent(i);
+                    if (x <= component.mBounds.right) {
+                        setSelectedItem((MenuItem) component);
+                        return true;
+                    }
+                }
+                setSelectedItem(null);
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mSelectionChanged == false) {
+                    setSelectedItem(null);
+                }
+        }
+        return true;
+
+    }
+}
diff --git a/new3d/src/com/android/gallery3d/ui/NinePatchTexture.java b/new3d/src/com/android/gallery3d/ui/NinePatchTexture.java
index 318b500..e5a980b 100644
--- a/new3d/src/com/android/gallery3d/ui/NinePatchTexture.java
+++ b/new3d/src/com/android/gallery3d/ui/NinePatchTexture.java
@@ -38,7 +38,10 @@
                 mContext.getResources(), mResId, options);
         mBitmap = bitmap;
         setSize(bitmap.getWidth(), bitmap.getHeight());
-        mChunk = NinePatchChunk.deserialize(bitmap.getNinePatchChunk());
+        byte[] chunkData = bitmap.getNinePatchChunk();
+        mChunk = chunkData == null
+                ? null
+                : NinePatchChunk.deserialize(bitmap.getNinePatchChunk());
         if (mChunk == null) {
             throw new RuntimeException("invalid nine-patch image: " + mResId);
         }
diff --git a/new3d/src/com/android/gallery3d/ui/OnClickedListener.java b/new3d/src/com/android/gallery3d/ui/OnClickedListener.java
new file mode 100644
index 0000000..2fe6f47
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/OnClickedListener.java
@@ -0,0 +1,5 @@
+package com.android.gallery3d.ui;
+
+public interface OnClickedListener {
+    public void onClicked(GLView source);
+}
diff --git a/new3d/src/com/android/gallery3d/ui/OnSelectedListener.java b/new3d/src/com/android/gallery3d/ui/OnSelectedListener.java
new file mode 100644
index 0000000..8dd0e5f
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/OnSelectedListener.java
@@ -0,0 +1,5 @@
+package com.android.gallery3d.ui;
+
+public interface OnSelectedListener {
+    public void onSelected(GLView source);
+}
diff --git a/new3d/src/com/android/gallery3d/ui/OverlayLayout.java b/new3d/src/com/android/gallery3d/ui/OverlayLayout.java
index d7c716f..ec691d5 100644
--- a/new3d/src/com/android/gallery3d/ui/OverlayLayout.java
+++ b/new3d/src/com/android/gallery3d/ui/OverlayLayout.java
@@ -3,9 +3,10 @@
 public class OverlayLayout extends GLView {
 
     @Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        int width = r - l;
-        int height = b - t;
+    protected void onLayout(
+            boolean changed, int left, int top, int right, int bottom) {
+        int width = right - left;
+        int height = bottom - top;
         for (int i = 0, n = getComponentCount(); i < n; ++i) {
             GLView component = getComponent(i);
             component.layout(0, 0, width, height);
diff --git a/new3d/src/com/android/gallery3d/ui/PopupWindow.java b/new3d/src/com/android/gallery3d/ui/PopupWindow.java
new file mode 100644
index 0000000..a9e5a5a
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/PopupWindow.java
@@ -0,0 +1,223 @@
+/*
+ * 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.gallery3d.ui;
+
+import android.graphics.Rect;
+import android.util.Log;
+import android.view.View.MeasureSpec;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.OvershootInterpolator;
+import android.view.animation.ScaleAnimation;
+
+import javax.microedition.khronos.opengles.GL11;
+
+class PopupWindow extends GLView {
+    private static final String TAG = "PopupWindow";
+
+    protected BasicTexture mAnchor;
+    protected int mAnchorOffset;
+
+    protected int mAnchorPosition;
+    private RawTexture mBackupTexture;
+    private GLView mContent;
+
+    protected Texture mBackground;
+    private boolean mUsingStencil;
+
+    public PopupWindow() {
+    }
+
+    @Override
+    protected void onAttachToRoot(GLRootView root) {
+        super.onAttachToRoot(root);
+        mUsingStencil = root.getEGLConfigChooser().getStencilBits() > 0;
+    }
+
+    public void setBackground(Texture background) {
+        if (background == mBackground) return;
+        mBackground = background;
+        if (background != null && background instanceof NinePatchTexture) {
+            setPaddings(((NinePatchTexture) mBackground).getPaddings());
+        } else {
+            setPaddings(0, 0, 0, 0);
+        }
+        invalidate();
+    }
+
+    public void setAnchor(BasicTexture anchor, int offset) {
+        mAnchor = anchor;
+        mAnchorOffset = offset;
+    }
+
+    @Override
+    public void addComponent(GLView component) {
+        throw new UnsupportedOperationException("use setContent(GLView)");
+    }
+
+    @Override
+    protected void onMeasure(int widthSpec, int heightSpec) {
+        Rect p = mPaddings;
+
+        int widthMode = MeasureSpec.getMode(widthSpec);
+        if (widthMode != MeasureSpec.UNSPECIFIED) {
+            int width = MeasureSpec.getSize(widthSpec);
+            widthSpec = MeasureSpec.makeMeasureSpec(
+                    Math.max(0, width - p.left - p.right), widthMode);
+        }
+
+        int heightMode = MeasureSpec.getMode(heightSpec);
+        if (heightMode != MeasureSpec.UNSPECIFIED) {
+            int height = MeasureSpec.getSize(heightSpec);
+            heightSpec = MeasureSpec.makeMeasureSpec(
+                    Math.max(0, height - p.top - p.bottom
+                    - mAnchor.getHeight() + mAnchorOffset), heightMode);
+        }
+
+        GLView child = mContent;
+        child.measure(widthSpec, heightSpec);
+
+        setMeasuredSize(child.getMeasuredWidth() + p.left + p.right,
+                child.getMeasuredHeight() + p.top + p.bottom
+                + mAnchor.getHeight() - mAnchorOffset);
+    }
+
+    @Override
+    protected void onLayout(
+            boolean change, int left, int top, int right, int bottom) {
+        Rect p = getPaddings();
+        GLView view = mContent;
+        view.layout(p.left, p.top, getWidth() - p.right,
+                getHeight() - p.bottom - mAnchor.getHeight() + mAnchorOffset);
+    }
+
+    public void setAnchorPosition(int xoffset) {
+        mAnchorPosition = xoffset;
+    }
+
+    private void renderBackgroundWithStencil(GLRootView root, GL11 gl) {
+        int width = getWidth();
+        int height = getHeight();
+        int aWidth = mAnchor.getWidth();
+        int aHeight = mAnchor.getHeight();
+
+        Rect p = mPaddings;
+
+        int aXoffset = Util.clamp(mAnchorPosition - aWidth / 2,
+                p.left, width - p.right - aWidth);
+        int aYoffset = height - aHeight;
+
+        if (mAnchor != null) {
+            gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_REPLACE);
+            gl.glStencilFunc(GL11.GL_ALWAYS, 1, 1);
+            mAnchor.draw(root, aXoffset, aYoffset);
+            gl.glStencilFunc(GL11.GL_NOTEQUAL, 1, 1);
+            gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP);
+        }
+
+        if (mBackground != null) {
+            mBackground.draw(root, 0, 0,
+                    width, height - aHeight + mAnchorOffset);
+        }
+    }
+
+    private void renderBackgroundWithoutStencil(GLRootView root, GL11 gl) {
+        int width = getWidth();
+        int height = getHeight();
+        int aWidth = mAnchor.getWidth();
+        int aHeight = mAnchor.getHeight();
+
+        Rect p = mPaddings;
+
+        int aXoffset = Util.clamp(mAnchorPosition - aWidth / 2,
+                p.left, width - p.right - aWidth);
+        int aYoffset = height - aHeight;
+
+        if (mAnchor != null) {
+            mAnchor.draw(root, aXoffset, aYoffset);
+        }
+
+        if (mBackupTexture == null || mBackupTexture.getBoundGL() != gl) {
+            mBackupTexture = RawTexture.newInstance(gl);
+        }
+
+        RawTexture backup = mBackupTexture;
+        try {
+            // Copy the current drawing results of the triangle area into
+            // "backup", so that we can restore the content after it is
+            // overlaid by the background.
+            root.copyTexture2D(backup, aXoffset, aYoffset, aWidth, aHeight);
+        } catch (GLOutOfMemoryException e) {
+            Log.e(TAG, "out of memory", e);
+        }
+
+        if (mBackground != null) {
+            mBackground.draw(root, 0, 0,
+                    width, height - aHeight + mAnchorOffset);
+        }
+
+        gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ZERO);
+        backup.drawBack(root, aXoffset, aYoffset, aWidth, aHeight);
+        gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA);
+    }
+
+    @Override
+    protected void renderBackground(GLRootView root, GL11 gl) {
+        if (mUsingStencil) {
+            renderBackgroundWithStencil(root, gl);
+        } else {
+            renderBackgroundWithoutStencil(root, gl);
+        }
+    }
+
+    public void setContent(GLView content) {
+        if (mContent != null) {
+            super.removeComponent(mContent);
+        }
+        mContent = content;
+        super.addComponent(content);
+    }
+
+    @Override
+    public void clearComponents() {
+        throw new UnsupportedOperationException();
+    }
+
+    public void popup() {
+        setVisibility(GLView.VISIBLE);
+
+        AnimationSet set = new AnimationSet(false);
+        Animation scale = new ScaleAnimation(
+                0.7f, 1f, 0.7f, 1f, mAnchorPosition, getHeight());
+        Animation alpha = new AlphaAnimation(0.5f, 1.0f);
+
+        set.addAnimation(scale);
+        set.addAnimation(alpha);
+        scale.setDuration(150);
+        alpha.setDuration(100);
+        scale.setInterpolator(new OvershootInterpolator());
+        startAnimation(set);
+    }
+
+    public void popoff() {
+        setVisibility(GLView.INVISIBLE);
+        Animation alpha = new AlphaAnimation(0.7f, 0.0f);
+        alpha.setDuration(100);
+        startAnimation(alpha);
+    }
+}
diff --git a/new3d/src/com/android/gallery3d/ui/TaggleButton.java b/new3d/src/com/android/gallery3d/ui/TaggleButton.java
new file mode 100644
index 0000000..b703c51
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/TaggleButton.java
@@ -0,0 +1,5 @@
+package com.android.gallery3d.ui;
+
+public class TaggleButton {
+
+}