All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mastfrog.swing.cell.TextCell Maven / Gradle / Ivy

/* 
 * The MIT License
 *
 * Copyright 2020 Tim Boudreau.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.mastfrog.swing.cell;

import java.awt.BasicStroke;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.Toolkit;
import java.awt.font.LineMetrics;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RectangularShape;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.swing.UIManager;

/**
 * A single cell of text which may have its own foreground, background, font,
 * font style and scaling, left and right and bottom margins, padding,
 * properties, and optionally a RectangularShape which wraps around the
 * background, and can have child cells with similar characteristics - basically
 * a celled cell-renderer minus the swing cell-rendering goo.  Cells can
 * be composed together to create complex effects using the append()
 * method (resetting removes them.
 * 

* A cell essentially consists of some text, an optional background or shape, * fonts and colors - the attributes needed to paint some text *

* * @author Tim Boudreau */ public class TextCell { private static final BasicStroke STROKE = new BasicStroke(1); private final Rectangle2D.Float RECT = new Rectangle2D.Float(); private static final Line2D.Float STRIKE = new Line2D.Float(); private Paint foreground; private Paint background; private Font font; private String text; private RectangularShape bgShape; private TextCell child; private TextCell oldChild; private boolean isChild; private int indent; private int leftMargin; private int rightMargin; private int padding; private int bottomMargin; private boolean bold; private boolean italic; private boolean stretch; private boolean strikethrough; private boolean shapeOutlinePainted; private int topMargin; private boolean monospaced; private AffineTransform scaleFont; private AffineTransform lastScaleFont; private static Font monospacedFont; Font monospacedFont() { if (monospacedFont != null) { return monospacedFont; } Font f = UIManager.getFont("EditorPane.font"); Font ctrl = UIManager.getFont("controlFont"); // in a bare swing application, this happens if ("Dialog".equals(f.getName()) || ctrl != null && ctrl.getName().equals(f.getName())) { f = null; } if (f == null) { // f = new Font("Monospaced", Font.PLAIN, 12); f = new Font("Courier New", Font.PLAIN, 12); } else { if (f.getStyle() != Font.PLAIN) { f = f.deriveFont(Font.PLAIN); } } return monospacedFont = f; } TextCell(String text, boolean isChild) { this.text = text; this.isChild = isChild; } public TextCell(String text) { this.text = text; } public TextCell monospaced() { this.monospaced = true; return this; } public TextCell reset(String newText) { reset(); this.text = newText; return this; } /** * Wipe the contents and properties of this cell, leaving it ready to be set * up with new contents and properties. * * @return this */ public TextCell reset() { text = ""; oldChild = child; if (scaleFont != null) { lastScaleFont = scaleFont; } child = null; monospaced = false; strikethrough = false; foreground = null; background = null; font = null; indent = 0; bottomMargin = 0; shapeOutlinePainted = false; leftMargin = 0; padding = 0; bold = false; italic = false; stretch = false; scaleFont = null; rightMargin = 0; topMargin = 0; return this; } /** * Create a cell which shares all the properties of this one other than * child cells, but uses the passed text as its text. * * @param text The text * @return A new cell */ public TextCell newCellLikeThis(String text) { TextCell nue = new TextCell(text, false); nue.foreground = foreground; nue.topMargin = topMargin; nue.background = background; nue.font = font; nue.bgShape = bgShape; nue.indent = indent; nue.bottomMargin = bottomMargin; nue.leftMargin = leftMargin; nue.padding = padding; nue.bold = bold; nue.strikethrough = strikethrough; nue.italic = italic; nue.stretch = stretch; nue.scaleFont = scaleFont; nue.rightMargin = rightMargin; nue.shapeOutlinePainted = shapeOutlinePainted; nue.lastScaleFont = lastScaleFont; nue.monospaced = monospaced; return nue; } /** * Set the bottom leftMargin in pixels. * * @param bottomMargin The bottom leftMargin * @return this */ public TextCell bottomMargin(int bottomMargin) { this.bottomMargin = bottomMargin; return this; } /** * Set this cell to strike through its text. * * @return this */ public TextCell strikethrough() { this.strikethrough = true; return this; } /** * Change the text of this cell. * * @param text The new text. * @return */ public TextCell withText(String text) { this.text = text == null ? "" : text; return this; } /** * Cause the background shape (if any) to stretch to the maxiumum x position * when painting, such that any child cells also are contained within it. * * @return this */ public TextCell stretch() { this.stretch = true; return this; } /** * Add bold to the font styles. * * @return this */ public TextCell bold() { bold = true; return this; } /** * Add italic to the font style. * * @return this */ public TextCell italic() { italic = true; return this; } /** * Set the right leftMargin in pixels. * * @param margin The leftMargin * @return this */ public TextCell rightMargin(int margin) { this.rightMargin = margin; return this; } /** * Set the font to use (the passed font will be used as a base for deriving * the actual font painted with, if the font scaling or style is set). * * @param font The font * @return this */ public TextCell withFont(Font font) { this.font = font; return this; } /** * Set the foreground paint. * * @param fg The foreground paint. * @return this */ public TextCell withForeground(Paint fg) { this.foreground = fg; return this; } /** * Set the background paint and a shape to use for the background. * * @param bg The background paint * @param bgShape The background shape * @return this */ public TextCell withBackground(Paint bg, RectangularShape bgShape) { this.bgShape = bgShape; this.background = bg; return this; } /** * Set the background paint. * * @param bg The background * @return this */ public TextCell withBackground(Paint bg) { this.background = bg; return this; } /** * Set the indentation of this cell - empty space before any background * shape or text is painted. * * @param by The indent * @return this */ public TextCell indent(int by) { indent = by; return this; } /** * Set the left leftMargin of this cell - empty space which may come after * the left edge of the background shape (if any) before text painting * begins. * * @param margin The leftMargin * @return this */ public TextCell leftMargin(int margin) { this.leftMargin = margin; return this; } /** * Set the padding - pixels added to all sides of this cell when painting or * computing the preferred size. * * @param padding The padding * @return this */ public TextCell pad(int padding) { this.padding = padding; return this; } /** * Set scaling on the font when painting. * * @param by A value > 1 to make the font larger, or < 1 to make it * smaller * @return this */ public TextCell scaleFont(double by) { if (by == 1D || by <= 0D) { scaleFont = null; } else { if (lastScaleFont != null && lastScaleFont.getScaleX() == by && lastScaleFont.getScaleY() == by) { scaleFont = lastScaleFont; } else { scaleFont = AffineTransform.getScaleInstance(by, by); } } return this; } /** * Set scaling on the font when painting. * * @param by A value > 1 to make the font larger, or < 1 to make it * smaller * @return this */ public TextCell scaleFont(float by) { return scaleFont((double) by); } public TextCell topMargin(int margin) { this.topMargin = topMargin; return this; } /** * Append a new cell to this one (technically adding a child cell which is * passed to the passed consumer for configuration) - the new cell will be * painted adjacent to this one and may have its own color and other * properties. * * @param text The text to use for the subsequent cell * @param childConsumer A consumer which is passed the child cell * @return this, not the child cell */ public TextCell append(String text, Consumer childConsumer) { if (oldChild != null) { child = oldChild.reset(); oldChild = null; childConsumer.accept(child.withText(text));; return this; } if (child != null) { child.append(text, childConsumer); } else { child = new TextCell(text, true); childConsumer.accept(child); } return this; } private Font baseFont(Font initialFont) { Font result; if (font == null) { if (monospaced) { result = monospacedFont(); if (result.getSize2D() != initialFont.getSize2D() || result.getStyle() != initialFont.getStyle()) { result = result.deriveFont(initialFont.getStyle(), initialFont.getSize2D()); } if (initialFont.isTransformed()) { result = result.deriveFont(initialFont.getTransform()); } } else { result = initialFont; } } else { result = font; } return result; } private Font _font(Font initialFont) { Font f = baseFont(initialFont); if (bold && italic && (!f.isBold() || !f.isItalic())) { f = f.deriveFont(Font.BOLD | Font.ITALIC); } else if (bold && !f.isBold()) { f = f.deriveFont(Font.BOLD); } else if (italic && !f.isItalic()) { f = f.deriveFont(Font.ITALIC); } if (scaleFont != null) { f = f.deriveFont(scaleFont); } return f; } /** * Compute the bounds of this cell and its children into the passed * Rectangle.' * * @param initialFont The font to use if no font is explicitly set on this * cell or its children * @param into A rectangle to reconfigure with the target bounds * @param x The starting x coordinate * @param y The starting y coordinate * @param func A function for retrieving a screen-tuned FontMetrics for a * font - in a Component, pass * this::getFontMetrics, or a handle to the equivalent method * on a Graphics2D. */ public void bounds(Font initialFont, Rectangle2D.Float into, float x, float y, Function func) { if (text == null || text.isEmpty()) { if (child != null) { child.bounds(initialFont, into, x, y, func); } else { into.width = into.height = 0; } return; } FontMetrics fm = func.apply(_font(initialFont)); int w = fm.stringWidth(text); int h = fm.getHeight() + fm.getDescent(); if (into.isEmpty()) { into.x = x; into.y = y; into.width = leftMargin + rightMargin + indent + w + (padding * 2); into.height = h + (padding * 2) + bottomMargin + topMargin; } else { into.width += leftMargin + rightMargin + indent + w + (padding * 2); into.height = Math.max(into.height, h + (padding * 2) + bottomMargin + topMargin); } if (child != null) { child.bounds(initialFont, into, x + into.width, y, func); } } /** * Get the text of this cell and any child cells, prepending the passed * delimiter before the text of child cells. * * @param delimiter A delmiter * @return The text of this and all descendant child cells, concatenated */ public String fullText(String delimiter) { StringBuilder sb = new StringBuilder(); fullText(sb, delimiter); return sb.toString(); } private void fullText(StringBuilder sb, String delimiter) { if (text != null && !text.isEmpty()) { sb.append(text).append(delimiter); } if (child != null) { child.fullText(sb, delimiter); } } /** * Returns true if the text of this and any child cells is null, empty or * entirely whitespace. * * @return True if the text is effectively empty */ public boolean isEmpty() { boolean result = text.trim().isEmpty(); if (child != null) { result |= child.isEmpty(); } return result; } /** * Paint this cell and its children. * * @param g A graphics * @param x The starting x coordinate * @param y The starting y coordinate * @param maxX The maximum x coordinate * @param maxY The maximum y coordinate * @param painted A rectangle to configure with the bounds modified by * painting * @return The text baseline in the y axis */ public float paint(Graphics2D g, float x, float y, float maxX, float maxY, Rectangle2D.Float painted) { return paint(g, x, y, maxX, maxY, -1, painted); } public TextCell shapeOutlinePainted() { shapeOutlinePainted = true; return this; } private float baseline(float y, Graphics2D g, float lastBaseline) { Font f = _font(font == null ? g.getFont() : font); FontMetrics fm = g.getFontMetrics(f); float result = Math.max(lastBaseline, y + fm.getAscent()) + padding; if (child != null) { result = Math.max(result + topMargin, child.baseline(y, g, result)); } else { result += topMargin; } return result; } String textAt(Point point, float x, float y, float maxX, float maxY, Font baseFont, Function mx) { if (point.x < x || point.y < y || point.x > maxX || point.y > maxY) { return null; } if (text == null || text.isEmpty()) { if (child != null) { return child.textAt(point, x, y, maxX, maxY, baseFont, mx); } return null; } Font f = _font(font == null ? baseFont : font); FontMetrics fm = mx.apply(f); int w = fm.stringWidth(text); int h = fm.getHeight() + fm.getDescent(); float textX = x + leftMargin + indent + padding; float textMaxX = Math.min(maxX, textX + w + padding + rightMargin); if (point.x >= textX && point.x <= textMaxX) { return text; } if (child != null) { return child.textAt(point, textMaxX, y, maxX, maxY, baseFont, mx); } return null; } private float paint(Graphics2D g, float x, float y, float maxX, float maxY, float baseline, Rectangle2D.Float painted) { // System.out.println("Paint cell '" + text + "' at " + x + ", " + y + " maxes " + maxX + "," + maxY); if (text == null || text.isEmpty()) { if (child != null) { return child.paint(g, x, y, maxX, maxY, baseline, painted); } } Font oldFont = g.getFont(); Paint oldPaint = g.getPaint(); Font f = _font(font == null ? g.getFont() : font); if (oldFont != f) { g.setFont(f); } if (baseline < 0) { baseline = baseline(y, g, baseline); } FontMetrics fm = g.getFontMetrics(); float baselineAdjust = baseline - (y + fm.getAscent()); int w = fm.stringWidth(text); int h = fm.getAscent() + fm.getDescent(); if (!isChild) { g.addRenderingHints(getHints(true)); } if (background != null) { RectangularShape shape = bgShape == null ? RECT : bgShape; float shapeW; if (!stretch) { shapeW = Math.min(rightMargin + leftMargin + w + (padding * 2) + indent, maxX - x); } else { shapeW = Math.max(rightMargin + leftMargin + w + (padding * 2) + indent, maxX - x); } float shapeHeight = Math.min(y + h + (padding * 2), maxY - y) - y; float shapeY = y + baselineAdjust + topMargin; if (shapeY + shapeHeight < y + h) { shapeY += h - shapeHeight; } shape.setFrame(x + leftMargin, shapeY, shapeW, shapeHeight); if (!isChild) { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } Stroke oldStroke = g.getStroke(); g.setStroke(STROKE); if (shapeOutlinePainted) { g.setPaint(foreground); } else { g.setPaint(background); } g.draw(shape); if (shapeOutlinePainted) { g.setPaint(background); } g.fill(shape); g.setStroke(oldStroke); if (painted.isEmpty()) { painted.setRect(shape.getX(), shape.getY(), shape.getWidth(), shape.getHeight()); } else { if (shape != RECT) { RECT.setFrame(shape.getX(), shape.getY(), shape.getWidth(), shape.getHeight()); } painted.add(RECT); } } else { if (painted.isEmpty()) { painted.setFrame(x, y, w + leftMargin + indent + (padding * 2), h + (padding * 2)); } else { RECT.setFrame(x, y, w + leftMargin + indent + (padding * 2), h + (padding * 2)); painted.add(RECT); } } float textY = baseline; if (foreground != null) { g.setPaint(foreground); } else { g.setPaint(oldPaint); } float textX = x + leftMargin + indent + padding; g.drawString(text, textX, textY); if (strikethrough) { LineMetrics lm = fm.getLineMetrics(text, g); float lineY = textY + lm.getStrikethroughOffset() + topMargin; float thick = lm.getStrikethroughThickness(); BasicStroke stroke = new BasicStroke(thick); Stroke oldStroke = g.getStroke(); g.setStroke(stroke); STRIKE.setLine(textX, lineY, textX + w, lineY); g.draw(STRIKE); g.setStroke(oldStroke); } if (oldFont != f) { g.setFont(oldFont); } g.setPaint(oldPaint); if (child != null) { child.paint(g, x + w + leftMargin + indent + (padding * 2) + rightMargin, y, maxX, maxY, baseline, painted); } return baseline; } @Override public String toString() { return "TextCell{" + "foreground=" + foreground + ", background=" + background + ", font=" + font + ", text=" + text + ", bgShape=" + bgShape + ", isChild=" + isChild + ", indent=" + indent + ", margin=" + leftMargin + ", padding=" + padding + ", bold=" + bold + ", italic=" + italic + ", stretch=" + stretch + ", child=" + child + '}'; } private static Map hintsMap; @SuppressWarnings("unchecked") public static final Map getHints(boolean antialias) { //XXX We REALLY need to put this in a graphics utils lib if (hintsMap == null) { //Thanks to Phil Race for making this possible hintsMap = (Map) (Toolkit.getDefaultToolkit().getDesktopProperty("awt.font.desktophints")); //NOI18N if (hintsMap == null) { hintsMap = new HashMap(); if (antialias) { hintsMap.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } } } Map ret = hintsMap; assert ret != null; // does this method need to be synchronized? return ret; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy