org.netbeans.swing.tabcontrol.plaf.AbstractTabCellRenderer 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.
*/
/*
* AbstractTabCellRenderer.java
*
* Created on December 2, 2003, 4:13 PM
*/
package org.netbeans.swing.tabcontrol.plaf;
import org.netbeans.swing.tabcontrol.TabData;
import org.netbeans.swing.tabcontrol.TabDisplayer;
import org.openide.awt.HtmlRenderer;
import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.awt.event.ContainerListener;
import java.awt.event.HierarchyBoundsListener;
import java.awt.event.HierarchyListener;
import java.awt.event.MouseEvent;
/**
* Base class for tab renderers for the tab control. This is a support class
* which will allow authors who want to provide a different look or behavior for
* tabbed controls with a minimum of coding. The main methods of interest are
* stateChanged()
- where the component should be
* configured to render a given tabgetState()
- where the
* current state is to be found at the time stateChanged is called
*
.
*
* Typical usage is to pass one or more TabPainter objects to the constructor
* which will be responsible for doing the actual painting, calling the convenience
* getters in this class (such as isSelected
) to determine how
* to paint.
*
*
* @author Tim Boudreau
*/
public abstract class AbstractTabCellRenderer extends JLabel
implements TabCellRenderer {
private int state = TabState.NOT_ONSCREEN;
TabPainter leftBorder;
TabPainter rightBorder;
TabPainter normalBorder;
private Dimension padding;
/**
* Creates a new instance of AbstractTabCellRenderer
*/
public AbstractTabCellRenderer(TabPainter leftClip, TabPainter noClip,
TabPainter rightClip, Dimension padding) {
setOpaque(false);
setFocusable(false);
setBorder(noClip);
normalBorder = noClip;
leftBorder = leftClip;
rightBorder = rightClip;
this.padding = padding;
}
public AbstractTabCellRenderer (TabPainter painter, Dimension padding) {
this (painter, painter, painter, padding);
}
private boolean showClose = true;
public final void setShowCloseButton (boolean b) {
showClose = b;
}
public final boolean isShowCloseButton() {
return showClose;
}
private Rectangle scratch = new Rectangle();
public String getCommandAtPoint(Point p, int tabState, Rectangle bounds) {
setBounds (bounds);
setState (tabState);
if (supportsCloseButton(getBorder()) && isShowCloseButton()) {
TabPainter cbp = (TabPainter) getBorder();
cbp.getCloseButtonRectangle (this, scratch, bounds);
if (getClass() != AquaEditorTabCellRenderer.class) {
//#47408 - hit test area of close button is too small
scratch.x -=3;
scratch.y -=3;
scratch.width += 6;
scratch.height += 6;
}
if (scratch.contains(p)) {
return TabDisplayer.COMMAND_CLOSE;
}
}
Polygon tabShape = getTabShape (tabState, bounds);
if (tabShape.contains(p)) {
return TabDisplayer.COMMAND_SELECT;
}
return null;
}
public String getCommandAtPoint(Point p, int tabState, Rectangle bounds, int mouseButton, int eventType, int modifiers) {
String result = null;
if (mouseButton == MouseEvent.BUTTON2 && eventType == MouseEvent.MOUSE_RELEASED) {
result = TabDisplayer.COMMAND_CLOSE;
}
else {
result = getCommandAtPoint (p, tabState, bounds);
}
if (result != null) {
if (TabDisplayer.COMMAND_SELECT == result) {
boolean clipped = isClipLeft() || isClipRight();
if ((clipped && eventType == MouseEvent.MOUSE_RELEASED && mouseButton == MouseEvent.BUTTON1) ||
(!clipped && eventType == MouseEvent.MOUSE_PRESSED && mouseButton == MouseEvent.BUTTON1)) {
return result;
}
} else if (TabDisplayer.COMMAND_CLOSE == result && eventType == MouseEvent.MOUSE_RELEASED && isShowCloseButton()) {
if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) {
return TabDisplayer.COMMAND_CLOSE_ALL;
} else if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0 && mouseButton != MouseEvent.BUTTON2) {
return TabDisplayer.COMMAND_CLOSE_ALL_BUT_THIS;
} else if( ((tabState & TabState.CLOSE_BUTTON_ARMED) == 0 || (tabState & TabState.MOUSE_PRESSED_IN_CLOSE_BUTTON) == 0)
&& mouseButton == MouseEvent.BUTTON1 ) {
//#208732
result = TabDisplayer.COMMAND_SELECT;
}
return result;
}
}
return null;
}
//********************** Subclass convenience API methods*****************
/**
* Convenience getter to determine if the current state includes the armed
* state (the mouse is in the tab the component is currently configured to
* render).
*/
protected final boolean isArmed() {
return isPressed() || (state & TabState.ARMED) != 0;
}
/**
* Convenience getter to determine if the current state includes the active
* state (a component in the container or the container itself has keyboard
* focus)
*/
protected final boolean isActive() {
return (state & TabState.ACTIVE) != 0;
}
/**
* Convenience getter to determine if the current state includes the pressed
* state (the mouse is in the tab this component is currently configured to
* render, and the mouse button is currently down)
*/
protected final boolean isPressed() {
return (state & TabState.PRESSED) != 0;
}
/**
* Convenience getter to determine if the current state includes the
* selected state (the tab this component is currently configured to render
* is the selected tab in a container)
*/
protected final boolean isSelected() {
return (state & TabState.SELECTED) != 0;
}
/**
* Convenience getter to determine if the current state includes the
* right-clipped state (the right hand side of the tab is not visible).
*/
protected final boolean isClipRight() {
return (state & TabState.CLIP_RIGHT) != 0;
}
/**
* Convenience getter to determine if the current state includes the
* left-clipped state (the right hand side of the tab is not visible).
*/
protected final boolean isClipLeft() {
return (state & TabState.CLIP_LEFT) != 0;
}
/**
* Convenience getter to determine if the current state indicates
* that the renderer is currently configured as the leftmost (non-clipped).
*/
protected final boolean isLeftmost() {
return (state & TabState.LEFTMOST) != 0;
}
/**
* Convenience getter to determine if the current state indicates
* that the renderer is currently configured as the rightmost (non-clipped).
*/
protected final boolean isRightmost() {
return (state & TabState.RIGHTMOST) != 0;
}
protected final boolean isAttention() {
return (state & TabState.ATTENTION) != 0
|| (state & TabState.HIGHLIGHT) != 0;
}
/**
*
* @return True if the tab should be highlighted, false otherwise.
* @since 1.38
*/
protected final boolean isHighlight() {
return (state & TabState.HIGHLIGHT) != 0;
}
/**
* Convenience getter to determine if the current state indicates
* that the renderer is currently configured appears to the left of
* the selected tab.
*/
protected final boolean isNextTabSelected() {
return (state & TabState.BEFORE_SELECTED) != 0;
}
/**
* Convenience getter to determine if the current state indicates
* that the renderer is currently configured appears to the left of
* the armed tab.
*/
protected final boolean isNextTabArmed() {
return (state & TabState.BEFORE_ARMED) != 0;
}
/**
* Convenience getter to determine if the current state indicates
* that the renderer is currently configured appears to the right of
* the selected tab.
*/
protected final boolean isPreviousTabSelected() {
return (state & TabState.AFTER_SELECTED) != 0;
}
/**
* @return True if the tab is busy.
*/
protected final boolean isBusy() {
return (state & TabState.BUSY) != 0;
}
public Dimension getPadding() {
return new Dimension(padding);
}
/** Set the state of the renderer, in preparation for painting it or evaluating a condition
* (such as the position of the close button) for which it must be correctly configured).
* This method will call stateChanged(), allowing the renderer to reconfigure itself if
* necessary, when the state changes.
*
* @param state
*/
protected final void setState(int state) {
//System.err.println("Renderer SetState " + TabState.stateToString(state));
boolean needChange = this.state != state;
if (needChange) {
int old = this.state;
//Set the state value here, so isArmed(), etc. will return
//correct values in stateChanged(), so subclasses can set
//up colors correctly
this.state = state;
int newState = stateChanged(old, state);
if ((newState & this.state) != state) {
this.state = state;
throw new IllegalStateException("StateChanged may add, but not remove bits from the " +
"state bitmask. Expected state: " + TabState.stateToString(
state) + " but got " + TabState.stateToString(this.state));
}
this.state = newState;
}
}
/**
* Returns the state as set up in getRendererComponent
*/
public final int getState() {
return state;
}
/**
* Implementation of getRendererComponent from TabCellRenderer. This
* method is final, and will configure the text, bounds and icon correctly
* according to the passed values, and call setState to set the state of the
* tab. Implementers must implement stateChanged()
to handle
* any changes (background color, border, etc) necessary to reflect the
* current state as returned by getState()
.
*/
public final javax.swing.JComponent getRendererComponent(TabData data,
Rectangle bounds,
int state) {
setBounds(bounds);
setText(data.getText());
setIcon(data.getIcon());
setState(state);
return this;
}
//***************SPI METHODS********************************************
/*
* Implementations of this method may not remove state bits
* that were passed in. A runtime check of the result will be performed,
* and in the case that some states were removed, a runtime exception will
* be thrown after this method exits.
*/
protected int stateChanged(int oldState, int newState) {
Color bg = isSelected() ?
isActive() ?
getSelectedActivatedBackground() : getSelectedBackground() :
UIManager.getColor("control");
Color fg = isSelected() ?
isActive() ?
getSelectedActivatedForeground() : getSelectedForeground() :
UIManager.getColor("textText");
if (isArmed() && isPressed() && (isClipLeft() || isClipRight())) {
//Create an armed appearance for clipped, pressed tabs, which will respond
//to mouseReleased, not mousePressed
bg = getSelectedActivatedBackground();
fg = getSelectedActivatedForeground();
}
if (isClipLeft()) {
setIcon(null);
setBorder(leftBorder);
} else if (isClipRight()) {
setBorder(rightBorder);
} else {
setBorder(normalBorder);
}
setBackground(bg);
setForeground(fg);
return newState;
}
/** Overridden to be a no-op for performance reasons */
@Override
public void revalidate() {
//do nothing - performance
}
/** Overridden to be a no-op for performance reasons */
@Override
public void repaint() {
//do nothing - performance
}
/** Overridden to be a no-op for performance reasons */
@Override
public void validate() {
//do nothing - performance
}
/** Overridden to be a no-op for performance reasons */
@Override
public void repaint(long tm) {
//do nothing - performance
}
/** Overridden to be a no-op for performance reasons */
@Override
public void repaint(long tm, int x, int y, int w, int h) {
//do nothing - performance
}
/** Overridden to be a no-op for performance reasons */
@Override
protected final void firePropertyChange(String s, Object a, Object b) {
//do nothing - performance
}
/** Overridden to be a no-op for performance reasons */
@Override
public final void addHierarchyBoundsListener(HierarchyBoundsListener hbl) {
//do nothing
}
/** Overridden to be a no-op for performance reasons */
@Override
public final void addHierarchyListener(HierarchyListener hl) {
//do nothing
}
/** Overridden to be a no-op for performance reasons */
@Override
public final void addContainerListener(ContainerListener cl) {
//do nothing
}
/**
* Overridden to paint the interior of the polygon if the border is an instance of TabPainter.
*/
@Override
public void paintComponent(Graphics g) {
g.setColor(getBackground());
if (getBorder() instanceof TabPainter) {
((TabPainter) getBorder()).paintInterior(g, this);
}
paintIconAndText(g);
}
/**
* Return non-zero to shift the text up or down by the specified number of pixels when painting.
* Used by the default implementation of {@link #getCaptionYPosition(Graphics)}.
*
* @return A positive or negative number of pixels
*/
protected int getCaptionYAdjustment() {
return -1;
}
/** Return non-zero to shift the icon up or down by the specified number of pixels when painting.
*
* @return A positive or negative number of pixels
*/
protected int getIconYAdjustment() {
return -1;
}
/**
* Get the Y position of the caption. May be overridden. The default implementation uses an old
* formula which is retained for backwards compatibility (since subclasses may have tuned their
* value of {@link #getCaptionYAdjustment()} based on it).
*
* @param g The graphics context
* @return The Y position of the caption's baseline
*/
protected int getCaptionYPosition(Graphics g) {
FontMetrics fm = g.getFontMetrics(getFont());
//Find out what height we need
int txtH = fm.getHeight();
Insets ins = getInsets();
//find out the available height
int availH = getHeight() - (ins.top + ins.bottom);
int txtY;
if (availH > txtH) {
txtY = txtH + ins.top + ((availH / 2) - (txtH / 2)) - 3;
} else {
txtY = txtH + ins.top;
}
txtY += getCaptionYAdjustment();
return txtY;
}
/**
* Actually paints the icon and text (using the lightweight HTML renderer)
*
* @param g The graphics context
*/
protected void paintIconAndText(Graphics g) {
g.setFont(getFont());
FontMetrics fm = g.getFontMetrics(getFont());
//Find out what height we need
int txtH = fm.getHeight();
Insets ins = getInsets();
//find out the available height
int availH = getHeight() - (ins.top + ins.bottom);
int txtX;
int centeringToAdd = getPixelsToAddToSelection() != 0 ?
getPixelsToAddToSelection() / 2 : 0;
Icon icon = getIcon();
//Check the icon non-null and height (see TabData.NO_ICON for why)
if (!isClipLeft() && icon != null && icon.getIconWidth() > 0 && icon.getIconHeight() > 0) {
int iconY = ins.top
+ Math.max(0, (availH - icon.getIconHeight())) / 2
//add 2 to make sure icon top pixels are not cut off by outline
+ 2;
int iconX = ins.left + centeringToAdd;
iconY += getIconYAdjustment();
icon.paintIcon(this, g, iconX, iconY);
txtX = iconX + icon.getIconWidth() + getIconTextGap();
} else {
txtX = ins.left + centeringToAdd;
}
if (icon != null && icon.getIconWidth() == 0) {
//Add some spacing so the text isn't flush for, e.g., the
//welcome screen tab
txtX += 5;
}
int txtY = getCaptionYPosition(g);
//Get the available horizontal pixels for text
int txtW = getWidth() - (txtX + ins.right);
if (isClipLeft()) {
//fiddle with the string to get "...blah"
String s = preTruncateString(getText(), g, txtW - 4); //subtract 4 so it's not flush w/ tab edge
Graphics2D g2d = null;
Shape clip = null;
if( g instanceof Graphics2D ) {
g2d = ( Graphics2D ) g;
clip = g2d.getClip();
g2d.clipRect( ins.left, ins.top, getWidth()-ins.left-ins.right, getHeight()-ins.top-ins.bottom);
}
txtW = (int)HtmlRenderer.renderString(s, g, txtX, txtY, Integer.MAX_VALUE, Integer.MAX_VALUE, getFont(),
getForeground(), HtmlRenderer.STYLE_CLIP, false);
txtX = getWidth()-ins.right-txtW;
txtW = (int)HtmlRenderer.renderString(s, g, txtX, txtY, txtW, txtH, getFont(),
getForeground(), HtmlRenderer.STYLE_CLIP, true);
if( null != g2d ) {
g2d.setClip( clip );
}
} else {
String s;
if (isClipRight()) {
//Jano wants to always show a "..." for cases where a tab is truncated,
//even if we've really painted all the text.
s = getText() + "..."; //NOI18N
} else {
s = getText();
}
txtW = (int)HtmlRenderer.renderString(s, g, txtX, txtY, txtW, txtH, getFont(),
getForeground(), HtmlRenderer.STYLE_TRUNCATE, true);
}
}
static String preTruncateString(String s, Graphics g, int availPixels) {
if (s.length() < 3) {
return s;
}
s = stripHTML(s);
if (s.length() < 2) {
return "..." + s; //NOI18N
}
FontMetrics fm = g.getFontMetrics();
int dotsWidth = fm.stringWidth("..."); //NOI18N
int beginIndex = s.length() - 2;
String test = s.substring(beginIndex);
String result = test;
while (fm.stringWidth(test) + dotsWidth < availPixels) {
beginIndex--;
if (beginIndex <= 0) {
break;
} else {
result = test;
test = s.substring(beginIndex);
}
}
return "..." + result; //NOI18N
}
static boolean isHTML(String s) {
boolean result = s.startsWith("")
|| s.startsWith(""); //NOI18N
return result;
}
static String stripHTML(String s) {
if (isHTML(s)) {
StringBuffer result = new StringBuffer(s.length());
char[] c = s.toCharArray();
boolean inTag = false;
for (int i = 0; i < c.length; i++) {
//XXX need to handle entity includes
boolean wasInTag = inTag;
if (!inTag) {
if (c[i] == '<') {
inTag = true;
}
} else {
if (c[i] == '>') {
inTag = false;
}
}
if (!inTag && wasInTag == inTag) {
result.append(c[i]);
}
}
return result.toString();
} else {
return s;
}
}
/**
* Get the shape of the tab. The implementation here will check if the
* border is an instance of TabPainter, and if so, use the polygon it
* returns, translating it to the position of the passed-in rectangle. If
* you are subclassing but do not intend to use TabPainter, you need to
* override this method
*/
public Polygon getTabShape(int tabState, Rectangle bounds) {
setBounds(bounds);
setState(tabState);
if (getBorder() instanceof TabPainter) {
TabPainter pb = (TabPainter) getBorder();
Polygon p = pb.getInteriorPolygon(this);
p.translate(bounds.x, bounds.y);
return p;
} else {
//punt and return the bounds as a polygon - what else to do?
return new Polygon(new int[]{
bounds.x, bounds.x + bounds.width - 1,
bounds.x + bounds.width - 1, bounds.x}, new int[]{
bounds.y, bounds.y, bounds.y + bounds.height - 1,
bounds.y + bounds.height - 1}, 4);
}
}
public Color getSelectedBackground() {
Color base = UIManager.getColor("control"); //NOI18N
Color towards = UIManager.getColor("controlHighlight"); //NOI18N
if (base == null) {
base = Color.GRAY;
}
if (towards == null) {
towards = Color.WHITE;
}
Color result = ColorUtil.adjustTowards(base, 30, towards);
return result;
}
public Color getSelectedActivatedBackground() {
return UIManager.getColor("TabRenderer.selectedActivatedBackground");
}
public Color getSelectedActivatedForeground() {
return UIManager.getColor("TabRenderer.selectedActivatedForeground");
}
public Color getSelectedForeground() {
return UIManager.getColor("TabRenderer.selectedForeground");
}
protected boolean inCloseButton() {
return (state & TabState.CLOSE_BUTTON_ARMED) != 0;
}
/**
* Subclasses which want to make the selected tab wider than it would otherwise be should return a value
* greater than 0 here. The default implementation returns 0.
*/
public int getPixelsToAddToSelection() {
return 0;
}
private boolean supportsCloseButton(Border b) {
if (b instanceof TabPainter) {
return ((TabPainter) b).supportsCloseButton(this);
} else {
return false;
}
}
}