/*
* 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) 2008 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;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import javax.swing.event.MouseInputListener;
import bibliothek.gui.DockController;
import bibliothek.gui.DockStation;
import bibliothek.gui.DockTheme;
import bibliothek.gui.DockUI;
import bibliothek.gui.Dockable;
import bibliothek.gui.dock.component.DefaultDockStationComponentRootHandler;
import bibliothek.gui.dock.component.DockComponentRootHandler;
import bibliothek.gui.dock.control.focus.DefaultFocusRequest;
import bibliothek.gui.dock.control.focus.FocusController;
import bibliothek.gui.dock.displayer.DisplayerCombinerTarget;
import bibliothek.gui.dock.displayer.DockableDisplayerHints;
import bibliothek.gui.dock.event.DockStationAdapter;
import bibliothek.gui.dock.event.DockStationListener;
import bibliothek.gui.dock.event.DockableListener;
import bibliothek.gui.dock.event.FocusVetoListener;
import bibliothek.gui.dock.layout.DockableProperty;
import bibliothek.gui.dock.layout.location.AsideRequest;
import bibliothek.gui.dock.security.SecureContainer;
import bibliothek.gui.dock.station.AbstractDockableStation;
import bibliothek.gui.dock.station.DisplayerCollection;
import bibliothek.gui.dock.station.DisplayerFactory;
import bibliothek.gui.dock.station.DockableDisplayer;
import bibliothek.gui.dock.station.DockableDisplayerListener;
import bibliothek.gui.dock.station.PlaceholderMapping;
import bibliothek.gui.dock.station.StationBackgroundComponent;
import bibliothek.gui.dock.station.StationChildHandle;
import bibliothek.gui.dock.station.StationDragOperation;
import bibliothek.gui.dock.station.StationDropItem;
import bibliothek.gui.dock.station.StationDropOperation;
import bibliothek.gui.dock.station.StationPaint;
import bibliothek.gui.dock.station.layer.DefaultDropLayer;
import bibliothek.gui.dock.station.layer.DockStationDropLayer;
import bibliothek.gui.dock.station.stack.DefaultStackDockComponent;
import bibliothek.gui.dock.station.stack.DndAutoSelectStrategy;
import bibliothek.gui.dock.station.stack.StackDnDAutoSelectSupport;
import bibliothek.gui.dock.station.stack.StackDockComponent;
import bibliothek.gui.dock.station.stack.StackDockComponentFactory;
import bibliothek.gui.dock.station.stack.StackDockComponentListener;
import bibliothek.gui.dock.station.stack.StackDockComponentParent;
import bibliothek.gui.dock.station.stack.StackDockComponentRepresentative;
import bibliothek.gui.dock.station.stack.StackDockProperty;
import bibliothek.gui.dock.station.stack.StackDockStationFactory;
import bibliothek.gui.dock.station.stack.TabContent;
import bibliothek.gui.dock.station.stack.TabContentFilterListener;
import bibliothek.gui.dock.station.stack.TabDropLayer;
import bibliothek.gui.dock.station.stack.tab.TabConfigurations;
import bibliothek.gui.dock.station.stack.tab.TabContentFilter;
import bibliothek.gui.dock.station.stack.tab.layouting.TabPlacement;
import bibliothek.gui.dock.station.support.CombinerTarget;
import bibliothek.gui.dock.station.support.ComponentDragOperation;
import bibliothek.gui.dock.station.support.ConvertedPlaceholderListItem;
import bibliothek.gui.dock.station.support.DockablePlaceholderList;
import bibliothek.gui.dock.station.support.DockableShowingManager;
import bibliothek.gui.dock.station.support.PlaceholderListMapping;
import bibliothek.gui.dock.station.support.PlaceholderList.Filter;
import bibliothek.gui.dock.station.support.PlaceholderList.Level;
import bibliothek.gui.dock.station.support.PlaceholderListItemAdapter;
import bibliothek.gui.dock.station.support.PlaceholderListItemConverter;
import bibliothek.gui.dock.station.support.PlaceholderMap;
import bibliothek.gui.dock.station.support.PlaceholderStrategy;
import bibliothek.gui.dock.themes.DefaultDisplayerFactoryValue;
import bibliothek.gui.dock.themes.DefaultStationPaintValue;
import bibliothek.gui.dock.themes.StationThemeItemValue;
import bibliothek.gui.dock.themes.ThemeManager;
import bibliothek.gui.dock.title.ControllerTitleFactory;
import bibliothek.gui.dock.title.DockTitle;
import bibliothek.gui.dock.title.DockTitleFactory;
import bibliothek.gui.dock.title.DockTitleVersion;
import bibliothek.gui.dock.util.BackgroundAlgorithm;
import bibliothek.gui.dock.util.BackgroundPanel;
import bibliothek.gui.dock.util.ConfiguredBackgroundPanel;
import bibliothek.gui.dock.util.DockUtilities;
import bibliothek.gui.dock.util.PropertyKey;
import bibliothek.gui.dock.util.PropertyValue;
import bibliothek.gui.dock.util.Transparency;
import bibliothek.gui.dock.util.extension.Extension;
import bibliothek.gui.dock.util.property.ConstantPropertyFactory;
import bibliothek.util.FrameworkOnly;
import bibliothek.util.Path;
/**
* On this station, only one of many children is visible. The other children
* are hidden behind the visible child. There are some buttons where the
* user can choose which child is visible. This station behaves like
* a {@link JTabbedPane}.<br>
* This station tries to register a {@link DockTitleFactory} to its
* {@link DockController} with the key {@link #TITLE_ID}.
* @author Benjamin Sigg
*/
public class StackDockStation extends AbstractDockableStation implements StackDockComponentParent{
/** The id of the titlefactory which is used by this station */
public static final String TITLE_ID = "stack";
/** This id is forwarded to {@link Extension}s which load additional {@link DisplayerFactory}s */
public static final String DISPLAYER_ID = "stack";
/** Key used to read the current {@link StackDockComponentFactory} */
public static final PropertyKey<StackDockComponentFactory> COMPONENT_FACTORY =
new PropertyKey<StackDockComponentFactory>( "stack dock component factory" );
/** Key for setting the side at which the tabs appear in relation to the selected dockable */
public static final PropertyKey<TabPlacement> TAB_PLACEMENT =
new PropertyKey<TabPlacement>( "stack dock station tab side",
new ConstantPropertyFactory<TabPlacement>( TabPlacement.TOP_OF_DOCKABLE ), true );
/** Key for setting the {@link TabContentFilter} */
public static final PropertyKey<TabContentFilter> TAB_CONTENT_FILTER =
new PropertyKey<TabContentFilter>( "stack dock tab content filter" );
/**
* If set to <code>true</code>, then dropping a {@link Dockable} onto a {@link StackDockStation} won't
* change the currently selected item.<br>
* This property does not prevent the {@link FocusController} from changing the focus, and updating the focus
* may lead to changing the selected index anyway. Clients using this property might also need to
* implement a {@link FocusVetoListener} and add it to the {@link FocusController}.
*/
public static final PropertyKey<Boolean> IMMUTABLE_SELECTION_INDEX =
new PropertyKey<Boolean>( "stack dock immutable selection index",
new ConstantPropertyFactory<Boolean>( false ), true );
/**
* Allows fine tuning of the look and behavior of tabs. The default settings however should suffice
* for most clients.
*/
public static final PropertyKey<TabConfigurations> TAB_CONFIGURATIONS =
new PropertyKey<TabConfigurations>( "stack dock tab configurations",
new ConstantPropertyFactory<TabConfigurations>( TabConfigurations.DEFAULT ), true );
/**
* Generic interface that listens to the drag and drop events, in order to auto select tabs.<br>
* This property is not intended to be set by clients, clients should set
* {@link #DND_AUTO_SELECT_STRATEGY} instead.
*/
@FrameworkOnly
public static final PropertyKey<StackDnDAutoSelectSupport> DND_AUTO_SELECT_SUPPORT =
new PropertyKey<StackDnDAutoSelectSupport>( "stack dock auto select" );
/**
* Decides what happens if the use drags some data over a tab (the default behavior is
* defined by {@link DndAutoSelectStrategy#DEFAULT}).
*/
public static final PropertyKey<DndAutoSelectStrategy> DND_AUTO_SELECT_STRATEGY =
new PropertyKey<DndAutoSelectStrategy>( "stack dock auto select strategy",
new ConstantPropertyFactory<DndAutoSelectStrategy>( DndAutoSelectStrategy.DEFAULT ), true );
/** A list of all children */
private DockablePlaceholderList<StationChildHandle> dockables = new DockablePlaceholderList<StationChildHandle>();
/**
* A list of {@link MouseInputListener MouseInputListeners} which are
* registered at this station.
*/
private List<MouseInputListener> mouseInputListeners = new ArrayList<MouseInputListener>();
/** A manager for firing events if a child changes its visibility-state */
private DockableShowingManager visibility;
/** A paint to draw lines */
private DefaultStationPaintValue paint;
/** A factory to create {@link DockableDisplayer} */
private DefaultDisplayerFactoryValue displayerFactory;
/** The set of displayers shown on this station */
private DisplayerCollection displayers;
/** The preferred location where a dropping {@link Dockable} should be added */
private Insert insert;
/** Information about the dockable that is going to be removed */
private ComponentDragOperation dragOperation;
/** The graphical representation of this station */
private Background background;
/** The panel where components are added */
private JComponent panel;
/** The background of the {@link #panel} */
private PanelBackground panelBackground = new PanelBackground();
/** A Component which shows two or more children of this station */
private StackDockComponent stackComponent;
/** Responsible for updating a {@link DockElementRepresentative} that covers the empty areas of {@link #stackComponent} */
private StackDockComponentRepresentative stackComponentRepresentative;
/** The current component factory */
private PropertyValue<StackDockComponentFactory> stackComponentFactory;
/** Where to put tabs */
private PropertyValue<TabPlacement> tabPlacement;
/** The version of titles which should be used for this station */
private DockTitleVersion title;
/** A listener observing the children for changes of their icon or titletext */
private Listener listener = new Listener();
/** whether the result of {@link Component#getMinimumSize()} should be a small value */
private boolean smallMinimumSize = true;
/** whether the theme of this station is currently updated */
private boolean updatingTheme = false;
/** strategy for selecting placeholders */
private PropertyValue<PlaceholderStrategy> placeholderStrategy = new PropertyValue<PlaceholderStrategy>( PlaceholderStrategy.PLACEHOLDER_STRATEGY ) {
@Override
protected void valueChanged( PlaceholderStrategy oldValue, PlaceholderStrategy newValue ){
dockables.setStrategy( newValue );
}
};
/** filter for setting appearance of tabs */
private PropertyValue<TabContentFilter> tabContentFilter = new PropertyValue<TabContentFilter>( TAB_CONTENT_FILTER ) {
@Override
protected void valueChanged( TabContentFilter oldValue, TabContentFilter newValue ){
if( oldValue != newValue ){
if( oldValue != null ){
oldValue.removeListener( tabContentFilterListener );
oldValue.uninstall( StackDockStation.this );
}
if( newValue != null ){
newValue.install( StackDockStation.this );
newValue.addListener( tabContentFilterListener );
}
tabContentFilterListener.contentChanged();
}
}
};
/** a listener to {@link #tabContentFilter} */
private TabContentFilterListener tabContentFilterListener = new TabContentFilterListener() {
public void contentChanged(){
int count = getDockableCount();
for( int i = 0; i < count; i++ ){
updateContent( i );
}
}
public void contentChanged( StackDockComponent component ){
// ignore
}
public void contentChanged( StackDockStation station ){
if( StackDockStation.this == station ){
contentChanged();
}
}
public void contentChanged( Dockable dockable ){
if( dockable.getDockParent() == StackDockStation.this ){
int index = indexOf( dockable );
if( index >= 0 ){
updateContent( index );
}
}
}
};
/** the current {@link StackDnDAutoSelectSupport} */
private PropertyValue<StackDnDAutoSelectSupport> autoSelectSupport = new PropertyValue<StackDnDAutoSelectSupport>( DND_AUTO_SELECT_SUPPORT ){
@Override
protected void valueChanged( StackDnDAutoSelectSupport oldValue, StackDnDAutoSelectSupport newValue ){
StackDockComponent component = getStackComponent();
if( component != null ){
if( oldValue != null ){
oldValue.uninstall( component );
}
if( newValue != null ){
newValue.install( StackDockStation.this, component );
}
}
}
};
/**
* A listener added to the parent of this station. The listener ensures
* that the visibility-state is always correct.
*/
private VisibleListener visibleListener;
/**
* The dockable which was or is currently selected.
*/
private Dockable lastSelectedDockable = null;
/**
* Constructs a new StackDockStation
*/
public StackDockStation(){
this( null );
}
/**
* Constructs a new station and sets the theme.
* @param theme the theme of the station, may be <code>null</code>
*/
public StackDockStation( DockTheme theme ){
super( theme );
init();
}
/**
* Creates a new station.
* @param theme the theme of this station, can be <code>null</code>
* @param init <code>true</code> if the fields of this object should
* be initialized, <code>false</code> otherwise. If <code>false</code>,
* then the subclass has to call {@link #init()} exactly once.
*/
protected StackDockStation( DockTheme theme, boolean init ){
super( theme );
if( init )
init();
}
/**
* Initializes the fields of this object, has to be called exactly once.
*/
protected void init(){
paint = new DefaultStationPaintValue( ThemeManager.STATION_PAINT + ".stack", this );
displayerFactory = new DefaultDisplayerFactoryValue( ThemeManager.DISPLAYER_FACTORY + ".stack", this );
visibleListener = new VisibleListener();
visibility = new DockableShowingManager( listeners );
displayers = new DisplayerCollection( this, displayerFactory, DISPLAYER_ID );
displayers.addDockableDisplayerListener( new DockableDisplayerListener(){
public void discard( DockableDisplayer displayer ){
StackDockStation.this.discard( displayer );
}
public void moveableElementChanged( DockableDisplayer displayer ){
// ignore
}
});
background = createBackground();
background.setController( getController() );
panel = background.getContentPane();
stackComponentFactory = new PropertyValue<StackDockComponentFactory>( COMPONENT_FACTORY ){
@Override
protected void valueChanged( StackDockComponentFactory oldValue, StackDockComponentFactory newValue ) {
if( newValue == null )
setStackComponent( createStackDockComponent() );
else
setStackComponent( newValue.create( StackDockStation.this ) );
}
};
tabPlacement = new PropertyValue<TabPlacement>( TAB_PLACEMENT ){
@Override
protected void valueChanged( TabPlacement oldValue, TabPlacement newValue ){
if( stackComponent != null ){
stackComponent.setDockTabPlacement( newValue );
}
}
};
stackComponent = createStackDockComponent();
stackComponent.addStackDockComponentListener( visibleListener );
stackComponentRepresentative = new StackDockComponentRepresentative();
stackComponentRepresentative.setComponent( stackComponent );
stackComponentRepresentative.setTarget( this );
addDockStationListener( new DockStationAdapter() {
@Override
public void dockableSelected( DockStation station, Dockable oldSelection, Dockable newSelection ){
lastSelectedDockable = newSelection;
}
});
panel.addHierarchyListener( new HierarchyListener(){
public void hierarchyChanged( HierarchyEvent e ){
if( (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 ){
if( getDockParent() == null ){
getDockableStateListeners().checkShowing();
}
visibility.fire();
}
}
});
}
/**
* Creates the panel onto which this station will lay its children.
* @return the new background
*/
protected Background createBackground(){
return new Background();
}
/**
* Creates the {@link StackDockComponent} which will be shown on
* this station if the station has more then one child.<br>
* This method is called directly by the constructor.
* @return the new component
*/
protected StackDockComponent createStackDockComponent(){
return new DefaultStackDockComponent();
}
protected DockComponentRootHandler createRootHandler() {
return new DefaultDockStationComponentRootHandler( this, displayers );
}
public DockStation getStackDockParent(){
return this;
}
/**
* Tells this station where to put the tabs.
* @param placement the side or <code>null</code> to use the default value
*/
public void setTabPlacement( TabPlacement placement ){
tabPlacement.setValue( placement );
}
/**
* Gets the location where tabs are currently placed.
* @return the side at which tabs are
*/
public TabPlacement getTabPlacement(){
return tabPlacement.getValue();
}
/**
* Sets the filter that tells this station how to set the content of the tabs.
* @param filter the filter, can be <code>null</code>
*/
public void setTabContentFilter( TabContentFilter filter ){
tabContentFilter.setValue( filter );
}
/**
* Gets the filter that tells this station how to set the content of the tabs.
* @return the filter, may be <code>null</code>
*/
public TabContentFilter getTabContentFilter(){
return tabContentFilter.getValue();
}
/**
* Tells whether this station should show its {@link StackDockComponent} even if it
* has only one child. This property may only be changed if the {@link StackDockComponent}
* is exchanged as well.
* @return <code>true</code> if the {@link StackDockComponent} is to be always shown
*/
protected boolean singleTabStackDockComponent(){
StackDockComponent component = getStackComponent();
if( component == null ){
return false;
}
return component.isSingleTabComponent();
}
/**
* Sets the {@link StackDockComponent} which should be used by this
* station. The component is shown when this station has more then
* one child. Note that the <code>stackComponent</code> depends also
* on the property {@link #COMPONENT_FACTORY}, and will be automatically
* exchanged if that property changes. Clients should use
* {@link #setStackComponentFactory(StackDockComponentFactory)} if they
* want to exchange the component permanently.
* @param stackComponent the new component
* @throws IllegalArgumentException if <code>stackComponent</code> is <code>null</code>
*/
public void setStackComponent( StackDockComponent stackComponent ) {
if( stackComponent == null )
throw new IllegalArgumentException( "Component must not be null" );
if( stackComponent != this.stackComponent ){
int selected = -1;
StackDnDAutoSelectSupport autoSelectSupport = this.autoSelectSupport.getValue();
if( this.stackComponent != null ){
this.stackComponent.setController( null );
Component component = this.stackComponent.getComponent();
for( MouseInputListener listener : mouseInputListeners ){
component.removeMouseListener( listener );
component.removeMouseMotionListener( listener );
}
selected = this.stackComponent.getSelectedIndex();
this.stackComponent.removeStackDockComponentListener( visibleListener );
this.stackComponent.removeAll();
if( autoSelectSupport != null ){
autoSelectSupport.uninstall( this.stackComponent );
}
}
this.stackComponent = stackComponent;
stackComponent.setDockTabPlacement( tabPlacement.getValue() );
stackComponentRepresentative.setComponent( stackComponent );
if( getDockableCount() < 2 && !singleTabStackDockComponent() ){
stackComponent.addStackDockComponentListener( visibleListener );
}
else{
panel.removeAll();
for( StationChildHandle handle : dockables.dockables() ){
DockableDisplayer displayer = handle.getDisplayer();
int index = stackComponent.getTabCount();
insertTab( displayer, index );
}
panel.add( stackComponent.getComponent() );
if( selected >= 0 && selected < stackComponent.getTabCount() )
stackComponent.setSelectedIndex( selected );
stackComponent.addStackDockComponentListener( visibleListener );
}
Component component = stackComponent.getComponent();
stackComponent.setController( getController() );
for( MouseInputListener listener : mouseInputListeners ){
component.addMouseListener( listener );
component.addMouseMotionListener( listener );
}
if( autoSelectSupport != null ){
autoSelectSupport.install( this, this.stackComponent );
}
updateConfigurableDisplayerHints();
}
}
/**
* Gets the currently used {@link StackDockComponent}
* @return the component
* @see #setStackComponent(StackDockComponent)
*/
public StackDockComponent getStackComponent() {
return stackComponent;
}
/**
* Sets the factory which will be used to create a {@link StackDockComponent}
* for this station.
* @param factory the new factory, can be <code>null</code> if the default-factory
* should be used
*/
public void setStackComponentFactory( StackDockComponentFactory factory ){
stackComponentFactory.setValue( factory );
}
/**
* Gets the factory which is used to create a {@link StackDockComponent}.
* This method returns <code>null</code> if no factory was set through
* {@link #setStackComponentFactory(StackDockComponentFactory)}.
* @return the factory or <code>null</code>
*/
public StackDockComponentFactory getStackComponentFactory(){
return stackComponentFactory.getOwnValue();
}
@Override
protected void callDockUiUpdateTheme() throws IOException {
try{
updatingTheme = true;
DockUI.updateTheme( this, new StackDockStationFactory());
}
finally{
updatingTheme = false;
}
}
@Override
public void setDockParent( DockStation station ) {
DockStation old = getDockParent();
if( old != null )
old.removeDockStationListener( visibleListener );
super.setDockParent(station);
if( station != null )
station.addDockStationListener( visibleListener );
visibility.fire();
}
@Override
public void setController( DockController controller ) {
if( this.getController() != controller ){
for( StationChildHandle handle : dockables.dockables() ){
handle.setTitleRequest( null );
}
boolean wasNull = getController() == null;
background.setController( controller );
stackComponentFactory.setProperties( controller );
super.setController(controller);
stackComponent.setController( controller );
tabPlacement.setProperties( controller );
placeholderStrategy.setProperties( controller );
tabContentFilter.setProperties( controller );
stackComponentRepresentative.setController( controller );
paint.setController( controller );
displayerFactory.setController( controller );
panelBackground.setController( controller );
autoSelectSupport.setProperties( controller );
if( controller != null ){
title = controller.getDockTitleManager().getVersion( TITLE_ID, ControllerTitleFactory.INSTANCE );
}
else{
title = null;
}
displayers.setController( controller );
boolean isNull = controller == null;
if( wasNull != isNull ){
if( wasNull ){
dockables.bind();
}
else{
dockables.unbind();
}
}
for( StationChildHandle handle : dockables.dockables() ){
handle.setTitleRequest( title, true );
}
visibility.fire();
}
}
/**
* Gets a {@link StationPaint} which is used to paint some lines onto
* this station. Use a {@link StationThemeItemValue#setDelegate(Object) delegate}
* to exchange the paint.
* @return the paint
*/
public DefaultStationPaintValue getPaint() {
return paint;
}
/**
* Gets a {@link DisplayerFactory} which is used to create new
* {@link DockableDisplayer} for this station. Use a
* {@link StationThemeItemValue#setDelegate(Object) delegate}
* to exchange the factory.
* @return the factory
*/
public DefaultDisplayerFactoryValue getDisplayerFactory() {
return displayerFactory;
}
/**
* Gets the set of {@link DockableDisplayer displayers} used on this station.
* @return the set of displayers
*/
public DisplayerCollection getDisplayers() {
return displayers;
}
@Override
public boolean isStationVisible() {
DockStation parent = getDockParent();
if( parent != null )
return parent.isChildShowing( this );
else
return panel.isDisplayable();
}
@Override
public boolean isVisible( Dockable dockable ) {
return isStationVisible() && (dockables.dockables().size() == 1 || indexOf( dockable ) == stackComponent.getSelectedIndex() );
}
public int getDockableCount() {
return dockables.dockables().size();
}
public Dockable getDockable( int index ) {
return dockables.dockables().get( index ).getDockable();
}
public DockableProperty getDockableProperty( Dockable dockable, Dockable target ) {
int index = indexOf( dockable );
PlaceholderStrategy strategy = getPlaceholderStrategy();
Path placeholder = null;
if( strategy != null ){
placeholder = strategy.getPlaceholderFor( target == null ? dockable : target );
if( placeholder != null ){
dockables.dockables().addPlaceholder( index, placeholder );
}
}
return new StackDockProperty( index, placeholder );
}
public void aside( AsideRequest request ){
DockableProperty location = request.getLocation();
int index;
Path newPlaceholder = request.getPlaceholder();
if( location instanceof StackDockProperty ){
StackDockProperty stackLocation = (StackDockProperty)location;
index = dockables.getNextListIndex( stackLocation.getIndex(), stackLocation.getPlaceholder() );
if( newPlaceholder != null ){
dockables.list().insertPlaceholder( index, newPlaceholder );
}
}
else {
index = dockables.dockables().size();
if( newPlaceholder != null ){
dockables.dockables().insertPlaceholder( index, newPlaceholder );
}
}
request.answer( new StackDockProperty( index, newPlaceholder ));
}
public Dockable getFrontDockable() {
if( dockables.dockables().size() == 0 )
return null;
if( dockables.dockables().size() == 1 )
return dockables.dockables().get( 0 ).getDockable();
int index = stackComponent.getSelectedIndex();
if( index >= 0 )
return dockables.dockables().get( index ).getDockable();
return null;
}
public void setFrontDockable( Dockable dockable ) {
if( dockables.dockables().size() > 1 && dockable != null )
stackComponent.setSelectedIndex( indexOf( dockable ));
fireDockableSelected();
}
/**
* Informs all {@link DockStationListener}s that the selected element of this station changed.
* This method only fires if there really is a change, hence it can be safely called multiple times.
*/
protected void fireDockableSelected(){
Dockable selection = getFrontDockable();
if( lastSelectedDockable != selection ){
listeners.fireDockableSelected( lastSelectedDockable, selection );
}
}
/**
* Gets the index of a child.
* @param dockable the child which is searched
* @return the index of <code>dockable</code> or -1 if it was not found
*/
public int indexOf( Dockable dockable ){
int index = 0;
for( StationChildHandle check : dockables.dockables() ){
if( check.getDockable() == dockable ){
return index;
}
index++;
}
return -1;
}
public PlaceholderMap getPlaceholders(){
return dockables.toMap();
}
public PlaceholderMapping getPlaceholderMapping() {
return new PlaceholderListMapping( this, dockables ){
public DockableProperty getLocationAt( Path placeholder ) {
int index = dockables.getDockableIndex( placeholder );
return new StackDockProperty( index, placeholder );
}
};
}
public void setPlaceholders( PlaceholderMap placeholders ){
if( getDockableCount() > 0 ){
throw new IllegalStateException( "there are children on this station" );
}
try{
DockablePlaceholderList<StationChildHandle> next = new DockablePlaceholderList<StationChildHandle>( placeholders );
if( getController() != null ){
dockables.setStrategy( null );
dockables.unbind();
dockables = next;
dockables.bind();
dockables.setStrategy( getPlaceholderStrategy() );
}
else{
dockables = next;
}
}
catch( IllegalArgumentException ex ){
// ignore
}
}
/**
* Gets the placeholders of this station using a {@link PlaceholderListItemConverter} to
* encode the children of this station. To be exact, the converter puts the following
* parameters for each {@link Dockable} into the map:
* <ul>
* <li>id: the integer from <code>children</code></li>
* <li>index: the location of the element in the dockables-list</li>
* <li>placeholder: the placeholder of the element, might not be written</li>
* </ul>
* @param children a unique identifier for each child of this station
* @return the map
*/
public PlaceholderMap getPlaceholders( final Map<Dockable, Integer> children ){
dockables.insertAllPlaceholders();
final PlaceholderStrategy strategy = getPlaceholderStrategy();
return dockables.toMap( new PlaceholderListItemAdapter<Dockable, StationChildHandle>() {
@Override
public ConvertedPlaceholderListItem convert( int index, StationChildHandle dockable ){
ConvertedPlaceholderListItem item = new ConvertedPlaceholderListItem();
Integer id = children.get( dockable.getDockable() );
if( id == null ){
return null;
}
item.putInt( "id", id );
item.putInt( "index", index );
if( strategy != null ){
Path placeholder = strategy.getPlaceholderFor( dockable.getDockable() );
if( placeholder != null ){
item.putString( "placeholder", placeholder.toString() );
item.setPlaceholder( placeholder );
}
}
return item;
}
public StationChildHandle convert( ConvertedPlaceholderListItem item ){
// ignore
return null;
}
});
}
/**
* Sets all placeholders and children of this station.
* @param placeholders the new children and placeholders
* @param children map to convert items to {@link Dockable}s
* @throws IllegalStateException if there are still children on this station
*/
public void setPlaceholders( PlaceholderMap placeholders, final Map<Integer, Dockable> children ){
DockUtilities.checkLayoutLocked();
if( getDockableCount() > 0 ){
throw new IllegalStateException( "there are children on this station" );
}
DockController controller = getController();
try{
if( controller != null ){
controller.freezeLayout();
}
dockables.setStrategy( null );
dockables.unbind();
DockablePlaceholderList<StationChildHandle> next = new DockablePlaceholderList<StationChildHandle>();
dockables = next;
next.read( placeholders, new PlaceholderListItemAdapter<Dockable, StationChildHandle>() {
private DockHierarchyLock.Token token;
private int size = 0;
@Override
public StationChildHandle convert( ConvertedPlaceholderListItem item ){
int id = item.getInt( "id" );
Dockable dockable = children.get( id );
if( dockable == null ){
return null;
}
DockUtilities.ensureTreeValidity( StackDockStation.this, dockable );
token = DockHierarchyLock.acquireLinking( StackDockStation.this, dockable );
listeners.fireDockableAdding( dockable );
dockable.addDockableListener( listener );
StationChildHandle handle = new StationChildHandle( StackDockStation.this, getDisplayers(), dockable, title );
return handle;
}
@Override
public void added( StationChildHandle handle ){
try{
Dockable dockable = handle.getDockable();
dockable.setDockParent( StackDockStation.this );
handle.updateDisplayer();
addToPanel( handle, size, size );
size++;
listeners.fireDockableAdded( dockable );
}
finally{
token.release();
}
}
});
if( controller != null ){
dockables.bind();
dockables.setStrategy( getPlaceholderStrategy() );
}
}
finally{
if( controller != null ){
controller.meltLayout();
}
}
}
/**
* Gets the {@link PlaceholderStrategy} that is currently in use.
* @return the current strategy, may be <code>null</code>
*/
public PlaceholderStrategy getPlaceholderStrategy(){
return placeholderStrategy.getValue();
}
/**
* Sets the {@link PlaceholderStrategy} to use, <code>null</code> will set
* the default strategy.
* @param strategy the new strategy, can be <code>null</code>
*/
public void setPlaceholderStrategy( PlaceholderStrategy strategy ){
placeholderStrategy.setValue( strategy );
}
/**
* Sets whether the result of {@link Component#getMinimumSize()} should be small. A small value
* allows clients to make this {@link StackDockStation} small if it is on a {@link SplitDockStation} or another station
* that pays attention to the minimum size. This even works if the {@link Dockable}s do not have a small minimum size.
* @param smallMinimumSize whether the minimum size should be really small
*/
public void setSmallMinimumSize( boolean smallMinimumSize ){
this.smallMinimumSize = smallMinimumSize;
getComponent().invalidate();
}
/**
* Tells whether the result of {@link Component#getMinimumSize()} should be small.
* @return whether the minimum size is small
* @see #setSmallMinimumSize(boolean)
*/
public boolean isSmallMinimumSize(){
return smallMinimumSize;
}
@Override
public DockStationDropLayer[] getLayers(){
// do not support drag and drop if the component is invisible
Component component = getComponent();
if( component.getWidth() <= 0 || component.getHeight() <= 0 ){
return new DockStationDropLayer[]{};
}
return new DockStationDropLayer[]{
new DefaultDropLayer( this ),
new TabDropLayer( this )
};
}
/**
* Tells whether the point <code>x/y</code> on the screen is exactly over a tab.
* @param x the x-coordinate on the screen
* @param y the y-coordinate on the screen
* @return <code>true</code> if the point is directly over a tab
*/
public boolean isOverTabs( int x, int y ){
Point point = new Point( x, y );
SwingUtilities.convertPointFromScreen( point, panel );
return exactTabIndexAt( point.x, point.y ) != null;
}
/**
* Tells whether the point <code>x/y</code> on the screen is exactly over the only
* {@link DockTitle} currently shown by this station. This method always returns
* <code>false</code> if this station has not exactly one child.
* @param x the x-coordinate on the screen
* @param y the y-coordinate on the screen
* @return <code>true</code> if the point is directly over the title
*/
public boolean isOverTitle( int x, int y ){
if( dockables.dockables().size() == 1 ){
DockTitle title = dockables.dockables().get( 0 ).getDisplayer().getTitle();
if( title != null ){
Component component = title.getComponent();
Point p = new Point( x, y );
SwingUtilities.convertPointFromScreen( p, component );
return component.getBounds().contains( p );
}
}
return false;
}
public StationDropOperation prepareMove( StationDropItem item ) {
Point point = new Point( item.getMouseX(), item.getMouseY() );
SwingUtilities.convertPointFromScreen( point, panel );
Insert insert = tabIndexAt( point.x, point.y );
if( validate( insert, item.getDockable() )){
return new StackDropOperation( item.getDockable(), insert, true );
}
return null;
}
public StationDropOperation prepareDrop( StationDropItem item ){
if( item.getDockable().getDockParent() == this ){
return prepareMove( item );
}
if( SwingUtilities.isDescendingFrom( getComponent(), item.getDockable().getComponent() )){
return null;
}
Point point = new Point( item.getMouseX(), item.getMouseY() );
SwingUtilities.convertPointFromScreen( point, panel );
Insert insert = tabIndexAt( point.x, point.y );
if( validate( insert, item.getDockable() )){
return new StackDropOperation( item.getDockable(), insert, false );
}
return null;
}
public StationDragOperation prepareDrag( Dockable dockable ){
dragOperation = new ComponentDragOperation( dockable, getComponent() ){
@Override
protected void destroy(){
dragOperation = null;
}
};
return dragOperation;
}
public void drop( Dockable dockable ) {
drop( dockable, true );
}
/**
* Adds <code>dockable</code> to this station.
* @param dockable the element to drop
* @param autoPlaceholder whether the {@link PlaceholderStrategy} can be invoked to search for a matching placeholder
*/
public void drop( Dockable dockable, boolean autoPlaceholder ){
Path placeholder = null;
if( autoPlaceholder ){
PlaceholderStrategy strategy = getPlaceholderStrategy();
placeholder = strategy == null ? null : strategy.getPlaceholderFor( dockable );
}
boolean done = false;
if( placeholder != null ){
done = drop( dockable, new StackDockProperty( dockables.dockables().size(), placeholder ) );
}
if( !done ){
add( dockable, dockables.dockables().size(), null );
}
}
public boolean drop( Dockable dockable, DockableProperty property ) {
if( property instanceof StackDockProperty ){
return drop( dockable, (StackDockProperty)property );
}
else
return false;
}
/**
* Adds a new child to this station, and tries to match the <code>property</code>
* as good as possible.
* @param dockable the new child
* @param property the preferred location of the child
* @return <code>true</code> if the child could be added, <code>false</code>
* if the child couldn't be added
*/
public boolean drop( Dockable dockable, StackDockProperty property ){
DockUtilities.checkLayoutLocked();
int index = property.getIndex();
Path placeholder = property.getPlaceholder();
boolean acceptable = acceptable( dockable );
DockableProperty successor = property.getSuccessor();
boolean result = false;
if( placeholder != null && successor != null ){
StationChildHandle preset = dockables.getDockableAt( placeholder );
if( preset != null ){
DockStation station = preset.getDockable().asDockStation();
if( station != null ){
if( station.drop( dockable, successor )){
result = true;
dockables.removeAll( placeholder );
}
}
}
}
else if( placeholder != null ){
if( acceptable && dockables.hasPlaceholder( placeholder )){
add( dockable, 0, placeholder );
result = true;
}
}
if( !result && dockables.dockables().size() == 0 ){
if( acceptable ){
drop( dockable, false );
result = true;
}
}
if( !result ){
index = Math.min( index, dockables.dockables().size() );
if( index < dockables.dockables().size() && successor != null ){
DockStation station = dockables.dockables().get( index ).getDockable().asDockStation();
if( station != null ){
if( station.drop( dockable, successor )){
result = true;
}
}
}
if( !result && acceptable ){
add( dockable, index );
result = true;
}
}
return result;
}
/**
* Checks whether <code>child</code> can be inserted at <code>insert</code>.
* @param insert the new location
* @param child the element to insert
* @return <code>true</code> if the combination is valid
*/
private boolean validate( Insert insert, Dockable child ){
return insert != null && accept( child ) && child.accept( this ) && getController().getAcceptance().accept( this, child );
}
/**
* Gets the location where {@link StationDropOperation#execute()} will insert the next
* {@link Dockable}.
* @return the insertion location, can be <code>null</code>
*/
public Insert getInsert(){
return insert;
}
public void move( Dockable dockable, DockableProperty property ) {
if( property instanceof StackDockProperty ){
DockUtilities.checkLayoutLocked();
int index = indexOf( dockable );
if( index < 0 )
throw new IllegalArgumentException( "dockable not child of this station" );
int destination = ((StackDockProperty)property).getIndex();
destination = Math.min( destination, getDockableCount()-1 );
destination = Math.max( 0, destination );
move( index, destination );
}
}
private void move( int source, int destination ){
if( source != destination ){
DockUtilities.checkLayoutLocked();
dockables.dockables().move( source, destination );
stackComponent.moveTab( source, destination );
fireDockablesRepositioned( Math.min( source, destination ), Math.max( source, destination ) );
}
}
/**
* Tells which gap (between tabs) is chosen if the mouse has the coordinates x/y.
* If there is no tab at this location, a default-tab is chosen.
* @param x x-coordinate in the system of this station
* @param y y-coordinate in the system of this station
* @return the location of a tab
*/
protected Insert tabIndexAt( int x, int y ){
if( dockables.dockables().size() == 0 )
return new Insert( 0, false );
if( dockables.dockables().size() == 1 )
return new Insert( 1, false );
Insert insert = exactTabIndexAt( x, y );
if( insert == null )
insert = new Insert( dockables.dockables().size()-1, true );
return insert;
}
/**
* Gets the gap which is selected when the mouse is at x/y.
* @param x x-coordinate in the system of this station
* @param y y-coordinate in the system of this station
* @return the location of a tab or <code>null</code> if no tab is
* under x/y
*/
protected Insert exactTabIndexAt( int x, int y ){
Point point = SwingUtilities.convertPoint( panel, x, y, stackComponent.getComponent() );
int size = dockables.dockables().size();
if( size > 1 || singleTabStackDockComponent() ){
for( int i = 0; i<size; i++ ){
Rectangle bounds = stackComponent.getBoundsAt( i );
if( bounds != null && bounds.contains( point )){
if( tabPlacement.getValue().isHorizontal() )
return new Insert( i, bounds.x + bounds.width/2 < point.x );
else
return new Insert( i, bounds.y + bounds.height/2 < point.y );
}
}
}
return null;
}
public boolean canDrag( Dockable dockable ) {
return true;
}
public void drag( Dockable dockable ) {
if( dockable.getDockParent() != this )
throw new IllegalArgumentException( "The dockable can't be dragged, it is not child of this station" );
int index = indexOf( dockable );
if( index < 0 )
throw new IllegalArgumentException( "The dockable is not part of this station." );
remove( index );
}
public boolean canReplace( Dockable old, Dockable next ) {
return true;
}
public void replace( DockStation old, Dockable next ){
replace( old.asDockable(), next, true );
}
public void replace( Dockable old, Dockable next ) {
replace( old, next, false );
}
public void replace( Dockable old, Dockable next, boolean station ) {
DockUtilities.checkLayoutLocked();
DockController controller = getController();
try{
if( controller != null )
controller.freezeLayout();
int index = indexOf( old );
int listIndex = dockables.levelToBase( index, Level.DOCKABLE );
DockablePlaceholderList<StationChildHandle>.Item oldItem = dockables.list().get( listIndex );
remove( index );
add( next, index );
DockablePlaceholderList<StationChildHandle>.Item newItem = dockables.list().get( listIndex );
if( station ){
newItem.setPlaceholderMap( old.asDockStation().getPlaceholders() );
}
else{
newItem.setPlaceholderMap( oldItem.getPlaceholderMap() );
}
newItem.setPlaceholderSet( oldItem.getPlaceholderSet() );
}
finally{
if( controller != null )
controller.meltLayout();
}
}
/**
* Adds a child to this station at the location <code>index</code>.
* @param dockable the new child
* @param index the preferred location of the new child
*/
public void add( Dockable dockable, int index ){
add( dockable, index, null );
}
/**
* Adds a child to this station at the location <code>index</code>.
* @param dockable the new child
* @param index the preferred location of the new child
* @param placeholder the preferred location of the new child, can be <code>null</code>
*/
protected void add( Dockable dockable, int index, Path placeholder ){
DockUtilities.ensureTreeValidity( this, dockable );
DockUtilities.checkLayoutLocked();
DockHierarchyLock.Token token = DockHierarchyLock.acquireLinking( this, dockable );
try{
listeners.fireDockableAdding( dockable );
StationChildHandle handle = new StationChildHandle( this, getDisplayers(), dockable, title );
handle.updateDisplayer();
int inserted = -1;
if( placeholder != null && dockables.getDockableAt( placeholder ) == null ){
inserted = dockables.put( placeholder, handle );
}
else if( placeholder != null ){
index = dockables.getDockableIndex( placeholder );
}
if( inserted == -1 ){
dockables.dockables().add( index, handle );
}
else{
index = inserted;
}
addToPanel( handle, index, dockables.dockables().size()-1 );
dockable.setDockParent( this );
dockable.addDockableListener( listener );
listeners.fireDockableAdded( dockable );
fireDockablesRepositioned( index+1 );
fireDockableSelected();
}
finally{
token.release();
}
}
/**
* Adds the contents of <code>handle</code> to the {@link #stackComponent} of this station. The new
* <code>handle</code> may or may not already be stored in {@link #dockables}.
* @param handle the handle to add
* @param index the index where to add the handle
* @param size the current amount of children of the panel, must be either the size of {@link #dockables} if
* <code>handle</code> is not yet stored or the size of {@link #dockables}-1 is <code>handle</code> already is
* stored.
*/
protected void addToPanel( StationChildHandle handle, int index, int size ){
if( size == 0 && !singleTabStackDockComponent() ){
DockableDisplayer displayer = handle.getDisplayer();
panel.add( displayer.getComponent() );
}
else{
int selectionIndex = index;
int oldSelectionIndex = 0;
if( size == 1 && !singleTabStackDockComponent() ){
panel.removeAll();
Filter<StationChildHandle> list = dockables.dockables();
if( list.get( 0 ) == handle ){
if( list.size() != 2 ){
throw new IllegalStateException( "handle is stored, size is 1, but number of known dockables is not 2" );
}
handle = list.get( 1 );
index = 1;
}
DockableDisplayer child = list.get( 0 ).getDisplayer();
insertTab( child, 0 );
panel.add( stackComponent.getComponent() );
}
else{
oldSelectionIndex = stackComponent.getSelectedIndex();
if( index <= oldSelectionIndex ){
oldSelectionIndex++;
}
}
DockableDisplayer displayer = handle.getDisplayer();
insertTab( displayer, index );
if( isImmutableSelectedIndex() ){
stackComponent.setSelectedIndex( oldSelectionIndex );
}
else {
stackComponent.setSelectedIndex( selectionIndex );
}
}
panel.revalidate();
panel.repaint();
}
private boolean isImmutableSelectedIndex(){
DockController controller = getController();
if( controller == null ){
return IMMUTABLE_SELECTION_INDEX.getDefault( null );
}
return controller.getProperties().get( IMMUTABLE_SELECTION_INDEX );
}
private void insertTab( DockableDisplayer displayer, int index ){
Dockable dockable = displayer.getDockable();
String title = dockable.getTitleText();
String tooltip = dockable.getTitleToolTip();
Icon icon = dockable.getTitleIcon();
TabContentFilter filter = getTabContentFilter();
if( filter != null ){
TabContent content = new TabContent( icon, title, tooltip );
content = filter.filter( content, this, dockable );
if( content == null ){
title = null;
tooltip = null;
icon = null;
}
else{
title = content.getTitle();
tooltip = content.getTooltip();
icon = content.getIcon();
}
}
stackComponent.insertTab( title, icon, displayer.getComponent(), dockable, index );
stackComponent.setTooltipAt( index, tooltip );
}
/**
* Replaces <code>displayer</code> with a new instance.
* @param displayer the displayer to replace
*/
protected void discard( DockableDisplayer displayer ){
Dockable dockable = displayer.getDockable();
int index = indexOf( dockable );
if( index < 0 )
throw new IllegalArgumentException( "displayer is not a child of this station: " + displayer );
StationChildHandle handle = dockables.dockables().get( index );
handle.updateDisplayer();
displayer = handle.getDisplayer();
if( dockables.dockables().size() == 1 && singleTabStackDockComponent() ){
panel.removeAll();
panel.add( displayer.getComponent() );
}
else{
stackComponent.setComponentAt( index, displayer.getComponent() );
}
}
/**
* Removes the child of location <code>index</code>.<br>
* Note: clients may need to invoke {@link DockController#freezeLayout()}
* and {@link DockController#meltLayout()} to ensure no-one else adds or
* removes <code>Dockable</code>s.
* @param index the location of the child which will be removed
*/
public void remove( int index ){
if( index < 0 || index >= dockables.dockables().size() )
throw new IllegalArgumentException( "Index out of bounds" );
DockUtilities.checkLayoutLocked();
StationChildHandle handle = dockables.dockables().get( index );
Dockable dockable = handle.getDockable();
boolean removingSelection = false;
DockHierarchyLock.Token token = DockHierarchyLock.acquireUnlinking( this, dockable );
try{
listeners.fireDockableRemoving( dockable );
dockable.removeDockableListener( listener );
visibleListener.ignoreSelectionChanges = true;
if( dockables.dockables().size() == 1 ){
if( singleTabStackDockComponent() ){
stackComponent.remove( 0 );
}
else{
panel.remove( dockables.dockables().get( 0 ).getDisplayer().getComponent() );
}
dockables.remove( 0 );
panel.repaint();
}
else if( dockables.dockables().size() == 2 && !singleTabStackDockComponent() ){
panel.remove( stackComponent.getComponent() );
dockables.remove( index );
stackComponent.removeAll();
panel.add( dockables.dockables().get( 0 ).getDisplayer().getComponent() );
}
else{
removingSelection = index == stackComponent.getSelectedIndex();
dockables.remove( index );
stackComponent.remove( index );
}
handle.destroy();
dockable.setDockParent( null );
visibleListener.ignoreSelectionChanges = false;
focusAfterRemoving( removingSelection );
panel.revalidate();
listeners.fireDockableRemoved( dockable );
}
finally{
visibleListener.ignoreSelectionChanges = false;
token.release();
}
fireDockablesRepositioned( index );
fireDockableSelected();
}
private void focusAfterRemoving( boolean removedSelectedDockable ){
if( removedSelectedDockable ){
DockController controller = getController();
if( controller != null && dockables.dockables().size() > 1 ){
Dockable next = controller.getFocusHistory().getNewestOn( this );
if( next != null && next.getDockParent() == this ){
stackComponent.setSelectedIndex( indexOf( next ) );
return;
}
}
}
visibleListener.selectionChanged( stackComponent );
}
public Component getComponent() {
return background;
}
@Override
public void configureDisplayerHints( DockableDisplayerHints hints ) {
super.configureDisplayerHints( hints );
updateConfigurableDisplayerHints();
}
/**
* Updates the {@link #getConfigurableDisplayerHints() displayer hints}
* of this station.
*/
protected void updateConfigurableDisplayerHints(){
DockableDisplayerHints hints = getConfigurableDisplayerHints();
if( hints != null ){
hints.setShowBorderHint( !getStackComponent().hasBorder() );
}
}
@Override
public void addMouseInputListener( MouseInputListener listener ) {
panel.addMouseListener( listener );
panel.addMouseMotionListener( listener );
mouseInputListeners.add( listener );
if( stackComponent != null ){
stackComponent.getComponent().addMouseListener( listener );
stackComponent.getComponent().addMouseMotionListener( listener );
}
}
@Override
public void removeMouseInputListener( MouseInputListener listener ) {
panel.removeMouseListener( listener );
panel.removeMouseMotionListener( listener );
mouseInputListeners.remove( listener );
if( stackComponent != null ){
stackComponent.getComponent().removeMouseListener( listener );
stackComponent.getComponent().removeMouseMotionListener( listener );
}
}
public String getFactoryID() {
return StackDockStationFactory.ID;
}
private void updateContent( int index ){
if( index >= 0 && (getDockableCount() > 1 || singleTabStackDockComponent()) ){
Dockable dockable = getDockable( index );
TabContentFilter filter = getTabContentFilter();
TabContent content = new TabContent( dockable.getTitleIcon(), dockable.getTitleText(), dockable.getTitleToolTip() );
if( filter != null ){
content = filter.filter( content, this, dockable );
}
if( content == null ){
stackComponent.setTitleAt( index, null );
stackComponent.setIconAt( index, null );
stackComponent.setTooltipAt( index, null );
}
else{
stackComponent.setTitleAt( index, content.getTitle() );
stackComponent.setIconAt( index, content.getIcon() );
stackComponent.setTooltipAt( index, content.getTooltip() );
}
}
}
/**
* A listener for the parent of this station. This listener will fire
* events if the visibility-state of this station changes.<br>
* This listener is also added to the {@link StackDockStation#getStackComponent() stack-component}
* of the station, and ensures that the visible child has the focus.
* @author Benjamin Sigg
*/
private class VisibleListener extends DockStationAdapter implements StackDockComponentListener{
private boolean ignoreSelectionChanges = false;
@Override
public void dockableShowingChanged( DockStation station, Dockable dockable, boolean visible ) {
visibility.fire();
}
public void selectionChanged( StackDockComponent stack ){
if( !ignoreSelectionChanges ){
DockController controller = getController();
if( controller != null && !updatingTheme ){
Dockable selection = getFrontDockable();
if( selection != null && !controller.getRelocator().isOnPut() ){
controller.setFocusedDockable( new DefaultFocusRequest( selection, null, false ));
}
fireDockableSelected();
}
visibility.fire();
}
}
public void tabChanged( StackDockComponent stack, Dockable dockable ){
// ignore
}
}
/**
* This listener is added to the children of the station. Whenever the
* icon or the title-text of a child changes, the listener will inform
* the {@link StackDockStation#getStackComponent() stack-component} about
* the change.
* @author Benjamin Sigg
*/
private class Listener implements DockableListener{
public void titleBound( Dockable dockable, DockTitle title ) {
// do nothing
}
public void titleUnbound( Dockable dockable, DockTitle title ) {
// do nothing
}
public void titleTextChanged( Dockable dockable, String oldTitle, String newTitle ) {
int index = indexOf( dockable );
if( index >= 0 ){
updateContent( index );
}
}
public void titleToolTipChanged( Dockable dockable, String oldTooltip, String newTooltip ) {
int index = indexOf( dockable );
if( index >= 0 ){
updateContent( index );
}
}
public void titleIconChanged( Dockable dockable, Icon oldIcon, Icon newIcon ) {
int index = indexOf( dockable );
if( index >= 0 ){
updateContent( index );
}
}
public void titleExchanged( Dockable dockable, DockTitle title ) {
// ignore
}
}
/**
* This panel is used as base of the station. All children of the station
* have this panel as parent too.
* @author Benjamin Sigg
*/
protected class Background extends SecureContainer{
private BackgroundPanel content;
/**
* Creates a new panel
*/
public Background(){
content = new ConfiguredBackgroundPanel( Transparency.SOLID ){
@Override
public void setTransparency( Transparency transparency ){
super.setTransparency( transparency );
Background.this.setSolid( transparency == Transparency.SOLID );
}
};
content.setBackground( panelBackground );
setBasePane( content );
content.setLayout( new GridLayout( 1, 1 ));
}
public Dimension getMinimumSize(){
if( isSmallMinimumSize() ){
return new Dimension( 5, 5 );
}
else{
return super.getMinimumSize();
}
}
@Override
protected void paintOverlay( Graphics g ) {
DefaultStationPaintValue paint = getPaint();
if( insert != null && dockables.dockables().size() > 1 ){
Rectangle bounds = null;
if( insert.tab >= 0 && insert.tab < stackComponent.getTabCount() )
bounds = stackComponent.getBoundsAt( insert.tab );
if( bounds != null ){
Point a = new Point();
Point b = new Point();
if( insert.right ){
insertionLine( bounds, insert.tab+1 < stackComponent.getTabCount() ? stackComponent.getBoundsAt( insert.tab+1 ) : null, a, b, true );
}
else{
insertionLine( insert.tab > 0 ? stackComponent.getBoundsAt( insert.tab-1 ) : null, bounds, a, b, false );
}
paint.drawInsertionLine( g, a.x, a.y, b.x, b.y );
}
}
if( insert != null || (dragOperation != null && dragOperation.getDockable() != null )){
Rectangle bounds = new Rectangle( 0, 0, getWidth(), getHeight() );
Rectangle frontBounds = null;
if( getDockableCount() < 2 )
frontBounds = bounds;
else{
int index = stackComponent.getSelectedIndex();
if( index >= 0 ){
Component front = dockables.dockables().get( index ).getDisplayer().getComponent();
Point location = new Point( 0, 0 );
location = SwingUtilities.convertPoint( front, location, this );
frontBounds = new Rectangle( location.x, location.y, front.getWidth(), front.getHeight() );
}
}
if( frontBounds != null ){
if( insert != null ){
paint.drawInsertion( g, bounds, frontBounds );
}
else if( dragOperation != null && dragOperation.getDockable() != null ){
paint.drawRemoval( g, bounds, frontBounds );
}
}
}
}
}
/**
* When dropping or moving a {@link Dockable}, a line has to be painted
* between two tabs. This method determines the exact location of that line.
* @param left the bounds of the tab left to the line, might be <code>null</code> if
* <code>leftImportant</code> is <code>false</code>.
* @param right the bounds of the tab right to the line, might be <code>null</code> if
* <code>leftImportant</code> is <code>true</code>.
* @param a the first point of the line, should be used as output of this method
* @param b the second point of the line, should be used as output of this method
* @param leftImportant <code>true</code> if the mouse is over the left tab, <code>false</code>
* if the mouse is over the right tab.
*/
protected void insertionLine( Rectangle left, Rectangle right, Point a, Point b, boolean leftImportant ){
if( tabPlacement.getValue().isHorizontal() ){
if( left != null && right != null ){
int top = Math.max( left.y, right.y );
int bottom = Math.min( left.y + left.height, right.y + right.height );
if( bottom > top ){
int dif = bottom - top;
if( dif >= 0.8*left.height && dif >= 0.8*right.height ){
a.x = (left.x+left.width+right.x) / 2;
a.y = top;
b.x = a.x;
b.y = bottom;
return;
}
}
}
if( leftImportant ){
a.x = left.x + left.width;
a.y = left.y;
b.x = a.x;
b.y = a.y + left.height;
}
else{
a.x = right.x;
a.y = right.y;
b.x = a.x;
b.y = a.y + right.height;
}
}
else{
if( left != null && right != null ){
int top = Math.max( left.x, right.x );
int bottom = Math.min( left.x + left.width, right.x + right.width );
if( bottom > top ){
int dif = bottom - top;
if( dif >= 0.8*left.width && dif >= 0.8*right.width ){
a.y = (left.y+left.height+right.y) / 2;
a.x = top;
b.y = a.y;
b.x = bottom;
return;
}
}
}
if( leftImportant ){
a.y = left.y + left.height;
a.x = left.x;
b.y = a.y;
b.x = a.x + left.width;
}
else{
a.y = right.y;
a.x = right.x;
b.y = a.y;
b.x = a.x + right.width;
}
}
}
/**
* Describes the gap between two tabs.
* @author Benjamin Sigg
*/
public static class Insert{
/** The location of a base-tab */
private final int tab;
/** Whether the gap is left or right of {@link #tab}*/
private final boolean right;
/**
* Constructs a new Gap-location
* @param tab The location of a base-tab
* @param right Whether the gap is left or right of <code>tab</code>
*/
public Insert( int tab, boolean right ){
this.tab = tab;
this.right = right;
}
/**
* A reference for this gap, the gap is either at the left
* or at the right side of this tab.
* @return the reference
*/
public int getTab(){
return tab;
}
/**
* Whether the tab is at the left or the right of {@link #getTab()}.
* @return <code>true</code> if the gab is at the right side
*/
public boolean isRight(){
return right;
}
}
/**
* Custom implementation of {@link StationDropOperation}.
* @author Benjamin Sigg
*/
protected class StackDropOperation implements StationDropOperation{
private Insert insert;
private Dockable dropping;
private boolean move;
/**
* Creates a new operation.
* @param dropping the item that is about to be dropped
* @param insert the new location of {@link #dropping}
* @param move whether this is a move operation
*/
public StackDropOperation( Dockable dropping, Insert insert, boolean move ){
this.dropping = dropping;
this.insert = insert;
this.move = move;
}
public boolean isMove(){
return move;
}
public void draw() {
StackDockStation.this.insert = insert;
panel.repaint();
}
public void destroy( StationDropOperation next ){
if( StackDockStation.this.insert == insert ){
StackDockStation.this.insert = null;
panel.repaint();
}
insert = null;
dropping = null;
}
public DockStation getTarget(){
return StackDockStation.this;
}
public Dockable getItem(){
return dropping;
}
public void execute(){
if( isMove() ){
move();
}
else{
drop();
}
}
public void move() {
int index = indexOf( dropping );
if( index >= 0 ){
int drop = insert.tab + (insert.right ? 1 : 0 );
if( drop > index ){
drop--;
}
StackDockStation.this.move( index, drop );
}
}
public void drop(){
add( dropping, insert.tab + (insert.right ? 1 : 0), null );
}
public CombinerTarget getCombination(){
return null;
}
public DisplayerCombinerTarget getDisplayerCombination(){
return null;
}
}
/**
* The algorithm that paints the background of this station.
* @author Benjamin Sigg
*/
private class PanelBackground extends BackgroundAlgorithm implements StationBackgroundComponent{
/**
* Creates a new algorithm.
*/
public PanelBackground(){
super( StationBackgroundComponent.KIND, ThemeManager.BACKGROUND_PAINT + ".station.stack" );
}
public Component getComponent(){
return StackDockStation.this.getComponent();
}
public DockStation getStation(){
return StackDockStation.this;
}
}
}