package net.xoetrope.swing.docking;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.lang.reflect.Method;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.TransferHandler;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.plaf.basic.BasicButtonUI;
/**
* <p>A header like panel that paints a gradient fill and displays a caption. The
* header also contains a minimize button that initiates docking. The header may
* also be double clicked to zoom in on the owner XDockingPanel and the header
* may also be dragged from one docking panel to another.</p>
* <p>
* The colours of the header are controlled with the <code>dockingHeader</code>
* style and <code>dockingHeader/active</code> for the active tab colours
* </p>
* <p>Copyright: Xoetrope Ltd. (c) 2003-2006<br>
* License: see license.txt</p>
* $Revision: 1.2 $
*/
public class XDockableHeader extends JLabel implements MouseListener, MouseMotionListener, ActionListener
{
/**
* The default header button size
*/
public static final int BUTTON_SIZE = 8;
private static final int NORMAL = 0;
private static final int ROLLOVER = 1;
private static final int PRESSED = 2;
private static final int NUM_BUTTON_STATES = 3;
public static final int MINIMIZE = 0;
public static final int ZOOM = 1;
public static final int CLOSE = 2;
public static final int RESTORE = 3;
public static final int NUM_IMAGE_TYPES = 4;
private static Image[] buttonImages;
private static Color activeColor, activeTextColor, pressedTextColor, headerTextColor, headerBkColor;
private static boolean useGradientHeaders = true;
private MouseEvent firstMouseEvent = null;
private XDockable dockable;
private boolean active;
private Container glassPane;
private JButton sysMinimizeBtn, sysCloseBtn, sysZoomBtn;
private JPanel buttonPanel;
private Method clipIfNecessary;
private int minHeaderHeight = 2;
/**
* Creates a new instance of XDockableHeader
* @param dockable the dockable object that to which this header contributes.
* @param translator the translator component or null if the tooltips are not translated
* @param colors the header colors: background, text color, active background, active text color
* @param tooltips the tooltip text for the minimize and close buttons
*/
public XDockableHeader( XDockable dockable, Color[] colors, String[] tooltips )
{
this.dockable = dockable;
if ( dockable.icon != null ) {
int iconHeight = dockable.icon.getIconHeight();
minHeaderHeight = Math.max( iconHeight + 6, minHeaderHeight );
}
active = true;
if ( colors != null ) {
headerBkColor = colors[ 0 ];
headerTextColor = colors[ 1 ];
activeColor = colors[ 2 ];
activeTextColor = colors[ 3 ];
pressedTextColor = colors[ 4 ];
}
else {
headerBkColor = dockable.header.headerBkColor;
headerTextColor = dockable.header.headerTextColor;
activeColor = dockable.header.activeColor;
activeTextColor = dockable.header.activeTextColor;
pressedTextColor = dockable.header.pressedTextColor;
}
if ( tooltips == null ) {
tooltips = new String[ NUM_IMAGE_TYPES -1 ];
tooltips[ 0 ] = dockable.header.sysMinimizeBtn.getToolTipText();
tooltips[ 1 ] = dockable.header.sysZoomBtn.getToolTipText();
tooltips[ 2 ] = dockable.header.sysCloseBtn.getToolTipText();
}
setLayout( new BorderLayout());
setBorder( new EmptyBorder( 1, 3, 1, 3 ));
buttonPanel = new JPanel();
buttonPanel.setLayout( new FlowLayout());
buttonPanel.setOpaque( false );
if ( buttonImages == null )
buttonImages = new Image[ NUM_IMAGE_TYPES * 3 ];
buttonPanel.add( sysMinimizeBtn = new JButton());
setButtonProperties( sysMinimizeBtn, MINIMIZE, tooltips[ MINIMIZE ], dockable.canMinimize );
buttonPanel.add( sysZoomBtn = new JButton());
setButtonProperties( sysZoomBtn, ZOOM, tooltips[ ZOOM ], dockable.canZoom );
buttonPanel.add( sysCloseBtn = new JButton());
setButtonProperties( sysCloseBtn, CLOSE, tooltips[ CLOSE ], dockable.canClose );
add( buttonPanel, BorderLayout.EAST );
setTransferHandler( new XDockableTransferHandler( dockable.dockedContainer ));
addMouseListener( this );
addMouseMotionListener( this );
}
/**
* Configure the header buttons
* @param headerBtn the button
* @param itemId the item index
* @param tooltip the tooltip text
* @param isVisible true if the button is visible
*/
protected void setButtonProperties( JButton headerBtn, int itemId, String tooltip, boolean isVisible )
{
headerBtn.setToolTipText( tooltip );
headerBtn.setIcon( new ImageIcon( getImage( itemId, NORMAL )));
headerBtn.setPressedIcon( new ImageIcon( getImage( itemId, PRESSED )));
headerBtn.setRolloverIcon( new ImageIcon( getImage( itemId, ROLLOVER )));
headerBtn.setPreferredSize( new Dimension( BUTTON_SIZE, BUTTON_SIZE ));
headerBtn.setBackground( headerBkColor );
headerBtn.setOpaque( false );
headerBtn.setBorderPainted( false );
headerBtn.setBorder( new EmptyBorder( 0, 0, 0, 0 ));
headerBtn.setUI( new BasicButtonUI());
headerBtn.addActionListener( this );
headerBtn.setVisible( isVisible );
}
/**
* Respond to the 'minimize' button and dock this panel. The panel is 'docked'
* into the sidebar specified in the constructor. Delegates to the owner
* XDockingPanel panel to remove the content
* @param e the mouse event
*/
public void actionPerformed( ActionEvent e )
{
if ( e.getSource() == sysZoomBtn ) {
int newState = RESTORE;
if ( isZoomed())
newState = ZOOM;
setZoomState( newState );
zoomPanel();
}
else
dockable.dockedContainer.removeDockable( dockable, ( e.getSource() == sysMinimizeBtn ));
}
/**
* Get the dockable object that this header is associated with.
* @return the managing dockable object
*/
public XDockable getDockable()
{
return dockable;
}
/**
* Mark this header as active or inactive. When active a highligh is shown
* along with a minimize/dock button
*/
public void setActive( boolean state )
{
getComponent( 0 ).setVisible( state );
active = state;
repaint();
}
/**
* Control the docking component's ability to close
* @param state true to allow closing, false to prevent closing
*/
public void setCanClose( boolean state )
{
dockable.canClose = state;
sysCloseBtn.setVisible( state );
}
/**
* Determine if the docking component can close
* @return true if the component can close
*/
public boolean getCanClose()
{
return dockable.canClose;
}
/**
* Control the docking component's ability to minimize
* @param state true to allow closing, false to prevent minimizing
*/
public void setCanMinimize( boolean state )
{
dockable.canMinimize = state;
sysMinimizeBtn.setVisible( state );
}
/**
* Determine if the docking component can minimize
* @return true if the component can minimize
*/
public boolean getCanMinimize()
{
return dockable.canMinimize;
}
/**
* Control the docking component's ability to dock. Once the component is
* minimized it will not be able to dock back into the container
* if this parameter is <code>false</code>
* @param state true to allow closing, false to prevent docking
*/
public void setCanDockClose( boolean state )
{
dockable.canDock = state;
}
/**
* Determine if the docking component can dock
* @return true if the component can dock
*/
public boolean getCanDock()
{
return dockable.canDock;
}
/**
* Control the docking component's ability to be dragged and dropped.
* if this parameter is <code>false</code>
* @param state true to allow dragging, false to prevent dragging
*/
public void setCanDrag( boolean state )
{
dockable.canDrag = state;
}
/**
* Determine if the docking component can be dragged
* @return true if the component can be dragged
*/
public boolean getCanDrag()
{
return dockable.canDrag;
}
/**
* Control the docking component's ability to be zoomed.
* if this parameter is <code>false</code>
* @param state true to allow zooming, false to prevent zooming
*/
public void setCanZoom( boolean state )
{
dockable.canZoom = state;
}
/**
* Determine if the docking component can be zoomed
* @return true if the component can be zoomed
*/
public boolean getCanZoom()
{
return dockable.canZoom;
}
/**
* If a border has been set on this component, returns the
* border's insets; otherwise calls <code>super.getInsets</code>.
*
* @return the value of the insets property
* @see #setBorder
*/
public Insets getInsets()
{
return new Insets( 2, 4, 2, 4 );
}
/**
* Set the state of the zoome/restore button
* @param newState the ZOOM or RESTORE state
*/
public void setZoomState( int newState )
{
sysZoomBtn.setIcon( new ImageIcon( getImage( newState, NORMAL )));
sysZoomBtn.setPressedIcon( new ImageIcon( getImage( newState, PRESSED )));
sysZoomBtn.setRolloverIcon( new ImageIcon( getImage( newState, ROLLOVER )));
}
/**
* Zoom in on this panel, maximizing it so that it consumes the entire
* dockin apnel
*/
public void zoomPanel()
{
XCardPanel cardPanel = dockable.getCardPanel();
if ( cardPanel != null ) {
cardPanel.swapViews( dockable );
dockable.header.setZoomState( cardPanel.isZoomed() ? RESTORE : ZOOM );
dockable.dockedContainer.fireDockingPanelListeners( cardPanel.isZoomed() ? XDockingPanel.MAXIMIZED : XDockingPanel.RESTORED );
}
}
/**
* Is the panel zoomed?
* @return true if it is zoomed.
*/
public boolean isZoomed()
{
XCardPanel cardPanel = dockable.getCardPanel();
if ( cardPanel != null )
return cardPanel.isZoomed();
return false;
}
/**
* If the <code>preferredSize</code> has been set to a
* non-<code>null</code> value just returns it.
* If the UI delegate's <code>getPreferredSize</code>
* method returns a non <code>null</code> value then return that;
* otherwise defer to the component's layout manager.
*
* @return the value of the <code>preferredSize</code> property
* @see #setPreferredSize
* @see ComponentUI
*/
public Dimension getPreferredSize()
{
Dimension sz = super.getPreferredSize();
return new Dimension( Math.max( 20, sz.width ), Math.min( 35, Math.max( minHeaderHeight, sz.height )));
}
/**
* Paint the component, drawing the background gradient and the active
* indicator if the header is active/selected
* @param g the graphics context
*/
public void paintComponent( Graphics g )
{
Rectangle rect = getBounds();
// Fill the background
Color bkColor = headerBkColor;
g.setColor( bkColor );
if ( useGradientHeaders && ( g instanceof Graphics2D )) {
Graphics2D g2d = ((Graphics2D)g);
float dx = rect.height/4.0F;
dx = dx * dx / (float)rect.width;
GradientPaint gradient = new GradientPaint( 0.0F, 0.0F, brightenColor( bkColor, 110 ),//.brighter(),
dx,
(float)3.0F*rect.height/4.0F,
brightenColor( bkColor, 90 ),//.darker(),
true );
g2d.setPaint( gradient );
g2d.fill( new Rectangle( 0, 0, rect.width, rect.height ));
g2d.draw3DRect( 0, 0, rect.width-1, rect.height-1, true );
}
else
g.fillRect( 0, 0, rect.width, rect.height );
if ( active ) {
/** @todo find the system color for the tab/button highlights */
/** @todo draw/replace the border */
int red = activeColor.getRed();
int green = activeColor.getGreen();
int blue = activeColor.getBlue();
g.setColor( activeColor.brighter());
g.drawLine( 0, 0, rect.width-1, 0 );
g.drawLine( rect.width-1, 0, rect.width-1, 2 );
g.drawLine( 0, 0, 0, 2 );
g.setColor( activeColor );
g.setColor( new Color( red, green, blue, 223 ));
g.drawLine( 1, 1, rect.width-2, 1 );
g.setColor( new Color( red, green, blue, 128 ));
g.drawLine( 1, 2, rect.width-2, 2 );
}
int iconWidth = 0;
if ( dockable.icon != null ) {
int iconHeight = dockable.icon.getIconHeight();
g.drawImage( dockable.icon.getImage(), 5, ( rect.height - iconHeight ) / 2, this );
iconWidth = 8 + dockable.icon.getIconWidth();
}
else {
for ( int i = 0; i < 3; i++ ) {
g.setColor( new Color( 255, 255, 255, 200 ));
g.fillRect( 4, 6 + i*4, 2, 2 );
g.setColor( new Color( 0, 0, 0, 128 ));
g.fillRect( 3, 5 + i*4, 2, 2 );
}
iconWidth = 10;
}
FontMetrics fm = g.getFontMetrics();
g.setColor( headerTextColor );
Shape oldClip = g.getClip();
int captionWidth = rect.width - ( iconWidth + ( active ? ( buttonPanel.getWidth() + 4 ) : 0 ));
g.setClip( iconWidth, 0, captionWidth, rect.height );
String caption = getText();
caption = clipStringIfNecessary( this, fm, caption, captionWidth );
g.drawString( caption, iconWidth, rect.height / 2 + fm.getDescent() + 1 );
g.setClip( oldClip );
}
/**
* Invoked when a mouse button has been pressed on a component.
*/
public void mousePressed( MouseEvent e )
{
//Don't bother to drag if there is no image.
firstMouseEvent = e;
//e.consume();
}
/**
* Invoked when the mouse button has been clicked (pressed
* and released) on a component. If a double click has been detected then the
* outer XCardPanel will swap views to and from the zoom view.
*/
public void mouseClicked( MouseEvent e )
{
if ( e.getClickCount() == 2 )
zoomPanel();
}
/**
* Invoked when the mouse enters a component.
*/
public void mouseEntered(MouseEvent e) {}
/**
* Invoked when the mouse exits a component.
*/
public void mouseExited(MouseEvent e) {}
/**
* Invoked when a mouse button is pressed on a component and then
* dragged. <code>MOUSE_DRAGGED</code> events will continue to be
* delivered to the component where the drag originated until the
* mouse button is released (regardless of whether the mouse position
* is within the bounds of the component).
* <p>
* <p>Initiates a drag of the docked panel</p>
* Due to platform-dependent Drag&Drop implementations,
* <code>MOUSE_DRAGGED</code> events may not be delivered during a native
* Drag&Drop operation.
*/
public void mouseDragged( MouseEvent e )
{
if ( firstMouseEvent != null ) {
e.consume();
//If they are holding down the control key, COPY rather than MOVE
int action = TransferHandler.MOVE;
int dx = Math.abs( e.getX() - firstMouseEvent.getX());
int dy = Math.abs( e.getY() - firstMouseEvent.getY());
//Arbitrarily define a 5-pixel shift as the
//official beginning of a drag.
if ((( dx > 5 ) || ( dy > 5 )) && dockable.canDrag ) {
//This is a drag, not a click.
JComponent c = (JComponent)e.getSource();
TransferHandler handler = c.getTransferHandler();
//Tell the transfer handler to initiate the drag.
handler.exportAsDrag( c, firstMouseEvent, action );
firstMouseEvent = null;
glassPane = (Container)getRootPane().getGlassPane();
glassPane.setVisible( true );
dockable.dockedContainer.addDragProxies( glassPane );
}
}
}
/**
* Invoked when the mouse cursor has been moved onto a component
* but no buttons have been pushed.
*/
public void mouseMoved( MouseEvent e )
{
}
/**
* Invoked when a mouse button has been released on a component.
*/
public void mouseReleased(MouseEvent e)
{
endDock();
dockable.dockedContainer.setActivateHeader( this );
}
/**
* Adjust the position of the drop area preview during a drag operation
* @param target the potential drop site
*/
public void showDock( JComponent c, Container target )
{
if ( glassPane != null ) {
Component[] children = glassPane.getComponents();
int numChildren = children.length;
for ( int i = 0; i < numChildren; i++ ) {
Component child = children[ i ];
((JComponent)child).setBorder( child == c ? new LineBorder( new Color( 255, 0, 0, 128 ), 3 ) : null );
}
}
}
/**
* End the drag and drop operation by removing the drop site preview indicator
*/
public void endDock()
{
if ( glassPane != null ) {
glassPane.removeAll();
glassPane.setVisible( false );
glassPane = null;
}
firstMouseEvent = null;
}
/**
* Create a minimize button image
* @param state 0 - the normal minimize icon image, 1 - the rollover image, 2 - the pressed image
* @param zoomed true for the zoomed state
* @return the image for the minimize button
* @todo change this image for a proper 'dock'/minimize icon
*/
private Image getImage( int imageType, int state )
{
int imageIdx = imageType * 3 + state;
if ( buttonImages[ imageIdx ] == null ) {
buttonImages[ imageIdx ] = new BufferedImage( BUTTON_SIZE, BUTTON_SIZE, BufferedImage.TYPE_INT_ARGB );
Graphics2D g2d = (Graphics2D)buttonImages[ imageIdx ].getGraphics();
Object hint = g2d.getRenderingHint( RenderingHints.KEY_RENDERING );
g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
g2d.setRenderingHint( RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY );
g2d.setStroke( new BasicStroke( 1.0F ));
for ( int i = 0; i < 2; i++ ) {
int offset = i == 0 ? 0 : -1;
int opacity = i == 0 ? 64 : 255;
Color c;
if ( state == ROLLOVER )
c = activeTextColor;
else if ( state == PRESSED )
c = pressedTextColor;
else
c = headerTextColor;
int red = c.getRed();
int green = c.getGreen();
int blue = c.getBlue();
g2d.setColor( new Color( red, green, blue, opacity ));
if ( imageType == MINIMIZE ) {
RoundRectangle2D.Double rect = new RoundRectangle2D.Double( 1 + offset, 1 + offset + BUTTON_SIZE-4, BUTTON_SIZE-2, 2, 2, 2 );
g2d.draw( rect );
}
else if ( imageType == CLOSE ) {
g2d.drawLine( 1 + offset, 1 + offset, BUTTON_SIZE - 1 + offset, BUTTON_SIZE - 1 + offset );
g2d.drawLine( BUTTON_SIZE - 1 + offset, 1 + offset, 1 + offset, BUTTON_SIZE - 1 + offset );
}
else if ( imageType == ZOOM ) {
RoundRectangle2D.Double rect = new RoundRectangle2D.Double( 1 + offset, 1 + offset, XDockableHeader.BUTTON_SIZE-2, XDockableHeader.BUTTON_SIZE -2, 2, 2 );
g2d.draw( rect );
}
else if ( imageType == RESTORE ) {
RoundRectangle2D.Double rect = new RoundRectangle2D.Double( 1 + offset, 3 + offset, XDockableHeader.BUTTON_SIZE-3, 4, 2, 2 );
g2d.draw( rect );
rect = new RoundRectangle2D.Double( 2 + offset, 1 + offset, XDockableHeader.BUTTON_SIZE-3, 4, 2, 2 );
g2d.draw( rect );
}
}
g2d.setRenderingHint( RenderingHints.KEY_RENDERING, hint );
g2d.dispose();
}
return buttonImages[ imageIdx ];
}
/**
* Toggle the drawing of gradients in the headers
* @param state false to turn off gradient painting
*/
public static void setUseGradientHeaders( boolean state )
{
useGradientHeaders = state;
}
/**
* Get an brighter version of a color
* @param color the original color
* @param percentage the percentage of the original color brightness to return
*/
public static Color brightenColor( Color color, int percentage )
{
if ( percentage == 100 )
return color;
float[] hsb = new float[ 3 ];
color.RGBtoHSB( color.getRed(), color.getGreen(), color.getBlue(), hsb );
return new Color( color.HSBtoRGB( hsb[ 0 ], hsb[ 1 ], Math.min( 1.0F, (( percentage * hsb[ 2 ] ) / 100.0F ))));
}
public String clipStringIfNecessary( JComponent c, FontMetrics fm,
String caption,
int availTextWidth )
{
if (( caption == null ) || ( caption.length() == 0 ))
return "";
try {
/* This is a hack, using a private class so expect it to break when the JDK
* is upgraded */
Class klass = Class.forName( "sun.swing.SwingUtilities2" );
Class params[] = new Class[ 4 ];
params[ 0 ] = JComponent.class;
params[ 1 ] = FontMetrics.class;
params[ 2 ] = String.class;
params[ 3 ] = int.class;
clipIfNecessary = klass.getMethod( "clipStringIfNecessary", params );
if ( clipIfNecessary != null ) {
Object args[] = new Object[ 4 ];
args[ 0 ] = this;
args[ 1 ] = fm;
args[ 2 ] = caption;
args[ 3 ] = new Integer( availTextWidth );
caption = (String)clipIfNecessary.invoke( null, args );
return caption;
}
}
catch ( Exception ex )
{
ex.printStackTrace();
}
return "";
}
}