/******************************************************************************* * 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.AlphaComposite; import java.awt.Composite; import java.awt.Cursor; import java.awt.RenderingHints; import java.awt.event.FocusEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import org.andrewberman.ui.Point; import org.andrewberman.ui.UIContext; import org.andrewberman.ui.UIEvent; import org.andrewberman.ui.UIPlatform; import org.andrewberman.ui.UIUtils; import org.andrewberman.ui.ifaces.UIListener; import org.andrewberman.ui.ifaces.UIObject; import org.andrewberman.ui.tween.PropertyTween; import org.andrewberman.ui.tween.Tween; import org.andrewberman.ui.tween.TweenFriction; import processing.core.PApplet; import processing.core.PGraphics; import processing.core.PGraphicsJava2D; /** * The <code>Menu</code> class represents a displayable, interactive menu. It * is abstract, so it can never be instantiated on its own. Instead, users * should call the constructor of one of its subclasses, such as * <code>Toolbar</code> or <code>Dock</code>. * <p> * The main purpose for the <code>Menu</code> class is to hold a large amount * of generic structure and logic for managing and displaying a menu-type object * within Processing. This includes: dealing with mouse events, keeping track of * which menu item is currently hovered, selected, and open, and creating and * drawing to an off-screen buffer when necessary (to allow us to use Java2D * functions within a P3D or OpenGL PApplet). * <p> * <p> * The <code>Menu</code> class is the base class for all other menu objects * within this package. It inherits from the <code>MenuItem</code> class, * because a Menu should act like a well-behaved MenuItem as well (this way, we * can cascade menus within other menus). It also helped keep down the amount of * code repetition. * <p> * The side effect of this completely inherited organization is, of course, some * complexity in thinking about the recursive method calls. You need to keep in * mind that when the <code>Menu</code> class calls * <code>super.someMethod()</code>, it's calling the method defined in the * <code>MenuItem</code> class. * <p> * There is also a slight issue with the fact that a <code>Menu</code> should * not be strictly considered a <code>MenuItem</code>, because more often * than not, the <code>Menu</code> object itself does not "act" like a menu. * Rather, its sub-items are what is shown to the screen and what interacts with * the user. See the classes that extend the <code>Menu</code> class for * examples of how to deal with this code organization. * <p> * <b>Important information for developers:</b> if you plan to create a new * type of menu using the <code>Menu</code> class as a base class, you should * keep in mind the following: * <ul> * <li>Check out the built-in Menu subclasses that already exist: * <code>Dock</code>, <code>Toolbar</code>, and <code>VerticalMenu</code>. * Examining how these classes interact with the <code>Menu</code> structure * will be very helpful when designing a novel Menu type.</li> * <li>Please, make use of the boolean "options" provided, and override the * <code>setOptions()</code> within your new class as a place to define your * new menu's behavioral and display options. Particularly important are * <code>useCameraCoordinates</code> and <code>usesJava2D</code>. See each * option's javadoc for more information. * * @author Greg * @see org.andrewberman.ui.menu.MenuItem * @see org.andrewberman.ui.menu.Dock * @see org.andrewberman.ui.menu.Toolbar */ public abstract class Menu extends MenuItem implements UIObject { public static final int CLICKAWAY_COLLAPSES = 1; public static final int CLICKAWAY_HIDES = 0; public static final int CLICKAWAY_IGNORED = 2; static final int PAD = 10; public static final int START_SIZE = 50; /** * If true, this menu's items will act like a "menu" and open up on the * mouse down event, as opposed to the more "button"-like behavior of * opening on the mouse up event. */ protected boolean actionOnMouseDown; /** * The current alpha value for this menu. */ public float alpha = 1.0f; /** * A Tween for the alpha value. Only created if <code>autoDim</code> is * set to true. */ protected PropertyTween aTween; // protected boolean hideOnAction = true; /** * If true, this menu will dim to loAlpha if the mouse is not inside the * menu or any of its sub-items. */ protected boolean autoDim; /** * The graphics2D object to which we may be drawing. */ protected PGraphicsJava2D buff; protected Rectangle2D.Float buffRect = new Rectangle2D.Float(0, 0, 0, 0); /** * The current "canvas" PApplet object. */ protected PApplet canvas; protected UIContext context; /** * Defines the behavior of a click that occurs outside the bounds of the * menu and all of its sub-menus). */ protected int clickAwayBehavior; /** * If true, a click will hide a submenu as well as show it. This option * works best when set to the OPPOSITE of the above hoverNavigable option. */ protected boolean clickToggles; public boolean consumeEvents; protected int cursor; /** * The "dim" alpha value to drop to. Only effective when * <code>autodim</code> is true. */ public float dimAlpha = .3f; /** * If set to true, then this menu will grab focus when the show() function * is called. It will then also release focus when the hide() function is * called. */ public boolean focusOnShow; /** * The full alpha value to jump to when the mouse is over this menu. Only * effective when <code>autoDim</code> is true. */ public float fullAlpha = 1f; /** * References to a few relevant MenuItems. */ protected MenuItem hovered, lastPressed, kbFocus; /** * If true, a mouse hover will expand a menu item. If false, a menu item * requires a click to expand. */ protected boolean hoverNavigable; /** * Our UIListeners. */ protected ArrayList<UIListener> listeners = new ArrayList<UIListener>(3); /** * If set to true, then this menu grabs modal focus when opened. */ public boolean modalFocus; /** * One Point object which will be passed to the sub-items during the * mouseEvent() cycle. */ Point mousePt = new Point(0, 0); private float offsetX = 0; private float offsetY = 0; /** * A Composite object, used to store and reload the Graphics2D's original * composite state during the draw cycle. */ Composite origComp; /** * A RenderingHints object, used during the <code>draw()</code> cycle to * store and reload the rendering hints on the graphics2D object being used. */ RenderingHints origRH; /** * Two Rectangle objects which are passed to the sub-items during the * getRect() phase of the draw cycle. */ Rectangle2D.Float rect = new Rectangle2D.Float(0, 0, 0, 0); /** * If true, only one of this MenuItem's submenus will be allowed to be shown * at once. Generally best left set to true. */ protected boolean singletNavigation; /* * =============================== GENERAL OPTIONS FOR SUBCLASSES */ /** * A very important option. If set to FALSE, then this <code>Menu</code> * instance will draw itself to SCREEN coordinates. If set to TRUE, however, * then this <code>Menu</code> will draw itself to the MODEL coordinates. * In general, this is best left to FALSE for UI objects, as they are * usually drawn relative to the screen, despite what the "model" camera may * be doing. * <p> * This option is made <code>public</code>, as opposed to most of the * other <code>protected</code> options, because it is easily forseeable * that the user might want to change the screen-vs-camera behavior of a * menu without going through the effort of making a new subclass and * overriding the <code>setOptions</code> method. */ public boolean useCameraCoordinates; /** * If true, this Menu will change to the hand cursor when one of its * constituent sub-MenuItems is selected. I am the walrus. */ protected boolean useHandCursor; /** * This parameter signals that the sub-classing menu is going to draw itself * using Java2D. As such, if the base canvas isn't Java2D, then we will draw * to the off-screen buffer and then blend the image back onto the canvas. * If this value is set to FALSE, then the <code>Menu</code> will always * draw directly to the on-screen PGraphics canvas. Schweet! */ protected boolean usesJava2D; public Menu(PApplet app) { super(); canvas = app; context = UIPlatform.getInstance().getAppContext(app); setMenu(this); /* * Give our subclasses a chance to set their options before we start * initing stuff. */ setOptions(); init(); context.event().add(this); // Add ourselves to EventManager. } @Override public void dispose() { if (context != null) { if (context.event() != null) { context.event().remove(this); } } super.dispose(); } public void addListener(UIListener o) { listeners.add(o); System.out.println("Listeners: " + listeners); } public void removeListener(UIListener o) { listeners.remove(o); } protected void clickaway() { switch (clickAwayBehavior) { case (CLICKAWAY_HIDES): close(); break; case (CLICKAWAY_COLLAPSES): closeMyChildren(); break; case (CLICKAWAY_IGNORED): default: break; } } public void close() { close(this); /* * Cause the cursor to be reverted back to normal in the case that this * Menu had changed it to a hand icon or something else. */ UIUtils.releaseCursor(this, canvas); /* * Cause this menu to release its focus, if it had grabbed it. */ if (modalFocus) context.focus().removeFromFocus(this); else if (focusOnShow) context.focus().removeFromFocus(this); /* * Finally, fire the MENU_HIDDEN event to our listeners. */ fireEvent(UIEvent.MENU_CLOSED); } public void close(MenuItem item) { ArrayList items = item.items; for (int i = 0; i < items.size(); i++) { MenuItem child = (MenuItem) items.get(i); close(child); child.setState(MenuItem.UP); } item.isOpen = false; } protected boolean containsPoint(Point pt) { return false; // Should we just do a simple rectangular overlap with this menu's rectangle? } public abstract MenuItem create(String label); protected void createBuffer(int w, int h) { buff = (PGraphicsJava2D) canvas.createGraphics(w, h, PApplet.JAVA2D); } public synchronized void draw() { // System.out.println(hovered); if (hidden) return; aTween.update(); if (!isRootMenu()) { hint(); super.draw(); unhint(); return; } if (UIUtils.isJava2D(canvas)) { /* * If this is a root menu and our canvas PGraphics is a * JavaGraphics2D instance, then we can draw with Java2D directly to * the canvas. */ canvas.pushMatrix(); resetMatrix(canvas.g); // canvas.translate(x, y); hint(); super.draw(); unhint(); canvas.popMatrix(); } else if (!usesJava2D) { /* * If this menu has indicated that it won't draw Java2D unless it * checks itself for a J2D canvas, then we can also draw directly to * the canvas. */ canvas.pushMatrix(); resetMatrix(canvas.g); // canvas.translate(x, y); super.draw(); canvas.popMatrix(); } else { /* * If our root canvas is either OpenGL or P3D, we need to draw to * the offscreen Java2D buffer and then copy it onto the canvas * PGraphics. */ resizeBuffer(); hint(); buff.beginDraw(); buff.background(menu.getStyle().getC("c.background").getRGB(), 0); buff.translate(-x, -y); buff.translate(-offsetX, -offsetY); // buff.translate(offsetX, offsetX); super.draw(); // Draws all of the sub segments. buff.setModified(true); buff.endDraw(); drawToCanvas(); unhint(); } } protected void drawToCanvas() { int w = (int) (rect.width + PAD * 2); int h = (int) (rect.height + PAD * 2); canvas.pushMatrix(); resetMatrix(canvas.g); canvas.image(buff, x + offsetX, y + offsetY, w, h, 0, 0, w, h); canvas.popMatrix(); /* * Draw some debug rectangles: - Red: bounding rectangle for the menu. - * Green: size of the buffer PGraphics - Blue: the area actually copied * from the buffer to the drawing canvas. */ // canvas.noFill(); // canvas.stroke(255,0,0); // canvas.rect(rect.x, rect.y, rect.width, rect.height); // canvas.stroke(0,255,0); // canvas.rect(x+offsetX,y+offsetY,buff.width,buff.height); // canvas.stroke(0,0,255); // canvas.rect(x+offsetX,y+offsetY,w,h); } public void fireEvent(int id) { UIEvent e = new UIEvent(this, id); for (int i = 0; i < listeners.size(); i++) { ((UIListener) listeners.get(i)).uiEvent(e); } } public void focusEvent(FocusEvent e) { if (e.getID() == FocusEvent.FOCUS_LOST) { close(); } } public float getX() { return x; } public float getY() { return y; } protected void hint() { origRH = buff.g2.getRenderingHints(); origComp = buff.g2.getComposite(); buff.g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); buff.g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); buff.g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); // buff.g2.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY); // buff.g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, // RenderingHints.VALUE_INTERPOLATION_BICUBIC); // buff.g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, // RenderingHints.VALUE_FRACTIONALMETRICS_OFF); buff.g2.setComposite(AlphaComposite.getInstance( AlphaComposite.SRC_OVER, alpha)); } protected void init() { if (UIUtils.isJava2D(canvas)) buff = (PGraphicsJava2D) canvas.g; else if (usesJava2D) createBuffer(START_SIZE, START_SIZE); // if (autoDim) aTween = new PropertyTween(this, "alpha", TweenFriction.tween(.25f), Tween.OUT, fullAlpha, fullAlpha, 15); } public boolean isRootMenu() { return (menu == this); } protected void itemMouseEvent(MouseEvent e, Point pt) { if (hidden) return; super.itemMouseEvent(e, pt); if (mouseInside && consumeEvents) { e.consume(); } // if (true) // return; if (autoDim) { if (mouseInside) { if (aTween != null) aTween.continueTo(fullAlpha); } else if (menu.hovered == null) { if (aTween != null) aTween.continueTo(dimAlpha); } } } public void keyEvent(KeyEvent e) { switch (e.getKeyCode()) { case (KeyEvent.VK_ESCAPE): close(); break; } if (kbFocus != null) kbFocus.keyEvent(e); super.keyEvent(e); } public synchronized void layout() { super.layout(); } public void mouseEvent(MouseEvent e, Point screen, Point model) { Point useMe = model; if (isRootMenu() && !useCameraCoordinates) useMe = screen; /* * create a copy of the point we decided to use, and translate it * accordingly. */ mousePt.setLocation(useMe); // mousePt.translate(-x, -y); setCursor(-1); /* * Send the mouse events through the tree of sub-items. */ itemMouseEvent(e, mousePt); if (!mouseInside && isOpen() && e.getID() == MouseEvent.MOUSE_PRESSED) { clickaway(); } if (useHandCursor && isOpen() && cursor == -1) { if (mouseInside) { UIUtils.setCursor(this, canvas, Cursor.HAND_CURSOR); } else { UIUtils.releaseCursor(this, canvas); } } } public void open() { open(this); if (modalFocus) context.focus().setModalFocus(this); else if (focusOnShow) context.focus().setFocus(this); fireEvent(UIEvent.MENU_OPENED); } public void open(MenuItem i) { if (!i.isEnabled()) return; if (i.isOpen()) close(); i.isOpen = true; } protected void resetMatrix(PGraphics graphics) { if (useCameraCoordinates) return; UIUtils.resetMatrix(graphics); } protected void resizeBuffer() { rect.setFrame(x, y, 0, 0); buffRect.setFrame(x, y, 0, 0); getRect(rect, buffRect); float dX = 0; float dY = 0; // if (rect.x - (x+offsetX) < PAD) dX = rect.x - (x + offsetX + PAD); // if (rect.y - (y+offsetY) < PAD) dY = rect.y - (y + offsetY + PAD); offsetX += dX; offsetY += dY; int newWidth = buff.width; int newHeight = buff.height; boolean resizeMe = false; if (rect.width > buff.width - PAD * 2) { newWidth = (int) (rect.width + PAD * 2); resizeMe = true; } if (rect.height > buff.height - PAD * 2) { newHeight = (int) (rect.height + PAD * 2); resizeMe = true; } if (resizeMe) { createBuffer(newWidth, newHeight); } } protected void setCursor(int c) { cursor = c; if (c != -1) { UIUtils.setCursor(this, canvas, c); } } public void setFontSize(float newSize) { getStyle().set("f.fontSize", newSize); // PFont curFont = (PFont) style.get("font"); // curFont.font = curFont.font.deriveFont(newSize); layout(); } public void setMenu(Menu m) { super.setMenu(m); if (!isRootMenu()) { /* * If we're no longer the root menu, then remove ourselves from the * EventManager's control. */ context.event().remove(this); } } public void setOptions() { useCameraCoordinates = true; usesJava2D = true; hoverNavigable = false; clickToggles = false; singletNavigation = true; actionOnMouseDown = false; clickAwayBehavior = CLICKAWAY_HIDES; useHandCursor = true; autoDim = false; focusOnShow = false; modalFocus = false; consumeEvents = true; // Subclassers should put changes in the boolean options here. } @Override protected void setParent(MenuItem item) { super.setParent(item); } /* * No multiple inheritance allowed, so I had to copy this boilerplate code * from AbstractUIObject instead of inheriting it. Crap! */ protected void setState(int state) { // Do nothing. } public void setState(MenuItem item, int newState) { /* * If we're not the item's menu, throw an exception. */ if (item.menu != this) throw new IllegalArgumentException(); if (item == this) return; /* * If the state hasn't changed, just return. */ if (item.getState() == newState) return; /* * Actually set the state variable. */ item.setState(newState); if (hoverNavigable) { if (newState == MenuItem.OVER || newState == MenuItem.DOWN) timer.setMenuItem(item); // item.menu.menuTriggerLogic(); else if (newState == MenuItem.UP) timer.unsetMenuItem(item); // item.menu.close(item); } if (newState == MenuItem.DOWN) { lastPressed = item; hovered = item; kbFocus = item; } else if (newState == MenuItem.OVER) { hovered = item; kbFocus = item; } else if (newState == MenuItem.UP) { if (item == hovered) { hovered = null; } } } protected void unhint() { buff.g2.setRenderingHints(origRH); buff.g2.setComposite(origComp); } }