/*
* 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.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import bibliothek.gui.DockController;
import bibliothek.gui.DockStation;
import bibliothek.gui.Dockable;
import bibliothek.gui.dock.DockElement;
import bibliothek.gui.dock.action.DockActionSource;
import bibliothek.gui.dock.action.MultiDockActionSource;
import bibliothek.gui.dock.common.CControl;
import bibliothek.gui.dock.common.group.CGroupBehavior;
import bibliothek.gui.dock.common.group.CGroupBehaviorCallback;
import bibliothek.gui.dock.common.group.CGroupMovement;
import bibliothek.gui.dock.common.group.StackGroupBehavior;
import bibliothek.gui.dock.common.mode.ExtendedMode;
import bibliothek.gui.dock.control.DockRegister;
import bibliothek.gui.dock.control.focus.DefaultFocusRequest;
import bibliothek.gui.dock.control.relocator.DockRelocatorEvent;
import bibliothek.gui.dock.control.relocator.VetoableDockRelocatorAdapter;
import bibliothek.gui.dock.event.DockHierarchyEvent;
import bibliothek.gui.dock.event.DockHierarchyListener;
import bibliothek.gui.dock.event.DockRegisterAdapter;
import bibliothek.gui.dock.event.DoubleClickListener;
import bibliothek.gui.dock.facile.mode.status.DefaultExtendedModeEnablement;
import bibliothek.gui.dock.facile.mode.status.ExtendedModeEnablement;
import bibliothek.gui.dock.facile.mode.status.ExtendedModeEnablementFactory;
import bibliothek.gui.dock.facile.mode.status.ExtendedModeEnablementListener;
import bibliothek.gui.dock.layout.location.AsideRequest;
import bibliothek.gui.dock.layout.location.AsideRequestFactory;
import bibliothek.gui.dock.support.mode.AffectedSet;
import bibliothek.gui.dock.support.mode.AffectingRunnable;
import bibliothek.gui.dock.support.mode.ModeManager;
import bibliothek.gui.dock.support.mode.ModeManagerListener;
import bibliothek.gui.dock.util.DockProperties;
import bibliothek.gui.dock.util.IconManager;
import bibliothek.gui.dock.util.PropertyKey;
import bibliothek.gui.dock.util.PropertyValue;
import bibliothek.gui.dock.util.property.ConstantPropertyFactory;
import bibliothek.util.Path;
/**
* {@link ModeManager} for the location of a {@link Dockable}. This manager is able to
* work together with {@link CControl} or together with {@link DockController}. Clients
* using it together with a {@link DockController} need to set icons for the
* modes manually, they can use the {@link IconManager} and the keys provided in
* each mode (e.g. {@link NormalMode#ICON_IDENTIFIER}).
* @author Benjamin Sigg
* @param <M> the kind of mode this manager handles
*/
public class LocationModeManager<M extends LocationMode> extends ModeManager<Location, M>{
/**
* {@link PropertyKey} for the {@link ExtendedModeEnablement} that should be used
* by a {@link LocationModeManager} to activate and deactivate the modes.
*/
public static final PropertyKey<ExtendedModeEnablementFactory> MODE_ENABLEMENT =
new PropertyKey<ExtendedModeEnablementFactory>( "locationmodemanager.mode_enablement",
new ConstantPropertyFactory<ExtendedModeEnablementFactory>( DefaultExtendedModeEnablement.FACTORY ), true );
/**
* {@link PropertyKey} for the {@link DoubleClickLocationStrategy} that should be used
* to change the {@link ExtendedMode} of an element which has been double-clicked.
*/
public static final PropertyKey<DoubleClickLocationStrategy> DOUBLE_CLICK_STRATEGY =
new PropertyKey<DoubleClickLocationStrategy>( "locationmodemanager.double_click_strategy",
new ConstantPropertyFactory<DoubleClickLocationStrategy>( DoubleClickLocationStrategy.DEFAULT ), true );
/** a set of listeners that will be automatically added or removed from a {@link LocationMode} */
private Map<Path, List<LocationModeListener>> listeners = new HashMap<Path, List<LocationModeListener>>();
/** registers new dockables */
private RegisterListener registerListener = new RegisterListener();
/** registers when dockables change their position */
private HierarchyListener hierarchyListener = new HierarchyListener();
/** registers dragged and dropped dockables */
private RelocatorListener relocatorListener = new RelocatorListener();
/** how to group dockables */
private CGroupBehavior behavior = new StackGroupBehavior();
/** the action that is currently executing */
private List<CGroupMovement> currentAction = new ArrayList<CGroupMovement>();
/** the list of {@link Dockable}s for which {@link #refresh(Dockable, boolean)} has to be called */
private LinkedHashSet<Dockable> pendingRefreshs = new LinkedHashSet<Dockable>();
/** the current {@link ExtendedModeEnablementFactory} */
private PropertyValue<ExtendedModeEnablementFactory> extendedModeFactory = new PropertyValue<ExtendedModeEnablementFactory>( MODE_ENABLEMENT ) {
@Override
protected void valueChanged( ExtendedModeEnablementFactory oldValue, ExtendedModeEnablementFactory newValue ){
updateEnablement();
}
};
/** the current {@link DoubleClickLocationStrategy} */
private PropertyValue<DoubleClickLocationStrategy> doubleClickStrategy = new PropertyValue<DoubleClickLocationStrategy>( DOUBLE_CLICK_STRATEGY ) {
@Override
protected void valueChanged( DoubleClickLocationStrategy oldValue, DoubleClickLocationStrategy newValue ){
// ignore
}
};
/** a listener added to the current {@link #enablement} */
private ExtendedModeEnablementListener enablementListener = new ExtendedModeEnablementListener() {
public void availabilityChanged( Dockable dockable, ExtendedMode mode, boolean available ){
refresh( dockable, true );
}
};
/** detects double-click events and changes the mode of the clicked element */
private DoubleClickListener doubleClickListener = new DoubleClickListener() {
public DockElement getTreeLocation(){
return null;
}
public boolean process( Dockable dockable, MouseEvent event ){
if( event.isConsumed() )
return false;
dockable = getDoubleClickTarget( dockable );
if( dockable != null ){
M current = getCurrentMode( dockable );
ExtendedMode next = getDoubleClickStrategy().handleDoubleClick( dockable, current == null ? null : current.getExtendedMode(), enablement );
if( next != null && isModeAvailable( dockable, next )){
setMode( dockable, next );
ensureValidLocation( dockable );
return true;
}
}
return false;
}
};
/** tells which modes are available for which element */
private ExtendedModeEnablement enablement;
/**
* if > 0 then the layout-mode is active. In this mode this manager does not react on some
* events to not intervene layouting
*/
private int layoutMode = 0;
/**
* Creates a new manager.
* @param controller the controller in whose realm this manager will work
*/
public LocationModeManager( DockController controller ){
super( controller );
registerListener.connect( controller );
controller.getRelocator().addVetoableDockRelocatorListener( relocatorListener );
updateEnablement();
extendedModeFactory.setProperties( controller );
addModeManagerListener( new LocationModeListenerAdapter() );
controller.getDoubleClickController().addListener( doubleClickListener );
}
public void destroy(){
registerListener.connect( null );
DockController controller = getController();
controller.getRelocator().removeVetoableDockRelocatorListener( relocatorListener );
controller.getDoubleClickController().removeListener( doubleClickListener );
for( LocationMode mode : this.modes() ){
mode.setController( null );
}
super.destroy();
extendedModeFactory.setProperties( (DockProperties)null );
}
/**
* Updates the current {@link ExtendedModeEnablement} using the factory
* provided by {@link #MODE_ENABLEMENT}.
*/
protected void updateEnablement(){
if( enablement != null ){
enablement.removeListener( enablementListener );
enablement.destroy();
enablement = null;
}
if( getController() != null ){
enablement = extendedModeFactory.getValue().create( this );
enablement.addListener( enablementListener );
}
rebuildAll();
}
/**
* Sets the group behavior. The group behavior is applied if {@link #setMode(Dockable, ExtendedMode)} is called and
* modifies the call such that another {@link Dockable} receives the event. Any call directly to any of
* the <code>apply</code> methods will not be modified by the group behavior.
* @param behavior the new behavior, not <code>null</code>
*/
public void setGroupBehavior( CGroupBehavior behavior ){
if( behavior == null ){
throw new IllegalArgumentException( "the group behavior must not be null" );
}
this.behavior = behavior;
}
/**
* Gets the current group behavior.
* @return the current behavior, not <code>null</code>
* @see #setGroupBehavior(CGroupBehavior)
*/
public CGroupBehavior getGroupBehavior(){
return behavior;
}
/**
* Sets the current mode of <code>dockable</code>.
* @param dockable the dockable whose mode is to be set
* @param extendedMode the mode
* @throws IllegalArgumentException if <code>extendedMode</code> is unknown
*/
public void setMode( final Dockable dockable, final ExtendedMode extendedMode ){
M mode = getMode( extendedMode.getModeIdentifier() );
if( mode == null ){
throw new IllegalArgumentException( "No mode '" + extendedMode.getModeIdentifier() + "' available" );
}
runTransaction( new Runnable(){
public void run(){
CGroupMovement action = behavior.prepare( LocationModeManager.this, dockable, extendedMode );
if( action == null ){
return;
}
apply( dockable, extendedMode, action );
}
});
}
/**
* Gets the action that is currently carried out.
* @return the current action, can be <code>null</code>
*/
public CGroupMovement getCurrentAction(){
if( currentAction.isEmpty() ){
return null;
}
return currentAction.get( currentAction.size()-1 );
}
/**
* Executes <code>action</code> in a transaction assuming that the result of this action will lead to
* <code>dockable</code> having the new mode <code>extendedMode</code>.
* @param dockable the primary {@link Dockable}, this item may very well be the new focus owner
* @param extendedMode the expected mode <code>dockable</code> will have after <code>action</code> completed
* @param action the action to execute
*/
public void apply( final Dockable dockable, final ExtendedMode extendedMode, final CGroupMovement action ){
runTransaction( new AffectingRunnable(){
public void run( final AffectedSet set ){
try{
getController().getFocusController().freezeFocus();
currentAction.add( action );
action.apply( new CGroupBehaviorCallback(){
public void setMode( Dockable element, ExtendedMode mode ){
apply( element, mode.getModeIdentifier(), false );
}
public void setLocation( Dockable element, Location location ){
apply( element, location.getMode(), location, set );
}
public LocationModeManager<? extends LocationMode> getManager(){
return LocationModeManager.this;
}
public Location getLocation( Dockable dockable ){
M mode = getCurrentMode( dockable );
if( mode == null ){
return null;
}
return mode.current( dockable );
}
});
}
finally{
currentAction.remove( action );
getController().getFocusController().meltFocus();
}
LocationMode mode = getMode( extendedMode.getModeIdentifier() );
if( mode != null ){
if( mode.shouldAutoFocus() ){
getController().setFocusedDockable( new DefaultFocusRequest( dockable, null, true, true, false ));
}
else{
getController().setFocusedDockable( new DefaultFocusRequest( null, null, true ));
}
}
}
});
}
/**
* Gets the current mode of <code>dockable</code>.
* @param dockable the element whose mode is searched
* @return the mode or <code>null</code> if not found
*/
public ExtendedMode getMode( Dockable dockable ){
LocationMode mode = getCurrentMode( dockable );
if( mode == null )
return null;
return mode.getExtendedMode();
}
/**
* Checks all {@link LocationMode}s of this manager and returns all
* {@link DockStation}s that were registered with the given id. The same
* station or the same id might be used for different modes.
* @param id the id of some station
* @return each mode-area pair where the area is not <code>null</code>, can be empty
*/
public Map<ExtendedMode, DockStation> getRepresentations( String id ){
if( id == null )
throw new IllegalArgumentException( "id must not be null" );
Map<ExtendedMode, DockStation> result = new HashMap<ExtendedMode, DockStation>();
for( LocationMode mode : modes() ){
DockStation station = mode.getRepresentation( id );
if( station != null ){
result.put( mode.getExtendedMode(), station );
}
}
return result;
}
/**
* Ignores the call, the position of {@link Dockable}s is set elsewhere.
*/
@Override
protected void applyDuringRead( String key, Path old, Path current, Dockable dockable ){
// ignore
}
@Override
public void apply( Dockable dockable, M mode, Location history, AffectedSet set ) {
super.apply( dockable, mode, history, set );
if( history != null ){
history.resetApplicationDefined();
}
}
/**
* Using the current {@link ExtendedModeEnablement} this method tells whether
* mode <code>mode</code> can be applied to <code>dockable</code>.
* @param dockable some element, not <code>null</code>
* @param mode some mode, not <code>null</code>
* @return the result of {@link ExtendedModeEnablement#isAvailable(Dockable, ExtendedMode)}
*/
public boolean isModeAvailable( Dockable dockable, ExtendedMode mode ){
if( enablement == null )
return false;
return enablement.isAvailable( dockable, mode ).isAvailable();
}
/**
* Using the current {@link ExtendedModeEnablement} this method tells whether
* mode <code>mode</code> is hidden from the user when looking at <code>dockable</code>. A hidden
* mode
* @param dockable some element, not <code>null</code>
* @param mode some mode, not <code>null</code>
* @return the result of {@link ExtendedModeEnablement#isAvailable(Dockable, ExtendedMode)}
*/
public boolean isModeHidden( Dockable dockable, ExtendedMode mode ){
if( enablement == null )
return false;
return enablement.isHidden( dockable, mode ).isHidden();
}
/**
* Adds a listener to the mode with unique identifier <code>identifier</code>. If the
* mode is exchanged then this listener is automatically removed and may be re-added
* to the new mode.
* @param identifier the identifier of some mode (not necessarily registered yet).
* @param listener the new listener, not <code>null</code>
*/
public void addListener( Path identifier, LocationModeListener listener ){
if( listener == null )
throw new IllegalArgumentException( "listener must not be null" );
List<LocationModeListener> list = listeners.get( identifier );
if( list == null ){
list = new ArrayList<LocationModeListener>();
listeners.put( identifier, list );
}
list.add( listener );
LocationMode mode = getMode( identifier );
if( mode != null ){
mode.addLocationModeListener( listener );
}
}
/**
* Removes a listener from the mode <code>identifier</code>.
* @param identifier the name of a mode
* @param listener the listener to remove
*/
public void removeListener( Path identifier, LocationModeListener listener ){
List<LocationModeListener> list = listeners.get( identifier );
if( list == null )
return;
list.remove( listener );
if( list.isEmpty() )
listeners.remove( identifier );
LocationMode mode = getMode( identifier );
if( mode != null ){
mode.removeLocationModeListener( listener );
}
}
@Override
public M getCurrentMode( Dockable dockable ){
while( dockable != null ){
for( M mode : modes() ){
if( mode.isCurrentMode( dockable ))
return mode;
}
DockStation station = dockable.getDockParent();
dockable = station == null ? null : station.asDockable();
}
return null;
}
/**
* Gets the current strategy for handing double-clicks.
* @return the strategy, never <code>null</code>
* @see #setDoubleClickStrategy(DoubleClickLocationStrategy)
*/
public DoubleClickLocationStrategy getDoubleClickStrategy(){
return doubleClickStrategy.getValue();
}
/**
* Sets the current strategy for handling double-clicks on {@link Dockable}s. This
* strategy will be asked what mode to assign to an element that has been double-clicked.
* Results that are not allowed by the current {@link ExtendedModeEnablement} are ignored.
* @param strategy the new strategy, can be <code>null</code> to set the default strategy
*/
public void setDoubleClickStrategy( DoubleClickLocationStrategy strategy ){
doubleClickStrategy.setValue( strategy );
}
/**
* Tells whether this mode is currently in layouting mode. Some
* methods of this manager do not react while in layouting mode.
* @return <code>true</code> if layouting mode is active
*/
public boolean isLayouting(){
return layoutMode > 0;
}
/**
* Activates the {@link #isLayouting() layout mode} while <code>run</code>
* is running.
* @param run some code to execute
*/
public void runLayoutTransaction( Runnable run ){
try{
layoutMode++;
runTransaction( run, true );
}
finally{
layoutMode--;
}
}
/**
* Ensures that <code>dockable</code> is not hidden behind another
* {@link Dockable}. That does not mean that <code>dockable</code> becomes
* visible, just that it is easier reachable without the need to change
* modes of any <code>Dockable</code>s.<br>
* This method returns immediately if in {@link #isLayouting() layouting mode}
* @param dockable the element which should not be hidden
*/
public void ensureNotHidden( final Dockable dockable ){
if( isLayouting() )
return;
runTransaction( new Runnable() {
public void run(){
for( LocationMode mode : modes() ){
mode.ensureNotHidden( dockable );
}
}
});
}
/**
* Empty method evaluating the correct location of a {@link Dockable}. To be
* overridden by subclasses to handle elements which have additional restrictions.
* @param dockable the element to check
*/
public void ensureValidLocation( Dockable dockable ){
// nothing
}
/**
* Iterates through all the {@link LocationMode}s for which <code>aside</code> has stored locations,
* and sets <code>dockable</code> as neighbor. This method does not change the actual location of <code>dockable</code>,
* rather a call to <code>apply</code> would be necessary to update the location.<br>
* It is the responsibility of the caller to ensure that <code>dockable</code> can actually be placed at any
* location where <code>aside</code> ever was.
* @param dockable the item whose location should change
* @param aside the item whose neighbor is set
*/
public void setLocationAside( Dockable dockable, Dockable aside ){
M current = getCurrentMode( aside );
List<M> history = getModeHistory( aside );
for( M mode : history ){
Location location;
if( mode == current ){
location = mode.current( aside );
}
else{
location = getHistory( aside, mode.getUniqueIdentifier() );
}
if( location != null ){
AsideRequestFactory factory = getController().getProperties().get( AsideRequest.REQUEST_FACTORY );
AsideRequest request = factory.createAsideRequest( location.getLocation(), dockable );
Location result = mode.aside( request, location );
if( result != null ){
addToModeHistory( dockable, mode, result );
}
}
}
}
@Override
public DockActionSource getSharedActions( DockStation station ){
Dockable selected = station.getFrontDockable();
if( selected == null ){
return null;
}
LocationMode mode = getCurrentMode( selected );
if( mode == null ){
return null;
}
MultiDockActionSource result = new MultiDockActionSource();
for( LocationMode other : modes() ){
if( behavior.shouldForwardActions( this, station, selected, other.getExtendedMode() ) ){
DockActionSource source = other.getActionsFor( selected, mode );
if( source != null ){
result.add( source );
}
}
}
return result;
}
/**
* Adds and removes listeners from {@link LocationMode}s according to the map
* {@link LocationModeManager#listeners}.
* @author Benjamin Sigg
*/
private class LocationModeListenerAdapter implements ModeManagerListener<Location, LocationMode>{
public void modeAdded( ModeManager<? extends Location, ? extends LocationMode> manager, LocationMode mode ){
mode.setManager( LocationModeManager.this );
mode.setController( getController() );
List<LocationModeListener> list = listeners.get( mode.getUniqueIdentifier() );
if( list != null ){
for( LocationModeListener listener : list ){
mode.addLocationModeListener( listener );
}
}
}
public void modeRemoved( ModeManager<? extends Location, ? extends LocationMode> manager, LocationMode mode ){
mode.setManager( null );
mode.setController( null );
List<LocationModeListener> list = listeners.get( mode.getUniqueIdentifier() );
if( list != null ){
for( LocationModeListener listener : list ){
mode.removeLocationModeListener( listener );
}
}
}
public void dockableAdded( ModeManager<? extends Location, ? extends LocationMode> manager, Dockable dockable ){
// ignore
}
public void dockableRemoved( ModeManager<? extends Location, ? extends LocationMode> manager, Dockable dockable ){
// ignore
}
public void modeChanged( ModeManager<? extends Location, ? extends LocationMode> manager, Dockable dockable, LocationMode oldMode, LocationMode newMode ){
// ignore
}
}
/**
* If the {@link DockRegister} is currently stalled, then a call to {@link LocationModeManager#refresh(Dockable, boolean)}
* is scheduled, otherwise the call is performed directly.
* @param dockable the dockable which should be forwarded to {@link LocationModeManager#refresh(Dockable, boolean)}
*/
public void delayedRefresh( Dockable dockable ){
if( getController().getRegister().isStalled() ){
pendingRefreshs.add( dockable );
}
else{
refresh( dockable, true );
}
}
/**
* This listener registers when {@link Dockable}s enter and leave and adds or
* removes a {@link DockHierarchyListener}.
* @author Benjamin Sigg
*/
private class RegisterListener extends DockRegisterAdapter{
private DockController controller;
public void connect( DockController controller ){
if( this.controller != null ){
DockRegister register = this.controller.getRegister();
register.removeDockRegisterListener( this );
for( Dockable dockable : register.listDockables() ){
dockable.removeDockHierarchyListener( hierarchyListener );
rebuild( dockable );
}
}
this.controller = controller;
if( controller != null ){
DockRegister register = controller.getRegister();
register.addDockRegisterListener( this );
for( Dockable dockable : register.listDockables() ){
dockable.addDockHierarchyListener( hierarchyListener );
}
}
}
@Override
public void dockableRegistered( DockController controller, Dockable dockable ){
dockable.addDockHierarchyListener( hierarchyListener );
rebuild( dockable );
}
@Override
public void dockableUnregistered( DockController controller, Dockable dockable ){
dockable.removeDockHierarchyListener( hierarchyListener );
}
@Override
public void registerUnstalled( DockController controller ){
while( pendingRefreshs.size() > 0 && !controller.getRegister().isStalled() ){
Iterator<Dockable> iter = pendingRefreshs.iterator();
Dockable next = iter.next();
iter.remove();
refresh( next, true );
}
}
}
/**
* Reacts on dockables that are changing their position by calling
* {@link LocationModeManager#refresh(Dockable, boolean)}.
* @author Benjamin Sigg
*/
private class HierarchyListener implements DockHierarchyListener{
public void controllerChanged( DockHierarchyEvent event ){
// ignore
}
public void hierarchyChanged( DockHierarchyEvent event ){
if( !isOnTransaction() ){
delayedRefresh( event.getDockable() );
}
}
}
/**
* Detects the drag-operation and calls {@link LocationModeManager#store(Dockable)}.
* @author Benjamin Sigg
*/
private class RelocatorListener extends VetoableDockRelocatorAdapter{
@Override
public void dragging( DockRelocatorEvent event ){
store( event.getDockable() );
for( Dockable dockable : event.getImplicitDockables() ){
store( dockable );
}
}
}
}