/*
* Copyright (C) 2009 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.cooliris.media;
import java.util.HashMap;
import javax.microedition.khronos.opengles.GL11;
import android.content.Context;
import android.view.MotionEvent;
import com.cooliris.app.App;
import com.cooliris.app.Res;
public final class MenuBar extends Layer implements PopupMenu.Listener {
public static final int HEIGHT = 45;
public static final StringTexture.Config MENU_TITLE_STYLE_TEXT = new StringTexture.Config();
private static final StringTexture.Config MENU_TITLE_STYLE = new StringTexture.Config();
private static final int MENU_HIGHLIGHT_EDGE_WIDTH = 21;
private static final int MENU_HIGHLIGHT_EDGE_INSET = 9;
private static final long LONG_PRESS_THRESHOLD_MS = 350;
private static final int HIT_TEST_MARGIN = 15;
static {
MENU_TITLE_STYLE.fontSize = 17 * App.PIXEL_DENSITY;
MENU_TITLE_STYLE.sizeMode = StringTexture.Config.SIZE_EXACT;
MENU_TITLE_STYLE.overflowMode = StringTexture.Config.OVERFLOW_FADE;
MENU_TITLE_STYLE_TEXT.fontSize = 15 * App.PIXEL_DENSITY;
MENU_TITLE_STYLE_TEXT.xalignment = StringTexture.Config.ALIGN_HCENTER;
MENU_TITLE_STYLE_TEXT.sizeMode = StringTexture.Config.SIZE_EXACT;
MENU_TITLE_STYLE_TEXT.overflowMode = StringTexture.Config.OVERFLOW_FADE;
}
private boolean mNeedsLayout = false;
private Menu[] mMenus = {};
private int mTouchMenu = -1;
private int mTouchMenuItem = -1;
private boolean mTouchActive = false;
private boolean mTouchOverMenu = false;
private final PopupMenu mSubmenu;
private static final int BACKGROUND = Res.drawable.selection_menu_bg;
private static final int SEPERATOR = Res.drawable.selection_menu_divider;
private static final int MENU_HIGHLIGHT_LEFT = Res.drawable.selection_menu_bg_pressed_left;
private static final int MENU_HIGHLIGHT_MIDDLE = Res.drawable.selection_menu_bg_pressed;
private static final int MENU_HIGHLIGHT_RIGHT = Res.drawable.selection_menu_bg_pressed_right;
private final HashMap<String, Texture> mTextureMap = new HashMap<String, Texture>();
private GL11 mGL;
private boolean mSecondTouch;
public MenuBar(Context context) {
mSubmenu = new PopupMenu(context);
mSubmenu.setListener(this);
}
public Menu[] getMenus() {
return mMenus;
}
public void setMenus(Menu[] menus) {
mMenus = menus;
mNeedsLayout = true;
}
public void updateMenu(Menu menu, int index) {
mMenus[index] = menu;
mNeedsLayout = true;
}
@Override
protected void onHiddenChanged() {
if (mHidden) {
mSubmenu.close(false);
}
}
@Override
protected void onSizeChanged() {
mNeedsLayout = true;
}
@Override
public void generate(RenderView view, RenderView.Lists lists) {
lists.blendedList.add(this);
lists.hitTestList.add(this);
lists.systemList.add(this);
lists.updateList.add(this);
mSubmenu.generate(view, lists);
}
@Override
public void renderBlended(RenderView view, GL11 gl) {
// Layout if needed.
if (mNeedsLayout) {
layoutMenus();
mNeedsLayout = false;
}
if (mGL != gl) {
mTextureMap.clear();
mGL = gl;
}
// Draw the background.
Texture background = view.getResource(BACKGROUND);
int backgroundHeight = background.getHeight();
int menuHeight = (int) (HEIGHT * App.PIXEL_DENSITY + 0.5f);
int extra = background.getHeight() - menuHeight;
view.draw2D(background, mX, mY - extra, mWidth, backgroundHeight);
// Draw the separators.
Menu[] menus = mMenus;
int numMenus = menus.length;
int y = (int) mY;
if (view.bind(view.getResource(SEPERATOR))) {
for (int i = 1; i < numMenus; ++i) {
view.draw2D(menus[i].x, y, 0, 1, menuHeight);
}
}
// Draw the selection / focus highlight.
int touchMenu = mTouchMenu;
if (canDrawHighlight()) {
drawHighlight(view, gl, touchMenu);
}
// Draw labels.
float height = mHeight;
for (int i = 0; i != numMenus; ++i) {
// Draw the icon and title.
Menu menu = menus[i];
ResourceTexture icon = view.getResource(menu.icon);
StringTexture titleTexture = (StringTexture) mTextureMap.get(menu.title);
if (titleTexture == null) {
titleTexture = new StringTexture(menu.title, menu.config, menu.titleWidth, MENU_TITLE_STYLE.height);
view.loadTexture(titleTexture);
menu.titleTexture = titleTexture;
mTextureMap.put(menu.title, titleTexture);
}
int iconWidth = icon != null ? icon.getWidth() : 0;
int width = iconWidth + menu.titleWidth;
int offset = (menu.mWidth - width) / 2;
if (icon != null) {
float iconY = y + (height - icon.getHeight()) / 2;
view.draw2D(icon, menu.x + offset, iconY);
}
float titleY = y + (height - MENU_TITLE_STYLE.height) / 2 + 1;
view.draw2D(titleTexture, menu.x + offset + iconWidth, titleY);
}
}
private void drawHighlight(RenderView view, GL11 gl, int touchMenu) {
Texture highlightLeft = view.getResource(MENU_HIGHLIGHT_LEFT);
Texture highlightMiddle = view.getResource(MENU_HIGHLIGHT_MIDDLE);
Texture highlightRight = view.getResource(MENU_HIGHLIGHT_RIGHT);
int height = highlightLeft.getHeight();
int extra = height - (int) (HEIGHT * App.PIXEL_DENSITY);
Menu menu = mMenus[touchMenu];
int x = menu.x + (int) (MENU_HIGHLIGHT_EDGE_INSET * App.PIXEL_DENSITY);
int width = menu.mWidth - (int) ((MENU_HIGHLIGHT_EDGE_INSET * 2) * App.PIXEL_DENSITY);
int y = (int) mY - extra;
// Draw left edge.
view.draw2D(highlightLeft, x - MENU_HIGHLIGHT_EDGE_WIDTH * App.PIXEL_DENSITY, y, MENU_HIGHLIGHT_EDGE_WIDTH
* App.PIXEL_DENSITY, height);
// Draw middle.
view.draw2D(highlightMiddle, x, y, width, height);
// Draw right edge.
view.draw2D(highlightRight, x + width, y, MENU_HIGHLIGHT_EDGE_WIDTH * App.PIXEL_DENSITY, height);
}
private int hitTestMenu(int x, int y) {
if (y > mY - HIT_TEST_MARGIN * App.PIXEL_DENSITY) {
Menu[] menus = mMenus;
for (int i = menus.length - 1; i >= 0; --i) {
if (x > menus[i].x) {
if (menus[i].onSelect != null || menus[i].options != null || menus[i].onSingleTapUp != null) {
return i;
} else {
return -1;
}
}
}
}
return -1;
}
private void selectMenu(int index) {
int oldIndex = mTouchMenu;
if (oldIndex != index) {
// Notify on deselect.
Menu[] menus = mMenus;
if (oldIndex != -1) {
Menu oldMenu = menus[oldIndex];
if (oldMenu.onDeselect != null) {
oldMenu.onDeselect.run();
}
}
// Select the new menu.
mTouchMenu = index;
mTouchMenuItem = -1;
// Show the submenu for the selected menu if one is provided.
PopupMenu submenu = mSubmenu;
boolean didShow = false;
if (index != -1) {
// Notify on select.
Menu menu = mMenus[index];
if (menu.onSelect != null) {
menu.onSelect.run();
}
// Show the popup menu if options are provided.
PopupMenu.Option[] options = menu.options;
if (options != null) {
int x = (int) mX + menu.x + menu.mWidth / 2;
int y = (int) mY;
didShow = true;
submenu.setOptions(options);
submenu.showAtPoint(x, y, (int) mWidth, (int) mHeight);
}
}
if (!didShow) {
submenu.close(true);
}
}
}
public void close() {
int oldIndex = mTouchMenu;
if (oldIndex != -1) {
// Notify on deselect.
Menu[] menus = mMenus;
if (oldIndex != -1) {
Menu oldMenu = menus[oldIndex];
if (oldMenu.onDeselect != null) {
oldMenu.onDeselect.run();
}
}
oldIndex = -1;
}
selectMenu(-1);
if (mSubmenu != null)
mSubmenu.close(false);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int hit = hitTestMenu(x, y);
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mTouchActive = true;
if (mTouchMenu == hit) {
mSecondTouch = true;
} else {
mSecondTouch = false;
}
case MotionEvent.ACTION_MOVE:
// Determine which menu the touch is over.
if (hit != -1) {
// Select the menu and invoke the action.
selectMenu(hit);
mTouchOverMenu = true;
} else {
// Forward events outside the menubar to the active popup menu.
mTouchOverMenu = false;
}
mSubmenu.onTouchEvent(event);
break;
case MotionEvent.ACTION_UP:
if (mTouchMenu == hit && mSecondTouch) {
mSubmenu.close(true);
mTouchMenu = -1;
break;
}
// Forward event to submenu.
mSubmenu.onTouchEvent(event);
// Leave the submenu open if the touch ends on the menu button in
// less than
// a time threshold.
long elapsed = event.getEventTime() - event.getDownTime();
if (hit != -1) {
// Notify on single tap.
Menu menu = mMenus[hit];
if (menu.onSingleTapUp != null) {
menu.onSingleTapUp.run();
}
if (menu.options == null)
selectMenu(-1);
} else if (elapsed > LONG_PRESS_THRESHOLD_MS) {
selectMenu(-1);
}
break;
case MotionEvent.ACTION_CANCEL:
// Always deselect if canceled.
selectMenu(-1);
break;
}
return true;
}
private boolean canDrawHighlight() {
return mTouchMenu != -1 && mTouchMenuItem == -1 && (!mTouchActive || mTouchOverMenu);
}
private void layoutMenus() {
mTextureMap.clear();
Menu[] menus = mMenus;
int numMenus = menus.length;
// we do the best attempt to fit the menu items and resize them
// also, it tries to minimize different sized menu items
// it finds the maximum width for a set of menu items, and checks
// whether that width
// can be used for all the cells, else, it goes to the next maximum
// width, so on and
// so forth
if (numMenus != 0) {
float viewWidth = mWidth;
int occupiedWidth = 0;
int previousMaxWidth = Integer.MAX_VALUE;
int totalDesiredWidth = 0;
for (int i = 0; i < numMenus; i++) {
totalDesiredWidth += menus[i].computeRequiredWidth();
}
if (totalDesiredWidth > viewWidth) {
// Just split the menus up by available size / nr of menus.
int widthPerMenu = (int) Math.floor(viewWidth / numMenus);
int x = 0;
for (int i = 0; i < numMenus; i++) {
Menu menu = menus[i];
menu.x = x;
menu.mWidth = widthPerMenu;
menu.titleWidth = widthPerMenu - (20 + (menu.icon != 0 ? 45 : 0)); // TODO
// factor
// out
// padding
// etc
// fix up rounding errors by adding the last pixel to the
// last menu.
if (i == numMenus - 1) {
menu.mWidth = (int) viewWidth - x;
}
x += widthPerMenu;
}
} else {
boolean foundANewMaxWidth = true;
int menusProcessed = 0;
while (foundANewMaxWidth && menusProcessed < numMenus) {
foundANewMaxWidth = false;
int maxWidth = 0;
for (int i = 0; i < numMenus; ++i) {
int width = menus[i].computeRequiredWidth();
if (width > maxWidth && width < previousMaxWidth) {
foundANewMaxWidth = true;
maxWidth = width;
}
}
// can all the menus have this width
int cumulativeWidth = maxWidth * (numMenus - menusProcessed) + occupiedWidth;
if (cumulativeWidth < viewWidth || !foundANewMaxWidth || menusProcessed == numMenus - 1) {
float delta = (viewWidth - cumulativeWidth) / numMenus;
if (delta < 0) {
delta = 0;
}
int x = 0;
for (int i = 0; i < numMenus; ++i) {
Menu menu = menus[i];
menu.x = x;
float width = menus[i].computeRequiredWidth();
if (width < maxWidth) {
width = maxWidth + delta;
} else {
width += delta;
}
menu.mWidth = (int) width;
menu.titleWidth = StringTexture.computeTextWidthForConfig(menu.title, menu.config); // (int)menus[i].title.computeTextWidth();
x += width;
}
break;
} else {
++menusProcessed;
previousMaxWidth = maxWidth;
occupiedWidth += maxWidth;
}
}
}
}
}
public static final class Menu {
public final String title;
public StringTexture titleTexture = null;
public int titleWidth = 0;
public final StringTexture.Config config;
public final int icon;
public final Runnable onSelect;
public final Runnable onDeselect;
public final Runnable onSingleTapUp;
public final boolean resizeToAccomodate;
public PopupMenu.Option[] options;
private int x;
private int mWidth;
private static final float ICON_WIDTH = 45.0f;
public static final class Builder {
private final String title;
private StringTexture.Config config;
private int icon = 0;
private Runnable onSelect = null;
private Runnable onDeselect = null;
private Runnable onSingleTapUp = null;
private PopupMenu.Option[] options = null;
private boolean resizeToAccomodate;
public Builder(String title) {
this.title = title;
config = MENU_TITLE_STYLE;
}
public Builder config(StringTexture.Config config) {
this.config = config;
return this;
}
public Builder resizeToAccomodate() {
this.resizeToAccomodate = true;
return this;
}
public Builder icon(int icon) {
this.icon = icon;
return this;
}
public Builder onSelect(Runnable onSelect) {
this.onSelect = onSelect;
return this;
}
public Builder onDeselect(Runnable onDeselect) {
this.onDeselect = onDeselect;
return this;
}
public Builder onSingleTapUp(Runnable onSingleTapUp) {
this.onSingleTapUp = onSingleTapUp;
return this;
}
public Builder options(PopupMenu.Option[] options) {
this.options = options;
return this;
}
public Menu build() {
return new Menu(this);
}
}
private Menu(Builder builder) {
config = builder.config;
title = builder.title; // new StringTexture(builder.title, config);
icon = builder.icon;
onSelect = builder.onSelect;
onDeselect = builder.onDeselect;
onSingleTapUp = builder.onSingleTapUp;
options = builder.options;
resizeToAccomodate = builder.resizeToAccomodate;
}
public int computeRequiredWidth() {
int width = 0;
if (icon != 0) {
width += (ICON_WIDTH); // * App.PIXEL_DENSITY);
}
if (title != null) {
width += StringTexture.computeTextWidthForConfig(title, config);// title.computeTextWidth();
}
// pad it
width += 20;
if (width < HEIGHT)
width = HEIGHT;
return width;
}
}
public void onSelectionChanged(PopupMenu menu, int selectedIndex) {
mTouchMenuItem = selectedIndex;
}
public void onSelectionClicked(PopupMenu menu, int selectedIndex) {
selectMenu(-1);
}
}