/******************************************************************************* * 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.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.geom.Arc2D; import java.awt.geom.Area; import java.awt.geom.Ellipse2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import java.util.ArrayList; import java.util.Collections; import org.andrewberman.ui.Color; import org.andrewberman.ui.Point; import org.andrewberman.ui.UIUtils; import processing.core.PApplet; import processing.core.PFont; import processing.core.PImage; public class RadialMenuItem extends MenuItem { protected static RoundRectangle2D.Float roundedRect = new RoundRectangle2D.Float(0, 0, 0, 0, 0, 0); public static final int HINT_DELAY = 60; public static final float SIZE_DECAY = .9f; protected Arc2D.Float tempArc = new Arc2D.Float(Arc2D.PIE); protected Ellipse2D.Float tempCircle = new Ellipse2D.Float(0, 0, 0, 0); protected float fontSize, hintSize; protected char hint; protected float hintX, hintY; protected PImage icon; float iconAlpha; String iconFile = null; protected float minRadius = 5f; protected float outerX, outerY, innerX, innerY; protected float radius; protected float rectX, rectY, rectW, rectH; protected float rLo, rHi, tLo, tHi; protected float textWidth, textHeight, pad; protected float textX, textY; protected Area wedge; protected int hintTrigger; public RadialMenuItem() { super(); } protected boolean alreadyContainsChar(char c) { if (hint == c) return true; for (int i = 0; i < items.size(); i++) { RadialMenuItem rmi = (RadialMenuItem) items.get(i); if (rmi.alreadyContainsChar(c)) return true; } return false; } public boolean containsPoint(Point pt) { // if (!isOpen()) // return false; boolean contained = false; if (wedge.contains(pt.x, pt.y)) contained = true; if (isShowingLabel()) { Rectangle2D.Float temp = new Rectangle2D.Float(rectX, rectY, rectW, rectH); if (temp.contains(pt.x, pt.y)) contained = true; } return contained; } void createShapes() { tempCircle.setFrameFromCenter(x, y, x + rLo, y + rLo); tempArc.setFrameFromCenter(x, y, x + rHi, y + rHi); float degLo = radToDeg(-tLo); float degHi = radToDeg(-tHi); tempArc.setAngleStart(degLo); tempArc.setAngleExtent(degHi - degLo); try { wedge = new Area(tempArc); Area delete = new Area(tempCircle); wedge.subtract(delete); } catch (Exception e) { e.printStackTrace(); } } @Override public void dispose() { super.dispose(); icon = null; } public void draw() { super.draw(); if (isShowingLabel()) { drawUnder(); drawText(); } drawShape(); int dt = menu.canvas.frameCount - hintTrigger; // if (hintTrigger == -1) // dt = -1; loadImage(); boolean drawHintInstead = (icon == null); // || (mouseInside && dt > HINT_DELAY); if (drawHintInstead) drawHint(); if (!drawHintInstead) drawIcon(); } void drawHint() { Graphics2D g2 = menu.buff.g2; PFont pf = getStyle().getFont("font"); Font f = pf.getFont().deriveFont(hintSize); g2.setFont(f); g2.setPaint(getStrokeColor()); g2.drawString(String.valueOf(hint), hintX, hintY); } public void drawIcon() { if (icon == null) return; Graphics2D g2 = menu.buff.g2; float rMid = (rLo + rHi) / 2; float imgDiag = (float) Math.sqrt(icon.width * icon.width + icon.height * icon.height); hintSize = (rHi - rLo) / imgDiag; hintSize = Math.min(hintSize, (float) Math.sin(tHi - tLo) * rMid); hintSize *= 0.8f; float imgW = icon.width * hintSize; float imgH = icon.height * hintSize; float midX = (innerX + outerX) / 2f; float midY = (innerY + outerY) / 2f; if (!isEnabled()) menu.canvas.tint(200); menu.canvas.image(icon, midX - imgW / 2, midY - imgH / 2, imgW, imgH); if (!isEnabled()) menu.canvas.noTint(); } protected boolean drawingHint() { return true; } void drawShape() { /* * Draw the main wedge shape. */ Graphics2D g2 = menu.buff.g2; // this.isAncestorOf(menu.currentlyHovered); // if (this.isAncestorOf(menu.currentlyHovered)) if (isOpen()) g2.setPaint(getStyle().getGradient(MenuItem.OVER, x - rHi, y - rHi, x + rHi, y + rHi)); else g2.setPaint(getStyle().getGradient(getState(), x - rHi, y - rHi, x + rHi, y + rHi)); g2.fill(wedge); g2.setStroke(getStroke()); g2.setPaint(getStrokeColor()); g2.draw(wedge); /* * Draw the sub-items triangle, if necessary */ if (items.size() > 0 && !isOpen()) { float theta = (tLo + tHi) / 2; float scale = (rHi - rLo) / 2; float dx = (float) (Math.cos(theta) * scale / 4f); float dy = (float) (Math.sin(theta) * scale / 4f); AffineTransform at = AffineTransform.getTranslateInstance(outerX + dx, outerY + dy); at.scale(scale, scale); at.rotate(theta); Area tri = (Area) getStyle().get("subTriangle"); Area newTri = tri.createTransformedArea(at); g2.setPaint(getStrokeColor()); g2.fill(newTri); } } void drawText() { Graphics2D g2 = menu.buff.g2; PFont pf = getStyle().getFont("font"); Font f = pf.getFont().deriveFont(fontSize); g2.setFont(f); g2.setPaint(getStrokeColor()); g2.drawString(getDisplayLabel(), textX, textY); } public void drawUnder() { Graphics2D g2 = menu.buff.g2; MenuUtils.drawWhiteTextRect(this, rectX, rectY, rectW, rectH); super.draw(); } public String getDisplayLabel() { String displayLabel = getName(); if (items.size() > 0) displayLabel = displayLabel.concat("..."); if (!drawingHint() && hint != 0) displayLabel = displayLabel.concat(" (" + String.valueOf(hint) + ")"); return displayLabel; } float getMaxRadius() { if (!isOpen()) return 0; float max = this.rHi; for (int i = 0; i < items.size(); i++) { RadialMenuItem rmi = (RadialMenuItem) items.get(i); float cur = rmi.getMaxRadius(); if (cur > max) max = cur; } return max; } public float getMinRadius() { return minRadius; } public void getRect(Rectangle2D.Float rect, Rectangle2D.Float buff) { // if (isOpen()) // { super.getRect(rect, buff); buff.setRect(wedge.getBounds2D()); Rectangle2D.union(rect, buff, rect); buff.setRect(rectX, rectY, rectW, rectH); Rectangle2D.union(rect, buff, rect); // } } boolean isShowingLabel() { if (!isOpen()) { MenuItem par = parent; int distToMenu = 1; while (par != menu) { distToMenu++; par = par.parent; } RadialMenu rm = (RadialMenu) menu; if (distToMenu == rm.maxLevelOpen) { return true; } } return false; } @Override protected void itemMouseEvent(MouseEvent e, Point tempPt) { boolean wasInside = mouseInside; super.itemMouseEvent(e, tempPt); boolean isInside = mouseInside; if (isInside && !wasInside) { if (menu != null && menu.canvas != null) hintTrigger = menu.canvas.frameCount; } } protected void keyHintEvent(KeyEvent e) { if (isOpen()) { for (int i = 0; i < items.size(); i++) { RadialMenuItem rmi = (RadialMenuItem) items.get(i); rmi.keyHintEvent(e); } } if (e.isConsumed()) return; char c = (char) e.getKeyChar(); if (Character.toLowerCase(c) == Character.toLowerCase(hint)) { this.performAction(); e.consume(); return; } } @Override public synchronized void layout() { super.layout(); } protected void layout(float radLo, float radHi, float thLo, float thHi) { // super.layout(); if (radHi - radLo < minRadius) { radHi = radLo + minRadius; } this.rLo = radLo; this.rHi = radHi; this.tLo = thLo; this.tHi = thHi; this.radius = radHi; this.layoutText(); this.createShapes(); /* * Start laying out our sub-items. */ float tMid = (tHi + tLo) / 2; float dTheta = tHi - tLo; /* * Sub-item sizing: Ensure that sub-items' thetas are constrained within * a certain range. */ float minTheta = PApplet.QUARTER_PI * .6f * items.size(); float maxTheta = Math.min(PApplet.HALF_PI * 1.5f, dTheta); dTheta = PApplet.constrain(dTheta, minTheta, maxTheta); layoutSubItems(rLo, rHi, tMid - dTheta / 2, tMid + dTheta / 2); } void layoutSubItems(float radLo, float radHi, float thLo, float thHi) { float dTheta = thHi - thLo; float thetaStep = dTheta / items.size(); for (int i = 0; i < items.size(); i++) { RadialMenuItem seg = (RadialMenuItem) items.get(i); seg.setPosition(x, y); float theta = thLo + i * thetaStep; seg.layout(radHi, radHi + (radHi - radLo) * SIZE_DECAY, theta, theta + thetaStep); } } void layoutText() { /* * Calculate the sine and cosine, which we'll need to use often. */ float theta = (tLo + tHi) / 2; float cos = (float) Math.cos(theta); float sin = (float) Math.sin(theta); outerX = x + cos * rHi; outerY = y + sin * rHi; innerX = x + cos * rLo; innerY = y + sin * rLo; PFont font = getStyle().getFont("font"); FontMetrics fm = UIUtils.getMetrics(menu.canvas.g, font.getFont(), 1); if (fm == null) return; float unitTextHeight = (float) fm.getMaxCharBounds(menu.buff.g2).getHeight(); fontSize = (rHi - rLo) / unitTextHeight * .75f; // Keep the font size readable. fontSize = Math.max(8, fontSize); fm = UIUtils.getMetrics(menu.buff, font.getFont(), fontSize); // float descent = fm.getDescent(); float ascent = fm.getAscent(); // Rectangle2D bounds = fm.getStringBounds(label, menu.buff.g2); textHeight = UIUtils.getTextHeight(menu.buff, font, fontSize, getDisplayLabel(), true); // textHeight = (float) bounds.getHeight(); // textWidth = (float) bounds.getWidth(); textWidth = UIUtils.getTextWidth(menu.buff, font, fontSize, getDisplayLabel(), true); // Calculate the necessary x and y offsets for the text. float outX = x + cos * (rHi + textHeight); float outY = y + sin * (rHi + textHeight); float pad = getStyle().getF("f.padX"); rectW = textWidth + 2 * pad; rectH = textHeight + 2 * pad; rectX = outX + cos * rectW / 2 - rectW / 2; rectY = outY + sin * rectH / 2 - rectH / 2; textX = rectX + pad; textY = rectY + pad + ascent; // textX = cos * textWidth/2; // textX += -textWidth / 2; // textX += outerX; // textY = sin * (textHeight)/2; // textY += -descent + (textHeight)/2; // textY += outerY; /* * Set the background rectangle. */ // rectX = textX-pad; // rectY = textY + descent - textHeight - pad; /* * Now, let's handle the hint characters. */ float rMid = (rLo + rHi) / 2; float centerX = x + cos * rMid; float centerY = y + sin * rMid; /* * Measure the character at 1px, then scale up accordingly. */ fm = UIUtils.getMetrics(menu.buff, font.getFont(), 1); String s = String.valueOf(hint); Rectangle2D charBounds = fm.getStringBounds(s, menu.buff.g2); float charHeight = (float) charBounds.getHeight(); float charWidth = (float) charBounds.getWidth(); float charDiagonal = PApplet.sqrt(charHeight * charHeight + charWidth * charWidth); hintSize = (rHi - rLo) / charDiagonal; hintSize = Math.min(hintSize, (float) Math.sin(tHi - tLo) * rMid); fm = UIUtils.getMetrics(menu.buff, font.getFont(), hintSize); charBounds = fm.getStringBounds(s, menu.buff.g2); charHeight = (float) charBounds.getHeight(); charWidth = (float) charBounds.getWidth(); charDiagonal = PApplet.sqrt(charHeight * charHeight + charWidth * charWidth); float charDesc = fm.getDescent(); hintX = centerX - charWidth / 2.0f; hintY = centerY - charDesc + charHeight / 2.0f; } protected synchronized void loadImage() { if (icon == null && iconFile != null && menu != null && menu.canvas != null) { icon = menu.canvas.loadImage(iconFile); } } float radToDeg(float rad) { return PApplet.degrees(rad); } public void setHint(String hint) { this.hint = hint.charAt(0); } public void setIcon(String s) { iconFile = s; loadImage(); } public void setMinRadius(float minRadius) { this.minRadius = minRadius; } protected void visibleMouseEvent(MouseEvent e, Point tempPt) { super.visibleMouseEvent(e, tempPt); if (getState() == MenuItem.OVER) e.consume(); } }