/* * Copyright 2003-2010 Tufts University Licensed under the * Educational Community 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.osedu.org/licenses/ECL-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 tufts.vue.gui; import tufts.vue.DEBUG; import tufts.vue.VueUtil; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.font.TextLayout; import java.awt.font.FontRenderContext; import java.awt.geom.Rectangle2D; /** * Provide an immutable single line of text with an exact bounding * box determined by font metrics of all characters in the string. * * We can't use just a TextLayout object, as there are Mac/PC platform * differences in the way the bounds offsets are provided, so we * handle that here in draw(), as well as provide for some visual debugging * diagnostics. * * @see java.awt.font.TextLayout */ public class TextRow { private static final boolean FunkyMacTextBounds = VueUtil.isMacPlatform() && VueUtil.getMacMRJVersion() < 232; /** default FontRenderContext: anti-alias=true and fractional-metrics=false */ private static final FontRenderContext DefaultFontContext = new FontRenderContext(null, true, false); //private static final FontRenderContext DefaultFontContext = new FontRenderContext(null, true, true); private final TextLayout mRow; private final Rectangle2D.Float mBounds; public final String text; public final float width; public final float height; //private static final java.util.Map<String,TextRow> Cache = new java.util.HashMap(); /** factory method, in case impl may want to cache instances of common short strings (e.g., file extensions) */ public static TextRow instance(String txt, Font font) { return new TextRow(txt, font); // // Would need to also key on the font! // if (txt.length() <= 3) { // TextRow r = Cache.get(txt); // if (r != null) { // return r; // } else { // r = new TextRow(txt, font); // Cache.put(txt, r); // return r; // } // } else { // return new TextRow(txt, font); // } } private TextRow(String text, Font font, FontRenderContext frc, TextLayout row, Rectangle2D bounds) { this.text = text; if (row == null) mRow = new TextLayout(text, font, frc); else mRow = row; final Rectangle2D.Float closeBounds = new Rectangle2D.Float(); closeBounds.height = (float) mRow.getBounds().getHeight(); closeBounds.width = (float) mRow.getBounds().getWidth(); closeBounds.x = (float) mRow.getBounds().getX(); closeBounds.y = (float) mRow.getBounds().getY(); if (bounds == null) { mBounds = closeBounds; } else { if (DEBUG.TEXT) System.out.println("TextRow[" + text + "] rowbnds=" + tufts.Util.fmt(row.getBounds())); mBounds = (Rectangle2D.Float) bounds; mBounds.width = closeBounds.width; // take the more accurate width from the TextLayout } this.width = mBounds.width; this.height = mBounds.height; if (DEBUG.TEXT) System.out.println("TextRow[" + text + "] bounds=" + tufts.Util.fmt(bounds)); } public TextRow(String text, Font font, FontRenderContext frc) { this(text, font, frc, null, null); } public TextRow(String text, Graphics g) { this(text, g.getFont(), ((Graphics2D)g).getFontRenderContext()); } public TextRow(String text, Font font) { // font.getStringBounds is less "precise", but more consistent for generating an // even bounding box around the text if you want to align the text. // E.g., TextLayout can report different pixel heights for strings as similar as "10", "11" and "12", // whereas getStringBounds, while reporting a bigger overall box, reports a consistent line height. //this(text, font, DefaultFontContext, null, font.getStringBounds(text, DefaultFontContext)); this(text, font, DefaultFontContext, null, null); // turned off for now: leave node icons as before } public void drawCenter(tufts.vue.DrawContext dc, java.awt.geom.RectangularShape shape) { //System.out.println("Height of [" + text + "]=" + height); draw(dc, (float) (shape.getX() + (shape.getWidth() - width) / 2), (float) (shape.getY() + (shape.getHeight() - height) / 2)); } public void draw(tufts.vue.DrawContext dc, float xoff, float yoff) { draw(dc.g, xoff, yoff); } public void draw(Graphics g, float xoff, float yoff) { draw((Graphics2D) g, xoff, yoff); } public void draw(Graphics2D g, float xoff, float yoff) { // Mac & PC 1.4.1 implementations haved reversed baselines // and differ in how descents are factored into bounds offsets // Update: As of Mac java 1.4.2_09-232, it appears to use the // same method as the PC. if (FunkyMacTextBounds) { //System.out.println("TextRow[" + text + "]@"+tb); yoff += this.height; yoff += mBounds.y; xoff += mBounds.x; // FYI, tb.x always appears to be zero in Mac Java 1.4.1 mRow.draw(g, xoff, yoff); if (DEBUG.BOXES) { final Rectangle2D.Float tb = (Rectangle2D.Float) mBounds.clone(); // draw a red bounding box for testing tb.x += xoff; // tb.y seems to default at to -1, and if // any chars have descent below baseline, tb.y // even less -- e.g., "txt" yields -1, "jpg" yields -2 // that with fractional metrics OFF. With fractional // metrics on, the x/y & size values go from integer // aligned to making use of the floating point. // The mac doesn't fill in tb.x, and thus when fractional // metrics is on, it's a tiny bit off in on the x-axis. tb.y = -tb.y; tb.y += yoff; tb.y -= tb.height; Graphics2D _g = (Graphics2D) g.create(); tufts.vue.DrawContext.setAbsoluteStroke(_g, 1); _g.setColor(Color.green); _g.draw(tb); _g.dispose(); } } else { // This is cleaner, thus I'm assuming the PC // implementation is also cleaner, and worthy of being // the default case. mRow.draw(g, -mBounds.x + xoff, -mBounds.y + yoff); //baseline = yoff + tb.height; if (DEBUG.BOXES) { final Rectangle2D.Float tb = (Rectangle2D.Float) mBounds.clone(); // draw a red bounding box for testing tb.x = xoff; tb.y = yoff; tufts.vue.DrawContext.setAbsoluteStroke(g, 1); g.setColor(Color.green); g.draw(tb); } } } public String toString() { return "TextRow[" + text + " " + tufts.Util.fmt(mBounds) + "]"; } }