/*******************************************************************************
* Copyright (c) 2007, 2008 Gregory Jordan
*
* This file is part of PhyloWidget.
*
* PhyloWidget is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 2 of the License, or (at your option) any later
* version.
*
* PhyloWidget is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* PhyloWidget. If not, see <http://www.gnu.org/licenses/>.
*/
package org.andrewberman.ui.menu;
import java.awt.BasicStroke;
import java.awt.Cursor;
import java.awt.Stroke;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import org.andrewberman.ui.Action;
import org.andrewberman.ui.Color;
import org.andrewberman.ui.Point;
import org.andrewberman.ui.Shortcut;
import org.andrewberman.ui.UIEvent;
import org.andrewberman.ui.UIPlatform;
import org.andrewberman.ui.UIUtils;
import org.andrewberman.ui.ifaces.Malleable;
import org.andrewberman.ui.ifaces.Positionable;
import org.andrewberman.ui.ifaces.Sizable;
import org.andrewberman.ui.ifaces.UIObject;
import processing.core.PFont;
/**
* The <code>MenuItem</code> class is the base class for ALL objects in the
* <code>Menu</code> package. Its main purpose is to provide the recursive
* functions necessary for managing a tree-like menu structure.
* <p>
* If you are interested in designing a new type of menu based on this
* structure, then you need to (a) create a <code>Menu</code> subclass and (b)
* create a <code>MenuItem</code> subclass. Your new <code>Menu</code>
* subclass will handle the root-level layout and logic handling, while your
* <code>MenuItem</code> subclass should draw itself and layout any sub-items
* it may have. See the examples within this package, all of which were designed
* in this way.
* <p>
*
* @author Greg
*/
public abstract class MenuItem implements Positionable, Sizable, Malleable,
UIObject
{
static class VisibleDepthComparator implements Comparator
{
public int compare(Object o1, Object o2)
{
MenuItem i1 = (MenuItem) o1;
MenuItem i2 = (MenuItem) o2;
int d1 = maxDepth(i1);
int d2 = maxDepth(i2);
if (d1 > d2)
return -1;
if (d1 < d2)
return 1;
return 0;
}
int maxDepth(MenuItem item)
{
int max = 0;
if (!item.isOpen())
{
max = 0;
} else
{
for (int i = 0; i < item.items.size(); i++)
{
MenuItem child = (MenuItem) item.items.get(i);
int childDepth = maxDepth(child);
if (childDepth + 1 > max)
max = childDepth + 1;
}
}
return max;
}
}
public static class ZDepthComparator implements Comparator
{
public int compare(Object o1, Object o2)
{
MenuItem i1 = (MenuItem) o1;
MenuItem i2 = (MenuItem) o2;
int z1 = i1.getZ();
int z2 = i2.getZ();
if (z1 > z2)
return 1;
if (z1 < z2)
return -1;
return 0;
}
}
public static final int DISABLED = 3;
public static final int DOWN = 2;
public static final int OVER = 1;
// protected static MenuTimer timer;
/**
* Constants that define the values for the "state" field.
*/
public static final int UP = 0;
protected static ZDepthComparator zComp;
protected Action action;
private boolean enabled = true;
protected boolean isOpen;
protected ArrayList<MenuItem> items;
private MenuStyle style;
protected Menu menu;
protected boolean mouseInside;
protected String name;
protected MenuTimer timer;
protected Menu nearestMenu;
protected boolean needsZSort;
protected MenuItem parent;
protected Shortcut shortcut;
protected int state;
protected float width, height;
protected float x, y;
protected boolean drawnOnce;
protected int z;
/**
* The same items as above, but z-sorted for hit detection and drawing
* purposes.
*/
protected ArrayList<MenuItem> zSortedItems;
public MenuItem()
{
this.name = new String();
items = new ArrayList<MenuItem>(1);
zSortedItems = new ArrayList<MenuItem>(1);
style = new MenuStyle();
timer = UIPlatform.getInstance().getThisAppContext().getMenuTimer();
}
ArrayList<MenuItem> itemsToAdd = new ArrayList();
private ConditionChecker condition;
protected boolean hidden;
public MenuItem add(MenuItem item)
{
items.add(item);
/*
* Add this item to the zSortedList and re-sort.
*/
zSortedItems.add(item);
zSort();
/*
* Set the sub-item's parent to this item, and its menu to our menu.
*/
item.setParent(this);
// item.setMenu(menu);
/*
* Layout the entire menu so things look nice.
*/
if (menu != null)
menu.layout();
return item;
}
public MenuItem add(String newLabel)
{
return add(create(newLabel));
}
public void dispose()
{
for (MenuItem i : items)
{
i.dispose();
}
}
protected void calcPreferredSize()
{
}
/**
* Shows this MenuItem and its direct sub-MenuItems.
*/
// private void open()
// {
//// if (isOpen)
//// close();
//// isOpen = true;
// menu.open(this);
// }
// private void close()
// {
//// for (int i = 0; i < items.size(); i++)
//// {
//// MenuItem item = (MenuItem) items.get(i);
//// item.close();
//// }
//// isOpen = false;
// menu.close(this);
// }
public void closeMyChildren()
{
for (int i = 0; i < items.size(); i++)
{
MenuItem item = (MenuItem) items.get(i);
menu.close(item);
}
}
/**
* Subclasses should return true if the point is contained within their
* mouse-responsive area.
*
* @param p
* a Point (in model coordinates) representing the mouse.
* @return true if this MenuItem contains the point, false if not.
*/
abstract protected boolean containsPoint(Point p);
/**
* Creates a MenuItem that this Menu can have added to it. Subclassers
* should implement this method to create a new top-level Menuitem, i.e.
* your DinnerMenu object should create and return a DinnerMenuItem that
* could then be inserted into your DinnerMenu using add(MenuItem).
*
* @param label
* the label of the MenuItem to be created
* @return a MenuItem that is compatible with the current Menu instance.
*/
public MenuItem create(String label)
{
if (nearestMenu != null)
return nearestMenu.create(label);
else if (menu != null)
return menu.create(label);
else
throw new RuntimeException("Error in MenuItem.create(String label)");
}
/**
* Draws this MenuItem to the current root menu's PGraphics object.
*/
public synchronized void draw()
{
if (hidden)
return;
drawMyself();
if (!isOpen())
return;
if (needsZSort)
{
zSort();
needsZSort = false;
}
/*
* Here's where the zSorted items come in handy. Draw items in order of
* the zSortedItems list, so that items on "top" (i.e. with the lowest z
* value) draw last.
*/
drawBefore();
for (int i = 0; i < zSortedItems.size(); i++)
{
MenuItem seg = (MenuItem) zSortedItems.get(i);
seg.draw();
}
drawAfter();
drawnOnce = true;
}
protected void drawAfter()
{
// Do nothing.
}
protected void drawBefore()
{
// Do nothing.
}
protected void drawMyself()
{
}
/**
* Searches through the sub-item tree for the MenuItem with a given name.
*
* @param search
* the name to search with.
* @return
*/
public MenuItem get(String search)
{
if (getName().equals(search))
return this;
else
{
for (int i = 0; i < items.size(); i++)
{
MenuItem mightBeNull = ((MenuItem) items.get(i)).get(search);
if (mightBeNull != null)
return mightBeNull;
}
}
return null;
}
public Action getAction()
{
return action;
}
/**
* If true, this menu item will hide itself when its action is performed. If
* false, it will remain open.
*/
public boolean getCloseOnAction()
{
return true;
}
public PFont getFont()
{
return getStyle().getFont("font");
}
public float getFontSize()
{
return getStyle().getF("f.fontSize");
}
public float getHeight()
{
return height;
}
protected float getMaxHeight()
{
float max = 0;
for (int i = 0; i < items.size(); i++)
{
MenuItem item = (MenuItem) items.get(i);
float curHeight = item.getHeight();
if (curHeight > max)
max = curHeight;
}
return max;
}
/**
* Calculates the maximum width among this MenuItem's sub-items.
*
* @return the maximum width of the MenuItems in the "items" arraylist.
*/
protected float getMaxWidth()
{
float max = 0;
for (int i = 0; i < items.size(); i++)
{
MenuItem item = (MenuItem) items.get(i);
float curWidth = item.width;
if (curWidth > max)
max = curWidth;
}
return max;
}
public MenuItem getMenu()
{
return menu;
}
public String getName()
{
return name;
}
public Menu getNearestMenu()
{
return nearestMenu;
}
public MenuItem loadNearestMenu()
{
// if (nearestMenu != null) return nearestMenu;
MenuItem item = this;
while (item != null)
{
if (item instanceof Menu)
{
nearestMenu = (Menu) item;
return nearestMenu;
} else
item = item.parent;
}
return null;
}
public float getPadX()
{
return getStyle().getF("f.padX");
}
public float getPadY()
{
return getStyle().getF("f.padY");
}
/**
* Subclasses should union their bounding rectangle with the Rectangle
* passed in as the rect parameter.
*
* @param rect
* The rectangle with which to union this MenuItem's rectangle.
* @param buff
* A buffer Rectangle2D object, to be used for anything.
*/
protected void getRect(Rectangle2D.Float rect, Rectangle2D.Float buff)
{
if (!isOpen())
return;
for (int i = 0; i < items.size(); i++)
{
MenuItem item = (MenuItem) items.get(i);
item.getRect(rect, buff);
}
}
public MenuStyle getRootStyle()
{
return menu.getStyle();
}
public Shortcut getShortcut()
{
return shortcut;
}
protected int getState()
{
if (!isEnabled())
return DISABLED;
else
return state;
}
public Stroke getStroke()
{
return new BasicStroke(getStyle().getF("f.strokeWeight"));
}
// protected void setOpenItem(MenuItem item)
// {
// for (int i = 0; i < items.size(); i++)
// {
// MenuItem cur = (MenuItem) items.get(i);
// if (cur == item)
// cur.open();
// else
// cur.close();
// }
// }
public Color getStrokeColor()
{
if (!isEnabled())
return getStyle().getC("c.foregroundDisabled");
return getStyle().getC("c.foreground");
}
public MenuStyle getStyle()
{
return style;
}
protected float getTextHeight()
{
int padY = getStyle().getI("f.padY");
PFont font = (PFont) getStyle().getO("font");
float fontSize = getStyle().getF("f.fontSize");
return UIUtils.getTextHeight(menu.buff, font, fontSize, name, true)
+ padY * 2;
}
public float getWidth()
{
return width;
}
public float getX()
{
return x;
}
public float getY()
{
return y;
}
public int getZ()
{
return z;
}
// private void setState(int state)
// {
// menu.setState(this, state);
// if (this.state == state)
// return;
// this.state = state;
// if (nearestMenu.hoverNavigable)
// {
// if (state == MenuItem.OVER || state == MenuItem.DOWN)
// timer.setMenuItem(this);
// else if (state == MenuItem.UP)
// timer.unsetMenuItem(this);
// }
// if (state == MenuItem.DOWN)
// {
// menu.lastPressed = this;
// menu.hovered = this;
//// menu.currentlyFocused = this;
//// menu.lastHovered = this;
// } else if (state == MenuItem.OVER)
// {
//// menu.lastHovered = this;
// menu.hovered = this;
// } else if (state == MenuItem.UP && menu.hovered == this)
// {
//// menu.hovered = null;
// }
// }
public boolean hasChildren()
{
return items.size() > 0;
}
public boolean hasOpenChildren()
{
for (int i = 0; i < items.size(); i++)
{
MenuItem item = (MenuItem) items.get(i);
if (item.isOpen())
return true;
}
return false;
}
protected boolean isAncestorOf(MenuItem child)
{
if (child == null)
return false;
else if (child.parent == this)
return true;
else
{
boolean found = false;
for (int i = 0; i < items.size(); i++)
{
MenuItem item = (MenuItem) items.get(i);
if (item.isAncestorOf(child))
found = true;
}
return found;
}
}
protected boolean isAncestorOfHovered()
{
if (menu == null)
return false;
if (this == menu.hovered)
return true;
else if (isAncestorOf(menu.hovered))
return true;
return false;
}
/**
* Returns whether this MenuItem is enabled or not. Could be used by
* subclasses to sometimes *not* be enabled.
*
* If isEnabled() returns false, the MenuItem will (a) return DISABLED in
* its getState() method, and (b) will not perform its action when pressed
* or otherwise activated.
*
* @return
*/
public boolean isEnabled()
{
return enabled;
}
public boolean isOpen()
{
if (items.size() == 0)
return false;
return isOpen;
}
protected void itemMouseEvent(MouseEvent e, Point tempPt)
{
mouseInside = false;
visibleMouseEvent(e, tempPt);
if (isOpen())
{
for (int i = zSortedItems.size() - 1; i >= 0; i--)
{
MenuItem item = (MenuItem) zSortedItems.get(i);
if (e.isConsumed())
continue;
item.itemMouseEvent(e, tempPt);
if (item.mouseInside)
mouseInside = true;
}
}
if (mouseInside && getZ() == 0)
{
setZ(1);
if (parent != null)
parent.needsZSort = true;
} else if (!mouseInside && getZ() == 1)
{
setZ(0);
if (parent != null)
parent.needsZSort = true;
}
/*
* If the mouse is inside but we're disabled, we want to not show the hand cursor.
*/
if (mouseInside && !isEnabled())
{
menu.setCursor(Cursor.DEFAULT_CURSOR);
}
}
public void keyEvent(KeyEvent e)
{
if (!isOpen())
return;
// for (int i = 0; i < items.size(); i++)
// {
// MenuItem seg = (MenuItem) items.get(i);
// seg.keyEvent(e);
// }
}
public boolean checkCondition()
{
if (condition != null)
return condition.isTrue();
else
return true;
}
/**
* Lays out this MenuItem and all of its sub-items.
*/
public synchronized void layout()
{
setEnabled(checkCondition());
for (int i = 0; i < items.size(); i++)
{
MenuItem seg = (MenuItem) items.get(i);
if (seg.isHidden())
continue;
seg.layout();
}
}
public boolean isHidden()
{
return hidden;
}
public void focusEvent(FocusEvent e)
{
// Nothing.
}
public void mouseEvent(MouseEvent e, Point screen, Point model)
{
// Nothing.
}
protected void menuTriggerLogic()
{
if (nearestMenu.timer.item == this || !nearestMenu.clickToggles)
{
if (nearestMenu.singletNavigation && parent != null)
{
parent.closeMyChildren();
menu.open(this);
} else
menu.open(this);
} else if (nearestMenu.clickToggles)
{
if (nearestMenu.singletNavigation)
{
if (parent != null)
{
if (isOpen())
parent.closeMyChildren();
else
{
parent.closeMyChildren();
menu.open(this);
}
}
} else
toggleChildren();
}
}
public void performAction()
{
if (items.size() > 0)
{
/*
* If we have sub-items, trigger an open or close event.
*/
menuTriggerLogic();
} else
{
if (isEnabled())
{
menu.fireEvent(UIEvent.MENU_ACTIONPERFORMED);
if (getCloseOnAction())
menu.clickaway();
if (action != null)
action.performAction();
}
}
}
public void remove(MenuItem item)
{
items.remove(item);
zSortedItems.remove(item);
zSort();
if (menu != null)
menu.layout();
}
public MenuItem setAction(Object object, String method)
{
// System.out.println(this.name);
action = new Action(object, method);
if (shortcut != null)
{
shortcut.action = action;
}
menu.layout();
return this;
}
public MenuItem setCondition(Object object, String method) throws Exception
{
this.condition = new ConditionChecker(object, method);
menu.layout();
return this;
}
public void setEnabled(boolean enabled)
{
this.enabled = enabled;
}
protected boolean shouldPerformFill()
{
if (getState() != MenuItem.UP || isOpen())
{
if (!isEnabled())
{
return false;
} else if (menu.hovered != null && menu.hovered != this
&& !isAncestorOfHovered())
{
return false;
}
return true;
}
return false;
}
public void setHeight(float height)
{
this.height = height;
}
protected void setMenu(Menu menu)
{
this.menu = menu;
loadNearestMenu();
for (int i = 0; i < items.size(); i++)
{
MenuItem item = (MenuItem) items.get(i);
item.setMenu(menu);
}
}
public void setName(String name)
{
this.name = name;
}
protected void setParent(MenuItem item)
{
parent = item;
getStyle().setParent(item.getStyle());
setMenu(item.menu);
}
public void setPosition(float x, float y)
{
setX(x);
setY(y);
layout();
}
public MenuItem setShortcut(String s)
{
shortcut = UIPlatform.getInstance().getAppContext(nearestMenu.canvas).shortcuts().createShortcut(s);
if (action != null)
{
shortcut.action = action;
} else
{
shortcut.action = new Action(this, "performAction");
}
menu.layout();
return this;
}
public void setSize(float w, float h)
{
setWidth(w);
setHeight(h);
}
protected void setState(int s)
{
state = s;
}
public void setWidth(float width)
{
this.width = width;
}
public void setX(float x)
{
this.x = x;
}
public void setY(float y)
{
this.y = y;
}
public void setZ(int z)
{
this.z = z;
}
protected void toggleChildren()
{
if (isOpen)
menu.close(this);
else
menu.open(this);
}
public String toString()
{
return name;
}
protected void visibleMouseEvent(MouseEvent e, Point tempPt)
{
if (isHidden())
return;
boolean containsPoint = containsPoint(tempPt);
if (containsPoint)
mouseInside = true;
if (!isEnabled())
return;
if (menu == null)
return;
switch (e.getID())
{
case MouseEvent.MOUSE_MOVED:
if (containsPoint)
{
menu.setState(this, MenuItem.OVER);
} else
{
menu.setState(this, MenuItem.UP);
}
break;
case MouseEvent.MOUSE_PRESSED:
if (containsPoint)
{
if (nearestMenu.actionOnMouseDown)
performAction();
}
// The switch statement continues on through the next case...
case MouseEvent.MOUSE_DRAGGED:
if (containsPoint)
{
menu.setState(this, MenuItem.DOWN);
} else
menu.setState(this, MenuItem.UP);
break;
case MouseEvent.MOUSE_RELEASED:
if (containsPoint)
{
if (!nearestMenu.actionOnMouseDown)
performAction();
// setState(MenuItem.OVER);
if (getState() == MenuItem.DOWN)
menu.setState(this, MenuItem.OVER);
} else
menu.setState(this, MenuItem.UP);
default:
break;
}
}
public void setFontSize(float size)
{
getStyle().set("f.fontSize", size);
}
public void zSort()
{
if (zComp == null)
zComp = new ZDepthComparator();
Collections.sort(zSortedItems, zComp);
}
public void setHidden(boolean hideMe)
{
hidden = hideMe;
}
class ConditionChecker
{
String ifMethod;
Object ifObject;
Method methodCall;
public ConditionChecker(Object obj, String method) throws Exception
{
this.ifObject = obj;
this.ifMethod = method;
methodCall = obj.getClass().getMethod(method);
}
public boolean isTrue()
{
if (methodCall != null)
{
try
{
return ((Boolean) methodCall.invoke(ifObject))
.booleanValue();
} catch (Exception e)
{
e.printStackTrace();
return false;
}
} else
return false;
}
}
}