org.openide.awt.HtmlRendererImpl Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.openide.awt;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.WritableRaster;
import java.beans.PropertyChangeListener;
import java.beans.VetoableChangeListener;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.AncestorListener;
/**
* Html renderer component implementation. The actual painting is done by HtmlLabelUI, which uses
* HtmlRenderer.renderString(). What this class does: Provide some methods for resetting its state
* between uses (see HtmlRenderer.createLabel() for why), overrides for a bunch of things for performance
* reasons, and some conversions to handle the case that the lightweight html renderer is disabled
* (-J-Dnb.useSwingHtmlRendering=true), to convert our minor extensions to html syntax to standard
* syntax for the swing renderer.
*
* Mainly this class provides an implementation of the various cell renderer interfaces which
* HtmlRenderer.Renderer aggregates, and the convenience methods it provides.
*
* @author Tim Boudreau
* @since 4.30
*
*/
class HtmlRendererImpl extends JLabel implements HtmlRenderer.Renderer {
private static final Rectangle bounds = new Rectangle();
private static final boolean swingRendering = Boolean.getBoolean("nb.useSwingHtmlRendering"); //NOI18N
private final Insets EMPTY_INSETS = new Insets(0, 0, 0, 0);
enum Type {UNKNOWN, TREE, LIST, TABLE}
//For experimentation - holding the graphics object may be the source of some
//strange painting problems on Apple
private static boolean noCacheGraphics = Boolean.getBoolean("nb.renderer.nocache"); //NOI18N
private ScratchGraphics cachedScratchGraphics = null;
private boolean centered = false;
private boolean parentFocused = false;
private Boolean html = null;
private int indent = 0;
private Border border = null;
private boolean selected = false;
private boolean leadSelection = false;
private Dimension prefSize = null;
private Type type = Type.UNKNOWN;
private int renderStyle = HtmlRenderer.STYLE_CLIP;
private boolean enabled = true;
private final boolean cellRenderer;
HtmlRendererImpl(boolean cellRenderer) {
this.cellRenderer = cellRenderer;
}
Type type() {
return type;
}
/** Restore the renderer to a pristine state */
public void reset() {
assert SwingUtilities.isEventDispatchThread();
parentFocused = false;
setCentered(false);
html = null;
indent = 0;
border = null;
setIcon(null);
setOpaque(false);
selected = false;
leadSelection = false;
prefSize = null;
type = Type.UNKNOWN;
renderStyle = HtmlRenderer.STYLE_CLIP;
setFont(UIManager.getFont("controlFont")); //NOI18N
setIconTextGap(3);
setEnabled(true);
border = null;
//Defensively ensure the insets haven't been messed with
EMPTY_INSETS.top = 0;
EMPTY_INSETS.left = 0;
EMPTY_INSETS.right = 0;
EMPTY_INSETS.bottom = 0;
}
public Component getTableCellRendererComponent(
JTable table, Object value, boolean selected, boolean leadSelection, int row, int column
) {
reset();
configureFrom(value, table, selected, leadSelection);
type = Type.TABLE;
if (swingRendering && selected) {
setBackground(table.getSelectionBackground());
setForeground(table.getSelectionForeground());
setOpaque(true);
}
return this;
}
public Component getTreeCellRendererComponent(
JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean leadSelection
) {
reset();
configureFrom(value, tree, selected, leadSelection);
type = Type.TREE;
if (swingRendering && selected) {
if (HtmlLabelUI.isGTK()) {
setBackground(HtmlLabelUI.getBackgroundFor(this));
setForeground(HtmlLabelUI.getForegroundFor(this));
}
setOpaque(true);
}
return this;
}
public Component getListCellRendererComponent(
JList list, Object value, int index, boolean selected, boolean leadSelection
) {
reset();
configureFrom(value, list, selected, leadSelection);
type = Type.LIST;
if (swingRendering && selected) {
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
setOpaque(true);
}
// ##93658: In GTK we have to paint borders in combo boxes
if (HtmlLabelUI.isGTK()) {
if (index == -1) {
Color borderC = UIManager.getColor("controlShadow"); //NOI18N
borderC = borderC == null ? Color.GRAY : borderC;
setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(borderC),
BorderFactory.createEmptyBorder(3, 2, 3, 2)));
} else {
setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
}
}
return this;
}
/** Generic code to set properties appropriately from any of the renderer
* fetching methods */
private void configureFrom(Object value, JComponent target, boolean selected, boolean leadSelection) {
setText((value == null) ? "" : value.toString());
setSelected(selected);
if (selected) {
setParentFocused(checkFocused(target));
} else {
setParentFocused(false);
}
setEnabled(target.isEnabled());
setLeadSelection(leadSelection);
setFont(target.getFont());
}
private boolean checkFocused(JComponent c) {
Component focused = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner();
boolean result = c == focused;
if (!result) {
result = c.isAncestorOf(focused);
}
return result;
}
public @Override void addNotify() {
if (swingRendering || !cellRenderer) {
super.addNotify();
}
}
public @Override void removeNotify() {
if (swingRendering || !cellRenderer) {
super.removeNotify();
}
}
public void setSelected(boolean val) {
selected = val;
}
public void setParentFocused(boolean val) {
parentFocused = val;
}
public void setLeadSelection(boolean val) {
leadSelection = val;
}
public void setCentered(boolean val) {
centered = val;
if (val) {
setIconTextGap(5);
}
if (swingRendering) {
if (val) {
setVerticalTextPosition(JLabel.BOTTOM);
setHorizontalAlignment(JLabel.CENTER);
setHorizontalTextPosition(JLabel.CENTER);
} else {
setVerticalTextPosition(JLabel.CENTER);
setHorizontalAlignment(JLabel.LEADING);
setHorizontalTextPosition(JLabel.TRAILING);
}
}
}
public void setIndent(int pixels) {
this.indent = pixels;
if (!cellRenderer || swingRendering) {
invalidate();
}
}
public void setHtml(boolean val) {
Boolean wasHtml = html;
String txt = getText();
html = val ? Boolean.TRUE : Boolean.FALSE;
if ((html != wasHtml) || (swingRendering || !cellRenderer)) {
//Ensure label UI gets updated and builds its little document tree...
firePropertyChange("text", txt, getText()); //NOI18N
}
}
public void setRenderStyle(int style) {
renderStyle = style;
}
int getRenderStyle() {
return renderStyle;
}
boolean isLeadSelection() {
return leadSelection;
}
boolean isCentered() {
return centered;
}
boolean isParentFocused() {
return parentFocused;
}
boolean isHtml() {
Boolean isHtml = html;
if (isHtml == null) {
String s = getText();
isHtml = checkHtml(s);
html = isHtml;
}
return isHtml.booleanValue();
}
private Boolean checkHtml(String s) {
Boolean result;
if (s == null) {
result = Boolean.FALSE;
} else if (s.startsWith(""; //NOI18N
}
return s;
}
/**
* Converts extended UI manager color tags into legal html in the case that we're using swing rendering
*
* @param s string to convert if it has questionable font tags
* @return The converted string
*/
private static String ensureLegalFontColorTags(String s) {
String check = s.toUpperCase();
int start = 0;
int fidx = check.indexOf("', start); //NOI18N
start = tagEnd + 1;
if (tagEnd == -1) {
break;
}
if (cidx != -1) {
if (cidx < tagEnd) {
//we have a font color tag
int eidx = check.indexOf('=', cidx); //NOI18N
if (eidx != -1) {
int bangIdx = check.indexOf('!', eidx); //NOI18N
if ((bangIdx != -1) && (bangIdx < tagEnd)) {
int colorStart = bangIdx + 1;
int colorEnd = tagEnd;
for (int i = colorStart; i < tagEnd; i++) {
char c = s.charAt(i);
if (!Character.isLetter(c)) {
colorEnd = i;
break;
}
}
if (sb == null) {
sb = new StringBuffer(s);
}
String colorString = s.substring(colorStart, colorEnd);
String converted = convertToStandardColor(colorString);
sb.replace(bangIdx, colorEnd, converted);
s = sb.toString();
check = s.toUpperCase();
}
}
}
}
fidx = check.indexOf("not use the
* internal HTML renderer is in effect, this will fire changes normally */
protected @Override final void firePropertyChange(String name, Object old, Object nue) {
if (swingRendering || !cellRenderer) {
if ("text".equals(name) && isHtml()) { //NOI18N
//Force in the HTML tags so the UI will set up swing HTML rendering appropriately
nue = getText();
}
super.firePropertyChange(name, old, nue);
}
}
public @Override Border getBorder() {
Border result;
if ((indent != 0) && (swingRendering || !cellRenderer)) {
result = BorderFactory.createEmptyBorder(0, indent, 0, 0);
} else {
result = border;
}
return result;
}
public @Override void setBorder(Border b) {
Border old = border;
border = b;
if (swingRendering || !cellRenderer) {
firePropertyChange("border", old, b);
}
}
public @Override Insets getInsets() {
return getInsets(null);
}
public @Override Insets getInsets(Insets insets) {
Insets result;
//Call getBorder(), not just read the field - if swingRendering, the border will be constructed, and the
//insets are what will make the indent property work; HtmlLabelUI doesn't need this, it just reads the
//insets property, but BasicLabelUI and its ilk do
Border b = getBorder();
if (b == null) {
// If being used externally, defensively return a new instance
result = cellRenderer ? EMPTY_INSETS : new Insets(0, 0, 0, 0);
} else {
//workaround for open jdk bug, see issue #192388
try {
result = b.getBorderInsets(this);
} catch( NullPointerException e ) {
Logger.getLogger(HtmlRendererImpl.class.getName()).log(Level.FINE, null, e);
result = cellRenderer ? EMPTY_INSETS : new Insets(0, 0, 0, 0);
}
}
if( null != insets ) {
insets.set( result.top, result.left, result.bottom, result.right);
return insets;
}
return result;
}
public @Override void setEnabled(boolean b) {
//OptimizeIt shows about 12Ms overhead calling back to Component.enable(), so avoid it if possible
enabled = b;
if (swingRendering || !cellRenderer) {
super.setEnabled(b);
}
}
public @Override boolean isEnabled() {
return enabled;
}
public @Override void updateUI() {
if (swingRendering) {
super.updateUI();
} else {
setUI(HtmlLabelUI.createUI(this));
}
}
/** Overridden to produce a graphics object even when isDisplayable() is
* false, so that calls to getPreferredSize() will return accurate
* dimensions (presuming the font and text are set correctly) even when
* not onscreen. */
public @Override Graphics getGraphics() {
Graphics result = null;
if (isDisplayable()) {
result = super.getGraphics();
}
if (result == null) {
result = scratchGraphics();
}
return result;
}
private static final class ScratchGraphics {
private final GraphicsConfiguration configuration;
private Reference graphics = new SoftReference<>(null);
public ScratchGraphics(GraphicsConfiguration configuration) {
if (configuration == null) {
throw new NullPointerException();
}
this.configuration = configuration;
}
public boolean isConfigurationCompatible(GraphicsConfiguration other) {
return configuration.getColorModel().equals(other.getColorModel())
&& configuration.getDefaultTransform().equals(other.getDefaultTransform());
}
public Graphics2D getGraphics() {
Graphics2D result = graphics.get();
if (result == null) {
/* Equivalent to configuration.createCompatibleImage(int, int), just to show that only the
ColorModel field of the GraphicsConfiguration is really relevant here. */
ColorModel model = configuration.getColorModel();
WritableRaster raster = model.createCompatibleWritableRaster(1, 1);
BufferedImage img = new BufferedImage(model, raster, model.isAlphaPremultiplied(), null);
result = img.createGraphics();
this.graphics = new SoftReference<>(result);
}
// Restore state on every call, just in case a client modified it for some reason.
result.setClip(null);
/* NETBEANS-2543: Apply the scaling HiDPI transform. This affects font measurements, via
FontRenderContext.getTransform(). */
result.setTransform(configuration.getDefaultTransform());
return result;
}
}
/** Fetch a scratch graphics object for calculating preferred sizes while
* offscreen */
private final Graphics2D scratchGraphics() {
GraphicsConfiguration gc = getGraphicsConfiguration();
if (gc == null) {
gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
}
ScratchGraphics scratchGraphics = cachedScratchGraphics;
if (scratchGraphics != null && scratchGraphics.isConfigurationCompatible(gc)) {
return scratchGraphics.getGraphics();
}
scratchGraphics = new ScratchGraphics(gc);
if (!noCacheGraphics) {
cachedScratchGraphics = scratchGraphics;
}
return scratchGraphics.getGraphics();
}
public @Override void setBounds(int x, int y, int w, int h) {
reshape(x, y, w, h);
}
@Deprecated
public @Override void reshape(int x, int y, int w, int h) {
if (swingRendering || !cellRenderer) {
super.reshape(x, y, w, h);
}
bounds.setBounds(x, y, w, h);
}
public @Override int getWidth() {
return bounds.width;
}
public @Override int getHeight() {
return bounds.height;
}
public @Override Point getLocation() {
return bounds.getLocation();
}
/** Overridden to do nothing for performance reasons */
public @Override void validate() {
if (!cellRenderer) {
super.validate();
}
}
public @Override void setText(String text) {
if (!cellRenderer) {
prefSize = null;
}
super.setText(text);
}
/** Overridden to do nothing for performance reasons */
public @Override void repaint(long tm, int x, int y, int w, int h) {
if (!cellRenderer) {
super.repaint(tm, x, y, w, h);
}
}
/** Overridden to do nothing for performance reasons */
public @Override void repaint() {
if (!cellRenderer) {
super.repaint();
}
}
/** Overridden to do nothing for performance reasons */
public @Override void invalidate() {
if (!cellRenderer) {
super.invalidate();
}
}
/** Overridden to do nothing for performance reasons */
public @Override void revalidate() {
if (!cellRenderer) {
super.revalidate();
}
}
/** Overridden to do nothing for performance reasons */
public @Override void addAncestorListener(AncestorListener l) {
if (swingRendering || !cellRenderer) {
super.addAncestorListener(l);
}
}
/** Overridden to do nothing for performance reasons */
public @Override void addComponentListener(ComponentListener l) {
if (swingRendering || !cellRenderer) {
super.addComponentListener(l);
}
}
/** Overridden to do nothing for performance reasons */
public @Override void addContainerListener(ContainerListener l) {
if (swingRendering || !cellRenderer) {
super.addContainerListener(l);
}
}
/** Overridden to do nothing for performance reasons */
public @Override void addHierarchyListener(HierarchyListener l) {
if (swingRendering || !cellRenderer) {
super.addHierarchyListener(l);
}
}
/** Overridden to do nothing for performance reasons */
public @Override void addHierarchyBoundsListener(HierarchyBoundsListener l) {
if (swingRendering || !cellRenderer) {
super.addHierarchyBoundsListener(l);
}
}
/** Overridden to do nothing for performance reasons */
public @Override void addInputMethodListener(InputMethodListener l) {
if (swingRendering || !cellRenderer) {
super.addInputMethodListener(l);
}
}
/** Overridden to do nothing for performance reasons */
public @Override void addFocusListener(FocusListener fl) {
if (swingRendering || !cellRenderer) {
super.addFocusListener(fl);
}
}
/** Overridden to do nothing for performance reasons */
public @Override void addMouseListener(MouseListener ml) {
if (swingRendering || !cellRenderer) {
super.addMouseListener(ml);
}
}
/** Overridden to do nothing for performance reasons */
public @Override void addMouseWheelListener(MouseWheelListener ml) {
if (swingRendering || !cellRenderer) {
super.addMouseWheelListener(ml);
}
}
/** Overridden to do nothing for performance reasons */
public @Override void addMouseMotionListener(MouseMotionListener ml) {
if (swingRendering || !cellRenderer) {
super.addMouseMotionListener(ml);
}
}
/** Overridden to do nothing for performance reasons */
public @Override void addVetoableChangeListener(VetoableChangeListener vl) {
if (swingRendering || !cellRenderer) {
super.addVetoableChangeListener(vl);
}
}
/** Overridden to do nothing for performance reasons, unless using standard swing rendering */
public @Override void addPropertyChangeListener(String s, PropertyChangeListener l) {
if (swingRendering || !cellRenderer) {
super.addPropertyChangeListener(s, l);
}
}
public @Override void addPropertyChangeListener(PropertyChangeListener l) {
if (swingRendering || !cellRenderer) {
super.addPropertyChangeListener(l);
}
}
}