org.jdesktop.swingx.plaf.basic.BasicHyperlinkUI Maven / Gradle / Ivy
/*
* $Id: BasicHyperlinkUI.java 3927 2011-02-22 16:34:11Z kleopatra $
*
* Copyright 2004 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.plaf.basic;
import java.awt.Color;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
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.StringReader;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.logging.Logger;
import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.ButtonModel;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JToolBar;
import javax.swing.LookAndFeel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.plaf.BorderUIResource;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicButtonListener;
import javax.swing.plaf.basic.BasicButtonUI;
import javax.swing.plaf.basic.BasicGraphicsUtils;
import javax.swing.plaf.basic.BasicHTML;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.Position;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.ImageView;
import javax.swing.text.html.StyleSheet;
import org.jdesktop.swingx.JXHyperlink;
import org.jdesktop.swingx.SwingXUtilities;
/**
* Basic implementation of the JXHyperlink
UI.
* This is copied from org.jdesktop.jdnc.plaf.basic.BasicLinkButtonUI
*/
public class BasicHyperlinkUI extends BasicButtonUI {
@SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(BasicHyperlinkUI.class
.getName());
public static ComponentUI createUI(JComponent c) {
return new BasicHyperlinkUI();
}
private static Rectangle viewRect = new Rectangle();
private static Rectangle textRect = new Rectangle();
private static Rectangle iconRect = new Rectangle();
// private static MouseListener handCursorListener = new HandCursor();
protected int dashedRectGapX;
protected int dashedRectGapY;
protected int dashedRectGapWidth;
protected int dashedRectGapHeight;
private Color focusColor;
private View ulv;
private PropertyChangeListener pcListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
// this method is called from the edt. only other place where ulv is used is in
// painting which also happens on edt so it should be safe even without synchronization
// sole purpose of this call is to reinitialize view on every property change
ulv = null;
}};
@Override
protected void installDefaults(AbstractButton b) {
super.installDefaults(b);
JXHyperlink link = (JXHyperlink) b;
LookAndFeel.installProperty(b, "opaque", false);
if (SwingXUtilities.isUIInstallable(link.getUnclickedColor())) {
link.setUnclickedColor(UIManager.getColor("Hyperlink.linkColor"));
}
if (SwingXUtilities.isUIInstallable(link.getClickedColor())) {
link.setClickedColor(UIManager.getColor("Hyperlink.visitedColor"));
}
b.setBorderPainted(false);
b.setRolloverEnabled(true);
if (SwingXUtilities.isUIInstallable(b.getBorder())) {
b.setBorder(new BorderUIResource(BorderFactory.createEmptyBorder(0, 1, 0, 0)));
}
dashedRectGapX = UIManager.getInt("ButtonUI.dashedRectGapX");
dashedRectGapY = UIManager.getInt("ButtonUI.dashedRectGapY");
dashedRectGapWidth = UIManager.getInt("ButtonUI.dashedRectGapWidth");
dashedRectGapHeight = UIManager.getInt("ButtonUI.dashedRectGapHeight");
focusColor = UIManager.getColor("ButtonUI.focus");
b.setHorizontalAlignment(SwingConstants.LEADING);
}
@Override
protected void installListeners(AbstractButton b) {
super.installListeners(b);
// b.addMouseListener(handCursorListener);
b.addPropertyChangeListener(pcListener);
}
@Override
protected void uninstallListeners(AbstractButton b) {
super.uninstallListeners(b);
// b.removeMouseListener(handCursorListener);
b.removePropertyChangeListener(pcListener);
}
protected Color getFocusColor() {
return focusColor;
}
@Override
public void paint(Graphics g, JComponent c) {
AbstractButton b = (AbstractButton) c;
ButtonModel model = b.getModel();
FontMetrics fm = g.getFontMetrics();
Insets i = c.getInsets();
viewRect.x = i.left;
viewRect.y = i.top;
viewRect.width = b.getWidth() - (i.right + viewRect.x);
viewRect.height = b.getHeight() - (i.bottom + viewRect.y);
textRect.x = textRect.y = textRect.width = textRect.height = 0;
iconRect.x = iconRect.y = iconRect.width = iconRect.height = 0;
Font f = c.getFont();
g.setFont(f);
// layout the text and icon
String text = SwingUtilities.layoutCompoundLabel(c, fm, b.getText(), b
.getIcon(), b.getVerticalAlignment(), b
.getHorizontalAlignment(), b.getVerticalTextPosition(), b
.getHorizontalTextPosition(), viewRect, iconRect, textRect, b
.getText() == null ? 0 : b.getIconTextGap());
clearTextShiftOffset();
// perform UI specific press action, e.g. Windows L&F shifts text
if (model.isArmed() && model.isPressed()) {
paintButtonPressed(g, b);
}
// Paint the Icon
if (b.getIcon() != null) {
paintIcon(g, c, iconRect);
}
// Composite oldComposite = ((Graphics2D) g).getComposite();
//
// if (model.isRollover()) {
// ((Graphics2D) g).setComposite(AlphaComposite.getInstance(
// AlphaComposite.SRC_OVER, 0.5f));
// }
if (text != null && !text.equals("")) {
View v = (View) c.getClientProperty(BasicHTML.propertyKey);
if (v != null) {
paintHTMLText(g, b, textRect, text, v);
} else {
paintText(g, b, textRect, text);
}
}
if (b.isFocusPainted() && b.hasFocus()) {
// paint UI specific focus
paintFocus(g, b, viewRect, textRect, iconRect);
}
// ((Graphics2D) g).setComposite(oldComposite);
}
/**
* Method which renders the text of the current button if html.
*
* @param g Graphics context
* @param b Current button to render
* @param textRect Bounding rectangle to render the text.
* @param text String to render
* @param v the View to use.
*/
protected void paintHTMLText(Graphics g, AbstractButton b,
Rectangle textRect, String text, View v) {
textRect.x += getTextShiftOffset();
textRect.y += getTextShiftOffset();
// fix #441-swingx - underline not painted for html
if (b.getModel().isRollover()) {
//paintUnderline(g, b, textRect, text);
if (ulv == null) {
ulv = ULHtml.createHTMLView(b, text);
}
ulv.paint(g, textRect);
} else {
v.paint(g, textRect);
}
textRect.x -= getTextShiftOffset();
textRect.y -= getTextShiftOffset();
}
/**
* {@inheritDoc}
* Overridden to paint the underline on rollover.
*/
@Override
protected void paintText(Graphics g, AbstractButton b, Rectangle textRect,
String text) {
//kgs -- SwingX #415: pixel-shift when disabled
//BasicButtonUI shifts disabled text to the left by 1 pixel
//we compensate for that here, so that all Hyperlinks paint
//at the same location regardless of state
if (!b.getModel().isEnabled()) {
textRect.x += 1;
}
super.paintText(g, b, textRect, text);
if (b.getModel().isRollover()) {
paintUnderline(g, b, textRect, text);
}
}
private void paintUnderline(Graphics g, AbstractButton b, Rectangle rect,
String text) {
// JW: copied from JXTable.LinkRenderer
FontMetrics fm = g.getFontMetrics();
int descent = fm.getDescent();
// REMIND(aim): should we be basing the underline on
// the font's baseline instead of the text bounds?
g.drawLine(rect.x + getTextShiftOffset(),
(rect.y + rect.height) - descent + 1 + getTextShiftOffset(),
rect.x + rect.width + getTextShiftOffset(),
(rect.y + rect.height) - descent + 1 + getTextShiftOffset());
}
@Override
protected void paintFocus(Graphics g, AbstractButton b, Rectangle viewRect,
Rectangle textRect, Rectangle iconRect) {
if (b.getParent() instanceof JToolBar) {
// Windows doesn't draw the focus rect for buttons in a toolbar.
return;
}
// focus painted same color as text
g.setColor(getFocusColor());
// paint the focus rect around the union of text rect and icon rect
// PENDING JW: duplicated to handle insets
Rectangle iconTextRect = getIconTextRect(b);
// PENDING JW: better factor handling of insets - the bare union doesn't respect insets
// Rectangle iconTextRect = textRect.union(iconRect);
BasicGraphicsUtils.drawDashedRect(g, iconTextRect.x, iconTextRect.y,
iconTextRect.width, iconTextRect.height);
// pre-#167-swingx: active area too large
// int width = b.getWidth();
// int height = b.getHeight();
// BasicGraphicsUtils.drawDashedRect(g, dashedRectGapX, dashedRectGapY,
// width - dashedRectGapWidth, height - dashedRectGapHeight);
}
@Override
protected void paintButtonPressed(Graphics g, AbstractButton b) {
setTextShiftOffset();
}
@Override
protected BasicButtonListener createButtonListener(AbstractButton b) {
return new BasicHyperlinkListener(b);
}
/**
* {@inheritDoc}
*
* Overridden to return true if the position is inside the union of the
* text and icon rectangle, false otherwise.
*/
@Override
public boolean contains(JComponent c, int x, int y) {
AbstractButton button = (AbstractButton) c;
return isInside(getIconTextRect(button), x, y);
}
/**
* @param iconTextRect
* @param point
* @return
*/
private boolean isInside(Rectangle iconTextRect, int x, int y) {
if (iconTextRect == null) return false;
return iconTextRect.contains(x, y);
}
/**
* C&p'ed from BasicGraphicsUtils (getPreferredButtonSize).
*
* @param b the button to analyse.
* @return the union of the text and icon rectangle of the AbstractButton
* or null if the button has children (??)
*/
protected Rectangle getIconTextRect(AbstractButton b) {
if (b.getComponentCount() > 0) {
return null;
}
Icon icon = b.getIcon();
String text = b.getText();
Font font = b.getFont();
FontMetrics fm = b.getFontMetrics(font);
Rectangle iconR = new Rectangle();
Rectangle textR = new Rectangle();
Rectangle viewR = new Rectangle(b.getSize());
SwingUtilities.layoutCompoundLabel(b, fm, text, icon,
b.getVerticalAlignment(), b.getHorizontalAlignment(), b
.getVerticalTextPosition(), b
.getHorizontalTextPosition(), viewR, iconR, textR,
(text == null ? 0 : b.getIconTextGap()));
/*
* The preferred size of the button is the size of the text and icon
* rectangles plus the buttons insets.
*/
Rectangle r = iconR.union(textR);
Insets insets = b.getInsets();
r.width += insets.left + insets.right;
r.height += insets.top + insets.bottom;
// PENDING JW: why not?
// r.x -= insets.left;
r.y -= insets.top;
return r;
}
/**
* A BasicButtonListener specialized to the needs of a Hyperlink.
*
* @author Jeanette Winzenburg
*/
public static class BasicHyperlinkListener extends BasicButtonListener {
/**
* @param b
*/
public BasicHyperlinkListener(AbstractButton b) {
super(b);
}
@Override
public void stateChanged(ChangeEvent e) {
AbstractButton button = (AbstractButton) e.getSource();
if (button.isRolloverEnabled()) {
button.setCursor(button.getModel().isRollover() ?
// PENDING JW: support customizable cursor
Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) : null);
}
super.stateChanged(e);
}
}
static class ULHtml extends BasicHTML {
/**
* Create an html renderer for the given component and
* string of html.
*/
public static View createHTMLView(JComponent c, String html) {
BasicEditorKit kit = getFactory();
Document doc = kit.createDefaultDocument(c.getFont(),
c.getForeground());
Object base = c.getClientProperty(documentBaseKey);
if (base instanceof URL) {
((HTMLDocument)doc).setBase((URL)base);
}
Reader r = new StringReader(html);
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);
return v;
}
static BasicEditorKit getFactory() {
if (basicHTMLFactory == null) {
basicHTMLViewFactory = new BasicHTMLViewFactory();
basicHTMLFactory = new BasicEditorKit();
}
return basicHTMLFactory;
}
/**
* The source of the html renderers
*/
private static BasicEditorKit basicHTMLFactory;
/**
* Creates the Views that visually represent the model.
*/
private static ViewFactory basicHTMLViewFactory;
/**
* Overrides to the default stylesheet. Should consider
* just creating a completely fresh stylesheet.
*/
private static final String styleChanges =
"p { margin-top: 0; margin-bottom: 0; margin-left: 0; margin-right: 0; text-decoration: underline }" +
"body { margin-top: 0; margin-bottom: 0; margin-left: 0; margin-right: 0; text-decoration: underline }"+
"font {text-decoration: underline}";
static class BasicEditorKit extends HTMLEditorKit {
/** Shared base style for all documents created by us use. */
private static StyleSheet defaultStyles;
/**
* Overriden to return our own slimmed down style sheet.
*/
@Override
public StyleSheet getStyleSheet() {
if (defaultStyles == null) {
defaultStyles = new StyleSheet();
StringReader r = new StringReader(styleChanges);
try {
defaultStyles.loadRules(r, null);
} catch (Throwable e) {
// don't want to die in static initialization...
// just display things wrong.
}
r.close();
defaultStyles.addStyleSheet(super.getStyleSheet());
}
return defaultStyles;
}
/**
* Sets the async policy to flush everything in one chunk, and
* to not display unknown tags.
*/
public Document createDefaultDocument(Font defaultFont,
Color foreground) {
StyleSheet styles = getStyleSheet();
StyleSheet ss = new StyleSheet();
ss.addStyleSheet(styles);
BasicDocument doc = new BasicDocument(ss, defaultFont, foreground);
doc.setAsynchronousLoadPriority(Integer.MAX_VALUE);
doc.setPreservesUnknownTags(false);
return doc;
}
/**
* Returns the ViewFactory that is used to make sure the Views don't
* load in the background.
*/
@Override
public ViewFactory getViewFactory() {
return basicHTMLViewFactory;
}
}
/**
* BasicHTMLViewFactory extends HTMLFactory to force images to be loaded
* synchronously.
*/
static class BasicHTMLViewFactory extends HTMLEditorKit.HTMLFactory {
@Override
public View create(Element elem) {
View view = super.create(elem);
if (view instanceof ImageView) {
((ImageView)view).setLoadsSynchronously(true);
}
return view;
}
}
/**
* The subclass of HTMLDocument that is used as the model. getForeground
* is overridden to return the foreground property from the Component this
* was created for.
*/
static class BasicDocument extends HTMLDocument {
private static Class clz;
private static Method displayPropertiesToCSS;
/** The host, that is where we are rendering. */
// private JComponent host;
// --------- 1.5 x 1.6 incompatibility handling ....
static {
String j5 = "com.sun.java.swing.SwingUtilities2";
String j6 = "sun.swing.SwingUtilities2";
try {
// assume 1.6
clz = Class.forName(j6);
} catch (ClassNotFoundException e) {
// or maybe not ..
try {
clz = Class.forName(j5);
} catch (ClassNotFoundException e1) {
throw new RuntimeException("Failed to find SwingUtilities2. Check the classpath.");
}
}
try {
displayPropertiesToCSS = clz.getMethod("displayPropertiesToCSS", new Class[] { Font.class, Color.class});
} catch (Exception e) {
throw new RuntimeException("Failed to use SwingUtilities2. Check the permissions and class version.");
}
}
private static String displayPropertiesToCSS(Font f, Color c) {
try {
return (String) displayPropertiesToCSS.invoke(null, new Object[] { f, c });
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// --------- EO 1.5 x 1.6 incompatibility handling ....
BasicDocument(StyleSheet s, Font defaultFont, Color foreground) {
super(s);
setPreservesUnknownTags(false);
setFontAndColor(defaultFont, foreground);
}
/**
* Sets the default font and default color. These are set by
* adding a rule for the body that specifies the font and color.
* This allows the html to override these should it wish to have
* a custom font or color.
*/
private void setFontAndColor(Font font, Color fg) {
getStyleSheet().addRule(displayPropertiesToCSS(font,fg));
}
}
/**
* Root text view that acts as an HTML renderer.
*/
static class Renderer extends View {
Renderer(JComponent c, ViewFactory f, View v) {
super(null);
host = c;
factory = f;
view = v;
view.setParent(this);
// initially layout to the preferred size
setSize(view.getPreferredSpan(X_AXIS), view.getPreferredSpan(Y_AXIS));
}
/**
* 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;
}
/**
* Determines the preferred span for this view along an axis.
*
* @param axis may be either X_AXIS or Y_AXIS
* @return the span the view would like to be rendered into.
* Typically the view is told to render into the span
* that is returned, although there is no guarantee.
* The parent may choose to resize or break the view.
*/
@Override
public float getPreferredSpan(int axis) {
if (axis == X_AXIS) {
// width currently laid out to
return width;
}
return view.getPreferredSpan(axis);
}
/**
* Determines the minimum span for this view along an axis.
*
* @param axis may be either X_AXIS or Y_AXIS
* @return the span the view would like to be rendered into.
* Typically the view is told to render into the span
* that is returned, although there is no guarantee.
* The parent may choose to resize or break the view.
*/
@Override
public float getMinimumSpan(int axis) {
return view.getMinimumSpan(axis);
}
/**
* Determines the maximum span for this view along an axis.
*
* @param axis may be either X_AXIS or Y_AXIS
* @return the span the view would like to be rendered into.
* Typically the view is told to render into the span
* that is returned, although there is no guarantee.
* The parent may choose to resize or break the view.
*/
@Override
public float getMaximumSpan(int axis) {
return Integer.MAX_VALUE;
}
/**
* Specifies that a preference has changed.
* Child views can call this on the parent to indicate that
* the preference has changed. The root view routes this to
* invalidate on the hosting component.
*
* This can be called on a different thread from the
* event dispatching thread and is basically unsafe to
* propagate into the component. To make this safe,
* the operation is transferred over to the event dispatching
* thread for completion. It is a design goal that all view
* methods be safe to call without concern for concurrency,
* and this behavior helps make that true.
*
* @param child the child view
* @param width true if the width preference has changed
* @param height true if the height preference has changed
*/
@Override
public void preferenceChanged(View child, boolean width, boolean height) {
host.revalidate();
host.repaint();
}
/**
* Determines the desired alignment for this view along an axis.
*
* @param axis may be either X_AXIS or Y_AXIS
* @return the desired alignment, where 0.0 indicates the origin
* and 1.0 the full span away from the origin
*/
@Override
public float getAlignment(int axis) {
return view.getAlignment(axis);
}
/**
* 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();
view.setSize(alloc.width, alloc.height);
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;
}
/**
* Provides a mapping from the document model coordinate space
* to the coordinate space of the view mapped to it.
*
* @param pos the position to convert
* @param a the allocated region to render into
* @return the bounding box of the given position
*/
@Override
public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException {
return view.modelToView(pos, a, b);
}
/**
* Provides a mapping from the document model coordinate space
* to the coordinate space of the view mapped to it.
*
* @param p0 the position to convert >= 0
* @param b0 the bias toward the previous character or the
* next character represented by p0, in case the
* position is a boundary of two views.
* @param p1 the position to convert >= 0
* @param b1 the bias toward the previous character or the
* next character represented by p1, in case the
* position is a boundary of two views.
* @param a the allocated region to render into
* @return the bounding box of the given position is returned
* @exception BadLocationException if the given position does
* not represent a valid location in the associated document
* @exception IllegalArgumentException for an invalid bias argument
* @see View#viewToModel
*/
@Override
public Shape modelToView(int p0, Position.Bias b0, int p1,
Position.Bias b1, Shape a) throws BadLocationException {
return view.modelToView(p0, b0, p1, b1, a);
}
/**
* Provides a mapping from the view coordinate space to the logical
* coordinate space of the model.
*
* @param x x coordinate of the view location to convert
* @param y y coordinate of the view location to convert
* @param a the allocated region to render into
* @return the location within the model that best represents the
* given point in the view
*/
@Override
public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) {
return view.viewToModel(x, y, a, bias);
}
/**
* Returns the document model underlying the view.
*
* @return the model
*/
@Override
public Document getDocument() {
return view.getDocument();
}
/**
* Returns the starting offset into the model for this view.
*
* @return the starting offset
*/
@Override
public int getStartOffset() {
return view.getStartOffset();
}
/**
* Returns the ending offset into the model for this view.
*
* @return the ending offset
*/
@Override
public int getEndOffset() {
return view.getEndOffset();
}
/**
* Gets the element that this view is mapped to.
*
* @return the view
*/
@Override
public Element getElement() {
return view.getElement();
}
/**
* Sets the view size.
*
* @param width the width
* @param height the height
*/
@Override
public void setSize(float width, float height) {
this.width = (int) width;
view.setSize(width, height);
}
/**
* 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 int width;
private View view;
private ViewFactory factory;
private JComponent host;
}
}
}