
org.jdesktop.swingx.JXLabel Maven / Gradle / Ivy
/*
* $Id: JXLabel.java,v 1.12 2007/09/29 20:01:47 rah003 Exp $
*
* Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
* Santa Clara, California 95054, U.S.A. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jdesktop.swingx;
import java.awt.Color;
import java.awt.Container;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Shape;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.SizeRequirements;
import javax.swing.plaf.basic.BasicHTML;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.BoxView;
import javax.swing.text.ComponentView;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.GlyphView;
import javax.swing.text.IconView;
import javax.swing.text.LabelView;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.ParagraphView;
import javax.swing.text.Position;
import javax.swing.text.Segment;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledEditorKit;
import javax.swing.text.TabableView;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;
import javax.swing.text.WrappedPlainView;
import org.jdesktop.swingx.painter.AbstractPainter;
import org.jdesktop.swingx.painter.Painter;
/**
*
* A {@link javax.swing.JLabel} subclass which supports {@link org.jdesktop.swingx.painter.Painter}s, multi-line text,
* and text rotation.
*
*
*
* Painter support consists of the foregroundPainter
and backgroundpainter
properties. The
* backgroundPainter
refers to a painter responsible for painting beneath the text and icon. This
* painter, if set, will paint regardless of the opaque
property. If the background painter does not
* fully paint each pixel, then you should make sure the opaque
property is set to false.
*
*
*
* The foregroundPainter
is responsible for painting the icon and the text label. If no foregroundPainter
* is specified, then the look and feel will paint the label. Note that if opaque is set to true and the look and feel
* is rendering the foreground, then the foreground may paint over the background. Most look and feels will
* paint a background when opaque
is true. To avoid this behavior, set opaque
to false.
*
*
*
* Since JXLabel is not opaque by default (isOpaque()
returns false), neither of these problems
* typically present themselves.
*
*
*
* Multi-line text is enabled via the lineWrap
property. Simply set it to true. By default, line wrapping
* occurs on word boundaries.
*
*
*
* The text (actually, the entire foreground and background) of the JXLabel may be rotated. Set the
* rotation
property to specify what the rotation should be.
*
* TODO not yet determined what API this will use.
*
* @author [email protected]
* @author rbair
* @author rah
* @author mario_cesar
*/
public class JXLabel extends JLabel {
// textOrientation value declarations...
public static final double NORMAL = 0;
public static final double INVERTED = Math.PI;
public static final double VERTICAL_LEFT = 3 * Math.PI / 2;
public static final double VERTICAL_RIGHT = Math.PI / 2;
private double textRotation = NORMAL;
private boolean painting = false;
private Painter foregroundPainter;
private Painter backgroundPainter;
private boolean multiLine;
private int pWidth;
private int pHeight;
private boolean ignoreRepaint;
private static final String oldRendererKey = "was" + BasicHTML.propertyKey;
/**
* Create a new JXLabel. This has the same semantics as creating a new JLabel.
*/
public JXLabel() {
super();
initPainterSupport();
initLineWrapSupport();
}
public JXLabel(Icon image) {
super(image);
initPainterSupport();
initLineWrapSupport();
}
public JXLabel(Icon image, int horizontalAlignment) {
super(image, horizontalAlignment);
initPainterSupport();
initLineWrapSupport();
}
/**
* Create a new JXLabel with the given text as the text for the label. This is shorthand for:
*
*
* JXLabel label = new JXLabel();
* label.setText("Some Text");
*
*
* @param text the text to set.
*/
public JXLabel(String text) {
super(text);
initPainterSupport();
initLineWrapSupport();
}
public JXLabel(String text, Icon image, int horizontalAlignment) {
super(text, image, horizontalAlignment);
initPainterSupport();
initLineWrapSupport();
}
public JXLabel(String text, int horizontalAlignment) {
super(text, horizontalAlignment);
initPainterSupport();
initLineWrapSupport();
}
private void initPainterSupport() {
foregroundPainter = new AbstractPainter() {
protected void doPaint(Graphics2D g, Object object, int width, int height) {
Insets i = getInsets();
g = (Graphics2D) g.create(-i.left, -i.top, width, height);
JXLabel.super.paintComponent(g);
g.dispose();
}
};
}
/**
* Helper method for initializing multiline support.
*/
private void initLineWrapSupport() {
addPropertyChangeListener(new MultiLineSupport());
}
/**
* Returns the current foregroundPainter. This is a bound property. By default the foregroundPainter will be an
* internal painter which executes the standard painting code (paintComponent()).
*
* @return the current foreground painter.
*/
public final Painter getForegroundPainter() {
return foregroundPainter;
}
/**
* Sets a new foregroundPainter on the label. This will replace the existing foreground painter. Existing painters
* can be wrapped by using a CompoundPainter.
*
* @param painter
*/
public void setForegroundPainter(Painter painter) {
Painter old = this.getForegroundPainter();
this.foregroundPainter = painter;
firePropertyChange("foregroundPainter", old, getForegroundPainter());
repaint();
}
/**
* Sets a Painter to use to paint the background of this component By default there is already a single painter
* installed which draws the normal background for this component according to the current Look and Feel. Calling
* setBackgroundPainter
will replace that existing painter.
*
* @param p the new painter
* @see #getBackgroundPainter()
*/
public void setBackgroundPainter(Painter p) {
Painter old = getBackgroundPainter();
backgroundPainter = p;
firePropertyChange("backgroundPainter", old, getBackgroundPainter());
repaint();
}
/**
* Returns the current background painter. The default value of this property is a painter which draws the normal
* JPanel background according to the current look and feel.
*
* @return the current painter
* @see #setBackgroundPainter(Painter)
*/
public final Painter getBackgroundPainter() {
return backgroundPainter;
}
/**
* Gets current value of text rotation in rads.
*
* @return
* @see #setTextRotation(double)
*/
public double getTextRotation() {
return textRotation;
}
/**
* Sets new value for text rotation. The value can be anything in range <0,2PI>. Note that although property name
* suggests only text rotation, the whole foreground painter is rotated in fact. Due to various reasons it is
* strongly discouraged to access any size related properties of the label from other threads then EDT when this
* property is set.
*
* @param textOrientation Value for text rotation in range <0,2PI>
* @see #getTextRotation()
*/
public void setTextRotation(double textOrientation) {
double old = getTextRotation();
this.textRotation = textOrientation;
if (old != getTextRotation()) {
firePropertyChange("textRotation", old, getTextRotation());
}
repaint();
}
/**
* Enables line wrapping support for plain text. By default this support is disabled to mimic default of the JLabel.
* Value of this property has no effect on HTML text.
*
* @param b the new value
* @see #isMultiLine()
*/
public void setLineWrap(boolean b) {
boolean old = isLineWrap();
this.multiLine = b;
if (isLineWrap() != old) {
firePropertyChange("lineWrap", old, isLineWrap());
if (getForegroundPainter() != null) {
// XXX There is a bug here. In order to make painter work with this, caching has to be disabled
((AbstractPainter) getForegroundPainter()).setCacheable(!b);
}
repaint();
}
}
/**
* Returns the current status of line wrap support. The default value of this property is false to mimic default
* JLabel behavior. Value of this property has no effect on HTML text.
*
* @return the current multiple line splitting status
* @see #setMultiLine(boolean)
*/
public boolean isLineWrap() {
return this.multiLine;
}
private boolean paintBorderInsets = true;
/**
* Returns true if the background painter should paint where the border is
* or false if it should only paint inside the border. This property is
* true by default. This property affects the width, height,
* and intial transform passed to the background painter.
* @return current value of the paintBorderInsets property
*/
public boolean isPaintBorderInsets() {
return paintBorderInsets;
}
/**
* Sets the paintBorderInsets property.
* Set to true if the background painter should paint where the border is
* or false if it should only paint inside the border. This property is true by default.
* This property affects the width, height,
* and intial transform passed to the background painter.
*
* This is a bound property.
* @param paintBorderInsets new value of the paintBorderInsets property
*/
public void setPaintBorderInsets(boolean paintBorderInsets) {
boolean old = this.isPaintBorderInsets();
this.paintBorderInsets = paintBorderInsets;
firePropertyChange("paintBorderInsets", old, isPaintBorderInsets());
}
/**
* @param g graphics to paint on
*/
@Override
protected void paintComponent(Graphics g) {
// resizing the text view causes recursive callback to the paint down the road. In order to prevent such
// computationaly intensive series of repiants every call to paint is skipped while top most call is being
// executed.
if (ignoreRepaint) {
return;
}
if (backgroundPainter == null && foregroundPainter == null) {
super.paintComponent(g);
} else {
Graphics2D g2 = (Graphics2D) g.create();
pWidth = getWidth();
pHeight = getHeight();
if(!isPaintBorderInsets()) {
Insets i = getInsets();
g2.translate(i.left, i.top);
pWidth = getWidth() - i.left - i.right;
pHeight = getHeight() - i.top - i.bottom;
}
if (backgroundPainter != null) {
backgroundPainter.paint(g2, this, pWidth, pHeight);
}
if (foregroundPainter != null) {
double tx = (double) getWidth();
double ty = (double) getHeight();
// orthogonal cases are most likely the most often used ones, so give them preferential treatment.
if ((textRotation > 4.697 && textRotation < 4.727) || (textRotation > 1.555 && textRotation < 1.585)) {
// vertical
int tmp = pHeight;
pHeight = pWidth;
pWidth = tmp;
tx = pWidth;
ty = pHeight;
} else if ((textRotation > -0.015 && textRotation < 0.015)
|| (textRotation > 3.140 && textRotation < 3.1430)) {
// normal & inverted
pHeight = getHeight();
pWidth = getWidth();
} else {
// the rest of it. Calculate best rectangle that fits the bounds. "Best" is considered one that
// allows whole text to fit in, spanned on preferred axis (X). If that doesn't work, fit the text
// inside square with diagonal equal min(height, width) (Should be the largest rectangular area that
// fits in, math proof available upon request)
ignoreRepaint = true;
double square = Math.min(getHeight(), getWidth()) * Math.cos(Math.PI / 4d);
View v = (View) getClientProperty(BasicHTML.propertyKey);
if (v == null) {
// no html and no wrapline enabled means no view
// ... find another way to figure out the heigh
ty = getFontMetrics(getFont()).getHeight();
double cw = (getWidth() - Math.abs(ty * Math.sin(textRotation)))
/ Math.abs(Math.cos(textRotation));
double ch = (getHeight() - Math.abs(ty * Math.cos(textRotation)))
/ Math.abs(Math.sin(textRotation));
// min of whichever is above 0 (!!! no min of abs values)
tx = cw < 0 ? ch : ch > 0 ? Math.min(cw, ch) : cw;
} else {
float w = v.getPreferredSpan(View.X_AXIS);
float h = v.getPreferredSpan(View.Y_AXIS);
double c = w;
double alpha = textRotation;// % (Math.PI/2d);
boolean ready = false;
while (!ready) {
// shorten the view len until line break is forced
while (h == v.getPreferredSpan(View.Y_AXIS)) {
w -= 10;
v.setSize(w, h);
}
if (w < square || h > square) {
// text is too long to fit no matter what. Revert shape to square since that is the
// best option (1st derivation for area size of rotated rect in rect is equal 0 for
// rotated rect with equal w and h i.e. for square)
w = h = (float) square;
// set view height to something big to prevent recursive resize/repaint requests
v.setSize(w, 100000);
break;
}
// calc avail width with new view height
h = v.getPreferredSpan(View.Y_AXIS);
double cw = (getWidth() - Math.abs(h * Math.sin(alpha))) / Math.abs(Math.cos(alpha));
double ch = (getHeight() - Math.abs(h * Math.cos(alpha))) / Math.abs(Math.sin(alpha));
// min of whichever is above 0 (!!! no min of abs values)
c = cw < 0 ? ch : ch > 0 ? Math.min(cw, ch) : cw;
// make it one pix smaller to ensure text is not cut on the left
c--;
if (c > w) {
v.setSize((float) c, 10 * h);
ready = true;
} else {
v.setSize((float) c, 10 * h);
if (v.getPreferredSpan(View.Y_AXIS) > h) {
// set size back to figure out new line break and height after
v.setSize(w, 10 * h);
} else {
w = (float) c;
ready = true;
}
}
}
tx = Math.floor(w);// xxx: watch out for first letter on each line missing some pixs!!!
ty = h;
}
pWidth = (int) tx;
pHeight = (int) ty;
ignoreRepaint = false;
}
double wx = Math.sin(textRotation) * ty + Math.cos(textRotation) * tx;
double wy = Math.sin(textRotation) * tx + Math.cos(textRotation) * ty;
double x = (getWidth() - wx) / 2 + Math.sin(textRotation) * ty;
double y = (getHeight() - wy) / 2;
g2.translate(x, y);
g2.rotate(textRotation);
painting = true;
// uncomment to highlight text area
// Color c = g2.getColor();
// g2.setColor(Color.RED);
// g2.fillRect(0, 0, getWidth(), getHeight());
// g2.setColor(c);
foregroundPainter.paint(g2, this, pWidth, pHeight);
painting = false;
pWidth = 0;
pHeight = 0;
}
g2.dispose();
}
}
@Override
public void repaint() {
if (ignoreRepaint) {
return;
}
super.repaint();
}
@Override
public void repaint(int x, int y, int width, int height) {
if (ignoreRepaint) {
return;
}
super.repaint(x, y, width, height);
}
@Override
public void repaint(long tm) {
if (ignoreRepaint) {
return;
}
super.repaint(tm);
}
@Override
public void repaint(long tm, int x, int y, int width, int height) {
if (ignoreRepaint) {
return;
}
super.repaint(tm, x, y, width, height);
}
// ----------------------------------------------------------
// textOrientation magic
@Override
public int getHeight() {
int retValue = super.getHeight();
if (painting) {
retValue = pHeight;
}
return retValue;
}
@Override
public int getWidth() {
int retValue = super.getWidth();
if (painting) {
retValue = pWidth;
}
return retValue;
}
protected MultiLineSupport getMultiLineSupport() {
return new MultiLineSupport();
}
// ----------------------------------------------------------
// WARNING:
// Anything below this line is related to lineWrap support and can be safely ignored unless
// in need to mess around with the implementation details.
// ----------------------------------------------------------
// FYI: This class doesn't reinvent line wrapping. Instead it makes use of existing support
// made for JTextComponent/JEditorPane.
// All the classes below named Alter* are verbatim copy of swing.text.* classes made to
// overcome package visibility of some of the code. All other classes here, when their name
// matches corresponding class from swing.text.* package are copy of the class with removed
// support for highlighting selection. In case this is ever merged back to JDK all of this
// can be safely removed as long as corresponding swing.text.* classes make appropriate checks
// before casting JComponent into JTextComponent to find out selected region since
// JLabel/JXLabel does not support selection of the text.
public static class MultiLineSupport implements PropertyChangeListener {
private static final String HTML = "";
private static ViewFactory basicViewFactory;
private static BasicEditorKit basicFactory;
public void propertyChange(PropertyChangeEvent evt) {
String name = evt.getPropertyName();
JXLabel src = (JXLabel) evt.getSource();
if (src.isLineWrap()) {
if ("font".equals(name) || "foreground".equals(name)) {
if (evt.getOldValue() != null) {
updateRenderer(src);
}
} else if ("text".equals(name)) {
if (isHTML((String) evt.getOldValue()) && evt.getNewValue() != null
&& !isHTML((String) evt.getNewValue())) {
// was html , but is not
if (src.getClientProperty(oldRendererKey) == null
&& src.getClientProperty(BasicHTML.propertyKey) != null) {
src.putClientProperty(oldRendererKey, src.getClientProperty(BasicHTML.propertyKey));
}
src.putClientProperty(BasicHTML.propertyKey, createView(src));
} else if (!isHTML((String) evt.getOldValue()) && evt.getNewValue() != null
&& !isHTML((String) evt.getNewValue())) {
// wasn't html and isn't
updateRenderer(src);
} else {
// either was html and is html or wasn't html, but is html
restoreHtmlRenderer(src);
}
} else if ("lineWrap".equals(name) && !isHTML(src.getText())) {
src.putClientProperty(BasicHTML.propertyKey, createView(src));
}
} else if ("lineWrap".equals(name)) {
restoreHtmlRenderer(src);
}
}
private static void restoreHtmlRenderer(JXLabel src) {
Object current = src.getClientProperty(BasicHTML.propertyKey);
if (current == null || current instanceof Renderer) {
src.putClientProperty(BasicHTML.propertyKey, src.getClientProperty(oldRendererKey));
}
}
private static boolean isHTML(String s) {
return s != null && s.toLowerCase().startsWith(HTML);
}
public static View createView(JXLabel c) {
BasicEditorKit kit = getFactory();
Document doc = kit.createDefaultDocument(c.getFont(), c.getForeground());
Reader r = new StringReader(c.getText());
try {
kit.read(r, doc, 0);
} catch (Throwable e) {
}
ViewFactory f = kit.getViewFactory();
View hview = f.create(doc.getDefaultRootElement());
View v = new Renderer(c, f, hview, true);
return v;
}
public static void updateRenderer(JXLabel c) {
View value = null;
View oldValue = (View) c.getClientProperty(BasicHTML.propertyKey);
if (oldValue == null || oldValue instanceof Renderer) {
value = createView(c);
}
if (value != oldValue && oldValue != null) {
for (int i = 0; i < oldValue.getViewCount(); i++) {
oldValue.getView(i).setParent(null);
}
}
c.putClientProperty(BasicHTML.propertyKey, value);
}
private static BasicEditorKit getFactory() {
if (basicFactory == null) {
basicViewFactory = new BasicViewFactory();
basicFactory = new BasicEditorKit();
}
return basicFactory;
}
private static class BasicEditorKit extends StyledEditorKit {
public Document createDefaultDocument(Font defaultFont, Color foreground) {
BasicDocument doc = new BasicDocument(defaultFont, foreground);
doc.setAsynchronousLoadPriority(Integer.MAX_VALUE);
return doc;
}
public ViewFactory getViewFactory() {
return basicViewFactory;
}
}
}
private static class BasicViewFactory implements ViewFactory {
public View create(Element elem) {
String kind = elem.getName();
View view = null;
if (kind == null) {
// default to text display
view = new LabelView(elem);
} else if (kind.equals(AbstractDocument.ContentElementName)) {
view = new LabelView(elem);
} else if (kind.equals(AbstractDocument.ParagraphElementName)) {
view = new ParagraphView(elem);
} else if (kind.equals(AbstractDocument.SectionElementName)) {
view = new BoxView(elem, View.Y_AXIS);
} else if (kind.equals(StyleConstants.ComponentElementName)) {
view = new ComponentView(elem);
} else if (kind.equals(StyleConstants.IconElementName)) {
view = new IconView(elem);
}
return view;
}
}
static class BasicDocument extends DefaultStyledDocument {
BasicDocument(Font defaultFont, Color foreground) {
setFontAndColor(defaultFont, foreground);
}
private void setFontAndColor(Font font, Color fg) {
if (fg != null) {
MutableAttributeSet attr = new SimpleAttributeSet();
StyleConstants.setForeground(attr, fg);
getStyle("default").addAttributes(attr);
}
if (font != null) {
MutableAttributeSet attr = new SimpleAttributeSet();
StyleConstants.setFontFamily(attr, font.getFamily());
getStyle("default").addAttributes(attr);
attr = new SimpleAttributeSet();
StyleConstants.setFontSize(attr, font.getSize());
getStyle("default").addAttributes(attr);
attr = new SimpleAttributeSet();
StyleConstants.setBold(attr, font.isBold());
getStyle("default").addAttributes(attr);
attr = new SimpleAttributeSet();
StyleConstants.setItalic(attr, font.isItalic());
getStyle("default").addAttributes(attr);
}
MutableAttributeSet attr = new SimpleAttributeSet();
StyleConstants.setSpaceAbove(attr, 0f);
getStyle("default").addAttributes(attr);
// TODO: add rest of the style stuff
// ... if anyone ever want's this (stuff like justification, etc.)
// attr = new SimpleAttributeSet();
// StyleConstants.setLeftIndent(attr,5f);
// getStyle("default").addAttributes(attr);
// MutableAttributeSet attr = new SimpleAttributeSet();
// StyleConstants.setAlignment(attr, StyleConstants.ALIGN_JUSTIFIED);
// getStyle("default").addAttributes(attr);
}
}
/**
* Root text view that acts as an renderer.
*/
static class Renderer extends WrappedPlainView {
JXLabel host;
private int width;
Renderer(JXLabel c, ViewFactory f, View v, boolean wordWrap) {
super(null, wordWrap);
factory = f;
view = v;
view.setParent(this);
host = c;
// initially layout to the preferred size
setSize(view.getPreferredSpan(X_AXIS), view.getPreferredSpan(Y_AXIS));
}
public void preferenceChanged(View child, boolean width, boolean height) {
if (host != null) {
host.revalidate();
host.repaint();
}
}
/**
* Fetches the attributes to use when rendering. At the root level there are no attributes. If an attribute is
* resolved up the view hierarchy this is the end of the line.
*/
public AttributeSet getAttributes() {
return null;
}
/**
* Renders the view.
*
* @param g the graphics context
* @param allocation the region to render into
*/
public void paint(Graphics g, Shape allocation) {
Rectangle alloc = allocation.getBounds();
view.setSize(alloc.width, alloc.height);
if (g.getClipBounds() == null) {
g.setClip(alloc);
view.paint(g, allocation);
g.setClip(null);
} else {
view.paint(g, allocation);
}
}
/**
* Sets the view parent.
*
* @param parent the parent view
*/
public void setParent(View parent) {
throw new Error("Can't set parent on root view");
}
/**
* Returns the number of views in this view. Since this view simply wraps the root of the view hierarchy it has
* exactly one child.
*
* @return the number of views
* @see #getView
*/
public int getViewCount() {
return 1;
}
/**
* Gets the n-th view in this container.
*
* @param n the number of the view to get
* @return the view
*/
public View getView(int n) {
return view;
}
/**
* Returns the document model underlying the view.
*
* @return the model
*/
public Document getDocument() {
return view == null ? null : view.getDocument();
}
/**
* Sets the view size.
*
* @param width the width
* @param height the height
*/
public void setSize(float width, float height) {
this.width = (int) width;
view.setSize(width, height);
}
@Override
public float getPreferredSpan(int axis) {
if (axis == X_AXIS) {
// width currently laid out to
return width;
}
return view.getPreferredSpan(axis);
}
/**
* Fetches the container hosting the view. This is useful for things like scheduling a repaint, finding out the
* host components font, etc. The default implementation of this is to forward the query to the parent view.
*
* @return the container
*/
public Container getContainer() {
return host;
}
/**
* Fetches the factory to be used for building the various view fragments that make up the view that represents
* the model. This is what determines how the model will be represented. This is implemented to fetch the
* factory provided by the associated EditorKit.
*
* @return the factory
*/
public ViewFactory getViewFactory() {
return factory;
}
private View view;
private ViewFactory factory;
}
/**
* AnotherAbstractDocument
*
* @inheritDoc
*/
private static abstract class AlterAbstractDocument extends AbstractDocument implements Document, Serializable {
/**
* Document property that indicates whether internationalization functions such as text reordering or reshaping
* should be performed. This property should not be publicly exposed, since it is used for implementation
* convenience only. As a side effect, copies of this property may be in its subclasses that live in different
* packages (e.g. HTMLDocument as of now), so those copies should also be taken care of when this property needs
* to be modified.
*/
static final String I18NProperty = "i18n";
public AlterAbstractDocument(Content data, AttributeContext context) {
super(data, context);
}
public AlterAbstractDocument(Content data) {
super(data);
}
}
/**
* Internally created view that has the purpose of holding the views that represent the children of the paragraph
* that have been arranged in rows.
*/
class Row extends AlterBoxView {
private int justification;
private float lineSpacing;
/** Indentation for the first line, from the left inset. */
protected int firstLineIndent = 0;
Row(Element elem) {
super(elem, View.X_AXIS);
}
/**
* This is reimplemented to do nothing since the paragraph fills in the row with its needed children.
*/
protected void loadChildren(ViewFactory f) {
}
/**
* Fetches the attributes to use when rendering. This view isn't directly responsible for an element so it
* returns the outer classes attributes.
*/
public AttributeSet getAttributes() {
View p = getParent();
return (p != null) ? p.getAttributes() : null;
}
public float getAlignment(int axis) {
if (axis == View.X_AXIS) {
switch (justification) {
case StyleConstants.ALIGN_LEFT:
return 0;
case StyleConstants.ALIGN_RIGHT:
return 1;
case StyleConstants.ALIGN_CENTER:
return 0.5f;
case StyleConstants.ALIGN_JUSTIFIED:
float rv = 0.5f;
// if we can justifiy the content always align to
// the left.
if (isJustifiableDocument()) {
rv = 0f;
}
return rv;
}
}
return super.getAlignment(axis);
}
/**
* Provides a mapping from the document model coordinate space to the coordinate space of the view mapped to it.
* This is implemented to let the superclass find the position along the major axis and the allocation of the
* row is used along the minor axis, so that even though the children are different heights they all get the
* same caret height.
*
* @param pos the position to convert
* @param a the allocated region to render into
* @return the bounding box of the given position
* @exception BadLocationException if the given position does not represent a valid location in the associated
* document
* @see View#modelToView
*/
public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException {
Rectangle r = a.getBounds();
View v = getViewAtPosition(pos, r);
if ((v != null) && (!v.getElement().isLeaf())) {
// Don't adjust the height if the view represents a branch.
return super.modelToView(pos, a, b);
}
r = a.getBounds();
int height = r.height;
int y = r.y;
Shape loc = super.modelToView(pos, a, b);
r = loc.getBounds();
r.height = height;
r.y = y;
return r;
}
/**
* Range represented by a row in the paragraph is only a subset of the total range of the paragraph element.
*
* @see View#getRange
*/
public int getStartOffset() {
int offs = Integer.MAX_VALUE;
int n = getViewCount();
for (int i = 0; i < n; i++) {
View v = getView(i);
offs = Math.min(offs, v.getStartOffset());
}
return offs;
}
public int getEndOffset() {
int offs = 0;
int n = getViewCount();
for (int i = 0; i < n; i++) {
View v = getView(i);
offs = Math.max(offs, v.getEndOffset());
}
return offs;
}
/**
* Perform layout for the minor axis of the box (i.e. the axis orthoginal to the axis that it represents). The
* results of the layout should be placed in the given arrays which represent the allocations to the children
* along the minor axis.
*
* This is implemented to do a baseline layout of the children by calling BoxView.baselineLayout.
*
* @param targetSpan the total span given to the view, which whould be used to layout the children.
* @param axis the axis being layed out.
* @param offsets the offsets from the origin of the view for each of the child views. This is a return value
* and is filled in by the implementation of this method.
* @param spans the span of each child view. This is a return value and is filled in by the implementation of
* this method.
* @return the offset and span for each child view in the offsets and spans parameters
*/
protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
baselineLayout(targetSpan, axis, offsets, spans);
}
protected SizeRequirements calculateMinorAxisRequirements(int axis, SizeRequirements r) {
return baselineRequirements(axis, r);
}
private boolean isLastRow() {
View parent;
return ((parent = getParent()) == null || this == parent.getView(parent.getViewCount() - 1));
}
private boolean isBrokenRow() {
boolean rv = false;
int viewsCount = getViewCount();
if (viewsCount > 0) {
View lastView = getView(viewsCount - 1);
if (lastView.getBreakWeight(X_AXIS, 0, 0) >= ForcedBreakWeight) {
rv = true;
}
}
return rv;
}
private boolean isJustifiableDocument() {
return (!Boolean.TRUE.equals(getDocument().getProperty(AlterAbstractDocument.I18NProperty)));
}
/**
* Whether we need to justify this {@code Row}. At this time (jdk1.6) we support justification on for non 18n
* text.
*
* @return {@code true} if this {@code Row} should be justified.
*/
private boolean isJustifyEnabled() {
boolean ret = (justification == StyleConstants.ALIGN_JUSTIFIED);
// no justification for i18n documents
ret = ret && isJustifiableDocument();
// no justification for the last row
ret = ret && !isLastRow();
// no justification for the broken rows
ret = ret && !isBrokenRow();
return ret;
}
// Calls super method after setting spaceAddon to 0.
// Justification should not affect MajorAxisRequirements
@Override
protected SizeRequirements calculateMajorAxisRequirements(int axis, SizeRequirements r) {
int oldJustficationData[] = justificationData;
justificationData = null;
SizeRequirements ret = super.calculateMajorAxisRequirements(axis, r);
if (isJustifyEnabled()) {
justificationData = oldJustficationData;
}
return ret;
}
@Override
protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
int oldJustficationData[] = justificationData;
justificationData = null;
super.layoutMajorAxis(targetSpan, axis, offsets, spans);
if (!isJustifyEnabled()) {
return;
}
int currentSpan = 0;
for (int span : spans) {
currentSpan += span;
}
if (currentSpan == targetSpan) {
// no need to justify
return;
}
// we justify text by enlarging spaces by the {@code spaceAddon}.
// justification is started to the right of the rightmost TAB.
// leading and trailing spaces are not extendable.
//
// GlyphPainter1 uses
// justificationData
// for all painting and measurement.
int extendableSpaces = 0;
int startJustifiableContent = -1;
int endJustifiableContent = -1;
int lastLeadingSpaces = 0;
int rowStartOffset = getStartOffset();
int rowEndOffset = getEndOffset();
int spaceMap[] = new int[rowEndOffset - rowStartOffset];
Arrays.fill(spaceMap, 0);
for (int i = getViewCount() - 1; i >= 0; i--) {
View view = getView(i);
if (view instanceof GlyphView) {
AlterGlyphView.JustificationInfo justificationInfo = ((AlterGlyphView) view)
.getJustificationInfo(rowStartOffset);
final int viewStartOffset = view.getStartOffset();
final int offset = viewStartOffset - rowStartOffset;
for (int j = 0; j < justificationInfo.spaceMap.length(); j++) {
if (justificationInfo.spaceMap.get(j)) {
spaceMap[j + offset] = 1;
}
}
if (startJustifiableContent > 0) {
if (justificationInfo.end >= 0) {
extendableSpaces += justificationInfo.trailingSpaces;
} else {
lastLeadingSpaces += justificationInfo.trailingSpaces;
}
}
if (justificationInfo.start >= 0) {
startJustifiableContent = justificationInfo.start + viewStartOffset;
extendableSpaces += lastLeadingSpaces;
}
if (justificationInfo.end >= 0 && endJustifiableContent < 0) {
endJustifiableContent = justificationInfo.end + viewStartOffset;
}
extendableSpaces += justificationInfo.contentSpaces;
lastLeadingSpaces = justificationInfo.leadingSpaces;
if (justificationInfo.hasTab) {
break;
}
}
}
if (extendableSpaces <= 0) {
// there is nothing we can do to justify
return;
}
int adjustment = (targetSpan - currentSpan);
int spaceAddon = (extendableSpaces > 0) ? adjustment / extendableSpaces : 0;
int spaceAddonLeftoverEnd = -1;
for (int i = startJustifiableContent - rowStartOffset, leftover = adjustment - spaceAddon
* extendableSpaces; leftover > 0; leftover -= spaceMap[i], i++) {
spaceAddonLeftoverEnd = i;
}
if (spaceAddon > 0 || spaceAddonLeftoverEnd >= 0) {
justificationData = (oldJustficationData != null) ? oldJustficationData : new int[END_JUSTIFIABLE + 1];
justificationData[SPACE_ADDON] = spaceAddon;
justificationData[SPACE_ADDON_LEFTOVER_END] = spaceAddonLeftoverEnd;
justificationData[START_JUSTIFIABLE] = startJustifiableContent - rowStartOffset;
justificationData[END_JUSTIFIABLE] = endJustifiableContent - rowStartOffset;
super.layoutMajorAxis(targetSpan, axis, offsets, spans);
}
}
// for justified row we assume the maximum horizontal span
// is MAX_VALUE.
@Override
public float getMaximumSpan(int axis) {
float ret;
if (View.X_AXIS == axis && isJustifyEnabled()) {
ret = Float.MAX_VALUE;
} else {
ret = super.getMaximumSpan(axis);
}
return ret;
}
/**
* Fetches the child view index representing the given position in the model.
*
* @param pos the position >= 0
* @return index of the view representing the given position, or -1 if no view represents that position
*/
protected int getViewIndexAtPosition(int pos) {
// This is expensive, but are views are not necessarily layed
// out in model order.
if (pos < getStartOffset() || pos >= getEndOffset())
return -1;
for (int counter = getViewCount() - 1; counter >= 0; counter--) {
View v = getView(counter);
if (pos >= v.getStartOffset() && pos < v.getEndOffset()) {
return counter;
}
}
return -1;
}
/**
* Gets the left inset.
*
* @return the inset
*/
protected short getLeftInset() {
View parentView;
int adjustment = 0;
if ((parentView = getParent()) != null) { // use firstLineIdent for the first row
if (this == parentView.getView(0)) {
adjustment = firstLineIndent;
}
}
return (short) (super.getLeftInset() + adjustment);
}
protected short getBottomInset() {
return (short) (super.getBottomInset() + ((minorRequest != null) ? minorRequest.preferred : 0)
* lineSpacing);
}
final static int SPACE_ADDON = 0;
final static int SPACE_ADDON_LEFTOVER_END = 1;
final static int START_JUSTIFIABLE = 2;
// this should be the last index in justificationData
final static int END_JUSTIFIABLE = 3;
int justificationData[] = null;
}
/**
* AnotherGlyphView
*
* @inheritDoc
*/
static class AlterGlyphView extends GlyphView implements TabableView, Cloneable {
/**
* Constructs a new view wrapped on an element.
*
* @param elem the element
*/
public AlterGlyphView(Element elem) {
super(elem);
}
/**
* Class to hold data needed to justify this GlyphView in a PargraphView.Row
*/
static class JustificationInfo {
// justifiable content start
final int start;
// justifiable content end
final int end;
final int leadingSpaces;
final int contentSpaces;
final int trailingSpaces;
final boolean hasTab;
final BitSet spaceMap;
JustificationInfo(int start, int end, int leadingSpaces, int contentSpaces, int trailingSpaces,
boolean hasTab, BitSet spaceMap) {
this.start = start;
this.end = end;
this.leadingSpaces = leadingSpaces;
this.contentSpaces = contentSpaces;
this.trailingSpaces = trailingSpaces;
this.hasTab = hasTab;
this.spaceMap = spaceMap;
}
}
JustificationInfo getJustificationInfo(int rowStartOffset) {
if (justificationInfo != null) {
return justificationInfo;
}
// states for the parsing
final int TRAILING = 0;
final int CONTENT = 1;
final int SPACES = 2;
int startOffset = getStartOffset();
int endOffset = getEndOffset();
Segment segment = getText(startOffset, endOffset);
int txtOffset = segment.offset;
int txtEnd = segment.offset + segment.count - 1;
int startContentPosition = txtEnd + 1;
int endContentPosition = txtOffset - 1;
int trailingSpaces = 0;
int contentSpaces = 0;
int leadingSpaces = 0;
boolean hasTab = false;
BitSet spaceMap = new BitSet(endOffset - startOffset + 1);
// we parse conent to the right of the rightmost TAB only.
// we are looking for the trailing and leading spaces.
// position after the leading spaces (startContentPosition)
// position before the trailing spaces (endContentPosition)
for (int i = txtEnd, state = TRAILING; i >= txtOffset; i--) {
if (' ' == segment.array[i]) {
spaceMap.set(i - txtOffset);
if (state == TRAILING) {
trailingSpaces++;
} else if (state == CONTENT) {
state = SPACES;
leadingSpaces = 1;
} else if (state == SPACES) {
leadingSpaces++;
}
} else if ('\t' == segment.array[i]) {
hasTab = true;
break;
} else {
if (state == TRAILING) {
if ('\n' != segment.array[i] && '\r' != segment.array[i]) {
state = CONTENT;
endContentPosition = i;
}
} else if (state == CONTENT) {
// do nothing
} else if (state == SPACES) {
contentSpaces += leadingSpaces;
leadingSpaces = 0;
}
startContentPosition = i;
}
}
SegmentCache.releaseSharedSegment(segment);
int startJustifiableContent = -1;
if (startContentPosition < txtEnd) {
startJustifiableContent = startContentPosition - txtOffset;
}
int endJustifiableContent = -1;
if (endContentPosition > txtOffset) {
endJustifiableContent = endContentPosition - txtOffset;
}
justificationInfo = new JustificationInfo(startJustifiableContent, endJustifiableContent, leadingSpaces,
contentSpaces, trailingSpaces, hasTab, spaceMap);
return justificationInfo;
}
private JustificationInfo justificationInfo = null;
}
/**
* SegmentCache caches Segment
s to avoid continually creating and destroying of Segment
s.
* A common use of this class would be:
*
*
* Segment segment = segmentCache.getSegment();
* // do something with segment
* ...
* segmentCache.releaseSegment(segment);
*
*
* @version 1.6 11/17/05
*/
static class SegmentCache {
/**
* A global cache.
*/
private static SegmentCache sharedCache = new SegmentCache();
/**
* A list of the currently unused Segments.
*/
private List segments;
/**
* Returns the shared SegmentCache.
*/
public static SegmentCache getSharedInstance() {
return sharedCache;
}
/**
* A convenience method to get a Segment from the shared SegmentCache
.
*/
public static Segment getSharedSegment() {
return getSharedInstance().getSegment();
}
/**
* A convenience method to release a Segment to the shared SegmentCache
.
*/
public static void releaseSharedSegment(Segment segment) {
getSharedInstance().releaseSegment(segment);
}
/**
* Creates and returns a SegmentCache.
*/
public SegmentCache() {
segments = new ArrayList(11);
}
/**
* Returns a Segment
. When done, the Segment
should be recycled by invoking
* releaseSegment
.
*/
public Segment getSegment() {
synchronized (this) {
int size = segments.size();
if (size > 0) {
return segments.remove(size - 1);
}
}
return new CachedSegment();
}
/**
* Releases a Segment. You should not use a Segment after you release it, and you should NEVER release the same
* Segment more than once, eg:
*
*
* segmentCache.releaseSegment(segment);
* segmentCache.releaseSegment(segment);
*
*
* Will likely result in very bad things happening!
*/
public void releaseSegment(Segment segment) {
if (segment instanceof CachedSegment) {
synchronized (this) {
segment.array = null;
segment.count = 0;
segments.add(segment);
}
}
}
/**
* CachedSegment is used as a tagging interface to determine if a Segment can successfully be shared.
*/
private static class CachedSegment extends Segment {
}
}
/**
* AnotherBoxView
*
* @inheritDoc
*/
static class AlterBoxView extends BoxView {
/** used in paint. */
Rectangle tempRect;
boolean majorAllocValid;
SizeRequirements minorRequest;
public AlterBoxView(Element elem, int axis) {
super(elem, axis);
}
@Override
protected short getRightInset() {
return super.getRightInset();
}
// TODO: override following 2 methods and Renderer.paint() to make this a ShapeView rather then BoxView ;)
@Override
public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) {
if (!isAllocationValid()) {
Rectangle alloc = a.getBounds();
setSize(alloc.width, alloc.height);
}
return super.viewToModel(x, y, a, bias);
}
}
/**
* This exception is to report the failure of state invarient assertion that was made. This indicates an internal
* error has occurred.
*
* @author Timothy Prinzing
* @version 1.18 11/17/05
*/
static class StateInvariantError extends Error {
/**
* Creates a new StateInvariantFailure object.
*
* @param s a string indicating the assertion that failed
*/
public StateInvariantError(String s) {
super(s);
}
}
}