/*
* Bibliothek - DockingFrames
* Library built on Java/Swing, allows the user to "drag and drop"
* panels containing any Swing-Component the developer likes to add.
*
* Copyright (C) 2007 Benjamin Sigg
*
* 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 Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Benjamin Sigg
* benjamin_sigg@gmx.ch
* CH - Switzerland
*/
package bibliothek.gui.dock.station.stack;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.border.Border;
import bibliothek.gui.DockController;
import bibliothek.gui.Dockable;
import bibliothek.gui.dock.DockElement;
import bibliothek.gui.dock.DockElementRepresentative;
import bibliothek.gui.dock.StackDockStation;
import bibliothek.gui.dock.disable.TabDisablingStrategyObserver;
import bibliothek.gui.dock.station.stack.tab.AbstractTabPane;
import bibliothek.gui.dock.station.stack.tab.AbstractTabPaneComponent;
import bibliothek.gui.dock.station.stack.tab.LonelyTabPaneComponent;
import bibliothek.gui.dock.station.stack.tab.TabConfiguration;
import bibliothek.gui.dock.station.stack.tab.TabConfigurations;
import bibliothek.gui.dock.station.stack.tab.TabLayoutManager;
import bibliothek.gui.dock.station.stack.tab.TabPane;
import bibliothek.gui.dock.station.stack.tab.TabPaneBackgroundComponent;
import bibliothek.gui.dock.station.stack.tab.TabPaneListener;
import bibliothek.gui.dock.themes.ThemeManager;
import bibliothek.gui.dock.themes.border.BorderForwarder;
import bibliothek.gui.dock.util.BackgroundAlgorithm;
import bibliothek.gui.dock.util.BackgroundPanel;
import bibliothek.gui.dock.util.ConfiguredBackgroundPanel;
import bibliothek.gui.dock.util.PropertyValue;
import bibliothek.gui.dock.util.SimpleDockElementRepresentative;
import bibliothek.gui.dock.util.Transparency;
import bibliothek.util.FrameworkOnly;
/**
* A {@link StackDockComponent} which is a combination of other components.<br>
* This class also implements {@link TabPane} and thus supports the
* {@link TabLayoutManager}.
* @author Benjamin Sigg
*
* @param <T> the type of the tabs
* @param <M> the type of the menus
* @param <I> the type of the additional info panel
*/
public abstract class CombinedStackDockComponent<T extends CombinedTab, M extends CombinedMenu, I extends CombinedInfoComponent> extends AbstractTabPane<T, M, I> implements StackDockComponent {
/** The panel which shows the children */
private CombinedStackDockContentPane panel;
/** A list of all {@link Component Components} which are shown on this {@link #componentPanel} */
private Map<Dockable, Meta> components = new HashMap<Dockable, Meta>();
/** The current configuration of the tabs */
private PropertyValue<TabConfigurations> tabConfiguration = new PropertyValue<TabConfigurations>( StackDockStation.TAB_CONFIGURATIONS ){
@Override
protected void valueChanged( TabConfigurations oldValue, TabConfigurations newValue ){
for( T tab : getTabsList() ){
tab.setConfiguration( newValue.getConfiguration( tab.getDockable() ));
}
}
};
/** the background of this component */
private BackgroundAlgorithm background;
/** The panel which displays one of the children of this pane */
private BackgroundPanel componentPanel = new ConfiguredBackgroundPanel( null, Transparency.DEFAULT ){
@Override
public void doLayout(){
int w = getWidth();
int h = getHeight();
for( int i = 0, n = getComponentCount(); i < n; i++ ) {
getComponent( i ).setBounds( 0, 0, w, h );
}
}
@Override
public Dimension getPreferredSize(){
Dimension base = new Dimension( 0, 0 );
for( int i = 0, n = getComponentCount(); i < n; i++ ) {
Dimension next = getComponent( i ).getPreferredSize();
base.width = Math.max( base.width, next.width );
base.height = Math.max( base.height, next.height );
}
return base;
}
@Override
public Dimension getMinimumSize(){
Dimension base = new Dimension( 0, 0 );
for( int i = 0, n = getComponentCount(); i < n; i++ ) {
Dimension next = getComponent( i ).getMinimumSize();
base.width = Math.max( base.width, next.width );
base.height = Math.max( base.height, next.height );
}
return base;
}
};
/** listeners to be informed when the selection changes */
private List<StackDockComponentListener> listeners = new ArrayList<StackDockComponentListener>();
/** Handles visibility of tabs */
private CombinedHandler<CombinedTab> tabHandler = new CombinedHandler<CombinedTab>(){
public void setVisible( CombinedTab tab, boolean visible ){
DockController controller = getController();
if( visible ) {
panel.add( tab.getComponent() );
if( controller != null ) {
controller.addRepresentative( tab );
}
}
else {
panel.remove( tab.getComponent() );
if( controller != null ) {
controller.removeRepresentative( tab );
}
}
}
public boolean isVisible( CombinedTab item ){
return item.getComponent() != null && item.getComponent().getParent() == panel;
}
public void setZOrder( CombinedTab item, int order ){
CombinedStackDockComponent.this.setZOrder( item.getComponent(), order );
}
public int getZOrder( CombinedTab item ){
return CombinedStackDockComponent.this.getZOrder( item.getComponent() );
}
};
/** Handles visibility of menus. */
private CombinedHandler<CombinedMenu> menuHandler = new CombinedHandler<CombinedMenu>(){
public void setVisible( CombinedMenu menu, boolean visible ){
if( visible ) {
panel.add( menu.getComponent() );
}
else {
panel.remove( menu.getComponent() );
}
}
public boolean isVisible( CombinedMenu item ){
return item.getComponent() != null && item.getComponent().getParent() == panel;
}
public void setZOrder( CombinedMenu item, int order ){
CombinedStackDockComponent.this.setZOrder( item.getComponent(), order );
}
public int getZOrder( CombinedMenu item ){
return CombinedStackDockComponent.this.getZOrder( item.getComponent() );
}
};
/** Handles visibility of info components */
private CombinedHandler<AbstractTabPaneComponent> infoHandler = new CombinedHandler<AbstractTabPaneComponent>(){
public void setVisible( AbstractTabPaneComponent item, boolean visible ){
if( visible ) {
panel.add( item.getComponent() );
}
else {
panel.remove( item.getComponent() );
}
}
public boolean isVisible( AbstractTabPaneComponent item ){
return item.getComponent() != null && item.getComponent().getParent() == panel;
}
public void setZOrder( AbstractTabPaneComponent item, int order ){
CombinedStackDockComponent.this.setZOrder( item.getComponent(), order );
}
public int getZOrder( AbstractTabPaneComponent item ){
return CombinedStackDockComponent.this.getZOrder( item.getComponent() );
}
};
/** keeps track of which tabs and menu items must be disabled */
private TabDisablingStrategyObserver tabDisabling = new TabDisablingStrategyObserver(){
@Override
public void setDisabled( Dockable dockable, boolean disabled ){
setEnabledAt( dockable, !disabled );
}
};
/**
* Constructs a new component.
*/
public CombinedStackDockComponent(){
background = createBackground( this );
panel = createContentPane( this );
panel.add( componentPanel );
panel.setBackground( background );
componentPanel.setBackground( background );
addTabPaneListener( new TabPaneListener(){
public void added( TabPane pane, Dockable dockable ){
// ignore
}
public void removed( TabPane pane, Dockable dockable ){
// ignore
}
public void selectionChanged( TabPane pane ){
for( StackDockComponentListener listener : listeners.toArray( new StackDockComponentListener[listeners.size()] ) ) {
listener.selectionChanged( CombinedStackDockComponent.this );
}
}
public void infoComponentChanged( TabPane pane, LonelyTabPaneComponent oldInfo, LonelyTabPaneComponent newInfo ){
// ignore
}
public void controllerChanged( TabPane pane, DockController controller ){
// ignore
}
} );
}
/**
* Creates the content pane for <code>this</code> component. This method
* may be called by the constructor.
* @param self <code>this</code>
* @return a new panel, not <code>null</code>
*/
protected CombinedStackDockContentPane createContentPane( CombinedStackDockComponent<T, M, I> self ){
return new CombinedStackDockContentPane( self );
}
/**
* Creates the background algorithm that is used for <code>this</code> component. This method
* may be called by the constructor.
* @param self <code>this</code>
* @return the new algorithm, not <code>null</code>
*/
protected BackgroundAlgorithm createBackground( CombinedStackDockComponent<T, M, I> self ){
return new Background();
}
@Override
public void revalidate(){
panel.revalidate();
repaint();
}
/**
* Repaints the contents of this component.
*/
public void repaint(){
panel.repaint();
}
public void addStackDockComponentListener( StackDockComponentListener listener ){
listeners.add( listener );
}
public void removeStackDockComponentListener( StackDockComponentListener listener ){
listeners.remove( listener );
}
/**
* Calls {@link StackDockComponentListener#tabChanged(StackDockComponent, Dockable)} on all listeners that
* are currently registered.
* @param dockable the element whose tab changed
*/
protected void fireTabChanged( Dockable dockable ){
for( StackDockComponentListener listener : listeners ){
listener.tabChanged( this, dockable );
}
}
public void setController( DockController controller ){
DockController old = getController();
if( old != controller ) {
List<T> tabs = getTabsList();
if( old != null ) {
for( T tab : tabs ) {
old.removeRepresentative( tab );
}
}
if( controller != null ) {
for( T tab : tabs ) {
controller.addRepresentative( tab );
}
}
for( Meta meta : components.values() ){
meta.setController( controller );
}
background.setController( controller );
tabDisabling.setController( controller );
tabConfiguration.setProperties( controller );
super.setController( controller );
}
}
/**
* Forwards <code>child</code> to the current {@link TabConfigurations}, in order to
* get the matching {@link TabConfiguration}.
* @param child some child of this {@link StackDockComponent}
* @return the {@link TabConfiguration} that should be used for its tab
*/
public TabConfiguration getConfiguration( Dockable child ){
return tabConfiguration.getValue().getConfiguration( child );
}
/**
* Gets a handler for tabs. This handler adds or
* removes {@link CombinedTab}s from this component in order to change
* their visibility.
* @return the handler
*/
public CombinedHandler<CombinedTab> getTabHandler(){
return tabHandler;
}
/**
* Gets a handler for menus. This handler adds or
* removes {@link CombinedMenu}s from this component in order to change
* their visibility.
* @return the handler
*/
public CombinedHandler<CombinedMenu> getMenuHandler(){
return menuHandler;
}
/**
* Gets a handler for info components. This handler adds or
* removes {@link AbstractTabPaneComponent}s from this component in order to change
* their visibility.
* @return the handler
*/
public CombinedHandler<AbstractTabPaneComponent> getInfoHandler(){
return infoHandler;
}
public Rectangle getAvailableArea(){
Insets insets = panel.getInsets();
if( insets == null ) {
insets = new Insets( 0, 0, 0, 0 );
}
else {
insets = new Insets( insets.top, insets.left, insets.bottom, insets.right );
}
return new Rectangle( insets.left, insets.top, Math.max( 1, panel.getWidth() - insets.left - insets.right ), Math.max( 1, panel.getHeight()
- insets.top - insets.bottom ) );
}
@Override
public Dimension getMinimumSize(){
Dimension result = super.getMinimumSize();
Insets insets = panel.getInsets();
if( insets != null ){
result = new Dimension(
result.width + insets.left + insets.right,
result.height + insets.top + insets.bottom );
}
return result;
}
@Override
public Dimension getPreferredSize(){
Dimension result = super.getPreferredSize();
Insets insets = panel.getInsets();
if( insets != null ){
result = new Dimension(
result.width + insets.left + insets.right,
result.height + insets.top + insets.bottom );
}
return result;
}
public Rectangle getSelectedBounds(){
return componentPanel.getBounds();
}
public void setSelectedBounds( Rectangle bounds ){
componentPanel.setBounds( bounds );
}
public int getSelectedIndex(){
return indexOf( getSelectedDockable() );
}
public void setSelectedIndex( int index ){
if( index < 0 || index >= getDockableCount() )
setSelectedDockable( null );
else
setSelectedDockable( getDockable( index ) );
}
@Override
public void setSelectedDockable( Dockable dockable ){
if( getSelectedDockable() != dockable ) {
super.setSelectedDockable( dockable );
for( Meta entry : components.values() ) {
entry.component.setVisible( entry.dockable == dockable );
}
}
}
public Rectangle getBoundsAt( int index ){
T tab = getTab( getDockable( index ) );
if( tab == null )
return null;
return tab.getBounds();
}
public int getIndexOfTabAt( Point mouseLocation ){
return getLayoutManager().getIndexOfTabAt( this, mouseLocation );
}
public int getTabCount(){
return getDockableCount();
}
public void addTab( String title, Icon icon, Component comp, Dockable dockable ){
insertTab( title, icon, comp, dockable, getTabCount() );
}
public void insertTab( String title, Icon icon, Component comp, Dockable dockable, int index ){
Component between = createLayerAt( comp, dockable );
Meta meta = new Meta( dockable, between, title, icon, null, !tabDisabling.isDisabled( dockable ) );
components.put( dockable, meta );
componentPanel.add( between );
insert( index, dockable );
meta.forward();
meta.component.setVisible( getSelectedDockable() == dockable );
tabDisabling.add( dockable );
}
public Dimension getMinimumSize( Dockable dockable ){
return components.get( dockable ).getComponent().getMinimumSize();
}
public Dimension getPreferredSize( Dockable dockable ){
return components.get( dockable ).getComponent().getPreferredSize();
}
public Dockable getDockableAt( int index ){
return getDockable( index );
}
public DockElementRepresentative getTabAt( int index ){
return getTab( getDockableAt( index ) );
}
public void moveTab( int source, int destination ){
if( source == destination ) {
return;
}
if( destination < 0 || destination >= getTabCount() ) {
throw new ArrayIndexOutOfBoundsException();
}
int selected = getSelectedIndex();
move( source, destination );
if( selected == source ) {
selected = destination;
}
else if( selected > source && selected <= destination ) {
selected++;
}
setSelectedIndex( selected );
}
@Override
public void remove( int index ){
Dockable dockable = getDockable( index );
tabDisabling.remove( dockable );
super.remove( index );
Meta meta = components.remove( dockable );
meta.setController( null );
componentPanel.remove( meta.component );
meta.component.setVisible( true );
}
@Override
public void removeAll(){
super.removeAll();
for( Meta meta : components.values() ) {
componentPanel.remove( meta.component );
meta.setController( null );
meta.component.setVisible( true );
tabDisabling.remove( meta.getDockable() );
}
components.clear();
}
@Override
public T putOnTab( Dockable dockable ){
T tab = super.putOnTab( dockable );
Meta meta = components.get( dockable );
tab.setIcon( meta.icon );
tab.setText( meta.text );
tab.setTooltip( meta.tooltip );
tab.setEnabled( meta.enabled );
return tab;
}
@Override
public T getOnTab( Dockable dockable ){
T tab = super.getOnTab( dockable );
Meta meta = components.get( dockable );
tab.setIcon( meta.icon );
tab.setText( meta.text );
tab.setTooltip( meta.tooltip );
tab.setEnabled( meta.enabled );
return tab;
}
@Override
protected T putTab( Dockable dockable, T tab ){
T result = super.putTab( dockable, tab );
fireTabChanged( dockable );
return result;
}
@Override
protected T removeTab( Dockable dockable ){
T result = super.removeTab( dockable );
if( result != null ){
fireTabChanged( dockable );
}
return result;
}
@Override
protected void clearTabs(){
super.clearTabs();
for( int i = 0, n = getDockableCount(); i<n; i++ ){
fireTabChanged( getDockable( i ) );
}
}
protected void addToMenu( M menu, Dockable dockable ){
int index = menu.getDockableCount();
menu.insert( index, dockable );
Meta meta = components.get( dockable );
menu.setIcon( index, meta.icon );
menu.setText( index, meta.text );
menu.setTooltip( index, meta.tooltip );
menu.setEnabled( index, meta.enabled );
}
protected void removeFromMenu( M menu, Dockable dockable ){
menu.remove( dockable );
}
/**
* Creates a layer between <code>component</code> and this panel. The
* object <code>component</code> is a representation of <code>dockable</code>
* but not necessarily <code>dockable</code> itself. The default
* behavior of this method is to return <code>component</code>.
* @param component the representation of <code>dockable</code>
* @param dockable the element for which a new layer is created
* @return the new layer which must be a parent of <code>component</code>
* or <code>component</code>
*/
protected Component createLayerAt( Component component, Dockable dockable ){
return component;
}
/**
* Gets the index'th {@link Component} on this tab. This <code>Component</code>
* is not a {@link Dockable} but a layer between dockable and this panel.
* @param index the index of a tab.
* @return the layer between tab and dockable
*/
public Component getLayerAt( int index ){
return getContentAt( index ).component;
}
/**
* Gets the meta information about the components at location <code>index</code>
* @param index the index of a tab
* @return information about that tab
*/
protected Meta getContentAt( int index ){
return components.get( getDockable( index ) );
}
public void setTitleAt( int index, String newTitle ){
Meta meta = components.get( getDockable( index ) );
if( newTitle == null ) {
meta.text = "";
}
else {
meta.text = newTitle;
}
meta.forward();
}
public void setTooltipAt( int index, String newTooltip ){
Meta meta = components.get( getDockable( index ) );
meta.tooltip = newTooltip;
meta.forward();
}
public void setIconAt( int index, Icon newIcon ){
Meta meta = components.get( getDockable( index ) );
meta.icon = newIcon;
meta.forward();
}
public void setComponentAt( int index, Component component ){
Meta meta = components.get( getDockable( index ) );
componentPanel.remove( meta.component );
meta.component = createLayerAt( component, meta.dockable );
componentPanel.add( meta.component );
meta.component.setVisible( getSelectedDockable() == meta.dockable );
revalidate();
}
/**
* Changes the enabled state of the item <code>dockable</code>.
* @param dockable the tab whose state is to be changed
* @param enabled the new enabled state
*/
protected void setEnabledAt( Dockable dockable, boolean enabled ){
Meta meta = components.get( dockable );
if( meta != null && meta.enabled != enabled ){
meta.enabled = enabled;
meta.forward();
}
}
public JComponent getComponent(){
return panel;
}
/**
* Sets the z order of <code>component</code>, as higher the z order
* as later the component is painted, as more components it can overlap.
* @param component some child of this pane
* @param order the order
*/
protected void setZOrder( Component component, int order ){
panel.setComponentZOrder( component, order );
}
/**
* Gets the z order of <code>component</code>.
* @param component some child of this pane
* @return the order
*/
protected int getZOrder( Component component ){
return panel.getComponentZOrder( component );
}
public DockElementRepresentative createDefaultRepresentation( final DockElement target ){
return new SimpleDockElementRepresentative( target, panel );
}
/**
* Creates a {@link BorderForwarder} for the content component for the tab of <code>dockable</code>
* @param dockable the item that is shown
* @param component the component which is influenced
* @return the forwarder or null
*/
protected BorderForwarder createContentBorderModifier( Dockable dockable, JComponent component ){
return null;
}
/**
* Meta information about a {@link Dockable} that is shown on this
* {@link CombinedStackDockComponent}.
* @author Benjamin Sigg
*/
protected class Meta {
/** the element that is shown */
private Dockable dockable;
/** visual representation of {@link #dockable} */
private Component component;
/** text to be displayed on the tab of {@link #dockable} */
private String text;
/** icon to be displayed on the tab of {@link #dockable} */
private Icon icon;
/** tooltip to be displayed on the tab of {@link #dockable} */
private String tooltip;
/** sets the border of {@link #component} */
private BorderForwarder border;
/** whether this tab is enabled */
private boolean enabled = true;
/**
* Creates new meta information.
* @param dockable the element for which the meta information is required
* @param component graphical representation of <code>dockable</code>
* @param text text to be shown in the tab
* @param icon icon to be shown in the tab
* @param tooltip tooltip to be shown on the tab
* @param enabled whether this tab is enabled or not
*/
public Meta( Dockable dockable, Component component, String text, Icon icon, String tooltip, boolean enabled ){
this.dockable = dockable;
this.component = component;
this.text = text;
this.icon = icon;
this.tooltip = tooltip;
this.enabled = enabled;
if( component instanceof JComponent ) {
border = createContentBorderModifier( dockable, (JComponent) component );
}
setController( getController() );
}
/**
* Sets the controller which is used to read values.
* @param controller the new controller
*/
@FrameworkOnly
public void setController( DockController controller ){
if( border != null ){
border.setController( controller );
}
}
/**
* Gets the element which is represented by this object
* @return the dockable
*/
public Dockable getDockable(){
return dockable;
}
/**
* Gets the component that is shown.
* @return the component to show
*/
public Component getComponent(){
return component;
}
/**
* Sets the border of the component. This method assumes that
* {@link CombinedStackDockComponent#createContentBorderModifier(Dockable, JComponent)} returns
* a non-<code>null</code> value.
* @param border the new border, can be <code>null</code>
*/
public void setBorder( Border border ){
if( this.border == null ) {
throw new IllegalStateException( "there was no border-modifier created" );
}
this.border.setBorder( border );
}
/**
* Searches {@link CombinedTab} or {@link CombinedMenu} of {@link #dockable}
* and updates {@link #text}, {@link #icon} and {@link #tooltip}.
*/
public void forward(){
CombinedTab tab = getTab( dockable );
if( tab != null ) {
tab.setIcon( icon );
tab.setText( text );
tab.setTooltip( tooltip );
tab.setEnabled( enabled );
}
CombinedMenu menu = getMenu( dockable );
if( menu != null ) {
Dockable[] dockables = menu.getDockables();
for( int i = 0; i < dockables.length; i++ ) {
if( dockables[i] == dockable ) {
menu.setIcon( i, icon );
menu.setText( i, text );
menu.setTooltip( i, tooltip );
menu.setEnabled( i, enabled );
break;
}
}
}
}
}
/**
* Represents the background of this component.
* @author Benjamin Sigg
*/
private class Background extends BackgroundAlgorithm implements TabPaneBackgroundComponent {
/**
* Creates a new object
*/
public Background(){
super( TabPaneBackgroundComponent.KIND, ThemeManager.BACKGROUND_PAINT + ".tabPane" );
}
public TabPane getPane(){
return CombinedStackDockComponent.this;
}
public Component getComponent(){
return panel;
}
}
}