/*
* 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) 2009 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.facile.mode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import bibliothek.gui.DockController;
import bibliothek.gui.DockStation;
import bibliothek.gui.Dockable;
import bibliothek.gui.dock.action.DockActionSource;
import bibliothek.gui.dock.layout.location.AsideAnswer;
import bibliothek.gui.dock.layout.location.AsideRequest;
import bibliothek.gui.dock.support.mode.AffectedSet;
import bibliothek.gui.dock.support.mode.Mode;
import bibliothek.gui.dock.support.mode.ModeManager;
import bibliothek.gui.dock.support.mode.ModeManagerListener;
/**
* This abstract class offers various properties that may be useful for any implementation
* of {@link LocationMode}. It also allows to store a set of {@link ModeArea}s. Basic methods
* to verify and change the {@link Mode} of a {@link Dockable} are implemented too.
* @author Benjamin Sigg
* @param <A> the values managed by this mode
*/
public abstract class AbstractLocationMode<A extends ModeArea> implements Iterable<A>, LocationMode{
/** The areas managed by this mode */
private Map<String, A> areas = new HashMap<String, A>();
/** The order in which the areas were added */
private List<A> areaOrder = new LinkedList<A>();
/** default location to use when a key is not found in {@link #areas} */
private A defaultArea;
/** the manager responsible for this mode */
private LocationModeManager<?> manager;
/** the list of known listeners */
private List<LocationModeListener> listeners = new ArrayList<LocationModeListener>();
/** the controller in whose realm this mode works */
private DockController controller;
/** added to all {@link ModeArea}s of this mode */
private AreaListener modeAreaListener = new AreaListener();
/** listener to register new dockables and remove old ones */
private ModeManagerListener<Location, LocationMode> managerListener = new ManagerListener();
/** temporary information associated with the currently registered Dockables */
private Map<Dockable, DockableHandle> handles = new HashMap<Dockable, DockableHandle>();
/** provides actions for the {@link Dockable}s known to this mode */
private LocationModeActionProvider actionProvider = new DefaultLocationModeActionProvider();
/** whether focus should be automatically transferred */
private boolean autoFocus = true;
/**
* Sets the {@link LocationModeActionProvider} for this mode.
* @param actionProvider the provider, not <code>null</code>
* @throws IllegalArgumentException if <code>actionProvider</code> is <code>null</code>
* @throws IllegalStateException if there are already {@link Dockable}s which show actions
* that are provided by the current {@link LocationModeActionProvider}
*/
public void setActionProvider( LocationModeActionProvider actionProvider ){
if( actionProvider == null )
throw new IllegalArgumentException( "actionProvider must not be null" );
if( !handles.isEmpty() )
throw new IllegalStateException( "can only set actionProvider if no Dockables are currently showing actions of the old provider" );
this.actionProvider = actionProvider;
}
public void setManager( LocationModeManager<?> manager ){
if( this.manager != null ){
for( A area : areas.values() ){
area.removeModeAreaListener( modeAreaListener );
}
}
this.manager = manager;
if( this.manager != null ){
for( A area : areas.values() ){
area.addModeAreaListener( modeAreaListener );
}
this.manager.addModeManagerListener( managerListener );
}
}
/**
* Gets the owner of this mode.
* @return the owner, not <code>null</code>
*/
public LocationModeManager<?> getManager(){
return manager;
}
public void setController( DockController controller ){
this.controller = controller;
for( A area : areas.values() ){
area.setController( controller );
}
}
/**
* Gets the controller in whose realm this mode works.
* @return the controller or <code>null</code>
*/
public DockController getController(){
return controller;
}
public void addLocationModeListener( LocationModeListener listener ){
if( listener == null )
throw new IllegalArgumentException( "listener must not be null" );
listeners.add( listener );
}
public void removeLocationModeListener( LocationModeListener listener ){
listeners.remove( listener );
}
/**
* Gets all the listeners that are currently registered at this mode.
* @return all the listeners
*/
protected LocationModeListener[] listeners(){
return listeners.toArray( new LocationModeListener[ listeners.size() ] );
}
public boolean shouldAutoFocus(){
return autoFocus;
}
/**
* Sets the result of {@link #shouldAutoFocus()}.
* @param autoFocus whether automatic focus transfer to {@link Dockable} in this mode
* should be allowed
*/
public void setShouldAutoFocus( boolean autoFocus ){
this.autoFocus = autoFocus;
}
/**
* Adds an area to this mode.
* @param area the new area, not <code>null</code>
*/
public void add( A area ){
if( area == null )
throw new IllegalArgumentException( "area must not be null" );
String key = area.getUniqueId();
if( areas.containsKey( key ))
throw new IllegalArgumentException( "key '" + key + "' already in use" );
area.setController( getController() );
area.setMode( this );
areas.put( key, area );
areaOrder.add( area );
if( getManager() != null ){
area.addModeAreaListener( modeAreaListener );
}
}
/**
* Removes the area with identifier <code>key</code> from this
* mode.
* @param key the identifier of the area
* @return the removed area or <code>null</code>
*/
public A remove( String key ){
A area = areas.remove( key );
if( defaultArea == area ){
defaultArea = null;
}
if( area != null ){
area.setController( null );
area.setMode( null );
area.removeModeAreaListener( modeAreaListener );
areaOrder.remove( area );
}
return area;
}
public Iterator<A> iterator(){
List<A> copy = new ArrayList<A>( areas.values() );
return copy.iterator();
}
/**
* Sets the default area of this mode. The default area is used when
* {@link #get(Dockable)} returns <code>null</code> for some key.
* @param defaultArea the default area, can be <code>null</code>. Must be
* registered using {@link #add(ModeArea)} first.
*/
public void setDefaultArea( A defaultArea ){
if( defaultArea != null ){
if( !areas.containsKey( defaultArea.getUniqueId() ))
throw new IllegalArgumentException( "default area must be registered, call 'add' first" );
}
this.defaultArea = defaultArea;
}
/**
* Gets the default area of this mode, can be <code>null</code>. The default area
* is the oldest area that was added to this mode and whose property
* {@link ModeArea#autoDefaultArea()} is <code>true</code>, or the one area set
* through {@link #setDefaultArea(ModeArea)}.
* @return the default area
*/
public A getDefaultArea(){
if( defaultArea == null ){
for( A area : areaOrder ){
if( area.autoDefaultArea() ){
return area;
}
}
}
return defaultArea;
}
/**
* Gets the area with the specified id.
* @param key the name of the area
* @return the area or <code>null</code>
*/
public A get( String key ){
return areas.get( key );
}
public DockStation getRepresentation( String uniqueId ){
A area = get( uniqueId );
if( area == null )
return null;
return area.getStation();
}
public Set<String> getRepresentationIds() {
return Collections.unmodifiableSet( areas.keySet() );
}
/**
* Recursively searches through all stations of <code>dockable</code>
* until a station is found that is registered at this mode.
* @param dockable the element whose root is searched
* @return the root or <code>null</code>, never <code>dockable</code> itself
*/
public A get( Dockable dockable ){
return get( dockable, false );
}
/**
* Recursively searches through all stations of <code>dockable</code>
* until a station is found that is registered at this mode.
* @param dockable the element whose root is searched
* @param locationRoot if <code>true</code>, then only those {@link ModeArea}s are returned
* which are {@link ModeArea#isLocationRoot()}
* @return the root or <code>null</code>, never <code>dockable</code> itself
*/
public A get( Dockable dockable, boolean locationRoot ){
while( dockable != null ){
for( A area : areas.values() ){
if( !locationRoot || area.isLocationRoot() ){
if( area.isChild( dockable ) ){
return area;
}
}
}
DockStation station = dockable.getDockParent();
if( station == null )
return null;
dockable = station.asDockable();
}
return null;
}
/**
* Recursively searches through all areas known to this mode until the
* mode is found that represents <code>station</code>. If <code>station</code>
* is a {@link Dockable} that its parent station is searched too.
* @param station the station whose area is to be found
* @return an area for which {@link ModeArea#getStation()} equals <code>station</code>,
* may be <code>null</code>
*/
public A get( DockStation station ){
// search area
while( station != null ){
for( A area : this ){
if( area.getStation() == station ){
return area;
}
}
Dockable dockable = station.asDockable();
station = dockable == null ? null : dockable.getDockParent();
}
return null;
}
public Location aside( AsideRequest request, Location location ){
A area = get( location.getRoot() );
if( area == null ){
return null;
}
AsideAnswer answer = request.execute( area.getStation() );
if( answer.isCanceled() ){
return null;
}
return new Location( getUniqueIdentifier(), location.getRoot(), answer.getLocation(), true );
}
public DockActionSource getActionsFor( Dockable dockable, Mode<Location> mode ){
if( mode == this ){
return null;
}
if( !isModeAvailable( dockable ) || isModeHidden( dockable )){
return null;
}
DockableHandle handle = handles.get( dockable );
if( handle == null )
return null;
else
return handle.getActions( mode );
}
/**
* Tells whether this mode is available for <code>dockable</code>.
* @param dockable some element to check
* @return <code>true</code> if this mode is available
*/
protected boolean isModeAvailable( Dockable dockable ){
LocationModeManager<?> manager = getManager();
if( manager == null )
return false;
return manager.isModeAvailable( dockable, getExtendedMode() );
}
/**
* Tells whether this mode is hidden for <code>dockable</code>. If the mode is hidden
* then the actions of the {@link #setActionProvider(LocationModeActionProvider) action provider}
* do not show up.
* @param dockable some element to check
* @return <code>true</code> if this mode is available
*/
protected boolean isModeHidden( Dockable dockable ){
LocationModeManager<?> manager = getManager();
if( manager == null )
return false;
return manager.isModeHidden( dockable, getExtendedMode() );
}
public boolean isRepresenting( DockStation station ){
for( A area : areas.values() ){
if( area.getStation() == station ){
return true;
}
}
return false;
}
public boolean apply( Dockable dockable, Location history, AffectedSet set ){
LocationModeEvent event = new LocationModeEvent( this, history, dockable, set );
for( LocationModeListener listener : listeners() ){
listener.applyStarting( event );
}
if( !event.isDone() ){
boolean success = runApply( dockable, history, set );
event.done(success);
}
for( LocationModeListener listener : listeners() ){
listener.applyDone( event );
}
return event.isSuccess();
}
/**
* Called by {@link #apply(Dockable, Location, AffectedSet)} after the {@link LocationModeListener}s
* are informed. Applies this mode to <code>dockable</code>.
* @param dockable the element whose mode becomes <code>this</code>
* @param history history information that was returned by this mode when calling
* {@link #current(Dockable)} the last time.
* @param set this method has to store all {@link Dockable}s which might have changed their
* mode in the set.
* @return <code>true</code> if <code>dockable</code> was moved, <code>false</code> if the method failed
* to set the location of <code>dockable</code> for any reason
*/
protected abstract boolean runApply( Dockable dockable, Location history, AffectedSet set );
/**
* Creates a new handle for <code>dockable</code>.
* @param dockable the newly registered element
* @return the new handle
*/
protected DockableHandle createHandle( Dockable dockable ){
return new DockableHandle( dockable );
}
/**
* A listener added to all {@link ModeArea}s.
* @author Benjamin Sigg
*/
private class AreaListener implements ModeAreaListener{
public void internalLocationChange( ModeArea source, Set<Dockable> dockables ){
LocationModeManager<?> manager = getManager();
if( manager != null ){
if( manager.isOnTransaction() ){
manager.addAffected( dockables );
}
else{
for( Dockable dockable : dockables ){
manager.refresh( dockable, true );
}
}
}
}
}
/**
* Listener added to register new and removed {@link Dockable}s.
* @author Benjamin Sigg
*/
private class ManagerListener implements ModeManagerListener<Location, LocationMode>{
public void dockableAdded( ModeManager<? extends Location, ? extends LocationMode> manager, Dockable dockable ){
if( !handles.containsKey( dockable )){
DockableHandle handle = createHandle( dockable );
handles.put( dockable, handle );
}
}
public void dockableRemoved( ModeManager<? extends Location, ? extends LocationMode> manager, Dockable dockable ){
DockableHandle handle = handles.remove( dockable );
if( handle != null ){
handle.destroy();
}
}
public void modeAdded( ModeManager<? extends Location, ? extends LocationMode> manager, LocationMode mode ){
// ignore
}
public void modeChanged( ModeManager<? extends Location, ? extends LocationMode> manager, Dockable dockable, LocationMode oldMode, LocationMode newMode ){
// ignore
}
public void modeRemoved( ModeManager<? extends Location, ? extends LocationMode> manager, LocationMode mode ){
// ignore
}
}
/**
* Meta information about a currently registered Dockable.
* @author Benjamin Sigg
*/
protected class DockableHandle{
/** the dockable which is handled by this handle */
private Dockable dockable;
/** the current actions added by this mode to {@link #dockable} */
private DockActionSource source;
/**
* Creates a new handle.
* @param dockable the new element, not <code>null</code>
*/
public DockableHandle( Dockable dockable ){
this.dockable = dockable;
}
/**
* Gets the element of this handle.
* @return the element, not <code>null</code>
*/
public Dockable getDockable(){
return dockable;
}
/**
* Called when this handle is no longer of any use
*/
public void destroy(){
actionProvider.destroy( dockable, source );
dockable = null;
source = null;
}
/**
* Called by {@link AbstractLocationMode#getActionsFor(Dockable, Mode)}
* to the actions related to this dockable. The default implementation
* is to return a source returned by the current {@link LocationModeActionProvider}.
* @param mode the current mode of this element
* @return the actions or <code>null</code>
*/
public DockActionSource getActions( Mode<Location> mode ){
source = actionProvider.getActions( dockable, mode, source );
return source;
}
}
}