/******************************************************************************* * 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; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.event.KeyEvent; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.MemoryImageSource; import java.io.IOException; import java.io.InputStream; import java.net.URL; import javax.imageio.ImageIO; import processing.core.PApplet; import processing.core.PFont; import processing.core.PGraphics; import processing.core.PGraphics2D; import processing.core.PGraphicsJava2D; import processing.core.PImage; import processing.core.PMatrix; import processing.core.PMatrix3D; /** * The <code>UIUtils</code> class is a disgusting mess of static utility * functions that were placed in a single place so as to reduce the amount of * duplicated code within the individual UI objects' classes. * <p> * Scroll to each method's documentation for further details. * * @author Greg */ public final class UIUtils { static Cursor baseCursor = Cursor.getDefaultCursor(); private static PMatrix3D camera = new PMatrix3D(); private static PMatrix3D cameraInv = new PMatrix3D(); static Object cursorOwner; private static PMatrix3D modelview = new PMatrix3D(); private static PMatrix3D modelviewInv = new PMatrix3D(); private static double[] temp = new double[6]; private static Point tPoint = new Point(0, 0); /** * Copies the transformation data from an <code>AffineTransformation</code> * into a <code>PMatrix3D</code>. * * @param tr * the source <code>AffineTransformation</code> * @param mat * the destination <code>PMatrix3D</code> */ public static void affineToPMatrix3D(AffineTransform tr, PMatrix3D mat) { tr.getMatrix(temp); mat.set((float) temp[0], (float) temp[2], 0, (float) temp[4], (float) temp[1], (float) temp[3], 0, (float) temp[5], 0, 0, 0, 0, 0, 0, 0, 0); } /** * Turns a <code>Color</code> object into an int value, using the current * <code>PGraphics</code> object's <code>color</code> method. Yeah, I'm just * lazy enough not to do it myself in the <code>Color</code> class. * * @param g * the current <code>PGraphics</code> * @param c * the <code>Color</code> object to convert * @return the integer representation of the given <code>Color</code> * object. */ public static int colorToInt(PGraphics g, Color c) { return g.color(c.getRed(), c.getGreen(), c.getBlue(), c.getAlpha()); } public static Cursor createBlankCursor(PApplet p) { Image image = p.createImage(new MemoryImageSource(0, 0, new int[0], 0, 0)); // Image image = UIUtils.PImageToImage(p,img); return Toolkit.getDefaultToolkit().createCustomCursor(image, new java.awt.Point(0, 0), "asdf"); } public static Cursor createCursor(PApplet p, String filename, int offsetX, int offsetY) { InputStream in = p.createInput(filename); BufferedImage img = null; try { img = ImageIO.read(in); } catch (IOException e) { e.printStackTrace(); } Dimension d = Toolkit.getDefaultToolkit().getBestCursorSize(img.getWidth(), img.getHeight()); BufferedImage img2 = new BufferedImage(d.width, d.height, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = img2.createGraphics(); g2.drawImage(img, 0, 0, null); g2.dispose(); // Image image = resized.getImage(); return Toolkit.getDefaultToolkit().createCustomCursor(img2, new java.awt.Point(offsetX, offsetY), "asdf"); } public static Object getCursorOwner() { return cursorOwner; } /** * Retrieves the "meta" mask for the current system. * <p> * In Windows, the meta mask should be equivalent to * <code>CTRL_DOWN_MASK</code>. * <p> * In Mac OSX, the meta mask usually ends up as <code>META_DOWN_MASK</code>. * <p> * Either way, if a UI object wishes to implement standard shortcuts like * <code>Ctrl-X</code>, then it should make sure to turn all references to * "control" into the current system's "meta" mask. See the * <code>Shortcut</code> class for an attempt at achieving this * platform-compatibility. * * @return * @see org.andrewberman.ui.Shortcut */ public static int getMetaMask() { return KeyEvent.CTRL_DOWN_MASK; // int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); // if (shortcutMask == KeyEvent.CTRL_MASK) // shortcutMask = KeyEvent.CTRL_DOWN_MASK; // else if (shortcutMask == KeyEvent.ALT_MASK) // shortcutMask = KeyEvent.ALT_DOWN_MASK; // else if (shortcutMask == KeyEvent.META_MASK) // shortcutMask = KeyEvent.META_DOWN_MASK; // return shortcutMask; } /** * Returns the <code>FontMetrics</code> object corresponding to the given * <code>PGraphicsJava2D</code> object. * * @param pg * a <code>PGraphicsJava2D</code> instance * @param font * a <code>java.awt.Font</code> instance * @param size * the desired font size * @return the corresponding <code>FontMetrics</code> object. */ public static FontMetrics getMetrics(PGraphics pg, Font font, float size) { // Graphics2D g2 = (Graphics2D)UIGlobals.g.getP().getGraphics(); if (pg == null) return null; if (pg instanceof PGraphicsJava2D) { Graphics2D g2 = ((PGraphicsJava2D) pg).g2; if (g2 == null) return null; if (font == null) return null; Font f = font.deriveFont(size); FontMetrics fm = g2.getFontMetrics(f); return fm; } else { return pg.parent.getGraphics().getFontMetrics(); } } /** * Retrieves the floating-point text ascent for the given font, size, and * PGraphics context. * * @param g * a PGraphics object * @param font * a <code>PFont</code> object * @param size * the font size * @param useNativeFonts * true if the caller wishes to use Java's built-in font * functionality * @return the font ascent */ public static float getTextAscent(PGraphics g, PFont font, float size, boolean useNativeFonts) { if (isJava2D(g) && useNativeFonts) { FontMetrics fm = getMetrics(g, font.getFont(), size); if (fm != null) return fm.getAscent(); } return font.ascent() * size; } /** * Retrieves the floating-point text descent for the given font, size, and * PGraphics context. * * @param g * a PGraphics object * @param font * a <code>PFont</code> object * @param size * the font size * @param useNativeFonts * true if the caller wishes to use Java's built-in font * functionality * @return the font descent */ public static float getTextDescent(PGraphics g, PFont font, float size, boolean useNativeFonts) { if (isJava2D(g) && useNativeFonts) { FontMetrics fm = getMetrics(g, font.getFont(), size); if (fm != null) return fm.getDescent(); } return font.descent() * size; } /** * Retrieves the floating-point text height for the given font, size, and * PGraphics context. * * @param g * a PGraphics object * @param font * a <code>PFont</code> object * @param size * the font size * @param useNativeFonts * true if the caller wishes to use Java's built-in font * functionality * @return the font height */ public static float getTextHeight(PGraphics g, PFont font, float size, String text, boolean useNativeFonts) { if (isJava2D(g) && useNativeFonts) { FontMetrics fm = getMetrics(g, font.getFont(), size); if (fm != null) return fm.getAscent() + fm.getDescent(); } return font.ascent() * size + font.descent() * size; } public static Rectangle2D getTextRect(PGraphics g, PFont font, float size, String text, boolean useNativeFonts) { if (isJava2D(g) && useNativeFonts) { FontMetrics fm = getMetrics(g, font.getFont(), size); if (fm != null) { Graphics2D g2 = ((PGraphicsJava2D) g).g2; return fm.getStringBounds(text, g2); } } Rectangle2D.Float rect = new Rectangle2D.Float(); rect.width = getTextWidth(g, font, size, text, useNativeFonts); rect.height = getTextHeight(g, font, size, text, useNativeFonts); return rect; } /** * Retrieves the floating-point text width for the given string, font, size, * and PGraphics context. * * @param g * a PGraphics object * @param font * a <code>PFont</code> object * @param size * the font size * @param useNativeFonts * true if the caller wishes to use Java's built-in font * functionality * @return the width of the indicated text */ public static float getTextWidth(PGraphics g, PFont font, float size, String text, boolean useNativeFonts) { if (isJava2D(g) && useNativeFonts) { FontMetrics fm = null; while (fm == null) { try { fm = getMetrics(g, font.getFont(), size); } catch (Exception e) { try { Thread.sleep(200); } catch (InterruptedException e1) { e1.printStackTrace(); } } } // return fm.stringWidth(text); } char[] chars = text.toCharArray(); float width = 0; for (int j = 0; j < chars.length; j++) { width += font.width(chars[j]) * size; } return width; } public static float getTextWidth(Graphics2D g2, Font font, float size, String text) { FontMetrics fm = g2.getFontMetrics(font.deriveFont(size)); return (float) fm.getStringBounds(text, g2).getWidth(); } public static float getTextHeight(Graphics2D g2, Font font, float size, String text) { FontMetrics fm = g2.getFontMetrics(font.deriveFont(size)); return (float) fm.getStringBounds(text, g2).getHeight(); } /** * Convenience function for <code>isJava2D(PGraphics pg)</code> * * @param p * the current <code>PApplet</code> * @return true if the current <code>PApplet's</code> associated * <code>PGraphics</code> object is an instance of * <code>PGraphicsJava2D</code> */ public static boolean isJava2D(PApplet p) { return isJava2D(p.g); } /** * Determines whether the indicated <code>PGraphics</code> object is an * instance of <code>PGraphicsJava2D</code> * * @param canvas * the <code>PGraphics</code> object to test * @return true if the <code>PGraphics</code> object is an instance of * <code>PGraphicsJava2D</code> */ public static boolean isJava2D(PGraphics pg) { boolean is = pg.getClass() == PGraphicsJava2D.class; return is; } /** * Calls the <code>lazyLoad()</code> method on all of the relevant "singlet" * classes that are required for the correct functioning of all UI objects * in this package. * <p> * This should be called in the constructor of every UIObject, so that the * user doesn't need to call it him or herself. * * @param p * The PApplet with which to associate the UI singlets. * @see org.andrewberman.ui.EventManager * @see org.andrewberman.ui.FocusManager * @see org.andrewberman.ui.ShortcutManager */ // public static void loadUISinglets(PApplet app) // { // if (p != app) // { // p = app; // FontLoader.lazyLoad(p); // FocusManager.lazyLoad(p); // EventManager.lazyLoad(p); // ShortcutManager.lazyLoad(p); // setRenderingHints(p); // } // } /** * Transforms a <code>Point</code> from model to scree ncoordinates in * place. * * @param pt * The point to transform in place. Should currently contain * model coordinates. */ public static void modelToScreen(Point2D.Float pt) { transform(modelview, pt); transform(cameraInv, pt); } public static Image PImageToImage(PApplet p, PImage image) { Image newImage = image.getImage(); // Image newImage = p.createImage(new MemoryImageSource(image.width, // image.height, image.pixels, 0, image.width)); return newImage; } /** * Releases the specified cursor (i.e. returns to the default cursor) only * if the specified object and cursor are currently set. This helps avoid * the "flickering" effect of a bunch of UI objects trying to set and unset * the cursor, because the cursor is only set back to default if the object * which most recently set the cursor is calling the * <code>releaseCursor</code> method. * <p> * Does that make any sense? * <p> * TODO: We could make a new class, CursorManager, to deal with this in a * more organized way... but is it really worth it? * * @param o * The object that is requesting this cursor release. * @param p * The current PApplet */ public static void releaseCursor(Object o, PApplet p) { if (cursorOwner != o) return; // System.out.println("Released."); cursorOwner = null; p.setCursor(baseCursor); } /** * Convenience method for <code>resetMatrix(PGraphics pg)</code>. * * @param p * a PApplet instance */ public static void resetMatrix(PApplet p) { resetMatrix(p.g); } /** * Calls the correct method to reset the matrix of a PGraphics instance. The * main reason for this is that PGraphicsJava2D requires * <code>resetMatrix()</code>, while P3D and OpenGL require a * <code>camera()</code> method, which Java2D unfortunately doesn't * implement. Should we really have to do the same thing in two different * ways? Probably not, But this seems to work well enough... * * @param pg */ public static void resetMatrix(PGraphics pg) { if (isJava2D(pg) || pg.getClass() == PGraphics2D.class) pg.resetMatrix(); else { pg.camera(); } } /** * Transforms a <code>Point</code> from screen to model coordinates in * place. * * @param pt * The point to transform in place. Should currently contain the * mouse coordinates. */ public static void screenToModel(Point2D.Float pt) { transform(camera, pt); transform(modelviewInv, pt); } /** * Converts a <code>Rectangle2D</code> from "screen" to "model" coordinates. * <p> * Note that this package's definition of "model" coordintes is different * from the standard Processing definition. It's all quite confusing, but * this strategy seems to work. Maybe there's a better way? * * @param rect * the <code>Redtangle2D</code> to convert (in place) from screen * to model coordinates. */ public static void screenToModel(Rectangle2D.Float rect) { /* * Strategy: Go through all points in the rectangle, transforming each * point into model coordinates. Then, find the smallest completely * bounding rectangle in model space. Not simple, but it should work. */ tPoint.x = rect.x; tPoint.y = rect.y; transform(camera, tPoint); transform(modelviewInv, tPoint); float x1 = tPoint.x; float y1 = tPoint.y; tPoint.x = rect.x + rect.width; tPoint.y = rect.y; transform(camera, tPoint); transform(modelviewInv, tPoint); float x2 = tPoint.x; float y2 = tPoint.y; tPoint.x = rect.x + rect.width; tPoint.y = rect.y + rect.height; transform(camera, tPoint); transform(modelviewInv, tPoint); float x3 = tPoint.x; float y3 = tPoint.y; tPoint.x = rect.x; tPoint.y = rect.y + rect.height; transform(camera, tPoint); transform(modelviewInv, tPoint); float x4 = tPoint.x; float y4 = tPoint.y; float loX = PApplet.min(new float[] { x1, x2, x3, x4 }); float loY = PApplet.min(new float[] { y1, y2, y3, y4 }); float hiX = PApplet.max(new float[] { x1, x2, x3, x4 }); float hiY = PApplet.max(new float[] { y1, y2, y3, y4 }); rect.setFrameFromDiagonal(loX, loY, hiX, hiY); } /** * Sets the base cursor. Generally called by ToolManager. * * @param c */ public static void setBaseCursor(PApplet p, Cursor c) { baseCursor = c; if (cursorOwner == null) p.setCursor(baseCursor); } /** * Sets the displayed cursor. This method effectively registers the Object * <code>o</code> as the "owner" of the cursor from this point on. This * means that for the cursor to be reset back to normal, the same object ( * <code>o</code>) must call the <code>releaseCursor</code> method. If any * other object calls <code>releaseCursor</code> before <code>o</code>, * nothing will happen. * <p> * Note, however, that any other object can, at any point, call * <code>setCursor</code> and override <code>o's</code> "ownership" of the cursor. * This causes some flickering when two objects are simultaneously trying to * <code>set</code> and <code>release</code> the cursor, in particular when * two objects are on top of each other. * <p> * TODO: Figure out a way to deal with objects on top of each other, without * resorting to something extremely annoying like z-values. * * @param o * The object which is requesting the cursor change. * @param p * The current PApplet * @param cursor * the cursor to change to (i.e. Cursor.HAND_CURSOR). */ public static void setCursor(Object o, PApplet p, int cursor) { cursorOwner = o; p.setCursor(Cursor.getPredefinedCursor(cursor)); } /** * Copies the relevant transformation matrices from the indicated * <code>PApplet</code> into <code>UIUtils'</code> internal static cache. * <p> * This should be called at every iteration of the draw() cycle, * <em><b>after</b></em> all relevant matrix transformations have been * performed (i.e. camera, rotation, etc.). The matrix cache created here is * then used when calling the <code>UIUtils.screenToModel</code> or * associated methods. * * @param p * the PApplet from which to glean the matrix information. * @see org.andrewberman.ui.UIUtils.screenToModel */ public static void setMatrix(PApplet p) { if (isJava2D(p)) { PGraphicsJava2D g = (PGraphicsJava2D) p.g; AffineTransform tr = g.g2.getTransform(); try { affineToPMatrix3D(tr, modelview); // tr.invert(); affineToPMatrix3D(tr.createInverse(), modelviewInv); camera.reset(); cameraInv.reset(); } catch (NoninvertibleTransformException e) { return; } } else { p.g.getMatrix(); camera.set(p.g.getMatrix()); PMatrix pm = p.g.getMatrix(); pm.invert(); cameraInv.set(pm); // p.g.get // modelview.set(p.g.modelview); // modelviewInv.set(p.g.modelviewInv); } } public static void setRenderingHints(Graphics2D g2) { g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); } public static void setRenderingHints(PGraphics g) { if (g instanceof PGraphicsJava2D) { PGraphicsJava2D pg = (PGraphicsJava2D) g; g.smooth(); Graphics2D g2 = pg.g2; setRenderingHints(g2); } } /** * Performs an in-place matrix transformation of a <code>Point</code>. * * @param mat * the transformation <code>PMatrix3D</code> * @param pt * the <code>Point</code> to be transformed in place */ public static void transform(PMatrix3D mat, Point2D.Float pt) { float x = pt.x; float y = pt.y; float z = 0; pt.x = mat.m00 * x + mat.m01 * y + mat.m02 * z + mat.m03; pt.y = mat.m10 * x + mat.m11 * y + mat.m12 * z + mat.m13; } }