/*
* Bibliothek - DockingFrames
* Library built on Java/Swing, allows the user to "drag and drop"
* panels containing any Swing-Component the developer likes to add.
*
* Copyright (C) 2007 Benjamin Sigg
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Benjamin Sigg
* benjamin_sigg@gmx.ch
* CH - Switzerland
*/
package bibliothek.gui.dock.station.split;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.util.Map;
import javax.swing.JComponent;
import bibliothek.gui.DockController;
import bibliothek.gui.DockStation;
import bibliothek.gui.Dockable;
import bibliothek.gui.dock.DockHierarchyLock;
import bibliothek.gui.dock.SplitDockStation;
import bibliothek.gui.dock.accept.DockAcceptance;
import bibliothek.gui.dock.layout.DockableProperty;
import bibliothek.gui.dock.layout.location.AsideAnswer;
import bibliothek.gui.dock.layout.location.AsideRequest;
import bibliothek.gui.dock.station.DockableDisplayer;
import bibliothek.gui.dock.station.StationChildHandle;
import bibliothek.gui.dock.station.span.Span;
import bibliothek.gui.dock.station.split.PutInfo.Put;
import bibliothek.gui.dock.station.support.PlaceholderMap;
import bibliothek.gui.dock.station.support.PlaceholderStrategy;
import bibliothek.gui.dock.title.DockTitle;
import bibliothek.util.Path;
/**
* Represents a leaf in the tree that is the structure of a {@link SplitDockStation}.
* A leaf also represents a single {@link Dockable} which is shown
* on the owner-station.
* @author Benjamin Sigg
*/
public class Leaf extends SpanSplitNode{
/** Information about the element that is shown by this leaf */
private StationChildHandle handle;
/**
* Creates a new leaf.
* @param access the access to the private functions of the owning {@link SplitDockStation}
*/
public Leaf( SplitDockAccess access ){
this( access, -1 );
}
/**
* Creates a new leaf.
* @param access the access to the private functions of the owning {@link SplitDockStation}
* @param id the unique id of this leaf
*/
public Leaf( SplitDockAccess access, long id ){
super( access, id );
}
/**
* Sets the element of this leaf.
* @param handle the element
*/
public void setHandle( StationChildHandle handle ){
this.handle = handle;
}
@Override
public Dimension getMinimumSize() {
SplitDockStation station = getStation();
DockableDisplayer displayer = null;
if( handle != null ){
displayer = handle.getDisplayer();
}
if( displayer == null ){
Dimension minimum;
if( station == null ){
minimum = new Dimension( 0, 0 );
}
else{
minimum = station.getMinimumLeafSize();
}
return minimum;
}
return displayer.getComponent().getMinimumSize();
}
@Override
public Dimension getPreferredSize(){
if( handle == null )
return new Dimension( 0, 0 );
DockableDisplayer displayer = handle.getDisplayer();
if( displayer == null )
return new Dimension( 0, 0 );
return displayer.getComponent().getPreferredSize();
}
@Override
public int getChildLocation( SplitNode child ) {
return -1;
}
@Override
public void setChild( SplitNode child, int location ) {
throw new IllegalStateException( "can't add children to a leaf" );
}
@Override
public int getMaxChildrenCount(){
return 0;
}
@Override
public SplitNode getChild( int location ){
return null;
}
/**
* Sets the element of this leaf. This method ensures that <code>dockable</code>
* is registered in the {@link DockStation}
* @param dockable the new element or <code>null</code> to remove the
* old {@link Dockable}
* @param token if <code>null</code>, then a token will be acquired by this method
* and this method will fire events, otherwise this methods is executed silently. Clients should
* use <code>null</code>.
*/
public void setDockable( Dockable dockable, DockHierarchyLock.Token token ){
setDockable( dockable, token, true, false );
}
/**
* Sets the element of this leaf. This method ensures that <code>dockable</code>
* is registered in the {@link DockStation}
* @param dockable the new element or <code>null</code> to remove the
* old {@link Dockable}
* @param token if <code>null</code>, then a token will be acquired by this method
* and this method will fire events, otherwise this methods is executed silently
* @param updatePlaceholders if <code>true</code>, the placeholder list of this leaf is
* automatically updated
* @param storePlaceholderMap if <code>true</code>, the current {@link PlaceholderMap} is
* replaced by the map provided by the current {@link Dockable} which is a {@link DockStation}
*/
public void setDockable( Dockable dockable, DockHierarchyLock.Token token, boolean updatePlaceholders, boolean storePlaceholderMap ){
if( handle != null ){
if( updatePlaceholders ){
getAccess().getPlaceholderSet().set( this, handle.getDockable() );
}
if( storePlaceholderMap ){
DockStation station = handle.getDockable().asDockStation();
if( station == null ){
throw new IllegalStateException( "no station as child but storePlaceholderMap is set" );
}
setPlaceholderMap( station.getPlaceholders() );
}
getAccess().removeHandle( handle, token );
handle = null;
}
if( dockable != null ){
handle = getAccess().newHandle( dockable );
if( updatePlaceholders ){
getAccess().getPlaceholderSet().set( this, dockable );
}
getAccess().addHandle( handle, token );
}
treeChanged();
}
/**
* Gets the {@link Dockable} which is shown on the displayer
* of this leaf.
* @return the Dockable
*/
public Dockable getDockable() {
return handle == null ? null : handle.getDockable();
}
/**
* Gets the displayer of this leaf.
* @return the displayer
*/
public DockableDisplayer getDisplayer(){
return handle == null ? null : handle.getDisplayer();
}
/**
* Gets the handle which is responsible for the current {@link Dockable}.
* @return the handle, might be <code>null</code>
*/
public StationChildHandle getDockableHandle(){
return handle;
}
@Override
public boolean isVisible(){
return true;
}
@Override
public SplitNode getVisible(){
return this;
}
@Override
public boolean isOfUse(){
if( !getAccess().isTreeAutoCleanupEnabled() ){
return true;
}
return handle != null || hasPlaceholders();
}
/**
* Disconnects this leaf from its {@link Dockable}. This leaf either deletes
* itself or replaces itself with a {@link Placeholder}.
* @param keepCurrent if <code>true</code>, the placeholder of the current
* {@link Dockable} is added to the set of the placeholders, otherwise the placeholder
* is removed.
*/
public void placehold( boolean keepCurrent ){
Dockable dockable = getDockable();
if( dockable != null ){
SplitDockAccess access = getAccess();
PlaceholderStrategy strategy = access.getOwner().getPlaceholderStrategy();
if( strategy != null ){
updatePlaceholders( dockable, keepCurrent, strategy );
}
DockStation station = dockable.asDockStation();
if( station != null && keepCurrent ){
setPlaceholderMap( station.getPlaceholders() );
}
}
if( hasPlaceholders() ){
Placeholder placeholder = createPlaceholder( getId() );
placeholder.setPlaceholders( getPlaceholders() );
movePlaceholderMap( placeholder );
replace( placeholder );
}
else{
delete( true );
}
}
private void updatePlaceholders( Dockable dockable, boolean keep, PlaceholderStrategy strategy ){
Path placeholder = strategy.getPlaceholderFor( dockable );
if( placeholder != null ){
if( !keep ){
getAccess().getPlaceholderSet().set( null, placeholder );
}
else {
getAccess().getPlaceholderSet().set( this, placeholder );
}
}
DockStation station = dockable.asDockStation();
if( station != null ){
for( int i = 0, n = station.getDockableCount(); i<n; i++ ){
updatePlaceholders( station.getDockable( i ), keep, strategy );
}
}
}
@Override
public void updateBounds( double x, double y, double width, double height, double factorW, double factorH, boolean components ) {
super.updateBounds( x, y, width, height, factorW, factorH, components );
if( components ){
resetDisplayerBounds();
}
}
@Override
public void setBounds( double x, double y, double width, double height, double factorW, double factorH, boolean updateComponentBounds ){
super.setBounds( x, y, width, height, factorW, factorH, updateComponentBounds );
if( updateComponentBounds ){
resetDisplayerBounds();
}
}
@Override
public void onSpanResize(){
resetDisplayerBounds();
}
/**
* Resets the boundaries of the {@link DockableDisplayer} of this {@link Leaf}, using the current {@link Span}s
* as well.
*/
public void resetDisplayerBounds(){
DockableDisplayer displayer = getDisplayer();
StationChildHandle fullscreen = getAccess().getFullScreenDockable();
if( displayer != null && (fullscreen == null || displayer != fullscreen.getDisplayer() )){
Rectangle bounds = getBounds();
bounds = getAccess().getSpanStrategy().modifyBounds( bounds, this );
Component component = displayer.getComponent();
component.setBounds( bounds );
if( component instanceof JComponent ){
((JComponent)component).revalidate();
}
}
}
@Override
public PutInfo getPut( int x, int y, double factorW, double factorH, Dockable drop ) {
DockableDisplayer displayer = getDisplayer();
if( displayer == null )
return null;
Rectangle bounds = getBounds();
PutInfo result = null;
boolean centered = false;
bounds = removeTitle( bounds );
if( isTitlePut( bounds, x, y )){
centered = true;
result = getAccess().validatePutInfo( new PutInfo( this, PutInfo.Put.TITLE, drop, true ));
}
if( result != null )
return result;
if( isCenterPut( bounds, x, y )){
centered = true;
result = getAccess().validatePutInfo( new PutInfo( this, PutInfo.Put.CENTER, drop, true ));
}
if( result != null )
return result;
result = createSidePut( bounds, x, y, drop, centered );
if( result != null )
return result;
return getAccess().validatePutInfo( new PutInfo( this, PutInfo.Put.CENTER, drop, centered ));
}
/**
* Asks the {@link DockableDisplayer} for its current {@link DockTitle} and removes the size
* of the title from <code>bounds</code>. Depending on the position of the title <code>bounds</code>
* is moved, shrunk only horizontally or vertically.
* @param bounds some boundaries, usually describing the boundaries of this {@link Leaf} but
* any {@link Rectangle} can be modified by this method
* @return a copy of <code>bounds</code> where the size of the title (if there is any) has been removed
*/
protected Rectangle removeTitle( Rectangle bounds ){
DockableDisplayer displayer = getDisplayer();
bounds = new Rectangle( bounds );
if( displayer.getTitle() != null ){
if( displayer.getTitleLocation() == DockableDisplayer.Location.TOP ){
int height = displayer.getTitle().getComponent().getHeight();
bounds.y += height;
bounds.height -= height;
}
else if( displayer.getTitleLocation() == DockableDisplayer.Location.BOTTOM ){
int height = displayer.getTitle().getComponent().getHeight();
bounds.height -= height;
}
else if( displayer.getTitleLocation() == DockableDisplayer.Location.LEFT ){
int width = displayer.getTitle().getComponent().getWidth();
bounds.x += width;
bounds.width -= width;
}
else if( displayer.getTitleLocation() == DockableDisplayer.Location.RIGHT ){
int width = displayer.getTitle().getComponent().getWidth();
bounds.width -= width;
}
}
return bounds;
}
/**
* Tells whether the position of the mouse <code>x/y</code> would result in a drag and drop operation where
* {@link Put#CENTER} is appropriate.
* @param bounds the boundaries of the {@link Dockable}, this {@link Leaf} or any other representation of
* the {@link Dockable}
* @param x the x-coordinate of the mouse
* @param y the y-coordinate of the mouse
* @return whether the location of the mouse would allow a combination of the {@link Dockable}s
*/
protected boolean isCenterPut( Rectangle bounds, int x, int y ){
float sideSnapSize = getAccess().getOwner().getSideSnapSize();
return x > bounds.x + sideSnapSize*bounds.width &&
x < bounds.x + bounds.width - sideSnapSize*bounds.width &&
y > bounds.y + sideSnapSize*bounds.height &&
y < bounds.y + bounds.height - sideSnapSize*bounds.height;
}
/**
* Tells whether the position of the mouse <code>x/y</code> would result in a drag and drop operation where
* {@link Put#TITLE} is appropriate.
* @param bounds the boundaries of the {@link Dockable}, this {@link Leaf} or any other representation of
* the {@link Dockable}
* @param x the x-coordinate of the mouse
* @param y the y-coordinate of the mouse
* @return whether the location of the mouse would allow a combination of the {@link Dockable}s
*/
protected boolean isTitlePut( Rectangle bounds, int x, int y ){
DockableDisplayer displayer = getDisplayer();
if( displayer.getTitle() != null ){
if( displayer.getTitleLocation() == DockableDisplayer.Location.TOP ){
return y <= bounds.y;
}
else if( displayer.getTitleLocation() == DockableDisplayer.Location.BOTTOM ){
return y >= bounds.y+bounds.height;
}
else if( displayer.getTitleLocation() == DockableDisplayer.Location.LEFT ){
return x <= bounds.x;
}
else if( displayer.getTitleLocation() == DockableDisplayer.Location.RIGHT ){
return x >= bounds.x + bounds.width;
}
}
return false;
}
/**
* Assuming the mouse at <code>x/y</code> is within <code>bounds</code>, this method calculates which one
* of the non-combining {@link Put}s describe the situation best. The method creates and validates a new
* {@link PutInfo}.
* @param bounds the boundaries of the {@link Dockable}, the {@link Leaf} or any other representation of the
* {@link Dockable}.
* @param x the x-coordinate of the mouse
* @param y the y-coordinate of the mouse
* @param drop the item that is about to be dropped
* @param centered whether the mouse position alone would usually require one of the combining {@link Put}s
* @return the new drag and drop operation, or <code>null</code> if the suggested operation is not valid
*/
protected PutInfo createSidePut( Rectangle bounds, int x, int y, Dockable drop, boolean centered ){
if( above( bounds.x, bounds.y, bounds.x + bounds.width, bounds.y + bounds.height, x, y )){
if( above( bounds.x, bounds.y + bounds.height, bounds.x + bounds.width, bounds.y, x, y ))
return getAccess().validatePutInfo( new PutInfo( this, PutInfo.Put.TOP, drop, centered ));
else
return getAccess().validatePutInfo( new PutInfo( this, PutInfo.Put.RIGHT, drop, centered ));
}
else{
if( above( bounds.x, bounds.y + bounds.height, bounds.x + bounds.width, bounds.y, x, y ))
return getAccess().validatePutInfo( new PutInfo( this, PutInfo.Put.LEFT, drop, centered ));
else
return getAccess().validatePutInfo( new PutInfo( this, PutInfo.Put.BOTTOM, drop, centered ));
}
}
@Override
public boolean isInOverrideZone( int x, int y, double factorW, double factorH ){
float sideSnapSize = getAccess().getOwner().getSideSnapSize();
Rectangle bounds = getBounds();
if( x > bounds.x + sideSnapSize*bounds.width &&
x < bounds.x + bounds.width - sideSnapSize*bounds.width &&
y > bounds.y + sideSnapSize*bounds.height &&
y < bounds.y + bounds.height - sideSnapSize*bounds.height ){
return false;
}
return true;
}
@Override
public void evolve( SplitDockTree<Dockable>.Key key, boolean checkValidity, Map<Leaf, Dockable> linksToSet ){
setPlaceholders( key.getTree().getPlaceholders( key ) );
setPlaceholderMap( key.getTree().getPlaceholderMap( key ) );
}
@Override
public boolean insert( SplitDockPlaceholderProperty property, Dockable dockable ){
Path placeholder = property.getPlaceholder();
if( hasPlaceholder( placeholder )){
// try to melt with child
DockStation station = getDockable().asDockStation();
DockableProperty stationLocation = property.getSuccessor();
if( station != null && stationLocation != null ){
if( dockable.accept( station ) && station.accept( dockable )){
DockController controller = getAccess().getOwner().getController();
DockAcceptance acceptance = controller == null ? null : controller.getAcceptance();
if( acceptance == null || acceptance.accept( station, dockable )){
boolean done = station.drop( dockable, stationLocation );
if( done ){
getAccess().getPlaceholderSet().set( null, placeholder, this );
return true;
}
}
}
}
// try using the theoretical boundaries of the element
SplitDockProperty selfLocation = new SplitDockProperty( getX(), getY(), getWidth(), getHeight() );
selfLocation.setSuccessor( property.getSuccessor() );
boolean done = getAccess().drop( dockable, selfLocation, this );
if( done ){
removePlaceholder( placeholder );
}
return done;
}
return false;
}
@Override
public boolean aside( AsideRequest request ){
if( request.getPlaceholder() != null ){
addPlaceholder( request.getPlaceholder() );
DockStation station = getDockable().asDockStation();
if( station == null ){
AsideAnswer answer = request.forward( getStation().getCombiner(), getPlaceholderMap() );
if( answer.isCanceled() ){
return false;
}
setPlaceholderMap( answer.getLayout() );
}
else{
AsideAnswer answer = request.forward( station );
if( answer.isCanceled() ){
return false;
}
}
}
return true;
}
@Override
public boolean aside( SplitDockPathProperty property, int index, AsideRequest request ){
if( request.getPlaceholder() != null ){
if( index < property.size() ){
DockStation station = getDockable().asDockStation();
if( station == null ){
Placeholder placeholder = createPlaceholder( property.getLeafId() );
split( property, index, placeholder );
return placeholder.aside( request );
}
}
else{
return aside( request );
}
}
return true;
}
@Override
public boolean insert( SplitDockPathProperty property, int depth, Dockable dockable ) {
if( depth < property.size() ){
// split up the leaf
Leaf leaf = create( dockable, property.getLeafId() );
if( leaf == null ){
return false;
}
split( property, depth, leaf );
leaf.setDockable( dockable, null );
return true;
}
else{
// try to melt with child
DockStation station = getDockable().asDockStation();
DockableProperty stationLocation = property.getSuccessor();
if( station != null && stationLocation != null ){
if( dockable.accept( station ) && station.accept( dockable )){
DockController controller = getAccess().getOwner().getController();
DockAcceptance acceptance = controller == null ? null : controller.getAcceptance();
if( acceptance == null || acceptance.accept( station, dockable )){
boolean done = station.drop( dockable, stationLocation );
if( done )
return true;
}
}
}
// try using the theoretical boundaries of the element
return getAccess().drop( dockable, property.toLocation( this ), this );
}
}
@Override
public <N> N submit( SplitTreeFactory<N> factory ){
PlaceholderMap map = getPlaceholderMap();
if( map == null ){
Dockable dockable = getDockable();
if( dockable != null ){
DockStation station = dockable.asDockStation();
if( station != null ){
map = station.getPlaceholders();
}
}
}
return factory.leaf( getDockable(), getId(), getPlaceholders(), map );
}
@Override
public Leaf getLeaf( Dockable dockable ) {
Dockable mine = getDockable();
if( mine != null && dockable == getDockable() ){
return this;
}
else{
return null;
}
}
@Override
public Node getDividerNode( int x, int y ){
return null;
}
@Override
public void visit( SplitNodeVisitor visitor ) {
visitor.handleLeaf( this );
}
@Override
public void toString( int tabs, StringBuilder out ) {
Dockable dockable = getDockable();
out.append( "Leaf[ " );
if( dockable != null ){
out.append( dockable.getTitleText() );
out.append( ", " );
}
out.append( "placeholders={" );
Path[] placeholders = getPlaceholders();
if( placeholders != null ){
for( int i = 0; i < placeholders.length; i++ ){
if( i > 0 ){
out.append( ", " );
}
out.append( placeholders[i].toString() );
}
}
out.append( "}, " );
out.append( "id=" );
out.append( getId() );
out.append( ", bounds=" );
out.append( getBounds() );
out.append( " ]" );
}
}