org.jdesktop.swingx.JXLabel Maven / Gradle / Ivy
Show all versions of swingx-all Show documentation
/*
* $Id: JXLabel.java 4225 2012-08-07 15:37:57Z kschaefe $
*
* 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 org.jdesktop.beans.JavaBean;
import org.jdesktop.swingx.painter.AbstractPainter;
import org.jdesktop.swingx.painter.Painter;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JViewport;
import javax.swing.SwingConstants;
import javax.swing.border.Border;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentEvent.ElementChange;
import javax.swing.plaf.basic.BasicHTML;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
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.IconView;
import javax.swing.text.LabelView;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.ParagraphView;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledEditorKit;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;
import javax.swing.text.WrappedPlainView;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
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.awt.event.HierarchyBoundsAdapter;
import java.awt.event.HierarchyEvent;
import java.awt.font.TextAttribute;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.Reader;
import java.io.StringReader;
/**
*
* A {@link JLabel} subclass which supports {@link 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. Specify rotation angle in radian units.
*
*
* @author [email protected]
* @author rbair
* @author rah
* @author mario_cesar
*/
@SuppressWarnings("rawtypes")
@JavaBean
public class JXLabel extends JLabel implements BackgroundPaintable {
/**
* Text alignment enums. Controls alignment of the text when line wrapping is enabled.
*/
public enum TextAlignment implements IValue {
LEFT(StyleConstants.ALIGN_LEFT),
CENTER(StyleConstants.ALIGN_CENTER),
RIGHT(StyleConstants.ALIGN_RIGHT),
JUSTIFY(StyleConstants.ALIGN_JUSTIFIED);
private final int value;
TextAlignment(int val) {
value = val;
}
@Override
public int getValue() {
return value;
}
}
protected interface IValue {
int getValue();
}
// 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;
// using reverse logic ... some methods causing re-flow of text are called from super constructor,
// but private variables are initialized only after call to super so have to rely on default for boolean being false
private boolean dontIgnoreRepaint = false;
private int occupiedWidth;
private static final String oldRendererKey = "was" + BasicHTML.propertyKey;
/**
* Create a new JXLabel. This has the same semantics as creating a new JLabel.
*/
public JXLabel() {
initPainterSupport();
initLineWrapSupport();
}
/**
* Creates new JXLabel with given icon.
*
* @param image the icon to set.
*/
public JXLabel(Icon image) {
super(image);
initPainterSupport();
initLineWrapSupport();
}
/**
* Creates new JXLabel with given icon and alignment.
*
* @param image the icon to set.
* @param horizontalAlignment the text alignment.
*/
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();
}
/**
* Creates new JXLabel with given text, icon and alignment.
*
* @param text the test to set.
* @param image the icon to set.
* @param horizontalAlignment the text alignment relative to the icon.
*/
public JXLabel(String text, Icon image, int horizontalAlignment) {
super(text, image, horizontalAlignment);
initPainterSupport();
initLineWrapSupport();
}
/**
* Creates new JXLabel with given text and alignment.
*
* @param text the test to set.
* @param horizontalAlignment the text alignment.
*/
public JXLabel(String text, int horizontalAlignment) {
super(text, horizontalAlignment);
initPainterSupport();
initLineWrapSupport();
}
private void initPainterSupport() {
foregroundPainter = new AbstractPainter() {
@Override
protected void doPaint(Graphics2D g, JXLabel label, int width, int height) {
Insets i = getInsets();
g = (Graphics2D) g.create(-i.left, -i.top, width, height);
try {
label.paint(g);
} finally {
g.dispose();
}
}
//if any of the state of the JButton that affects the foreground has changed,
//then I must clear the cache. This is really hard to get right, there are
//bound to be bugs. An alternative is to NEVER cache.
@Override
protected boolean shouldUseCache() {
return false;
}
@Override
public boolean equals(Object obj) {
return obj != null && this.getClass().equals(obj.getClass());
}
};
((AbstractPainter>) foregroundPainter).setAntialiasing(false);
}
/**
* Helper method for initializing multi line support.
*/
private void initLineWrapSupport() {
addPropertyChangeListener(new MultiLineSupport());
// FYI: no more listening for componentResized. Those events are delivered out
// of order and without old values are meaningless and forcing us to react when
// not necessary. Instead overriding reshape() ensures we have control over old AND new size.
addHierarchyBoundsListener(new HierarchyBoundsAdapter() {
@Override
public void ancestorResized(HierarchyEvent e) {
// if one of the parents is viewport, resized events will not be propagated down unless viewport is changing visibility of scrollbars.
// To make sure Label is able to re-wrap text when viewport size changes, initiate re-wrapping here by changing size of view
if (e.getChanged() instanceof JViewport) {
Rectangle viewportBounds = e.getChanged().getBounds();
if (viewportBounds.getWidth() < getWidth()) {
View view = getWrappingView();
if (view != null) {
view.setSize(viewportBounds.width, viewportBounds.height);
}
}
}
}
});
}
/**
* 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;
}
@Override
@SuppressWarnings("deprecation")
public void reshape(int x, int y, int w, int h) {
int oldH = getHeight();
super.reshape(x, y, w, h);
if (!isLineWrap()) {
return;
}
if (oldH == 0) {
return;
}
if (w > getVisibleRect().width) {
w = getVisibleRect().width;
}
View view = (View) getClientProperty(BasicHTML.propertyKey);
if (view instanceof Renderer) {
view.setSize(w - occupiedWidth, h);
}
}
/**
* 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();
if (painter == null) {
//restore default painter
initPainterSupport();
} else {
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()
*/
@Override
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)
*/
@Override
public final Painter getBackgroundPainter() {
return backgroundPainter;
}
/**
* Gets current value of text rotation in rads.
*
* @return a double representing the current rotation of the text
* @see #setTextRotation(double)
*/
public double getTextRotation() {
return textRotation;
}
@Override
public Dimension getPreferredSize() {
Dimension size = super.getPreferredSize();
if (isPreferredSizeSet()) {
return size;
} else if (this.textRotation != NORMAL) {
// #swingx-680 change the preferred size when rotation is set ... ideally this would be solved in the LabelUI rather then here
double theta = getTextRotation();
size.setSize(rotateWidth(size, theta), rotateHeight(size, theta));
} else {
// #swingx-780 preferred size is not set properly when parent container doesn't enforce the width
View view = getWrappingView();
if (view == null) {
if (isLineWrap() && !MultiLineSupport.isHTML(getText())) {
getMultiLineSupport();
// view might get lost on LAF change ...
putClientProperty(BasicHTML.propertyKey, MultiLineSupport.createView(this));
view = (View) getClientProperty(BasicHTML.propertyKey);
} else {
return size;
}
}
Insets insets = getInsets();
int dx = insets.left + insets.right;
int dy = insets.top + insets.bottom;
Rectangle textR = new Rectangle();
Rectangle viewR = new Rectangle();
textR.x = textR.y = textR.width = textR.height = 0;
viewR.x = dx;
viewR.y = dy;
viewR.width = viewR.height = Short.MAX_VALUE;
// layout label
// 1) icon
Rectangle iconR = calculateIconRect();
// 2) init textR
boolean textIsEmpty = getText() == null || getText().isEmpty();
/* Unless both text and icon are non-null, we effectively ignore
* the value of textIconGap.
*/
int gap;
if (textIsEmpty) {
textR.width = textR.height = 0;
gap = 0;
} else {
gap = iconR.width == 0 ? 0 : getIconTextGap();
occupiedWidth = dx + iconR.width + gap;
Object parent = getParent();
if (parent instanceof JPanel) {
JPanel panel = (JPanel) parent;
Border b = panel.getBorder();
if (b != null) {
Insets in = b.getBorderInsets(panel);
occupiedWidth += in.left + in.right;
}
}
int availTextWidth;
if (getHorizontalTextPosition() == CENTER) {
availTextWidth = viewR.width;
} else {
availTextWidth = viewR.width - (iconR.width + gap);
}
float xPrefSpan = view.getPreferredSpan(View.X_AXIS);
textR.width = Math.min(availTextWidth, (int) xPrefSpan);
if (maxLineSpan > 0) {
textR.width = Math.min(textR.width, maxLineSpan);
if (xPrefSpan > maxLineSpan) {
view.setSize(maxLineSpan, textR.height);
}
}
textR.height = (int) view.getPreferredSpan(View.Y_AXIS);
if (textR.height == 0) {
textR.height = getFont().getSize();
}
}
// 3) set text xy based on h/v text pos
if (getVerticalTextPosition() == TOP) {
if (getHorizontalTextPosition() != CENTER) {
textR.y = 0;
} else {
textR.y = -(textR.height + gap);
}
} else if (getVerticalTextPosition() == CENTER) {
textR.y = iconR.height / 2 - textR.height / 2;
} else { // (verticalTextPosition == BOTTOM)
if (getVerticalTextPosition() != CENTER) {
textR.y = iconR.height - textR.height;
} else {
textR.y = iconR.height + gap;
}
}
if (getHorizontalTextPosition() == LEFT) {
textR.x = -(textR.width + gap);
} else if (getHorizontalTextPosition() == CENTER) {
textR.x = iconR.width / 2 - textR.width / 2;
} else { // (horizontalTextPosition == RIGHT)
textR.x = iconR.width + gap;
}
// 4) shift label around based on its alignment
int labelRx = Math.min(iconR.x, textR.x);
int labelRwidth = Math.max(iconR.x + iconR.width, textR.x + textR.width) - labelRx;
int labelRy = Math.min(iconR.y, textR.y);
int labelRheight = Math.max(iconR.y + iconR.height, textR.y + textR.height) - labelRy;
int day;
if (getVerticalAlignment() == TOP) {
day = viewR.y - labelRy;
} else if (getVerticalAlignment() == CENTER) {
day = viewR.y + viewR.height / 2 - (labelRy + labelRheight / 2);
} else { // (verticalAlignment == BOTTOM)
day = viewR.y + viewR.height - (labelRy + labelRheight);
}
int dax;
if (getHorizontalAlignment() == LEFT) {
dax = viewR.x - labelRx;
} else if (getHorizontalAlignment() == RIGHT) {
dax = viewR.x + viewR.width - (labelRx + labelRwidth);
} else { // (horizontalAlignment == CENTER)
dax = viewR.x + viewR.width / 2 - (labelRx + labelRwidth / 2);
}
textR.x += dax;
textR.y += day;
iconR.x += dax;
iconR.y += day;
int lsb = 0;
if (lsb < 0) {
// lsb is negative. Shift the x location so that the text is
// visually drawn at the right location.
textR.x -= lsb;
}
// EO layout label
int x1 = Math.min(iconR.x, textR.x);
int x2 = Math.max(iconR.x + iconR.width, textR.x + textR.width);
int y1 = Math.min(iconR.y, textR.y);
int y2 = Math.max(iconR.y + iconR.height, textR.y + textR.height);
Dimension rv = new Dimension(x2 - x1, y2 - y1);
rv.width += dx;
rv.height += dy;
return rv;
}
return size;
}
private View getWrappingView() {
if (super.getTopLevelAncestor() == null) {
return null;
}
View view = (View) getClientProperty(BasicHTML.propertyKey);
if (!(view instanceof Renderer)) {
return null;
}
return view;
}
private Rectangle calculateIconRect() {
Rectangle iconR = new Rectangle();
Icon icon = isEnabled() ? getIcon() : getDisabledIcon();
iconR.x = iconR.y = iconR.width = iconR.height = 0;
if (icon != null) {
iconR.width = icon.getIconWidth();
iconR.height = icon.getIconHeight();
} else {
iconR.width = iconR.height = 0;
}
return iconR;
}
public int getMaxLineSpan() {
return maxLineSpan;
}
public void setMaxLineSpan(int maxLineSpan) {
int old = getMaxLineSpan();
this.maxLineSpan = maxLineSpan;
firePropertyChange("maxLineSpan", old, getMaxLineSpan());
}
private static int rotateWidth(Dimension size, double theta) {
return (int) Math.round(size.width * Math.abs(Math.cos(theta)) + size.height * Math.abs(Math.sin(theta)));
}
private static int rotateHeight(Dimension size, double theta) {
return (int) Math.round(size.width * Math.abs(Math.sin(theta)) + size.height * Math.abs(Math.cos(theta)));
}
/**
* 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
*/
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);
}
}
}
/**
* 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
*/
public boolean isLineWrap() {
return this.multiLine;
}
private boolean paintBorderInsets = true;
private int maxLineSpan = -1;
public boolean painted;
private TextAlignment textAlignment = TextAlignment.LEFT;
/**
* Gets current text wrapping style.
*
* @return the text alignment for this label
*/
public TextAlignment getTextAlignment() {
return textAlignment;
}
/**
* Sets style of wrapping the text.
*
* @param alignment
* @see TextAlignment for accepted values.
*/
public void setTextAlignment(TextAlignment alignment) {
TextAlignment old = getTextAlignment();
this.textAlignment = alignment;
firePropertyChange("textAlignment", old, getTextAlignment());
}
/**
* 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 initial transform passed to the background painter.
*
* @return current value of the paintBorderInsets property
*/
@Override
public boolean isPaintBorderInsets() {
return paintBorderInsets;
}
@Override
public boolean isOpaque() {
return painting ? false : super.isOpaque();
}
/**
* 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 initial transform passed to the background painter.
*
* This is a bound property.
*
* @param paintBorderInsets new value of the paintBorderInsets property
*/
@Override
public void setPaintBorderInsets(boolean paintBorderInsets) {
boolean old = this.isPaintBorderInsets();
this.paintBorderInsets = paintBorderInsets;
firePropertyChange("paintBorderInsets", old, isPaintBorderInsets());
}
/**
* @param g graphics to paint on
*/
@SuppressWarnings("unchecked")
@Override
protected void paintComponent(Graphics g) {
// resizing the text view causes recursive callback to the paint down the road. In order to prevent such
// computationally intensive series of repaints every call to paint is skipped while top most call is being
// executed.
// if (!dontIgnoreRepaint) {
// return;
// }
painted = true;
if (painting || backgroundPainter == null && foregroundPainter == null) {
super.paintComponent(g);
} else {
pWidth = getWidth();
pHeight = getHeight();
if (backgroundPainter != null) {
Graphics2D tmp = (Graphics2D) g.create();
try {
SwingXUtilities.paintBackground(this, tmp);
} finally {
tmp.dispose();
}
}
if (foregroundPainter != null) {
Insets i = getInsets();
pWidth = getWidth() - i.left - i.right;
pHeight = getHeight() - i.top - i.bottom;
Point2D tPoint = calculateT();
double wx = Math.sin(textRotation) * tPoint.getY() + Math.cos(textRotation) * tPoint.getX();
double wy = Math.sin(textRotation) * tPoint.getX() + Math.cos(textRotation) * tPoint.getY();
double x = (getWidth() - wx) / 2 + Math.sin(textRotation) * tPoint.getY();
double y = (getHeight() - wy) / 2;
Graphics2D tmp = (Graphics2D) g.create();
if (i != null) {
tmp.translate(i.left + x, i.top + y);
} else {
tmp.translate(x, y);
}
tmp.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);
//log.fine("PW:" + pWidth + ", PH:" + pHeight);
foregroundPainter.paint(tmp, this, pWidth, pHeight);
tmp.dispose();
painting = false;
pWidth = 0;
pHeight = 0;
}
}
}
private Point2D calculateT() {
double tx = getWidth();
double ty = 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)
dontIgnoreRepaint = false;
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 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)
double 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;
dontIgnoreRepaint = true;
}
return new Point2D.Double(tx, ty);
}
@Override
public void repaint() {
if (!dontIgnoreRepaint) {
return;
}
super.repaint();
}
@Override
public void repaint(int x, int y, int width, int height) {
if (!dontIgnoreRepaint) {
return;
}
super.repaint(x, y, width, height);
}
@Override
public void repaint(long tm) {
if (!dontIgnoreRepaint) {
return;
}
super.repaint(tm);
}
@Override
public void repaint(long tm, int x, int y, int width, int height) {
if (!dontIgnoreRepaint) {
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;
@Override
public void propertyChange(PropertyChangeEvent evt) {
String name = evt.getPropertyName();
JXLabel src = (JXLabel) evt.getSource();
if ("ancestor".equals(name)) {
src.dontIgnoreRepaint = true;
}
if (src.isLineWrap()) {
if ("font".equals(name) ||
"foreground".equals(name) ||
"maxLineSpan".equals(name) ||
"textAlignment".equals(name) ||
"icon".equals(name) ||
"iconTextGap".equals(name)) {
if (evt.getOldValue() != null && !isHTML(src.getText())) {
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) && !((Boolean) evt.getNewValue()).booleanValue()) {
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();
float rightIndent = 0;
if (c.getIcon() != null && c.getHorizontalTextPosition() != SwingConstants.CENTER) {
rightIndent = c.getIcon().getIconWidth() + c.getIconTextGap();
}
Document doc = kit.createDefaultDocument(c.getFont(), c.getForeground(), c.getTextAlignment(), rightIndent);
Reader r = new StringReader(c.getText() == null ? "" : c.getText());
try {
kit.read(r, doc, 0);
} catch (Throwable e) {
// ignore
}
ViewFactory f = kit.getViewFactory();
View hview = f.create(doc.getDefaultRootElement());
return new Renderer(c, f, hview, true);
}
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, TextAlignment textAlignment, float rightIndent) {
BasicDocument doc = new BasicDocument(defaultFont, foreground, textAlignment, rightIndent);
doc.setAsynchronousLoadPriority(Integer.MAX_VALUE);
return doc;
}
@Override
public ViewFactory getViewFactory() {
return basicViewFactory;
}
}
}
private static class BasicViewFactory implements ViewFactory {
@Override
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;
}
}
private static class BasicDocument extends DefaultStyledDocument {
BasicDocument(Font defaultFont, Color foreground, TextAlignment textAlignment, float rightIndent) {
setFontAndColor(defaultFont, foreground);
MutableAttributeSet attr = new SimpleAttributeSet();
StyleConstants.setAlignment(attr, textAlignment.getValue());
getStyle("default").addAttributes(attr);
attr = new SimpleAttributeSet();
StyleConstants.setRightIndent(attr, rightIndent);
getStyle("default").addAttributes(attr);
}
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);
attr = new SimpleAttributeSet();
Object underline = font.getAttributes().get(TextAttribute.UNDERLINE);
boolean canUnderline = underline instanceof Integer && ((Integer) underline).intValue() != -1;
StyleConstants.setUnderline(attr, canUnderline);
getStyle("default").addAttributes(attr);
}
MutableAttributeSet attr = new SimpleAttributeSet();
StyleConstants.setSpaceAbove(attr, 0f);
getStyle("default").addAttributes(attr);
}
}
/**
* Root text view that acts as an renderer.
*/
private static class Renderer extends WrappedPlainView {
private final JXLabel host;
private boolean invalidated = false;
private float width;
private float height;
Renderer(JXLabel c, ViewFactory f, View v, boolean wordWrap) {
super(null, wordWrap);
factory = f;
view = v;
view.setParent(this);
host = c;
int w;
if (host.getVisibleRect().width == 0) {
invalidated = true;
return;
} else {
w = host.getVisibleRect().width;
}
setSize(c.getMaxLineSpan() > -1 ? c.getMaxLineSpan() : w, host.getVisibleRect().height);
}
@Override
protected void updateLayout(ElementChange ec, DocumentEvent e, Shape a) {
if (a != null) {
// should damage more intelligently
preferenceChanged(null, true, true);
Container host = getContainer();
if (host != null) {
host.repaint();
}
}
}
@Override
public void preferenceChanged(View child, boolean width, boolean height) {
if (host != null && host.painted) {
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.
*/
@Override
public AttributeSet getAttributes() {
return null;
}
/**
* Renders the view.
*
* @param g the graphics context
* @param allocation the region to render into
*/
@Override
public void paint(Graphics g, Shape allocation) {
Rectangle alloc = allocation.getBounds();
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
*/
@Override
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
*/
@Override
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
*/
@Override
public View getView(int n) {
return view;
}
/**
* Returns the document model underlying the view.
*
* @return the model
*/
@Override
public Document getDocument() {
return view == null ? null : view.getDocument();
}
/**
* Sets the view size.
*
* @param width the width
* @param height the height
*/
@Override
public void setSize(float width, float height) {
if (host.maxLineSpan > 0) {
width = Math.min(width, host.maxLineSpan);
}
if (width == this.width && height == this.height) {
return;
}
this.width = (int) width;
this.height = (int) height;
view.setSize(width, height == 0 ? Short.MAX_VALUE : height);
if (this.height == 0) {
this.height = view.getPreferredSpan(View.Y_AXIS);
}
}
@Override
public float getPreferredSpan(int axis) {
if (axis == X_AXIS) {
//log.fine("inv: " + invalidated + ", w:" + width + ", vw:" + host.getVisibleRect());
// width currently laid out to
if (invalidated) {
int w = host.getVisibleRect().width;
if (w != 0) {
invalidated = false;
setSize(w - host.getOccupiedWidth(), host.getVisibleRect().height);
}
}
return width > 0 ? width : view.getPreferredSpan(axis);
} else {
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
*/
@Override
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
*/
@Override
public ViewFactory getViewFactory() {
return factory;
}
private final View view;
private final ViewFactory factory;
@Override
public int getWidth() {
return (int) width;
}
@Override
public int getHeight() {
return (int) height;
}
}
protected int getOccupiedWidth() {
return occupiedWidth;
}
}