package net.xoetrope.swing.docking;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.SystemColor;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import org.jdesktop.swingx.MultiSplitLayout;
import org.jdesktop.swingx.MultiSplitLayout.Leaf;
import org.jdesktop.swingx.MultiSplitLayout.Node;
import org.jdesktop.swingx.MultiSplitLayout.Split;
import org.jdesktop.swingx.JXMultiSplitPane;
/**
* <p>A panel for use in a docking framework. The panel is an area into which
* various content panels may be docked. These docked panels are displayed with
* a header containing a title and a dock/minimize button.
* Clicking on the minimize button caused the panel to hide itself within the
* MultiSplitPane's layout and add a button to the side bar if one has been
* specified. Double clicking the header causes the panel to occupy the
* full area of its parent</p>
* <p>An individual XDockingPanel can contain multiple child panels, each of
* which is given a separate header, similar to a tab in a tab pane. The header
* contains a button, which when clicked docks the panel to a sidebar. The header
* may also contain a highlight to indicate that it is selected/active</p>
* <p>The dockable components are managed via the XDockable object which contains
* a collection of the participating objects, including the XDockingPanel, the
* XDockingSidebar that holds the minimized panel and the XDockableHeader that
* is used within this panel. The value of the dockable object's fields are
* set as the dockable panel's state is changed.</p>
* <p>In docking a panel the panel is hidden and the sidebar is informed that it
* should assume ownership of the dockable. However the content/panel remains
* a child of this docking panel unless shown in a preview panel. In the preview
* the content is 'borrowed' by the preview panel, but when the preview panel
* is dismissed the content is restored to this instance of the docking panel.</p>
* <p>Copyright: (c) Xoetrope Ltd., 1998-2008<br>
* License: see license.txt
* @version $Revision: 1.2 $
*/
public class XDockingPanel extends JPanel
{
public static final boolean USE_REMOVE_STRATEGY = false;
public static final int CLOSED = 0;
public static final int MAXIMIZED = 1;
public static final int MINIMIZED = 2;
public static final int PREVIEW_CLOSED = 3;
public static final int PREVIEW_OPENED = 4;
public static final int RESTORED = 5;
protected XDockableHeader activeHeader;
protected JPanel contentPane;
protected JPanel headerPanel;
protected Dimension defSize;
// private XDockingSideBar sidebar;
private CardLayout contentManager;
private String constraint;
// private static Rectangle emptyRect = new Rectangle( 0, 0, 0, 0 );
protected JXMultiSplitPane splitPane;
protected ArrayList listeners;
/**
* Creates a new instance of XDockingPanel
*
* @param constraint the name/constraint by which this panel is known. Note
* that this name is not displayed to the user and is instead used
* programatically
*/
public XDockingPanel( String constraint )
{
this.constraint = constraint;
listeners = new ArrayList();
setName( constraint );
setBackground( SystemColor.control );
setLayout( new BorderLayout());
setTransferHandler( new XDockableTransferHandler( this ));
headerPanel = new JPanel();
headerPanel.setLayout( new GridLayout( 1, 0 ));
add( headerPanel, BorderLayout.NORTH );
add( contentPane = new JPanel(), BorderLayout.CENTER );
contentPane.setOpaque( true );
contentPane.setBackground( Color.white );
// Use a card layout so that only one of the content panels is visible
// The one that corresponds to the selected header.
contentPane.setLayout( contentManager = new CardLayout());
}
/**
* Add a listener to this panel
*/
public void addDockingPanelListener( XDockingPanelListener l )
{
listeners.add( l );
}
/**
* Remove a listener from this panel
*/
public void removeDockingPanelListener( XDockingPanelListener l )
{
listeners.remove( l );
}
/**
* Invoke a dockingpanel listener method
* @param state a flasg indicating the new state and hence the method to invoke
*/
public void fireDockingPanelListeners( int state )
{
int numListeners = listeners.size();
for ( int i = 0; i < numListeners; i++ ) {
XDockingPanelListener l = (XDockingPanelListener)listeners.get( i );
switch ( state ) {
case CLOSED: l.panelClosed(); break;
case MAXIMIZED: l.panelMaximized(); break;
case MINIMIZED: l.panelMimimized(); break;
case PREVIEW_CLOSED: l.panelPreviewClosed(); break;
case PREVIEW_OPENED: l.panelPreviewOpened(); break;
case RESTORED: l.panelRestored(); break;
}
}
}
/**
* <p>Add proxies for each draggable target area in the split pane. If you wish
* to change this behavior and define areas that are not already in the
* split pane then overload this method.</p>
* <p>The method temporarily adds instances
* of XDockableProxy to the glass pane while the drag operation is in progress.
* Once the drag operation is complete the proxies are removed.</p>
* @param glassPane the glass pane.
*/
void addDragProxies( Container glassPane )
{
glassPane.setLayout( null );
splitPane = ((JXMultiSplitPane)getParent());
if ( splitPane != null ) {
ArrayList proxies = new ArrayList();
Component[] children = splitPane.getComponents();
for ( int i = 0; i < children.length; i++ ) {
Component child = children[ i ];
if ( child instanceof XDockingPanel ) {
XDockableProxy proxy = new XDockableProxy( (XDockingPanel)child );
Point p = SwingUtilities.convertPoint( child, 0, 0, glassPane );
proxy.setLocation( p );
proxy.setVisible( true );
proxy.setSize( child.getSize());
proxy.setTransferHandler( new XDockableTransferHandler( this ));
proxies.add( proxy );
}
}
// Sort the proxies by size so that most can be selected and so that the
// big areas do not hide the small ones, particularly when there is only
// one area visible.
Collections.sort( proxies, new Comparator() {
public int compare( Object proxyA, Object proxyB ) {
Rectangle rectA = ((XDockableProxy)proxyA).getBounds();
Rectangle rectB = ((XDockableProxy)proxyB).getBounds();
if ( rectA.intersects( rectB )) {
int a = ( rectA.width * rectA.height );
int b = ( rectB.width * rectB.height );
if ( a < b )
return -1;
else if ( a > b )
return 1;
else
return 0;
}
else if ( rectA.contains( rectB ))
return 1;
else if ( rectA.equals( rectB ))
return 0;
return -1;
}
});
int numProxies = proxies.size();
for ( int j = 0; j < numProxies; j++ ) {
XDockableProxy child = (XDockableProxy)proxies.get( j );
glassPane.add( child );
}
}
}
private void addChildAreas( ArrayList rects, Node n )
{
if ( n instanceof Leaf )
rects.add( n.getBounds());
else if ( n instanceof Split ) {
List children = ((Split)n).getChildren();
for ( int i = 0; i < children.size(); i++ ) {
Node nc = (Node)children.get( i );
addChildAreas( rects, nc );
}
}
}
/**
* Get the constraint used by this docking panel.
*/
public String getConstraint()
{
return constraint;
}
/**
* Set the constraint used by this docking panel.
* @param c the new layout constraint
*/
public void setConstraint( String c )
{
constraint = c;
}
/**
* Get the container into which the content should be added
* @return the content pane
*/
public Container getContentPane()
{
return contentPane;
}
/**
* Dock the content of the dockable object back into this container. The
* dockable object is updated in the process to refelct its new ownership.
* @param dockable the object being docked.
*/
public void restoreContent( XDockable dockable )
{
int numHeaders = headerPanel.getComponentCount();
for ( int i = 0; i < numHeaders; i++ )
((XDockableHeader)headerPanel.getComponent( i )).setActive( false );
contentPane.add( dockable.content, dockable.getId() );
contentManager.show( contentPane, dockable.getId() );
setVisible( true );
restoreDividers( dockable.dockedContainer );
headerPanel.add( dockable.header );
dockable.header.setZoomState( dockable.header.ZOOM );
dockable.header.setVisible( true );
dockable.header.setActive( true );
activeHeader = dockable.header;
splitPane.revalidate();
fireDockingPanelListeners( RESTORED );
}
/**
* Add a panel to this container. The panel will have a tab showing the
* title. The dockable object is updated to reflect its new ownership
* @param dockable the object being docked.
* @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 void addDockable( XDockable dockable, Color[] colors, String[] tooltips )
{
if ( activeHeader != null )
activeHeader.setActive( false );
dockable.dockedContainer = this;
XDockableHeader header = null;
if ( dockable.header == null )
header = new XDockableHeader( dockable, colors, tooltips );
else
header = dockable.header;
header.setText( dockable.title );
header.setToolTipText( dockable.title );
dockable.header = header;
headerPanel.add( header );
/** @todo remove this hack, its probably no longer needed - insetad use
* getConstraint */
dockable.content.setName( dockable.constraint );
contentPane.add( dockable.content, dockable.getId() );
contentManager.show( contentPane, dockable.getId() );
activeHeader = header;
activeHeader.setActive( true );
}
/**
* Set the active header
* @param dh the new active header
*/
public void setActivateHeader( XDockableHeader dh )
{
if ( activeHeader != null )
activeHeader.setActive( false );
activeHeader = dh;
XDockable activeDockable = activeHeader.getDockable();
contentManager.show( contentPane, activeDockable.getId());
activeHeader.setActive( true );
}
/**
* Remove the content referred to by the dockable from this docking panel
* @param dockable the object being docked.
* @param addToSideBar true to add the content to the dockable's sidebar
* @param removeDocked true to remove a docked component from the sidebar if
* the component is already minimized and docked
*/
public void removeDockable( XDockable dockable, boolean addToSidebar, boolean removeDocked )
{
removeDockable( dockable, addToSidebar );
if ( removeDocked && ( dockable.dockingSideBar != null ))
dockable.dockingSideBar.removeDockable( dockable );
}
/**
* Remove the content referred to by the dockable from this docking panel
* @param dockable the object being docked.
* @param addToSideBar true to add the content to the dockable's sidebar
*/
public void removeDockable( XDockable dockable, boolean addToSidebar )
{
activeHeader = dockable.header;
Container cont = getParent();
if ( !( cont instanceof JXMultiSplitPane )) {
while ( !( cont instanceof XCardPanel ))
cont = cont.getParent();
((XCardPanel)cont).swapViews( dockable );
}
splitPane = ((JXMultiSplitPane)getParent());
int activePos = getComponentZOrder( headerPanel, activeHeader );
if ( activePos > 0 )
activePos--;
headerPanel.remove( activeHeader );
headerPanel.revalidate();
boolean hasVisibleHeaders = headerPanel.getComponentCount() > 0;
headerPanel.doLayout();
headerPanel.repaint();
if ( !hasVisibleHeaders ) {
if ( USE_REMOVE_STRATEGY ) {
splitPane.remove( this );
splitPane.repaint();
}
else {
setVisible( false );
hideDividers( dockable.dockedContainer );
}
}
if ( addToSidebar )
dockable.dockingSideBar.add( dockable );
contentManager.removeLayoutComponent( dockable.content );
contentPane.remove( dockable.content );
activeHeader = null;
if ( hasVisibleHeaders ) {
if ( activePos >= 0 )
activeHeader = (XDockableHeader)headerPanel.getComponent( activePos );
if ( activeHeader != null ) {
contentManager.show( contentPane, activeHeader.getDockable().getId());
activeHeader.setActive( true );
}
}
revalidate();
splitPane.revalidate();
fireDockingPanelListeners( addToSidebar ? MINIMIZED : CLOSED );
}
/**
* Dock this panel
*/
public void dock()
{
removeDockable( activeHeader.getDockable(), true );
}
// /**
// * Remove the 'docked' panel from the side bar and restore it to its original
// * location within the layout
// */
// public void restore()
// {
// if ( USE_REMOVE_STRATEGY )
// splitPane.add( this, constraint );
// else
// setVisible( true );
// restoreDividers( null );
//
// revalidate();
// splitPane.revalidate();
// fireDockingPanelListeners( RESTORED );
// }
/**
* Get the window title of teh active header
* @return the title
*/
public String getTitle()
{
return activeHeader.getText();
}
/**
* Set the window title of the active header
* @param title the new title
*/
public void setTitle( String title )
{
activeHeader.setText( title );
activeHeader.setToolTipText( title );
}
/**
* Get the preferred size of the component. The
* @return the size of the panel or 0x0 if the panel is docked
*/
public Dimension getPreferredSize()
{
if ( defSize != null )
return defSize;
return super.getPreferredSize();
}
/**
* Hide dividers that are not required due to one of the nodes being non visible / hidden.
* Checks for dividers where the associated component is not visible and set the
* visible state of that node to false. For other nodes the visible state is
* set to true.
* @param comp the component that has just been hidden
*/
private void hideDividers( Component comp )
{
MultiSplitLayout layout = (MultiSplitLayout)splitPane.getLayout();
MultiSplitLayout.Node node = layout.getNodeForComponent( comp );
if ( node != null ) {
MultiSplitLayout.Split p = node.getParent();
p.hide( node );
if ( !p.isVisible())
p.getParent().hide( p );
p.checkDividers( p );
// If the split has become invisible then the parent may also have a divider
// that needs to be hidden.
while ( !p.isVisible()) {
p = p.getParent();
if ( p != null )
p.checkDividers( p );
else
break;
}
}
layout.setFloatingDividers( false );
}
/**
* Restore dividers that were not required due to one of the nodes being non visible / hidden.
* Checks for dividers where two adjacent nodes are not separated by a visible divider
* @param comp the component that has just been shown
*/
private void restoreDividers( Component comp )
{
MultiSplitLayout layout = (MultiSplitLayout)splitPane.getLayout();
MultiSplitLayout.Node node = layout.getNodeForComponent( comp );
if ( node != null ) {
node.setVisible( true );
MultiSplitLayout.Split p = node.getParent();
p.restoreDividers( p );
}
layout.setFloatingDividers( false );
}
/**
* Returns the z-order index of the component inside the container.
* The higher a component is in the z-order hierarchy, the lower
* its index. The component with the lowest z-order index is
* painted last, above all other child components.
*
* @param comp the component being queried
* @return the z-order index of the component; otherwise
* returns -1 if the component is <code>null</code
* or doesn't belong to the container
*/
private int getComponentZOrder( Container cont, Component comp )
{
if ( comp == null ) {
return -1;
}
synchronized( getTreeLock()) {
// Quick check - container should be immediate parent of the component
if ( comp.getParent() != cont )
return -1;
int ncomponents = cont.getComponentCount();
for ( int i = 0; i < ncomponents; i++ ) {
if ( cont.getComponent( i ) == comp )
return i;
}
}
// To please javac
return -1;
}
/**
* A proxy for hidden panels. The proxy is used during drag and drop operations
* and is temporarily placed on the glass pane.
*/
public class XDockableProxy extends JLabel
{
private XDockingPanel dockingPanel;
/**
* Create a new proxy
* @param dp the panel being proxied
*/
XDockableProxy( XDockingPanel dp )
{
dockingPanel = dp;
}
/**
* Get the original panel being proxied.
*/
public XDockingPanel getDockingPanel()
{
return dockingPanel;
}
}
}