/*
* 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;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.HierarchyBoundsListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.swing.SwingUtilities;
import javax.swing.event.MouseInputAdapter;
import bibliothek.gui.DockController;
import bibliothek.gui.DockStation;
import bibliothek.gui.DockTheme;
import bibliothek.gui.DockUI;
import bibliothek.gui.Dockable;
import bibliothek.gui.dock.accept.DockAcceptance;
import bibliothek.gui.dock.action.DockAction;
import bibliothek.gui.dock.action.DockActionSource;
import bibliothek.gui.dock.action.ListeningDockAction;
import bibliothek.gui.dock.component.DockComponentRootHandler;
import bibliothek.gui.dock.control.focus.DefaultFocusRequest;
import bibliothek.gui.dock.control.focus.FocusController;
import bibliothek.gui.dock.control.focus.MouseFocusObserver;
import bibliothek.gui.dock.disable.DisablingStrategy;
import bibliothek.gui.dock.disable.DisablingStrategyListener;
import bibliothek.gui.dock.displayer.DisplayerCombinerTarget;
import bibliothek.gui.dock.event.DockStationAdapter;
import bibliothek.gui.dock.event.DockableAdapter;
import bibliothek.gui.dock.event.DockableFocusEvent;
import bibliothek.gui.dock.event.DockableFocusListener;
import bibliothek.gui.dock.event.FlapDockListener;
import bibliothek.gui.dock.event.FocusVetoListener;
import bibliothek.gui.dock.layout.DockableProperty;
import bibliothek.gui.dock.layout.location.AsideRequest;
import bibliothek.gui.dock.station.AbstractDockableStation;
import bibliothek.gui.dock.station.Combiner;
import bibliothek.gui.dock.station.DisplayerCollection;
import bibliothek.gui.dock.station.DisplayerFactory;
import bibliothek.gui.dock.station.DockableDisplayer;
import bibliothek.gui.dock.station.PlaceholderMapping;
import bibliothek.gui.dock.station.StationBackgroundComponent;
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.flap.ButtonPane;
import bibliothek.gui.dock.station.flap.DefaultFlapLayoutManager;
import bibliothek.gui.dock.station.flap.DefaultFlapWindowFactory;
import bibliothek.gui.dock.station.flap.FlapDockHoldToggle;
import bibliothek.gui.dock.station.flap.FlapDockProperty;
import bibliothek.gui.dock.station.flap.FlapDockStationFactory;
import bibliothek.gui.dock.station.flap.FlapDockStationSource;
import bibliothek.gui.dock.station.flap.FlapDropInfo;
import bibliothek.gui.dock.station.flap.FlapLayoutManager;
import bibliothek.gui.dock.station.flap.FlapLayoutManagerListener;
import bibliothek.gui.dock.station.flap.FlapWindow;
import bibliothek.gui.dock.station.flap.FlapWindowFactory;
import bibliothek.gui.dock.station.flap.button.ButtonContent;
import bibliothek.gui.dock.station.flap.button.ButtonContentFilter;
import bibliothek.gui.dock.station.flap.button.DefaultButtonContentFilter;
import bibliothek.gui.dock.station.flap.layer.FlapOverrideDropLayer;
import bibliothek.gui.dock.station.flap.layer.FlapSideDropLayer;
import bibliothek.gui.dock.station.flap.layer.WindowDropLayer;
import bibliothek.gui.dock.station.layer.DefaultDropLayer;
import bibliothek.gui.dock.station.layer.DockStationDropLayer;
import bibliothek.gui.dock.station.support.CombinerSource;
import bibliothek.gui.dock.station.support.CombinerSourceWrapper;
import bibliothek.gui.dock.station.support.CombinerTarget;
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.Enforcement;
import bibliothek.gui.dock.station.support.PlaceholderList;
import bibliothek.gui.dock.station.support.PlaceholderList.Level;
import bibliothek.gui.dock.station.support.PlaceholderListItem;
import bibliothek.gui.dock.station.support.PlaceholderListItemAdapter;
import bibliothek.gui.dock.station.support.PlaceholderListItemConverter;
import bibliothek.gui.dock.station.support.PlaceholderListMapping;
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.StationCombinerValue;
import bibliothek.gui.dock.themes.ThemeManager;
import bibliothek.gui.dock.themes.basic.BasicButtonTitleFactory;
import bibliothek.gui.dock.title.ActivityDockTitleEvent;
import bibliothek.gui.dock.title.ControllerTitleFactory;
import bibliothek.gui.dock.title.DockTitle;
import bibliothek.gui.dock.title.DockTitleRequest;
import bibliothek.gui.dock.title.DockTitleVersion;
import bibliothek.gui.dock.util.BackgroundAlgorithm;
import bibliothek.gui.dock.util.DockProperties;
import bibliothek.gui.dock.util.DockUtilities;
import bibliothek.gui.dock.util.PropertyKey;
import bibliothek.gui.dock.util.PropertyValue;
import bibliothek.gui.dock.util.extension.Extension;
import bibliothek.gui.dock.util.property.ConstantPropertyFactory;
import bibliothek.gui.dock.util.property.DynamicPropertyFactory;
import bibliothek.util.Path;
/**
* This {@link DockStation} shows only a title for each of it's children.<br>
* If the user clicks on one of the titles, a window will popup. The {@link Dockable}
* which owns the clicked title is shown in this window.
* @author Benjamin Sigg
*/
public class FlapDockStation extends AbstractDockableStation {
/**
* The direction in which the window with the <code>Dockable</code> will popup,
* in respect to the location of this station.
*/
public static enum Direction{ NORTH, WEST, SOUTH, EAST };
/**
* This id is used to get a {@link DockTitleVersion} from the
* {@link DockController} which owns this station. The titles that are
* created for this version are used on the popup-window.
*/
public static final String WINDOW_TITLE_ID = "flap window";
/**
* This id is used to get a {@link DockTitleVersion} from the
* {@link DockController} which owns this station. The titles that are
* created for this version are used as buttons on this station.
*/
public static final String BUTTON_TITLE_ID = "flap button";
/**
* This id is forwarded to {@link Extension}s which load additional {@link DisplayerFactory}s.
*/
public static final String DISPLAYER_ID = "flap";
/**
* Key for the {@link FlapLayoutManager} that is used by all {@link FlapDockStation}s.
*/
public static final PropertyKey<FlapLayoutManager> LAYOUT_MANAGER = new PropertyKey<FlapLayoutManager>(
"flap dock station layout manager",
new DynamicPropertyFactory<FlapLayoutManager>(){
public FlapLayoutManager getDefault(
PropertyKey<FlapLayoutManager> key,
DockProperties properties ){
return new DefaultFlapLayoutManager();
}
}, true );
/**
* Key for all {@link DockTheme}s, tells the theme what content on the buttons
* should be visible. Note that some themes might ignore that setting. Changing this property will call
* {@link #recreateTitles()}, meaning all {@link DockTitle}s are removed and recreated.
*/
public static final PropertyKey<ButtonContent> BUTTON_CONTENT = new PropertyKey<ButtonContent>(
"flap dock station button content", new ConstantPropertyFactory<ButtonContent>( ButtonContent.THEME_DEPENDENT ), true );
/**
* Key for all elements that depend from {@link #BUTTON_CONTENT}, adds additional information to the {@link ButtonContent}.
*/
public static final PropertyKey<ButtonContentFilter> BUTTON_CONTENT_FILTER = new PropertyKey<ButtonContentFilter>(
"flap dock station button content connector", new ConstantPropertyFactory<ButtonContentFilter>( new DefaultButtonContentFilter() ), true );
/**
* Key for the minimum size of all {@link FlapDockStation}s.
*/
public static final PropertyKey<Dimension> MINIMUM_SIZE = new PropertyKey<Dimension>( "flap dock station empty size",
new ConstantPropertyFactory<Dimension>( new Dimension( 0, 0 ) ), true );
/**
* Key for a factory that creates the windows of this station.
*/
public static final PropertyKey<FlapWindowFactory> WINDOW_FACTORY = new PropertyKey<FlapWindowFactory>("flap dock station window factory",
new ConstantPropertyFactory<FlapWindowFactory>( new DefaultFlapWindowFactory() ), true );
/**
* A listener that is added to the current {@link #layoutManager}
*/
private FlapLayoutManagerListener layoutManagerListener = new FlapLayoutManagerListener(){
public void holdSwitchableChanged( FlapLayoutManager manager, FlapDockStation station, Dockable dockable ){
if( station == null || station == FlapDockStation.this ){
updateIsHoldSwitchable( dockable );
}
}
};
/**
* The layoutManager which is responsible to layout this station
*/
private PropertyValue<FlapLayoutManager> layoutManager = new PropertyValue<FlapLayoutManager>( LAYOUT_MANAGER ){
@Override
protected void valueChanged( FlapLayoutManager oldValue, FlapLayoutManager newValue ) {
if( oldValue != null ){
oldValue.removeListener( layoutManagerListener );
oldValue.uninstall( FlapDockStation.this );
}
if( newValue != null ){
newValue.addListener( layoutManagerListener );
newValue.install( FlapDockStation.this );
}
}
};
/** current {@link PlaceholderStrategy} */
private PropertyValue<PlaceholderStrategy> placeholderStrategy = new PropertyValue<PlaceholderStrategy>(PlaceholderStrategy.PLACEHOLDER_STRATEGY) {
@Override
protected void valueChanged( PlaceholderStrategy oldValue, PlaceholderStrategy newValue ){
handles.setStrategy( newValue );
}
};
/**
* How to layout the buttons on this station
*/
private PropertyValue<ButtonContent> buttonContent = new PropertyValue<ButtonContent>( BUTTON_CONTENT ){
@Override
protected void valueChanged( ButtonContent oldValue, ButtonContent newValue ){
if( oldValue != newValue ){
recreateTitles();
}
}
};
/** the minimum size this station has */
private PropertyValue<Dimension> minimumSize = new PropertyValue<Dimension>( MINIMUM_SIZE ) {
protected void valueChanged( Dimension oldValue, Dimension newValue ){
buttonPane.revalidate();
}
};
/** the factory creating {@link FlapWindow}s for this station */
private PropertyValue<FlapWindowFactory> windowFactory = new PropertyValue<FlapWindowFactory>( WINDOW_FACTORY ){
protected void valueChanged( FlapWindowFactory oldValue, FlapWindowFactory newValue ){
if( oldValue != null ){
oldValue.uninstall( FlapDockStation.this );
}
if( newValue != null ){
newValue.install( FlapDockStation.this );
}
updateWindow( getFrontDockable(), true );
}
};
/** Access to the current {@link DisablingStrategy} */
private PropertyValue<DisablingStrategy> disablingStrategy = new PropertyValue<DisablingStrategy>( DisablingStrategy.STRATEGY ){
@Override
protected void valueChanged( DisablingStrategy oldValue, DisablingStrategy newValue ){
if( oldValue != null ){
oldValue.removeDisablingStrategyListener( disablingStrategyListener );
}
if( newValue != null ){
newValue.addDisablingStrategyListener( disablingStrategyListener );
if( newValue.isDisabled( FlapDockStation.this )){
setFrontDockable( null );
}
}
}
};
/** observes the {@link #disablingStrategy} and closes the front dockable if necessary */
private DisablingStrategyListener disablingStrategyListener = new DisablingStrategyListener(){
public void changed( DockElement item ){
if( item == FlapDockStation.this ){
if( disablingStrategy.getValue().isDisabled( item )){
setFrontDockable( null );
}
}
}
};
/** The direction in which the popup-window is, in respect to this station */
private Direction direction = Direction.SOUTH;
/**
* This property tells this station whether the station can change the
* {@link #direction} property automatically or not
*/
private boolean autoDirection = true;
/** The popup-window */
private FlapWindow window;
/** The size of the border, which can be grabbed by the user, of the popup-window */
private int windowBorder = 3;
/** The minimal size of the popup-window */
private int windowMinSize = 25;
/** The initial size of windows, can be overridden by the layout manager */
private int defaultWindowSize = 400;
/**
* This variable is set when the front-dockable is removed, because
* the {@link DockController} is removed. If the controller is added
* again, then the front-dockable can be restored with the value of
* this variable.
*/
private Dockable oldFrontDockable;
/** A list of all {@link Dockable Dockables} registered on this station */
private DockablePlaceholderList<DockableHandle> handles = new DockablePlaceholderList<DockableHandle>();
/** a listener for all {@link Dockable}s of this station */
private Listener dockableListener = new Listener();
/** The component on which all "buttons" are shown (the titles created with the id {@link #BUTTON_TITLE_ID}) */
private ButtonPane buttonPane;
/** This version is obtained by using {@link #BUTTON_TITLE_ID} */
private DockTitleVersion buttonVersion;
/** This version is obtained by using {@link #WINDOW_TITLE_ID} */
private DockTitleVersion titleVersion;
/** The {@link StationPaint} used to paint on this station */
private DefaultStationPaintValue paint;
/** The {@link Combiner} user to combine {@link Dockable Dockables}*/
private StationCombinerValue combiner;
/** The {@link DisplayerFactory} used to create displayers*/
private DefaultDisplayerFactoryValue displayerFactory;
/** Collection used to handle the {@link DockableDisplayer} */
private DisplayerCollection displayers;
/**
* Temporary information needed when a {@link Dockable} is moved
* over this station.
*/
private FlapDropInfo dropInfo;
/** Information about a dockable that is removed from this station */
private StationDragOperation dragInfo;
/** A listener added to the {@link MouseFocusObserver} */
private ControllerListener controllerListener = new ControllerListener();
/**
* The button-titles are organized in a way that does not need much
* space if this property is <code>true</code>
*/
private boolean smallButtons = true;
/**
* An action that will be added to all children of this station.
*/
private ListeningDockAction holdAction;
/** A listener that is added to the parent of this dockable station. */
private VisibleListener visibleListener = new VisibleListener();
/** the last checked state of {@link #isDockableVisible()} */
private boolean lastShowing = false;
/** A list of listeners that were added to this station */
private List<FlapDockListener> flapDockListeners = new ArrayList<FlapDockListener>();
/** Manager for the visibility of the children of this station */
private DockableShowingManager showingManager;
/** the background algorithm of this component */
private Background background = new Background();
/** tells how far the {@link FlapSideDropLayer} stretches */
private int borderSideSnapSize = 15;
/**
* Default constructor of a {@link FlapDockStation}
*/
public FlapDockStation(){
init();
}
/**
* Creates a new {@link FlapDockStation}.
* @param init <code>true</code> if the fields of this station should
* be initialized, <code>false</code> otherwise. If <code>false</code>, then
* {@link #init()} must be called by a subclass.
*/
protected FlapDockStation( boolean init ){
if( init ){
init();
}
}
/**
* Initializes the fields of this station, hast to be called exactly once
*/
protected void init(){
showingManager = new DockableShowingManager( listeners );
buttonPane = createButtonPane();
buttonPane.setBackground( background );
buttonPane.setController( getController() );
setDirection( Direction.SOUTH );
displayerFactory = new DefaultDisplayerFactoryValue( ThemeManager.DISPLAYER_FACTORY + ".flap", this );
displayers = new DisplayerCollection( this, displayerFactory, DISPLAYER_ID );
paint = new DefaultStationPaintValue( ThemeManager.STATION_PAINT + ".flap", this );
combiner = new StationCombinerValue( ThemeManager.COMBINER + ".flap", this );
buttonPane.addComponentListener( new ComponentAdapter(){
@Override
public void componentResized( ComponentEvent e ) {
if( autoDirection )
selfSetDirection();
else
updateWindowBounds();
}
});
buttonPane.addHierarchyBoundsListener( new HierarchyBoundsListener(){
public void ancestorMoved( HierarchyEvent e ) {
if( autoDirection )
selfSetDirection();
else
updateWindowBounds();
}
public void ancestorResized( HierarchyEvent e ) {
if( autoDirection )
selfSetDirection();
else
updateWindowBounds();
}
});
buttonPane.addHierarchyListener( new HierarchyListener(){
public void hierarchyChanged( HierarchyEvent e ){
if( (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 ){
if( getDockParent() == null ){
getDockableStateListeners().checkShowing();
}
checkShowing();
}
}
});
holdAction = createHoldAction();
}
/**
* Creates the panel which will show buttons for the children of this station.
* @return the new panel
*/
protected ButtonPane createButtonPane(){
return new ButtonPane( this );
}
/**
* Creates a {@link DockAction} that is added to all children
* of this station. The action should change the <code>hold</code>
* state of the associated {@link Dockable}, this can be done
* through the method {@link #setHold(Dockable, boolean)}.
* @return The action, or <code>null</code> if no action should
* be added to the children
*/
protected ListeningDockAction createHoldAction(){
return new FlapDockHoldToggle( this );
}
protected DockComponentRootHandler createRootHandler() {
return new DockComponentRootHandler( this ){
protected TraverseResult shouldTraverse( Component component ) {
if( buttonPane.getBasePane() == component ){
return TraverseResult.EXCLUDE_CHILDREN;
}
if( displayers.isDisplayerComponent( component )){
return TraverseResult.EXCLUDE;
}
return TraverseResult.INCLUDE_CHILDREN;
}
};
}
@Override
public void setDockParent( DockStation station ) {
if( getDockParent() != null ){
getDockParent().removeDockStationListener( visibleListener );
}
super.setDockParent(station);
if( station != null ){
station.addDockStationListener( visibleListener );
}
}
@Override
public void setController( DockController controller ) {
if( getController() != controller ){
boolean remove = getController() != null;
if( remove ){
handles.unbind();
getController().removeDockableFocusListener( controllerListener );
getController().getFocusController().removeVetoListener( controllerListener );
oldFrontDockable = getFrontDockable();
setFrontDockable( null );
for( DockableHandle dockable : handles.dockables() ){
if( dockable != null ){
dockable.setTitle( null );
}
}
if( window != null ){
window.setDockTitle( null );
}
titleVersion = null;
buttonVersion = null;
}
super.setController(controller);
placeholderStrategy.setProperties( controller );
displayers.setController( controller );
paint.setController( controller );
displayerFactory.setController( controller );
combiner.setController( controller );
background.setController( controller );
if( window != null ){
window.setController( controller );
}
disablingStrategy.setProperties( controller );
buttonPane.setController( controller );
FlapLayoutManager oldLayoutManager = layoutManager.getValue();
layoutManager.setProperties( controller );
FlapLayoutManager newLayoutManager = layoutManager.getValue();
if( oldLayoutManager == newLayoutManager ){
if( controller == null ){
if( oldLayoutManager != null ){
oldLayoutManager.uninstall( this );
}
}
else{
if( newLayoutManager != null ){
newLayoutManager.install( this );
}
}
}
buttonContent.setProperties( controller );
minimumSize.setProperties( controller );
if( holdAction != null )
holdAction.setController( controller );
if( controller != null ){
handles.bind();
titleVersion = controller.getDockTitleManager().getVersion( WINDOW_TITLE_ID, ControllerTitleFactory.INSTANCE );
buttonVersion = controller.getDockTitleManager().getVersion( BUTTON_TITLE_ID, BasicButtonTitleFactory.FACTORY );
for( DockableHandle dockable : handles.dockables() ){
if( dockable != null ){
dockable.setTitle( buttonVersion );
}
}
if( window != null ){
window.setDockTitle( titleVersion );
}
controller.addDockableFocusListener( controllerListener );
controller.getFocusController().addVetoListener( controllerListener );
if( isStationShowing() )
setFrontDockable( oldFrontDockable );
}
windowFactory.setProperties( controller );
buttonPane.setProperties( controller );
buttonPane.resetTitles();
showingManager.fire();
}
}
@Override
protected void callDockUiUpdateTheme() throws IOException {
DockUI.updateTheme( this, new FlapDockStationFactory());
}
/**
* Gets the direction in which the popup-window is currently opened.
* @return The direction
*/
public Direction getDirection() {
return direction;
}
/**
* Sets the direction in which the popup-window points. The direction
* may be overridden, if the property {@link #isAutoDirection() autoDirection}
* is set to <code>true</code>.
* @param direction The direction of the popup-window
*/
public void setDirection( Direction direction ) {
if( direction == null )
throw new IllegalArgumentException();
this.direction = direction;
DockTitle.Orientation orientation = orientation( direction );
for( DockableHandle dockable : handles.dockables() ){
DockTitle title = dockable.getTitle();
if( title != null ){
title.setOrientation( orientation );
}
}
buttonPane.resetTitles();
updateWindowBounds();
buttonPane.revalidate();
}
/**
* Determines the orientation of the {@link DockTitle DockTitles} on this
* station.
* @param direction the direction in which the flap opens
* @return the orientation of the titles
*/
protected DockTitle.Orientation orientation( Direction direction ){
switch( direction ){
case NORTH:
return DockTitle.Orientation.SOUTH_SIDED;
case SOUTH:
return DockTitle.Orientation.NORTH_SIDED;
case EAST:
return DockTitle.Orientation.WEST_SIDED;
case WEST:
return DockTitle.Orientation.EAST_SIDED;
}
return null;
}
/**
* Recalculates the size and the location of the popup-window, if
* there is a window.
*/
protected void updateWindowBounds(){
if( window != null )
window.updateBounds();
}
/**
* Gets the minimum size this station should have.
* @return the minimum size, never <code>null</code>
*/
public Dimension getMinimumSize(){
return minimumSize.getValue();
}
/**
* Sets the minimum size this station should have. A value of <code>null</code>
* is valid and will let this station use the property {@link #MINIMUM_SIZE}.
* @param size the new minimum size or <code>null</code>
*/
public void setMinimumSize( Dimension size ){
minimumSize.setValue( size );
}
/**
* Gets the factory to create new {@link DockableDisplayer}.
* @return the factory
*/
public DefaultDisplayerFactoryValue getDisplayerFactory() {
return displayerFactory;
}
/**
* Gets the set of displayers currently used on this station.
* @return the set of displayers
*/
public DisplayerCollection getDisplayers() {
return displayers;
}
/**
* Gets the {@link Combiner} to merge {@link Dockable Dockables}
* @return the combiner
*/
public StationCombinerValue getCombiner() {
return combiner;
}
/**
* Gets the {@link StationPaint} to paint on this station.
* @return The paint
*/
public DefaultStationPaintValue getPaint() {
return paint;
}
/**
* Gets the rectangle to which a flap-window will be attached. The default
* is a rectangle that lies exactly over this component. The coordinates
* of the result are relative to the component of this station.
* @return the free area near a window
*/
public Rectangle getExpansionBounds(){
Component component = getComponent();
return new Rectangle( 0, 0, component.getWidth(), component.getHeight() );
}
/**
* Tells whether this station can change the
* {@link #setDirection(bibliothek.gui.dock.FlapDockStation.Direction) direction}
* itself, or if only the user can change the direction.
* @return <code>true</code> if the station chooses the direction itself
* @see #setAutoDirection(boolean)
*/
public boolean isAutoDirection() {
return autoDirection;
}
/**
* Tells this station whether it can choose the
* {@link #setDirection(bibliothek.gui.dock.FlapDockStation.Direction) direction}
* of the popup-window itself, or if the direction remains always the
* same.
* @param autoDirection <code>true</code> if the station can choose the
* direction itself, <code>false</code> otherwise
*/
public void setAutoDirection( boolean autoDirection ) {
this.autoDirection = autoDirection;
if( autoDirection )
selfSetDirection();
}
/**
* Calculates the best
* {@link #setDirection(bibliothek.gui.dock.FlapDockStation.Direction) direction}
* for the popup-window of this station.
*/
public void selfSetDirection(){
Component c = getComponent();
Point center = new Point( c.getWidth()/2, c.getHeight()/2 );
SwingUtilities.convertPointToScreen( center, c );
Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
Direction direction;
if( c.getWidth() > c.getHeight() ){
if( center.y < size.height/2 ){
direction = Direction.SOUTH;
}
else{
direction = Direction.NORTH;
}
}
else{
if( center.x < size.width/2 ){
direction = Direction.EAST;
}
else{
direction = Direction.WEST;
}
}
if( direction != this.direction )
setDirection( direction );
else
updateWindowBounds();
}
public Dockable getFrontDockable() {
if( window == null ) //|| !window.isVisible() )
return null;
else
return window.getDockable();
}
public void setFrontDockable( Dockable dockable ) {
Dockable oldFrontDockable = getFrontDockable();
if( oldFrontDockable == dockable ){
return;
}
updateWindow( dockable, false );
if( getController() != null ){
if( oldFrontDockable != null ){
DockTitle[] titles = oldFrontDockable.listBoundTitles();
boolean active = getController().isFocused( oldFrontDockable );
for( DockTitle title : titles )
changed( oldFrontDockable, title, active );
}
}
if( window != null ){
if( window.getDockable() == null )
window.setWindowVisible( false );
else
window.repaint();
}
if( getController() != null ){
if( dockable != null ){
DockTitle[] titles = dockable.listBoundTitles();
boolean active = getController().isFocused( dockable );
for( DockTitle title : titles )
changed( dockable, title, active );
}
}
showingManager.fire();
listeners.fireDockableSelected( oldFrontDockable, dockable );
}
/**
* Makes sure that <code>dockable</code> is shown on the current {@link FlapWindow}. May replace
* the current window if necessary.
* @param dockable the item to show, can be <code>null</code>
* @param forceReplace whether the window should be replaced anyway
*/
private void updateWindow( Dockable dockable, boolean forceReplace ){
if( dockable == null ){
if( window != null ){
window.setDockable( null );
if( forceReplace ){
setFlapWindow( null );
}
}
}
else{
Window owner = SwingUtilities.getWindowAncestor( getComponent() );
if( window == null || forceReplace || !windowFactory.getValue().isValid(window, this) ){
if( window != null ){
window.setDockable( null );
}
FlapWindow window = createFlapWindow( buttonPane );
if( window != null )
setFlapWindow( window );
}
if( window != null && owner != null ){
window.setDockable( dockable );
if( owner.isVisible() )
window.setWindowVisible( true );
updateWindowBounds();
}
}
}
/**
* Creates a window for this station.
* @param buttonPane the panel needed to calculate the size of the window
* @return the window or <code>null</code> if no window could be created
*/
protected FlapWindow createFlapWindow( ButtonPane buttonPane ){
FlapWindow window = windowFactory.getValue().create(this, buttonPane);
if( window != null ){
window.setDockTitle( titleVersion );
}
return window;
}
/**
* Tells the <code>hold</code>=property of <code>dockable</code>.
* @param dockable the {@link Dockable} whose property is asked
* @return the current state
* @see #setHold(Dockable, boolean)
*/
public boolean isHold( Dockable dockable ) {
FlapLayoutManager manager = layoutManager.getValue();
if( manager == null )
return false;
return manager.isHold( this, dockable );
}
/**
* Tells whether the station should close the popup when the
* {@link Dockable} looses the focus, or if the popup should
* remain open until the user closes the popup. The value is forwarded
* to the {@link FlapLayoutManager layout manager} of this station, the
* layout manager can then decide if and how it would like to react.
* @param dockable the {@link Dockable} whose settings should change
* @param hold <code>true</code> if the popup should remain open,
* <code>false</code> if it should close
*/
public void setHold( Dockable dockable, boolean hold ) {
FlapLayoutManager manager = layoutManager.getValue();
if( manager != null ){
boolean old = manager.isHold( this, dockable );
manager.setHold( this, dockable, hold );
hold = manager.isHold( this, dockable );
if( old != hold )
updateHold( dockable );
}
}
/**
* Updates the hold property of <code>dockable</code>.
* The new value is provided by the {@link FlapLayoutManager layout manager}.
* @param dockable the element whose property is updated
*/
public void updateHold( Dockable dockable ){
FlapLayoutManager manager = layoutManager.getValue();
if( manager != null ){
boolean hold = manager.isHold( this, dockable );
fireHoldChanged( dockable, hold );
if( !hold && getController() != null && getFrontDockable() == dockable ){
if( !getController().isFocused( dockable ))
setFrontDockable( null );
}
}
}
/**
* How the buttons are organized.
* @return <code>true</code> if the buttons are layout in a way that
* needs not much space.
* @see #setSmallButtons(boolean)
*/
public boolean isSmallButtons() {
return smallButtons;
}
/**
* Sets how the buttons are layout. If <code>true</code>, then the buttons
* have their preferred size. If <code>false</code> the buttons take
* all available space of this station.
* @param smallButtons <code>true</code> if the buttons should be small
*/
public void setSmallButtons( boolean smallButtons ) {
this.smallButtons = smallButtons;
}
/**
* Gets the {@link DockTitleVersion} that is used to create titles
* for the popup-window.
* @return the version of titles for the popup, can be <code>null</code>
*/
public DockTitleVersion getTitleVersion() {
return titleVersion;
}
/**
* Gets the {@link DockTitleVersion} that is used to create titles
* for the button-panel.
* @return the version of titles for buttons, can be <code>null</code>
*/
public DockTitleVersion getButtonVersion() {
return buttonVersion;
}
/**
* Gets the size of the border of the popup-window, where the user
* can change the size of the window itself.
* @return the popup-size
* @see #setWindowBorder(int)
*/
public int getWindowBorder() {
return windowBorder;
}
/**
* Sets the size of the draggable area on the popup-window, that is used
* to change the size of the window.
* @param windowBorder the border, at least 0
*/
public void setWindowBorder( int windowBorder ) {
if( windowBorder < 0 )
throw new IllegalArgumentException( "Border must not be less than 0" );
this.windowBorder = windowBorder;
updateWindowBounds();
}
/**
* Gets the minimal size the popup-window can have.
* @return the minimal size
* @see #setWindowMinSize(int)
*/
public int getWindowMinSize() {
return windowMinSize;
}
/**
* Sets the minimal size which the popup-window can have.
* @param windowMinSize the minimal size
*/
public void setWindowMinSize( int windowMinSize ) {
if( windowMinSize < 0 )
throw new IllegalArgumentException( "Min size must not be smaller than 0" );
this.windowMinSize = windowMinSize;
updateWindowBounds();
}
/**
* Gets the current size of the popup-window
* @param dockable the element for which the size should be returned
* @return the current size
*/
public int getWindowSize( Dockable dockable ){
FlapLayoutManager manager = layoutManager.getValue();
if( manager == null )
return 0;
return manager.getSize( this, dockable );
}
/**
* Sets the size of the popup-window for <code>dockable</code>. The
* value will be forwarded to the {@link FlapLayoutManager layout manager}
* of this station, the layout manager can decide if and how the new size
* is to be stored.
* @param dockable the element for which the size should be set
* @param size the size, at least 0
*/
public void setWindowSize( Dockable dockable, int size ){
if( size < 0 )
throw new IllegalArgumentException( "Size must at least be 0" );
FlapLayoutManager manager = layoutManager.getValue();
if( manager != null ){
manager.setSize( this, dockable, size );
updateWindowSize( dockable );
}
}
/**
* Updates the size of the window if <code>dockable</code> is currently
* shown. The new size is provided by the {@link FlapLayoutManager layout manager}.
* @param dockable the element whose size should be updated
*/
public void updateWindowSize( Dockable dockable ){
if( getFrontDockable() == dockable ){
updateWindowBounds();
}
}
/**
* Sets the default size a window should have. This property might be
* overridden by the {@link FlapLayoutManager layout manager}.
* @param defaultWindowSize the default size of windows
*/
public void setDefaultWindowSize( int defaultWindowSize ) {
this.defaultWindowSize = defaultWindowSize;
}
/**
* Gets the default size of a new window.
* @return the default size
*/
public int getDefaultWindowSize() {
return defaultWindowSize;
}
/**
* Sets the layout manager which should be used by this station. The
* manager can be changed on a global level using {@link #LAYOUT_MANAGER}.
* @param manager the manager or <code>null</code> when a default
* manager should be used
*/
public void setFlapLayoutManager( FlapLayoutManager manager ){
layoutManager.setValue( manager );
}
/**
* Gets the layout manager which was explicitly set by {@link #setFlapLayoutManager(FlapLayoutManager)}.
* @return the manager or <code>null</code>
*/
public FlapLayoutManager getFlapLayoutManager(){
return layoutManager.getOwnValue();
}
/**
* Gets the currently used {@link FlapLayoutManager}.
* @return the current manager
*/
public FlapLayoutManager getCurrentFlapLayoutManager(){
return layoutManager.getValue();
}
/**
* 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 );
}
/**
* Adds a listener to this station. The listener will be invoked when
* some properties of this station change.
* @param listener the new listener
*/
public void addFlapDockStationListener( FlapDockListener listener ){
flapDockListeners.add( listener );
}
/**
* Removes an earlier added listener from this station.
* @param listener the listener to remove
*/
public void removeFlapDockStationListener( FlapDockListener listener ){
flapDockListeners.remove( listener );
}
/**
* Informs all registered {@link FlapDockListener FlapDockListeners}
* that the hold-property of a {@link Dockable} has been changed.
* @param dockable the <code>Dockable</code> whose property is changed
* @param value the new value of the property
*/
protected void fireHoldChanged( Dockable dockable, boolean value ){
for( FlapDockListener listener : flapDockListeners.toArray( new FlapDockListener[ flapDockListeners.size() ] ))
listener.holdChanged( this, dockable, value );
}
@Override
public DockActionSource getDirectActionOffers( Dockable dockable ) {
int index = indexOf( dockable );
if( index < 0 )
return null;
DockableHandle handle = handles.dockables().get( index );
return handle.getActions();
}
private void updateIsHoldSwitchable( Dockable dockable ){
if( dockable == null ){
for( DockableHandle handle : handles.dockables() ){
handle.getActions().updateHoldSwitchable();
}
}
else{
int index = indexOf( dockable );
if( index >= 0 ){
DockableHandle handle = handles.dockables().get( index );
handle.getActions().updateHoldSwitchable();
}
}
}
@Override
public void changed( Dockable dockable, DockTitle title, boolean active ) {
ActivityDockTitleEvent event = new ActivityDockTitleEvent( this, dockable, active );
event.setPreferred( dockable == getFrontDockable() );
title.changed( event );
}
/**
* Sets the current drop-information. The information is forwarded
* to the popup-window and the button-panel (if they exist).
* @param info the new information, or <code>null</code>
*/
private void setDropInfo( FlapDropInfo info ){
this.dropInfo = info;
if( window != null )
window.setDropInfo( info );
if( buttonPane != null )
buttonPane.setDropInfo( info );
}
/**
* Sets the popup-window that will be used in the future. The popup-window
* can be replaced by another window if the root window of the tree in which
* this {@link Component} is changes.
* @param window the new window, can be <code>null</code>
*/
private void setFlapWindow( FlapWindow window ){
if( this.window != null ){
getRootHandler().removeRoot( window.getComponent() );
this.window.setController( null );
this.window.destroy();
}
this.window = window;
if( window != null ){
window.setController( getController() );
window.setDropInfo( dropInfo );
getRootHandler().removeRoot( window.getComponent() );
}
}
/**
* Checks whether the currently used {@link FlapWindow} equals
* <code>window</code>.
* @param window a window
* @return <code>true</code> if <code>window</code> is currently used
* by this station
*/
public boolean isFlapWindow( FlapWindow window ){
return this.window == window;
}
/**
* Gets the window which is currently used by this station. The window
* may or may not be shown currently. Callers should not modify the window.
* @return the current window, might be <code>null</code>
*/
public FlapWindow getFlapWindow(){
return window;
}
public PlaceholderMap getPlaceholders(){
return handles.toMap();
}
public PlaceholderMapping getPlaceholderMapping() {
return new PlaceholderListMapping( this, handles ){
public DockableProperty getLocationAt( Path placeholder ) {
int index = handles.getDockableIndex( placeholder );
return new FlapDockProperty( index, false, -1, placeholder );
}
};
}
public void setPlaceholders( PlaceholderMap placeholders ){
if( getDockableCount() > 0 ){
throw new IllegalStateException( "only allowed if there are not children present" );
}
try{
DockablePlaceholderList<DockableHandle> next = new DockablePlaceholderList<DockableHandle>( placeholders );
if( getController() != null ){
handles.setStrategy( null );
handles.unbind();
handles = next;
handles.bind();
handles.setStrategy( getPlaceholderStrategy() );
}
else{
handles = next;
}
}
catch( IllegalArgumentException ex ){
// silent
}
}
/**
* 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>hold: the return value of {@link #isHold(Dockable)}</li>
* <li>size: the return value of {@link #getWindowSize(Dockable)}</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 ){
final PlaceholderStrategy strategy = getPlaceholderStrategy();
return handles.toMap( new PlaceholderListItemAdapter<Dockable, DockableHandle>() {
@Override
public ConvertedPlaceholderListItem convert( int index, DockableHandle dockable ){
Integer id = children.get( dockable.getDockable() );
if( id == null ){
return null;
}
ConvertedPlaceholderListItem item = new ConvertedPlaceholderListItem();
item.putInt( "id", id );
item.putInt( "index", index );
item.putBoolean( "hold", isHold( dockable.getDockable() ));
item.putInt( "size", getWindowSize( dockable.getDockable() ) );
if( strategy != null ){
Path placeholder = strategy.getPlaceholderFor( dockable.getDockable() );
if( placeholder != null ){
item.putString( "placeholder", placeholder.toString() );
item.setPlaceholder( placeholder );
}
}
return item;
}
});
}
/**
* Sets a new layout on this station, this method assumes that <code>map</code> was created
* using {@link #getPlaceholders(Map)}.
* @param map the map to read
* @param children the new children of this stations
* @throws IllegalStateException if there are children left on this station
*/
public void setPlaceholders( PlaceholderMap map, final Map<Integer, Dockable> children ){
DockUtilities.checkLayoutLocked();
if( getDockableCount() > 0 ){
throw new IllegalStateException( "must not have any children" );
}
DockController controller = getController();
try{
if( controller != null ){
controller.freezeLayout();
handles.setStrategy( null );
handles.unbind();
}
handles = new DockablePlaceholderList<DockableHandle>();
handles.read( map, new PlaceholderListItemAdapter<Dockable, DockableHandle>(){
private DockHierarchyLock.Token token;
@Override
public DockableHandle convert( ConvertedPlaceholderListItem item ){
int id = item.getInt( "id" );
Dockable dockable = children.get( id );
if( dockable != null ){
DockUtilities.ensureTreeValidity( FlapDockStation.this, dockable );
token = DockHierarchyLock.acquireLinking( FlapDockStation.this, dockable );
boolean hold = item.getBoolean( "hold" );
int size = item.getInt( "size" );
listeners.fireDockableAdding( dockable );
DockableHandle handle = link( dockable );;
setHold( dockable, hold );
setWindowSize( dockable, size );
return handle;
}
return null;
}
@Override
public void added( DockableHandle dockable ){
try{
dockable.getDockable().setDockParent( FlapDockStation.this );
listeners.fireDockableAdded( dockable.getDockable() );
}
finally{
token.releaseNoCheck();
}
}
});
if( getController() != null ){
handles.bind();
handles.setStrategy( getPlaceholderStrategy() );
}
}
finally{
if( controller != null ){
controller.meltLayout();
}
}
buttonPane.resetTitles();
}
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[]{};
}
if( getDockableCount() == 0 ){
return new DockStationDropLayer[]{
new DefaultDropLayer( this ),
new FlapOverrideDropLayer( this ),
new WindowDropLayer( this ),
new FlapSideDropLayer( this )
};
}
else{
return new DockStationDropLayer[]{
new DefaultDropLayer( this ),
new FlapOverrideDropLayer( this ),
new WindowDropLayer( this )
};
}
}
/**
* Sets the size of the outside layer. If the mouse is outside this station, but within
* <code>borderSideSnapSize</code>, then this station may still be the target of a drag and
* drop operation.
* @param borderSideSnapSize the size in pixels
*/
public void setBorderSideSnapSize( int borderSideSnapSize ){
this.borderSideSnapSize = borderSideSnapSize;
}
/**
* Tells how far the layer outside the station stretches.
* @return the size of the outside layer
* @see #setBorderSideSnapSize(int)
*/
public int getBorderSideSnapSize(){
return borderSideSnapSize;
}
public StationDragOperation prepareDrag( Dockable dockable ){
if( dragInfo != null ){
dragInfo.canceled();
}
if( window != null && window.getDockable() == dockable ){
window.setRemoval( true );
dragInfo = new StationDragOperation(){
public void succeeded(){
window.setRemoval( false );
dragInfo = null;
}
public void canceled(){
window.setRemoval( false );
dragInfo = null;
}
};
}
return dragInfo;
}
public StationDropOperation prepareDrop( StationDropItem item ){
int mouseX = item.getMouseX();
int mouseY = item.getMouseY();
Dockable dockable = item.getDockable();
boolean move = dockable.getDockParent() == this;
if( SwingUtilities.isDescendingFrom( getComponent(), dockable.getComponent() )){
return null;
}
Point mouse = new Point( mouseX, mouseY );
SwingUtilities.convertPointFromScreen( mouse, buttonPane );
FlapDropInfo dropInfo = null;
DockAcceptance acceptance = getController().getAcceptance();
// if mouse over window title: force combination
if( window != null && window.isWindowVisible() ){
DockTitle title = window.getDockTitle();
if( title != null ){
Component c = title.getComponent();
Point point = new Point( mouseX, mouseY );
SwingUtilities.convertPointFromScreen( point, c );
Dockable child = window.getDockable();
boolean combine = c.contains( point ) && acceptable( child, dockable );
if( combine ){
dropInfo = prepareCombine( dockable, window, new Point( mouseX, mouseY ), combine, Enforcement.HARD );
}
}
}
// maybe a parent station wants to catch the event
// if mouse over window: force combination
if( window != null && window.isWindowVisible() && dropInfo == null ){
Point point = new Point( mouseX, mouseY );
Dockable child = window.getDockable();
boolean combine = window.containsScreenPoint(point) && acceptable( child, dockable );
if( combine ){
dropInfo = prepareCombine( dockable, window, point, false, Enforcement.HARD );
}
}
if( dropInfo != null && dockable == getFrontDockable() )
return null;
if( dropInfo == null ){
if( dockable.accept( this ) &&
accept( dockable ) &&
acceptance.accept( this, dockable )){
dropInfo = new FlapDropInfo( this, dockable ){
public Point getMousePosition(){
return null;
}
public Dockable getOld(){
return null;
}
public DockableDisplayer getOldDisplayer(){
return null;
}
public PlaceholderMap getPlaceholders(){
return null;
}
public Dimension getSize(){
return null;
}
public boolean isMouseOverTitle(){
return false;
}
};
dropInfo.setIndex( buttonPane.indexAt( mouse.x, mouse.y ) );
}
}
if( dropInfo == null ){
return null;
}
return new FlapDropOperation( dropInfo, move );
}
/**
* Prepares a combination of <code>dockable</code> and <code>window</code>.
* @param dockable the element that is going to be dropped
* @param window the visible window under the mouse
* @param mouseOnScreen the location of the mouse on the screen
* @param mouseOverTitle whether the mouse is currently over a title
* @param force whether a combination must happen or not
* @return the combination, <code>null</code> if a combination is not desired for the given arguments
*/
private FlapDropInfo prepareCombine( Dockable dockable, FlapWindow window, final Point mouseOnScreen, final boolean mouseOverTitle, Enforcement force ){
final Dockable child = window.getDockable();
final DockableDisplayer displayer = window.getDisplayer();
FlapDropInfo info = new FlapDropInfo( this, dockable ){
public boolean isMouseOverTitle(){
return mouseOverTitle;
}
public Dimension getSize(){
return child.getComponent().getSize();
}
public PlaceholderMap getPlaceholders(){
for( DockablePlaceholderList<DockableHandle>.Item item : handles.list() ){
DockableHandle handle = item.getDockable();
if( handle != null && handle.getDockable() == child ){
return item.getPlaceholderMap();
}
}
return null;
}
public Point getMousePosition(){
Point mouse = new Point( mouseOnScreen );
SwingUtilities.convertPointFromScreen( mouse, getOld().getComponent() );
return mouse;
}
public Dockable getOld(){
return child;
}
public DockableDisplayer getOldDisplayer(){
return displayer;
}
};
CombinerTarget target = combiner.prepare( info, force );
if( target == null ){
return null;
}
info.setCombineTarget( target );
return info;
}
public void drop( Dockable dockable ) {
add( dockable );
}
public boolean drop( Dockable dockable, DockableProperty property ) {
if( property instanceof FlapDockProperty )
return drop( dockable, (FlapDockProperty)property );
return false;
}
/**
* Adds the {@link Dockable} <code>dockable</code> to this station or
* to a child of this station, according to the contents of
* <code>property</code>.
* @param dockable the new child
* @param property the location of the new child
* @return <code>true</code> if the new child could be added,
* <code>false</code> if the child has been rejected
*/
public boolean drop( final Dockable dockable, FlapDockProperty property ) {
DockUtilities.checkLayoutLocked();
boolean result = false;
final Path placeholder = property.getPlaceholder();
DockableProperty successor = property.getSuccessor();
int index = property.getIndex();
boolean acceptable = acceptable( dockable );
if( placeholder != null && successor != null ){
DockableHandle current = handles.getDockableAt( placeholder );
if( current != null ){
final Dockable oldDockable = current.getDockable();
DockStation station = oldDockable.asDockStation();
if( station != null ){
if( station.drop( dockable, successor )){
result = true;
handles.removeAll( placeholder );
}
}
else{
result = combine( current.getDockable(), dockable, successor );
}
}
}
if( placeholder != null && !result ){
int listIndex = handles.getListIndex( placeholder );
if( listIndex >= 0 ){
add( dockable, property.getIndex(), listIndex );
setHold( dockable, property.isHolding() );
int size = property.getSize();
if( size >= getWindowMinSize() )
setWindowSize( dockable, size );
result = true;
}
else{
index = handles.getDockableIndex( placeholder );
if( index == -1 ){
index = property.getIndex();
}
}
}
if( !result && index >= getDockableCount() && acceptable ){
add( dockable );
setHold( dockable, property.isHolding() );
int size = property.getSize();
if( size >= getWindowMinSize() )
setWindowSize( dockable, size );
result = true;
}
if( !result && successor != null ){
DockStation previous = getDockable( index ).asDockStation();
if( previous != null ){
if( previous.drop( dockable, successor )){
result = true;
}
}
else{
result = combine( getDockable( index ), dockable, successor );
}
}
if( !result && acceptable ){
add( dockable, index );
setHold( dockable, property.isHolding() );
int size = property.getSize();
if( size >= getWindowMinSize() )
setWindowSize( dockable, size );
result = true;
}
return result;
}
public DockableProperty getDockableProperty( Dockable dockable, Dockable target ) {
int index = indexOf( dockable );
boolean holding = isHold( dockable );
int size = getWindowSize( dockable );
PlaceholderStrategy strategy = getPlaceholderStrategy();
Path placeholder = null;
if( strategy != null ){
placeholder = strategy.getPlaceholderFor( target == null ? dockable : target );
if( placeholder != null ){
handles.dockables().addPlaceholder( index, placeholder );
}
}
return new FlapDockProperty( index, holding, size, placeholder );
}
public void aside( AsideRequest request ){
DockableProperty location = request.getLocation();
if( location instanceof FlapDockProperty ){
FlapDockProperty flapLocation = (FlapDockProperty)location;
DockablePlaceholderList<DockableHandle>.Item item = getItem( flapLocation );
if( item != null ){
delegate().combine( item, getCombiner(), request );
}
FlapDockProperty copy = flapLocation.copy();
copy.setSuccessor( null );
copy.setPlaceholder( request.getPlaceholder() );
request.answer( copy );
}
}
private DockablePlaceholderList<DockableHandle>.Item getItem( FlapDockProperty property ){
Path oldPlaceholder = property.getPlaceholder();
if( oldPlaceholder != null ){
DockablePlaceholderList<DockableHandle>.Item item = handles.getItem( oldPlaceholder );
if( item != null ){
return item;
}
}
if( property.getIndex() >= 0 && property.getIndex() < handles.dockables().size() ){
int index = handles.levelToBase( property.getIndex(), Level.DOCKABLE );
return handles.list().get( index );
}
else{
return null;
}
}
public void move( Dockable dockable, DockableProperty property ) {
DockUtilities.checkLayoutLocked();
if( property instanceof FlapDockProperty ){
int index = indexOf( dockable );
if( index < 0 )
throw new IllegalArgumentException( "dockable is not child of this station" );
int destination = ((FlapDockProperty)property).getIndex();
destination = Math.min( destination, handles.dockables().size()-1 );
destination = Math.max( 0, destination );
if( destination != index ){
handles.dockables().move( index, destination );
buttonPane.resetTitles();
fireDockablesRepositioned( Math.min( index, destination ), Math.max( index, destination ) );
}
}
}
/**
* Tells whether the point <code>x/y</code> is over the buttons of this station.
* @param x the x-coordinate on the screen
* @param y the y-coordinate on the screen
* @return <code>true</code> if the point <code>x/y</code> is over the buttons
*/
public boolean isOverButtons( int x, int y ){
Point mouse = new Point( x, y );
SwingUtilities.convertPointFromScreen( mouse, buttonPane );
return buttonPane.contains( mouse );
}
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" );
remove( dockable );
}
public String getFactoryID() {
return FlapDockStationFactory.ID;
}
public Component getComponent() {
return buttonPane;
}
public int getDockableCount() {
return handles.dockables().size();
}
public Dockable getDockable( int index ) {
return handles.dockables().get( index ).getDockable();
}
/**
* Gets the title which is used as button for the <code>index</code>'th dockable.
* Clients should not modify the result of this method.
* @param index the index of a {@link Dockable}
* @return the title or <code>null</code>
*/
public DockTitle getButton( int index ){
return handles.dockables().get( index ).getTitle();
}
@Override
public boolean isVisible( Dockable dockable ) {
return isStationShowing() && (getFrontDockable() == dockable);
}
/**
* Deletes all titles of the button pane and then recreates them.
*/
protected void recreateTitles(){
for( DockableHandle handle : handles.dockables() ){
handle.setTitle( buttonVersion );
}
}
/**
* Removes <code>dockable</code> from this station.<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 dockable the child to remove
*/
public void remove( Dockable dockable ){
int index = indexOf( dockable );
if( index >= 0 )
remove( index );
}
/**
* Removes the child with the given <code>index</code> from this station.<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 index of the child that will be removed
*/
public void remove( int index ){
DockUtilities.checkLayoutLocked();
Dockable dockable = getDockable( index );
if( getFrontDockable() == dockable )
setFrontDockable( null );
if( oldFrontDockable == dockable )
oldFrontDockable = null;
DockHierarchyLock.Token token = DockHierarchyLock.acquireUnlinking( this, dockable );
try{
listeners.fireDockableRemoving( dockable );
dockable.setDockParent( null );
DockableHandle handle = handles.dockables().get( index );
handles.remove( index );
handle.setTitle( null );
dockable.removeDockableListener( dockableListener );
// race condition, only required if not called from the EDT
buttonPane.resetTitles();
listeners.fireDockableRemoved( dockable );
}
finally{
token.release();
}
fireDockablesRepositioned( index );
}
/**
* Adds <code>dockable</code> as new child to this station. The child
* is added at the end of all children.
* @param dockable the new child
*/
public void add( Dockable dockable ){
add( dockable, getDockableCount() );
}
/**
* Inserts <code>dockable</code> as new child in the list of
* children.
* @param dockable the new child
* @param index the location in the button-panel of the child
*/
public void add( Dockable dockable, int index ){
add( dockable, index, -1 );
}
private void add( Dockable dockable, int index, int listIndex ){
DockUtilities.checkLayoutLocked();
DockUtilities.ensureTreeValidity( this, dockable );
DockHierarchyLock.Token token = DockHierarchyLock.acquireLinking( this, dockable );
try{
listeners.fireDockableAdding( dockable );
DockableHandle handle = link( dockable );
if( listIndex == -1 || handles.list().get( listIndex ).getDockable() != null ){
handles.dockables().add( index, handle );
}
else if( handles.list().get( listIndex ).getDockable() == null ){
handles.list().get( listIndex ).setDockable( handle );
}
dockable.setDockParent( this );
buttonPane.resetTitles(); // race condition, only required if not called from the EDT
listeners.fireDockableAdded( dockable );
fireDockablesRepositioned( index+1 );
if( getController().isFocused( dockable )){
setFrontDockable( dockable );
}
}
finally{
token.release();
}
}
private DockableHandle link( Dockable dockable ){
DockableHandle handle = createHandle( dockable );
handle.setTitle( buttonVersion );
dockable.addDockableListener( dockableListener );
return handle;
}
/**
* Creates a new wrapper around <code>dockable</code>, the wrapper is used as internal representation
* of <code>dockable</code>.
* @param dockable the element for which a new wrapper is created
* @return the new wrapper, must not be <code>null</code>
*/
protected DockableHandle createHandle( Dockable dockable ){
return new DockableHandle( dockable );
}
/**
* Gets the wrapper of <code>dockable</code>.
* @param dockable a child of this station
* @return the wrapper or <code>null</code> if not yet created or if
* <code>dockable</code> is not a child of this station
*/
protected DockableHandle getHandle( Dockable dockable ){
int index = indexOf( dockable );
if( index < 0 ){
return null;
}
return handles.dockables().get( index );
}
/**
* Creates a combination out of <code>child</code>, which must be a
* child of this station, and <code>append</code> which must not be
* a child of this station.
* @param child a child of this station
* @param append a {@link Dockable} that is not a child of this station
* @return <code>true</code> if the combination was successful,
* <code>false</code> otherwise (the <code>child</code> will remain
* on this station)
*/
public boolean combine( Dockable child, Dockable append ){
return combine( child, append, null );
}
/**
* Creates a combination out of <code>child</code>, which must be a
* child of this station, and <code>append</code> which must not be
* a child of this station.
* @param child a child of this station
* @param append a {@link Dockable} that is not a child of this station
* @param property location information associated with <code>append</code>
* @return <code>true</code> if the combination was successful,
* <code>false</code> otherwise (the <code>child</code> will remain
* on this station)
*/
public boolean combine( final Dockable child, Dockable append, DockableProperty property ){
DockUtilities.checkLayoutLocked();
int index = indexOf( child );
if( index < 0 )
throw new IllegalArgumentException( "Child must be a child of this station" );
int listIndex = handles.levelToBase( index, Level.DOCKABLE );
DockablePlaceholderList<DockableHandle>.Item oldItem = handles.list().get( listIndex );
final PlaceholderMap placeholders = oldItem.getPlaceholderMap();
FlapDropInfo info = new FlapDropInfo( this, append ){
public boolean isMouseOverTitle(){
return true;
}
public Dimension getSize(){
return null;
}
public PlaceholderMap getPlaceholders(){
return placeholders;
}
public Dockable getOld(){
return child;
}
public DockableDisplayer getOldDisplayer(){
return null;
}
public Point getMousePosition(){
return null;
}
};
CombinerTarget target = combiner.prepare( info, Enforcement.HARD );
return combine( info, target, property );
}
private boolean combine( CombinerSource source, CombinerTarget target, DockableProperty property ){
DockUtilities.checkLayoutLocked();
DockController controller = getController();
Dockable child = source.getOld();
Dockable append = source.getNew();
DockUtilities.ensureTreeValidity( this, append );
try{
if( controller != null )
controller.freezeLayout();
int index = indexOf( child );
if( index < 0 )
throw new IllegalArgumentException( "old dockable must be a child of this station" );
if( append.getDockParent() != null )
append.getDockParent().drag( append );
boolean hold = isHold( child );
int listIndex = handles.levelToBase( index, Level.DOCKABLE );
DockablePlaceholderList<DockableHandle>.Item oldItem = handles.list().get( listIndex );
final PlaceholderMap placeholders = oldItem.getPlaceholderMap();
oldItem.setPlaceholderMap( null );
remove( index );
int other = indexOf( append );
if( other >= 0 ){
remove( other );
if( other < index )
index--;
}
index = Math.min( index, getDockableCount());
Dockable combination = combiner.combine( new CombinerSourceWrapper( source ){
@Override
public PlaceholderMap getPlaceholders(){
return placeholders;
}
}, target );
if( property != null ){
DockStation combined = combination.asDockStation();
if( combined != null && append.getDockParent() == combined ){
combined.move( append, property );
}
}
add( combination, index );
listIndex = handles.levelToBase( index, Level.DOCKABLE );
DockablePlaceholderList<DockableHandle>.Item newItem = handles.list().get( listIndex );
newItem.setPlaceholderSet( newItem.getPlaceholderSet() );
setHold( combination, hold );
return true;
}
finally{
if( controller != null )
controller.meltLayout();
}
}
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 child, Dockable append ){
replace( child, append, false );
}
private void replace( Dockable child, Dockable append, boolean station ){
DockUtilities.checkLayoutLocked();
DockController controller = getController();
try{
if( controller != null )
controller.freezeLayout();
int index = indexOf( child );
if( index < 0 )
throw new IllegalArgumentException( "Child must be a child of this station" );
boolean hold = isHold( child );
boolean open = getFrontDockable() == child;
int listIndex = handles.levelToBase( index, Level.DOCKABLE );
DockablePlaceholderList<DockableHandle>.Item oldItem = handles.list().get( listIndex );
remove( index );
handles.list().remove( oldItem );
add( append, index );
DockablePlaceholderList<DockableHandle>.Item newItem = handles.list().get( listIndex );
if( station ){
newItem.setPlaceholderMap( child.asDockStation().getPlaceholders() );
}
else{
newItem.setPlaceholderMap( oldItem.getPlaceholderMap() );
}
newItem.setPlaceholderSet( oldItem.getPlaceholderSet() );
setHold( append, hold );
if( open )
setFrontDockable( append );
}
finally{
if( controller != null )
controller.meltLayout();
}
}
/**
* Gets the location of <code>dockable</code> in the button-panel.
* @param dockable the {@link Dockable} to search
* @return the location or -1 if the child was not found
*/
public int indexOf( Dockable dockable ){
PlaceholderList.Filter<DockableHandle> list = handles.dockables();
int index = 0;
for( DockableHandle handle : list ){
if( handle.getDockable() == dockable ){
return index;
}
index++;
}
return -1;
}
private void checkShowing(){
boolean showing = isDockableShowing();
if( showing != lastShowing ){
lastShowing = showing;
if( showing ){
if( oldFrontDockable != null )
setFrontDockable( oldFrontDockable );
}
else{
oldFrontDockable = getFrontDockable();
setFrontDockable( null );
if( !isHold( oldFrontDockable ))
oldFrontDockable = null;
}
showingManager.fire();
}
}
/**
* This listener is added to the direct parent of the enclosing
* {@link FlapDockStation}. The listener fires events if the visibility
* changes, and the listener can remove the popup-window if the station
* looses its visibility.
* @author Benjamin Sigg
*/
private class VisibleListener extends DockStationAdapter{
@Override
public void dockableShowingChanged( DockStation station, Dockable dockable, boolean visible ) {
if( dockable == FlapDockStation.this ){
checkShowing();
}
}
}
/**
* Listener added to the {@link Dockable}s of the enclosing
* {@link FlapDockStation}, reacts on changes of the {@link DockTitle}.
* @author Benjamin Sigg
*/
private class Listener extends DockableAdapter{
@Override
public void titleExchanged( Dockable dockable, DockTitle title ) {
int index = indexOf( dockable );
if( index < 0 )
return;
DockableHandle handle = handles.dockables().get( index );
if( handle.getTitle() == title ){
handle.setTitle( buttonVersion );
}
}
}
/**
* Custom implementation of {@link StationDropOperation}.
* @author Benjamin Sigg
*/
protected class FlapDropOperation implements StationDropOperation{
private FlapDropInfo dropInfo;
private boolean move;
/**
* Creates a new operation.
* @param dropInfo the location information of the dropped {@link Dockable}
* @param move whether this is a move operation
*/
public FlapDropOperation( FlapDropInfo dropInfo, boolean move ){
if( dropInfo == null ){
throw new IllegalArgumentException( "dropInfo must not be null" );
}
this.dropInfo = dropInfo;
this.move = move;
}
public boolean isMove(){
return move;
}
public void draw(){
setDropInfo( dropInfo );
}
public void destroy( StationDropOperation next ){
if( FlapDockStation.this.dropInfo == dropInfo ){
if( next == null || !(next instanceof FlapDropOperation) || next.getTarget() != getTarget() ){
setDropInfo( null );
}
}
}
public Dockable getItem(){
return dropInfo.getDockable();
}
public DockStation getTarget(){
return FlapDockStation.this;
}
public CombinerTarget getCombination(){
return dropInfo.getCombineTarget();
}
public DisplayerCombinerTarget getDisplayerCombination(){
CombinerTarget target = getCombination();
if( target == null ){
return null;
}
return target.getDisplayerCombination();
}
public void execute(){
if( isMove() ){
move();
}
else{
drop();
}
}
public void move() {
if( dropInfo.getCombineTarget() != null ){
remove( dropInfo.getDockable() );
combine( dropInfo, dropInfo.getCombineTarget(), null );
}
else{
int index = indexOf( dropInfo.getDockable() );
if( index < dropInfo.getIndex() ){
dropInfo.setIndex( dropInfo.getIndex()-1 );
}
handles.dockables().move( index, dropInfo.getIndex() );
buttonPane.resetTitles();
fireDockablesRepositioned( Math.min( index, dropInfo.getIndex() ), Math.max( index, dropInfo.getIndex() ) );
}
}
public void drop(){
if( dropInfo.getCombineTarget() != null ){
combine( dropInfo, dropInfo.getCombineTarget(), null );
}
else{
add( dropInfo.getDockable(), dropInfo.getIndex() );
}
}
}
/**
* Handles title, listeners and actions that are associated with a {@link Dockable}.
* @author Benjamin Sigg
*/
protected class DockableHandle implements PlaceholderListItem<Dockable>{
/** the element that is handled by this handler */
private Dockable dockable;
/** the title used */
private DockTitleRequest title;
/** the listener that gets added to the title of this handle */
private ButtonListener buttonListener;
/** the actions added by this station to {@link #dockable} */
private FlapDockStationSource actions;
/**
* Creates a new wrapper around <code>dockable</code>
* @param dockable the dockable to wrap
*/
public DockableHandle( Dockable dockable ){
this( dockable, false );
}
/**
* Creates a new wrapper around <code>dockable</code>
* @param dockable the dockable to wrap
* @param forceActionSourceCreation whether {@link #getActions()} must always return a value other
* than <code>null</code>
*/
public DockableHandle( Dockable dockable, boolean forceActionSourceCreation ){
this.dockable = dockable;
buttonListener = new ButtonListener( dockable );
if( holdAction != null || forceActionSourceCreation ){
actions = new FlapDockStationSource( FlapDockStation.this, dockable, holdAction );
actions.updateHoldSwitchable();
}
}
/**
* Gets the {@link DockActionSource} that should be shown on the {@link Dockable}.
* @return the action source, can be <code>null</code>
*/
public FlapDockStationSource getActions(){
return actions;
}
/**
* Sets the action of {@link #getActions()} back to the action that was created
* by {@link FlapDockStation#createHoldAction()}.
*/
public void resetHoldAction(){
if( actions != null ){
actions.setHoldAction( holdAction );
}
}
public Dockable getDockable(){
return dockable;
}
public Dockable asDockable(){
return getDockable();
}
public DockTitle getTitle(){
if( title == null )
return null;
return title.getAnswer();
}
public void setTitle( DockTitleVersion version ){
if( title != null ){
DockTitle answer = title.getAnswer();
if( answer != null ){
answer.removeMouseInputListener( buttonListener );
dockable.unbind( answer );
buttonPane.resetTitles();
}
title.uninstall();
title = null;
}
if( version != null ){
title = new DockTitleRequest( FlapDockStation.this, dockable, version ) {
@Override
protected void answer( DockTitle previous, DockTitle title ){
if( previous != null ){
previous.removeMouseInputListener( buttonListener );
dockable.unbind( previous );
}
if( title != null ){
title.addMouseInputListener( buttonListener );
title.setOrientation( orientation( direction ) );
dockable.bind( title );
}
buttonPane.resetTitles();
}
};
title.install();
title.request();
}
}
}
private class ControllerListener implements FocusVetoListener, DockableFocusListener{
public FocusVeto vetoFocus( FocusController controller, Dockable dockable ){
return FocusVeto.NONE;
}
public FocusVeto vetoFocus( FocusController controller, DockTitle title ){
for( DockableHandle handle : handles.dockables() ){
if( handle.getTitle() == title ){
return FocusVeto.VETO_NO_CONSUME;
}
}
return FocusVeto.NONE;
}
public void dockableFocused( DockableFocusEvent event ) {
Dockable front = getFrontDockable();
if( isStationShowing() ){
if( front == null || (front != null && isHold( front )))
return;
DockController controller = event.getController();
Dockable dockable = event.getNewFocusOwner();
if( controller.isFocused( FlapDockStation.this ))
return;
if( dockable == null || !DockUtilities.isAncestor( FlapDockStation.this, dockable ) ){
setFrontDockable( null );
}
}
}
}
/**
* Listens to the buttons. If one button is pressed, the popup-window
* will be made visible.
*/
private class ButtonListener extends MouseInputAdapter{
/**
* The <code>Dockable</code> whose button is observed by this
* listener.
*/
private Dockable dockable;
/**
* Constructs a new listener.
* @param dockable the owner of the observed button
*/
public ButtonListener( Dockable dockable ){
this.dockable = dockable;
}
@Override
public void mouseReleased( MouseEvent e ){
if( dockable.getDockParent() == FlapDockStation.this ){
final int MASK = InputEvent.BUTTON1_DOWN_MASK | InputEvent.BUTTON2_DOWN_MASK | InputEvent.BUTTON3_DOWN_MASK;
DisablingStrategy strategy = disablingStrategy.getValue();
boolean enabled = strategy == null || (!strategy.isDisabled( dockable ) && !strategy.isDisabled( FlapDockStation.this ));
if( enabled && e.getButton() == MouseEvent.BUTTON1 && (e.getModifiersEx() & MASK ) == 0 ){
int index = indexOf( dockable );
if( index < 0 )
return;
DockableHandle handle = handles.dockables().get( index );
DockTitle title = handle.getTitle();
if( getFrontDockable() == dockable && title.isActive() ){
getController().setFocusedDockable( new DefaultFocusRequest( FlapDockStation.this, null, true ));
setFrontDockable( null );
}
else
getController().setFocusedDockable( new DefaultFocusRequest( dockable, null, true ));
}
}
}
}
/**
* The background algorithm of this {@link FlapDockStation}.
* @author Benjamin Sigg
*/
private class Background extends BackgroundAlgorithm implements StationBackgroundComponent{
/**
* Creates a new algorithm
*/
public Background(){
super( StationBackgroundComponent.KIND, ThemeManager.BACKGROUND_PAINT + ".station.flap" );
}
public DockStation getStation(){
return FlapDockStation.this;
}
public Component getComponent(){
return FlapDockStation.this.getComponent();
}
}
}