/*
* 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.support.mode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
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.ActionGuard;
import bibliothek.gui.dock.action.DockAction;
import bibliothek.gui.dock.action.DockActionSource;
import bibliothek.gui.dock.action.LocationHint;
import bibliothek.gui.dock.action.MultiDockActionSource;
import bibliothek.gui.dock.control.DockRegister;
import bibliothek.gui.dock.util.DockUtilities;
import bibliothek.util.Path;
/**
* Associates {@link Dockable}s with one {@link Mode} out of a set
* of modes. This manager remembers in which order the modes were applied
* to a {@link Dockable}.
* @param <H> the kind of properties that are to be stored in this manager
* @param <M> the kind of {@link Mode}s used by this manager
* @author Benjamin Sigg
*/
public abstract class ModeManager<H, M extends Mode<H>> {
/** the ordered list of available modes */
private List<ModeHandle> modes = new ArrayList<ModeHandle>();
/** factories for creating {@link ModeSetting}s */
private Map<Path, ModeSettingFactory<H>> factories = new HashMap<Path, ModeSettingFactory<H>>();
/** lists for all known {@link Dockable}s their {@link DockableHandle} */
private Map<Dockable, DockableHandle> dockables = new HashMap<Dockable, DockableHandle>();
/** list all {@link DockableHandle}s ever created and not dismissed by this manager */
private Map<String, DockableHandle> entries = new HashMap<String, DockableHandle>();
/** all the listeners that are registered at this manager */
private List<ModeManagerListener<? super H, ? super M>> listeners =
new ArrayList<ModeManagerListener<? super H,? super M>>();
/** whether a mode is currently applying itself */
private int onTransaction = 0;
/** continuous mode without storing the position of elements */
private int onContinuous = 0;
/** the controller in whose realm this manager works */
private DockController controller;
/** the set of affected dockables of the current transaction */
private ChangeSet affected;
/** how often the {@link #affected} set was opened */
private int affectedCount = 0;
/** used to change the history of {@link Dockable}s before applying a new mode */
private HistoryRewriter<H,M> historyRewriter;
private ActionGuard guard = new ActionGuard() {
public boolean react( Dockable dockable ){
return getHandle( dockable ) != null;
}
public DockActionSource getSource( Dockable dockable ){
DockableHandle handle = getHandle( dockable );
if( handle == null )
return null;
return handle.source;
}
};
/**
* This {@link ActionGuard} adds {@link DockAction}s to unregistered parents of
* registered {@link Dockable}s.
*/
private ActionGuard stationGuard = new ActionGuard(){
public boolean react( Dockable dockable ){
DockStation station = dockable.asDockStation();
return station != null && getHandle( dockable ) == null;
}
public DockActionSource getSource( Dockable dockable ){
return new ModeForwardingActionSource<H>( dockable.asDockStation(), ModeManager.this );
}
};
/**
* Creates a new manager.
* @param controller the controller in whose realm this manager will work
*/
public ModeManager( DockController controller ){
controller.addActionGuard( stationGuard );
controller.addActionGuard( guard );
this.controller = controller;
}
/**
* Unregisters listeners which this manager added to the {@link DockController} and
* other components.
*/
public void destroy(){
if( controller != null ){
controller.removeActionGuard( stationGuard );
controller.removeActionGuard( guard );
controller = null;
}
}
/**
* Gets the controller in whose realm this manager works.
* @return the controller
*/
public DockController getController(){
return controller;
}
/**
* Adds a listener to this manager, the listener will be informed about
* changes in this manager.
* @param listener the new listener, not <code>null</code>
*/
public void addModeManagerListener( ModeManagerListener<? super H, ? super M> listener ){
if( listener == null )
throw new IllegalArgumentException( "listener must not be null" );
listeners.add( listener );
}
/**
* Removes <code>listener</code> from this manager.
* @param listener the listener to remove
*/
public void removeModeManagerListener( ModeManagerListener<? super H, ? super M> listener ){
listeners.remove( listener );
}
/**
* Puts a new mode in this manager. If there is already a mode with the
* same id registered, then the old mode gets replaced by the new one.
* @param mode the new mode
*/
public void putMode( M mode ){
if( mode == null )
throw new IllegalArgumentException( "mode must not be null" );
for( ModeHandle handle : modes ){
if( handle.mode.getUniqueIdentifier().equals( mode.getUniqueIdentifier() )){
fireRemoved( handle.mode );
handle.mode = mode;
fireAdded( mode );
return;
}
}
modes.add( new ModeHandle( mode ) );
fireAdded( mode );
}
/**
* Adds a factory to this {@link ModeManager}. The factory will be used by the
* {@link ModeSettings} to read and write data of the mode with the same identifier
* as <code>factory</code> persistently.<br>
* <b>Note:</b> A {@link Mode} might also provide a {@link ModeSettingFactory}, if
* there is a collision of unique identifiers the factory of the mode is used.
* @param factory the new factory
*/
public void putFactory( ModeSettingFactory<H> factory ){
factories.put( factory.getModeId(), factory );
}
/**
* Gets a set containing all the {@link ModeSettingFactory}s that were added
* to this manager.
* @return the factories
*/
public Collection<ModeSettingFactory<H>> getFactories(){
return Collections.unmodifiableCollection( factories.values() );
}
/**
* Removes <code>mode</code> from this manager. Note that history information
* about the mode remains.
* @param mode the mode to remove
*/
public void removeMode( M mode ){
if( mode == null )
throw new IllegalArgumentException( "mode must not be null" );
for( ModeHandle handle : modes ){
if( handle.mode.getUniqueIdentifier().equals( mode.getUniqueIdentifier() )){
handle.mode = null;
fireRemoved( handle.mode );
modes.remove( handle );
return;
}
}
}
/**
* Searches and returns the mode with given unique identifier <code>path</code>.
* @param path some unique identifier
* @return the mode with that identifier or <code>null</code>
*/
public M getMode( Path path ){
ModeHandle handle = getAccess( path );
return handle == null ? null : handle.mode;
}
private ModeHandle getAccess( Path path ){
for( ModeHandle mode : modes ){
if( mode.mode.getUniqueIdentifier().equals( path ))
return mode;
}
return null;
}
/**
* Sets the current {@link HistoryRewriter}. The rewriter is invoked every time before
* the {@link Mode#apply(Dockable, Object, AffectedSet) apply} method of a {@link Mode} is
* called. The rewriter can then change the history of one {@link Dockable}, e.g. to apply
* additional checks whether an old state is still valid.<br>
* A history rewriter does not change the history permanently. It creates a new history
* object before the <code>apply</code> method is called, but that new history object
* will not be stored by the {@link ModeManager}.
* @param historyRewriter the new rewriter, can be <code>null</code>
*/
public void setHistoryRewriter( HistoryRewriter<H,M> historyRewriter ){
this.historyRewriter = historyRewriter;
}
/**
* Gets the current {@link HistoryRewriter}.
* @return the rewriter, can be <code>null</code>
* @see #setHistoryRewriter(HistoryRewriter)
*/
public HistoryRewriter<H,M> getHistoryRewriter(){
return historyRewriter;
}
/**
* Gets all the listeners that are currently registered in this manager.
* @return the list of registered listeners
*/
@SuppressWarnings("unchecked")
protected ModeManagerListener<? super H, ? super M>[] listeners(){
return listeners.toArray( new ModeManagerListener[ listeners.size() ] );
}
/**
* Calls {@link ModeManagerListener#dockableAdded(ModeManager, Dockable)}
* on all listeners that are currently registered
* @param dockable the new element
*/
protected void fireAdded( Dockable dockable ){
for( ModeManagerListener<? super H, ? super M> listener : listeners() ){
listener.dockableAdded( this, dockable );
}
}
/**
* Calls {@link ModeManagerListener#dockableRemoved(ModeManager, Dockable)}
* on all listeners that are currently registered.
* @param dockable the removed element
*/
protected void fireRemoved( Dockable dockable ){
for( ModeManagerListener<? super H, ? super M> listener : listeners() ){
listener.dockableRemoved( this, dockable );
}
}
/**
* Calls {@link ModeManagerListener#modeChanged(ModeManager, Dockable, Mode, Mode)}
* on all listeners that are currently registered.
* @param dockable the element whose mode changed
* @param oldMode its old mode
* @param newMode its new mode
*/
protected void fireModeChanged( Dockable dockable, M oldMode, M newMode ){
for( ModeManagerListener<? super H, ? super M> listener : listeners() ){
listener.modeChanged( this, dockable, oldMode, newMode );
}
}
/**
* Calls {@link ModeManagerListener#modeAdded(ModeManager, Mode)} on
* all listeners that are currently registered.
* @param mode the added mode
*/
protected void fireAdded( M mode ){
for( ModeManagerListener<? super H, ? super M> listener : listeners() ){
listener.modeAdded( this, mode );
}
}
/**
* Calls {@link ModeManagerListener#modeRemoved(ModeManager, Mode)} on
* all listeners that are currently registered.
* @param mode the removed mode
*/
protected void fireRemoved( M mode ){
for( ModeManagerListener<? super H, ? super M> listener : listeners() ){
listener.modeRemoved( this, mode );
}
}
/**
* Registers a new {@link Dockable} at this manager. If there is already
* mode-information for <code>key</code> present, then <code>dockable</code>
* inherits this information.
* @param key the unique key of <code>dockable</code>
* @param dockable the new element
* @throws NullPointerException if either <code>key</code> or <code>dockable</code>
* is <code>null</code>
* @throws IllegalArgumentException if there is already a dockable registered
* with <code>key</code>
*/
public void add( String key, Dockable dockable ){
if( key == null )
throw new NullPointerException( "key must not be null" );
if( dockable == null )
throw new NullPointerException( "dockable must not be null" );
DockableHandle entry = entries.get( key );
if( entry != null && entry.dockable != null )
throw new IllegalArgumentException( "There is already a dockable registered with the key: " + key );
if( dockables.containsKey( dockable ))
throw new IllegalArgumentException( "The dockable is already known to this manager (but it does have a different name)" );
if( entry == null ){
entry = new DockableHandle( dockable, key );
entries.put( entry.id, entry );
}
else{
entry.dockable = dockable;
}
dockables.put( dockable, entry );
entry.putMode( access( getCurrentMode( dockable ) ) );
fireAdded( dockable );
rebuild( dockable );
}
/**
* Registers a new {@link Dockable} at this manager. This method works
* like {@link #add(String, Dockable)} but does not throw an exception
* if another {@link Dockable} is already registered with <code>key</code>.
* Instead the other <code>Dockable</code> is unregistered and <code>dockable</code>
* inherits its mode-information.
* @param key the unique identifier of <code>dockable</code>
* @param dockable some new element
* @throws NullPointerException if either <code>key</code> or <code>dockable</code>
* is <code>null</code>
*/
public void put( String key, Dockable dockable ){
if( key == null )
throw new NullPointerException( "key must not be null" );
if( dockable == null )
throw new NullPointerException( "dockable must not be null" );
DockableHandle entry = entries.get( key );
if( entry != null ){
if( entry.dockable != null ){
dockables.remove( entry.dockable );
fireRemoved( entry.dockable );
}
entry.dockable = dockable;
dockables.put( dockable, entry );
}
else{
// was not inserted
entry = new DockableHandle( dockable, key );
dockables.put( dockable, entry );
entries.put( entry.id, entry );
entry.putMode( access( getCurrentMode( dockable ) ) );
}
fireAdded( dockable );
rebuild( dockable );
}
/**
* Gets the unique identifier which is used for <code>dockable</code>.
* @param dockable some element
* @return the unique identifier or <code>null</code> if <code>dockable</code>
* is not registered
*/
public String getKey( Dockable dockable ){
DockableHandle handle = getHandle( dockable );
if( handle == null )
return null;
return handle.id;
}
/**
* Tells whether this {@link ModeManager} knows <code>dockable</code>
* and can handle a call to any of the <code>apply</code> methods.
* @param dockable the element to check
* @return <code>true</code> if the element is known, <code>false</code> otherwise
*/
public boolean isRegistered( Dockable dockable ){
return getKey( dockable ) != null;
}
/**
* Returns a set containing all {@link Dockable}s that are currently
* registered at this manager.
* @return the set of dockables
*/
public Set<Dockable> listDockables(){
return Collections.unmodifiableSet( dockables.keySet() );
}
/**
* Runs an algorithm which affects the mode of some {@link Dockable}s.
* @param runnable the algorithm, <code>null</code> will be ignored
*/
public void runTransaction( final AffectingRunnable runnable ){
runTransaction( runnable, false );
}
/**
* Runs an algorithm which affects the mode of some {@link Dockable}s.
* @param run the algorithm, <code>null</code> will be ignored
* @param continuous if set to <code>true</code> the transaction should run without changing
* the internal cache storing the position of all {@link Dockable}s. This can be important
* if an operation runs an <code>apply</code> method and additional work will change
* the position of some elements again. Clients should call {@link #store(Dockable)}
* afterwards.
*/
public void runTransaction( final AffectingRunnable run, boolean continuous ){
if( run == null )
return;
try{
openAffected();
runTransaction( new Runnable() {
public void run(){
run.run( affected );
}
}, continuous );
}
finally{
closeAffected();
}
}
/**
* Runs <code>run</code> as transaction, the {@link DockRegister} is stalled
* and {@link #isOnTransaction()} returns <code>true</code> while
* <code>run</code> runs.
* @param run the runnable to execute
*/
public void runTransaction( Runnable run ){
runTransaction( run, false );
}
/**
* Runs <code>run</code> as transaction, the {@link DockRegister} is stalled
* and {@link #isOnTransaction()} returns <code>true</code> while
* <code>run</code> runs.
* @param run the runnable to execute
* @param continuous if set to <code>true</code> the transaction should run without changing
* the internal cache storing the position of all {@link Dockable}s. This can be important
* if an operation runs an <code>apply</code> method and additional work will change
* the position of some elements again. Clients should call {@link #store(Dockable)}
* afterwards.
*/
public void runTransaction( Runnable run, boolean continuous ){
try{
controller.getRegister().setStalled( true );
onTransaction++;
if( continuous ){
onContinuous++;
}
run.run();
}
finally{
controller.getRegister().setStalled( false );
onTransaction--;
if( continuous ){
onContinuous--;
}
}
}
/**
* Alters the mode of <code>dockable</code> to <code>mode</code>.
* This method just calls {@link #apply(Dockable, Mode, boolean)}.
* @param dockable the element whose mode is going to be changed
* @param mode the new mode
* @param force if <code>true</code> <code>dockable</code> is relocated even if the
* current mode already is <code>mode</code>
* @throws IllegalArgumentException if <code>dockable</code> is <code>null</code>,
* <code>mode</code> is <code>null</code> or <code>dockable</code> is not
* registered.
* @return <code>true</code> if <code>mode</code> was found, <code>false</code>
* otherwise
*/
public boolean apply( Dockable dockable, Path mode, boolean force ){
M resolved = getMode( mode );
if( resolved != null ){
apply( dockable, resolved, force );
return true;
}
return false;
}
/**
* Alters the mode of <code>dockable</code> to <code>mode</code>.
* This method just calls {@link #apply(Dockable, Mode, AffectedSet, boolean)}.
* @param dockable the element whose mode is going to be changed
* @param mode the new mode
* @param force if <code>true</code> <code>dockable</code> is relocated even if the
* current mode already is <code>mode</code>
* @throws IllegalArgumentException if <code>dockable</code> is <code>null</code>,
* <code>mode</code> is <code>null</code> or <code>dockable</code> is not
* registered.
*/
public void apply( Dockable dockable, M mode, boolean force ){
try{
openAffected();
apply( dockable, mode, affected, force );
}
finally{
closeAffected();
}
}
/**
* Alters the mode of <code>dockable</code> to <code>mode</code>.
* This method just calls {@link #apply(Dockable, Mode, AffectedSet, boolean)}.
* @param dockable the element whose mode is going to be changed
* @param mode the new mode
* @param set to store all dockables whose mode might have been changed
* @param force if <code>true</code> <code>dockable</code> is relocated even if the
* current mode already is <code>mode</code>
* @return <code>true</code> if <code>mode</code> was found,
* <code>false</code> otherwise
* @throws IllegalArgumentException if <code>dockable</code> is <code>null</code>,
* <code>mode</code> is <code>null</code>, <code>set</code> is <code>null</code>,
* or <code>dockable</code> is not registered.
*/
public boolean apply( Dockable dockable, Path mode, AffectedSet set, boolean force ){
M resolved = getMode( mode );
if( resolved != null ){
apply( dockable, resolved, set, force );
return true;
}
return false;
}
/**
* Alters the mode of <code>dockable</code> to <code>mode</code>. This
* method does nothing if the current mode of <code>dockable</code>
* already is <code>mode</code>.<br>
* After initial checks and reading the history, this method calls
* {@link #apply(Dockable, Mode, Object, AffectedSet)}.
* @param dockable the element whose mode is going to be changed
* @param mode the new mode
* @param set to store all dockables whose mode might have been changed
* @param force if <code>true</code> <code>dockable</code> is relocated even if the
* current mode already is <code>mode</code>
* @throws IllegalArgumentException if <code>dockable</code> is <code>null</code>,
* <code>mode</code> is <code>null</code>, <code>set</code> is <code>null</code>,
* or <code>dockable</code> is not registered.
*/
public void apply( Dockable dockable, M mode, AffectedSet set, boolean force ){
if( dockable == null )
throw new IllegalArgumentException( "dockable is null" );
if( mode == null )
throw new IllegalArgumentException( "mode is null" );
if( set == null )
throw new IllegalArgumentException( "set is null" );
DockableHandle entry = dockables.get( dockable );
if( entry == null )
throw new IllegalArgumentException( "dockable not registered" );
M dockableMode = getCurrentMode( dockable );
if( !force && dockableMode == mode )
return;
H history = entry.properties.get( mode.getUniqueIdentifier() );
apply( dockable, mode, history, set );
}
/**
* Gets the history of <code>dockable</code> in mode <code>modeId</code>.
* @param dockable the element whose history is searched
* @param modeId the identifier of the mode
* @return the history information or <code>null</code> if not found
*/
public H getHistory( Dockable dockable, Path modeId ){
DockableHandle entry = dockables.get( dockable );
if( entry == null )
return null;
return entry.properties.get( modeId );
}
/**
* Alters the mode of <code>dockable</code> to be <code>mode</code>.
* This method just calls {@link #apply(Dockable, Mode, Object, AffectedSet)}.
* @param dockable the element whose mode is changed
* @param mode the new mode of <code>dockable</code>
* @param history history information for {@link Mode#apply(Dockable, Object, AffectedSet)},
* can be <code>null</code>
* @param set to store elements that have changed
* @throws IllegalArgumentException if either <code>dockable</code>, <code>mode</code>
* or <code>set</code> is <code>null</code>
* @return <code>true</code> if <code>mode</code> was found, <code>false</code>
* otherwise
*/
public boolean apply( Dockable dockable, Path mode, H history, AffectedSet set ){
M resolved = getMode( mode );
if( resolved != null ){
apply( dockable, resolved, history, set );
return true;
}
return false;
}
/**
* Alters the mode of <code>dockable</code> to be <code>mode</code>.
* This method does not alter the modes of other dockables, notice however
* that the methods {@link Mode#apply(Dockable, Object, AffectedSet)} may
* trigger additional mode-changes.
* @param dockable the element whose mode is changed
* @param mode the new mode of <code>dockable</code>
* @param history history information for {@link Mode#apply(Dockable, Object, AffectedSet)},
* can be <code>null</code>
* @param set to store elements that have changed
* @throws IllegalArgumentException if either <code>dockable</code>, <code>mode</code>
* or <code>set</code> is <code>null</code>
*/
public void apply( final Dockable dockable, final M mode, final H history, final AffectedSet set ){
if( dockable == null )
throw new IllegalArgumentException( "dockable is null" );
if( mode == null )
throw new IllegalArgumentException( "mode is null" );
if( set == null )
throw new IllegalArgumentException( "set is null" );
M dockableMode = getCurrentMode( dockable );
if( dockableMode != null ){
store( dockable );
}
set.add( dockable );
runTransaction( new Runnable(){
public void run(){
H rewritten = history;
if( historyRewriter != null ){
rewritten = historyRewriter.rewrite( dockable, mode, history );
}
mode.apply( dockable, rewritten, set );
}
});
}
/**
* Stores a property for <code>dockable</code> if in mode <code>mode</code>. This
* method does not trigger any version of the <code>apply</code> methods.
* @param mode the mode which is affected
* @param dockable the dockables whose property is changed
* @param property the new property, can be <code>null</code>
*/
protected void setProperties( M mode, Dockable dockable, H property ){
DockableHandle entry = dockables.get( dockable );
if( entry != null ){
if( property == null )
entry.properties.remove( mode.getUniqueIdentifier() );
else
entry.properties.put( mode.getUniqueIdentifier(), property );
}
}
/**
* Gets the properties which correspond to <code>dockable</code>
* and <code>mode</code>.
* @param mode the first part of the key
* @param dockable the second part of the key
* @return the properties or <code>null</code>
*/
protected H getProperties( M mode, Dockable dockable ){
DockableHandle entry = dockables.get( dockable );
if( entry == null )
return null;
return entry.properties.get( mode.getUniqueIdentifier() );
}
/**
* Tells whether this manager is currently changing the {@link Mode} of a {@link Dockable}.
* @return <code>true</code> if a mode is currently working
*/
public boolean isOnTransaction(){
return onTransaction > 0;
}
/**
* Tells whether this manager currently runs a continuous transaction. As long as a continuous
* transaction is running the internal states of this manager do not change.
* @return whether a continuous transaction is running
*/
public boolean isOnContinuous(){
return onContinuous > 0;
}
/**
* Updates the modes of all {@link Dockable}s that
* are registered at this {@link ModeManager}.
*/
public void refresh(){
for( Dockable dockable : dockables.keySet() ){
refresh( dockable, false );
}
}
/**
* Updates the mode of <code>dockable</code> and updates the actions
* associated with <code>dockable</code>. This method is intended to be
* called by any code that changes the mode in a way that is not automatically
* registered by this {@link ModeManager}.
* @param dockable the element whose mode might have changed
* @param recursive if set, then the children of <code>dockable</code>
* are refreshed as well.
*/
public void refresh( Dockable dockable, boolean recursive ){
DockableHandle handle = getHandle( dockable );
if( handle != null ){
handle.putMode( access( getCurrentMode( dockable ) ) );
}
if( recursive ){
DockStation station = dockable.asDockStation();
if( station != null ){
for( int i = 0, n = station.getDockableCount(); i<n; i++ ){
refresh( station.getDockable( i ), recursive );
}
}
}
}
/**
* Removes the properties that belong to <code>dockable</code>.
* @param dockable the element to remove
*/
public void remove( Dockable dockable ){
DockableHandle entry = dockables.remove( dockable );
if( entry != null ){
if( !entry.empty ){
entries.remove( entry.id );
}
fireRemoved( dockable );
}
}
/**
* Removes <code>dockable</code> itself, put the properties of
* <code>dockable</code> remain in the system.
* @param dockable the element to reduce
*/
public void reduceToEmpty( Dockable dockable ){
DockableHandle entry = dockables.get( dockable );
if( entry != null ){
entry.dockable = null;
fireRemoved( dockable );
}
}
/**
* Called while reading modes in {@link #readSettings(ModeSettings)}.
* Subclasses might change the mode according to <code>newMode</code>.
* @param key the identifier of <code>dockable</code>
* @param old the mode <code>dockable</code> is currently in
* @param current the mode <code>dockable</code> is going to be
* @param dockable the element that changes its mode, might be <code>null</code>
*/
protected abstract void applyDuringRead( String key, Path old, Path current, Dockable dockable );
/**
* Tells whether an entry for a missing {@link Dockable} should be created.
* This will result in a call to {@link #addEmpty(String)} during
* {@link #readSettings(ModeSettings)}.
* The default implementation returns always <code>false</code>.
* @param key the key for which to create a new entry
* @return <code>true</code> if an entry should be created
*/
protected boolean createEntryDuringRead( String key ){
return false;
}
/**
* Adds an empty entry to this manager. The empty entry can be used to store
* information for a {@link Dockable} that has not yet been created. It is
* helpful if the client intends to load first its properties and create
* only those {@link Dockable}s which are visible.<br>
* Also an empty entry gets never deleted unless {@link #removeEmpty(String)} is called.
* @param key the name of the empty entry
* @throws NullPointerException if <code>key</code> is <code>null</code>
*/
public void addEmpty( String key ){
if( key == null )
throw new NullPointerException( "name must not be null" );
DockableHandle entry = entries.get( key );
if( entry == null ){
entry = new DockableHandle( null, key );
entries.put( key, entry );
}
entry.empty = true;
}
/**
* Removes the entry for <code>name</code> but only if the entry is not
* associated with any {@link Dockable}.
* @param name the name of the entry which might be empty
* @throws NullPointerException if <code>key</code> is <code>null</code>
*/
public void removeEmpty( String name ){
if( name == null )
throw new NullPointerException( "name must not be null" );
DockableHandle entry = entries.get( name );
if( entry != null ){
entry.empty = false;
if( entry.dockable == null ){
entries.remove( name );
}
}
}
/**
* Tells whether information about dockable <code>key</code> gets
* stored indefinitely or not.
* @param key the key to check
* @return <code>true</code> if the key is never removed automatically
* <code>false</code> otherwise
*/
public boolean isEmpty( String key ){
DockableHandle entry = entries.get( key );
return entry != null && entry.empty;
}
/**
* Given some {@link Dockable} on which an event was registered, searches a
* registered dockable that is a child of <code>target</code> or
* <code>target</code> itself.
* @param target the target whose registered child is searched
* @return <code>target</code>, a child of <code>target</code>, or <code>null</code>
*/
public Dockable getDoubleClickTarget( Dockable target ){
if( target == null ){
return null;
}
if( dockables.get( target ) != null ){
return target;
}
DockStation station = target.asDockStation();
if( station == null ){
return null;
}
return getDoubleClickTarget( station.getFrontDockable() );
}
/**
* Gets the default mode of <code>dockable</code>, the mode
* <code>dockable</code> is in if nothing else is specified. This method checks
* {@link Mode#isDefaultMode(Dockable)} and returns the first
* {@link Mode} where the answer was <code>true</code>.
* @param dockable some dockable, not <code>null</code>
* @return its default mode, must be registered at this {@link ModeManager}
* and not be <code>null</code>
*/
protected M getDefaultMode( Dockable dockable ){
if( modes.isEmpty() )
throw new IllegalStateException( "no modes available" );
for( ModeHandle mode : modes ){
if( mode.mode.isDefaultMode( dockable )){
return mode.mode;
}
}
throw new IllegalStateException( "no mode is the default mode for '" + dockable.getTitleText() + "'" );
}
/**
* Tries to find the mode <code>dockable</code> is currently in. This method
* calls {@link Mode#isCurrentMode(Dockable)} and returns the first
* {@link Mode} where the answer was <code>true</code>.
* @param dockable some dockable, not <code>null</code>
* @return the current mode or <code>null</code> if not found
*/
public M getCurrentMode( Dockable dockable ){
for( ModeHandle mode : modes ){
if( mode.mode.isCurrentMode( dockable )){
return mode.mode;
}
}
return null;
}
/**
* Reading the history this method tells which mode
* <code>dockable</code> was in before the current mode.
* @param dockable some element
* @return the previous mode or <code>null</code> if this
* information is not available
*/
public M getPreviousMode( Dockable dockable ){
DockableHandle handle = getHandle( dockable );
if( handle == null )
return null;
ModeHandle mode = handle.previousMode();
if( mode == null )
return null;
return mode.mode;
}
/**
* Gets the history which modes <code>dockable</code>
* used in the past. The older entries are at the beginning
* of the list. The current mode may or may not be included
* in the list.
* @param dockable the element whose history is asked
* @return the history or an empty list if no history is available
*/
public List<M> getModeHistory( Dockable dockable ){
DockableHandle handle = getHandle( dockable );
if( handle == null )
return Collections.emptyList();
M lastMode = getCurrentMode( dockable );
List<M> result = new ArrayList<M>();
for( Path path : handle.history ){
M mode = getMode( path );
addMode( mode, result );
if( mode == lastMode ){
lastMode = null;
}
}
addMode( lastMode, result );
return result;
}
private void addMode( M mode, List<M> result ){
if( mode != null ){
result.add( mode );
}
}
/**
* Adds the history data <code>history</code> to <code>dockable</code> for mode <code>mode</code>, and
* stores <code>mode</code> as the newest used mode.
* @param dockable the element whose history is modified must be known to this manager
* @param mode the mode whose history is modified
* @param history the new history
* @throws IllegalStateException if <code>dockable</code> is not known to this manager
*/
public void addToModeHistory( Dockable dockable, M mode, H history ){
DockableHandle handle = getHandle( dockable );
if( handle == null ){
throw new IllegalArgumentException( "unknown dockable" );
}
handle.addToHistory( mode.getUniqueIdentifier(), history );
}
/**
* Gets the history which properties <code>dockable</code>
* used in the past. Entries of value <code>null</code> are ignored.
* The older entries are at the beginning of the list.
* @param dockable the element whose history is asked
* @return the history or an empty list if no history is available
*/
public List<H> getPropertyHistory( Dockable dockable ){
DockableHandle handle = getHandle( dockable );
if( handle == null )
return Collections.emptyList();
List<H> result = new ArrayList<H>();
for( Path path : handle.history ){
H history = handle.properties.get( path );
if( history != null ){
result.add( history );
}
}
return result;
}
/**
* Stores the current location of <code>dockable</code> and all its children in respect
* to their current {@link Mode}. Dockables that are not registered at this manager
* are ignored.<br>
* This method does nothing if {@link #isOnContinuous()} returns <code>true</code>
* @param dockable a root of a tree
*/
public void store( Dockable dockable ){
if( isOnContinuous() )
return;
DockUtilities.visit( dockable, new DockUtilities.DockVisitor(){
@Override
public void handleDockable( Dockable check ) {
M mode = getCurrentMode( check );
if( mode != null )
store( mode, check );
}
});
}
/**
* Stores the location of <code>dockable</code> under the key <code>mode</code>.<br>
* This method does nothing if {@link #isOnContinuous()} returns <code>true</code>
* @param mode the mode <code>dockable</code> is currently in
* @param dockable the element whose location will be stored
*/
protected void store( M mode, Dockable dockable ){
if( isOnContinuous() )
return;
DockableHandle handle = getHandle( dockable );
if( handle != null ){
handle.properties.put( mode.getUniqueIdentifier(), mode.current( dockable ) );
}
}
/**
* Gets the <code>ModeAccess</code> which represents <code>mode</code>.
* @param mode some mode or <code>null</code>
* @return its access or <code>null</code>
* @throws IllegalArgumentException if <code>mode</code> is unknown
*/
private ModeHandle access( M mode ){
if( mode == null )
return null;
for( ModeHandle access : modes ){
if( access.mode == mode ){
return access;
}
}
throw new IllegalArgumentException( "unknown mode: " + mode );
}
/**
* Returns an iteration of all modes that are stored in this manager.
* @return the iteration
*/
public Iterable<M> modes(){
return new Iterable<M>(){
public Iterator<M> iterator(){
final Iterator<ModeHandle> handles = modes.iterator();
return new Iterator<M>(){
public boolean hasNext(){
return handles.hasNext();
}
public M next(){
return handles.next().mode;
}
public void remove(){
throw new UnsupportedOperationException( "cannot remove modes this way" );
}
};
}
};
}
/**
* Rebuilds the actions sources for all {@link Dockable}s.
*/
protected void rebuildAll(){
for( DockableHandle handle : dockables.values() ){
handle.updateActionSource();
}
}
/**
* Rebuilds the action sources of <code>dockable</code>.
* @param dockable the element whose actions are to be updated
*/
protected void rebuild( Dockable dockable ){
DockableHandle entry = dockables.get( dockable );
if( entry != null ){
entry.updateActionSource();
}
}
/**
* Gets a list of actions that should be shown on <code>station</code> depending on the
* current children of <code>station</code>. This method is called every time when either
* a child is added, removed or selected on <code>station</code>.
* @param station the station whose actions are asked
* @return the actions, can be <code>null</code>
*/
public abstract DockActionSource getSharedActions( DockStation station );
private DockableHandle getHandle( Dockable dockable ){
return dockables.get( dockable );
}
/**
* Creates a new {@link ModeSetting} which is configured to transfer data from
* this {@link ModeManager} to persistent storage or the other way. The new setting
* contains all the {@link ModeSettingFactory}s which are currently known to this manager.
* @param <B> the intermediate format
* @param converter conversion tool from this manager's meta-data format to the intermediate
* format.
* @return the new empty settings
*/
public <B> ModeSettings<H, B> createSettings( ModeSettingsConverter<H, B> converter ){
ModeSettings<H, B> settings = createModeSettings( converter );
for( ModeSettingFactory<H> factory : factories.values() ){
settings.addFactory( factory );
}
for( ModeHandle mode : modes ){
if( mode.mode != null ){
ModeSettingFactory<H> factory = mode.mode.getSettingFactory();
if( factory != null ){
settings.addFactory( factory );
}
}
}
return settings;
}
/**
* Creates the empty set of settings for this {@link ModeManager}. Subclasses
* may override this method to use another set of settings. This method does
* not need to call {@link ModeSettings#addFactory(ModeSettingFactory)}.
* @param <B> the intermediate format
* @param converter conversion tool from this manager's meta-data format to the
* intermediate format.
* @return the new empty settings
*/
public <B> ModeSettings<H, B> createModeSettings( ModeSettingsConverter<H, B> converter ){
return new ModeSettings<H, B>( converter );
}
/**
* Writes all the information stored in this {@link ModeManager} to
* <code>setting</code>.
* @param setting the settings to fill
*/
public void writeSettings( ModeSettings<H,?> setting ){
// dockables
for( DockableHandle handle : entries.values() ){
setting.add( handle.id, handle.getCurrent(), handle.properties, handle.history );
}
// modes
for( ModeHandle handle : modes ){
if( handle.mode != null ){
setting.add( handle.mode );
}
}
}
/**
* Reads the contents of <code>settings</code> and stores it.
* @param settings the settings to read
*/
public void readSettings( ModeSettings<H, ?> settings ){
readSettings( settings, null );
}
/**
* Reads the contents of <code>settings</code>, creates new entries if either
* {@link #createEntryDuringRead(String)} or if <code>pending</code> allows the setting
* to be undone if not needed.
* @param settings the settings to read
* @param pending undoable settings, can be <code>null</code>
* @return an algorithm that will remove any entry that was created because <code>pending</code>
* did advise so, <code>null</code> if <code>pending</code> was <code>null</code>
*/
public Runnable readSettings( ModeSettings<H, ?> settings, UndoableModeSettings pending ){
final List<String> temporary = new ArrayList<String>();
// dockables
for( int i = 0, n = settings.size(); i < n; i++ ){
String key = settings.getId( i );
DockableHandle entry = entries.get( key );
if( entry == null ){
if( createEntryDuringRead( key )){
addEmpty( key );
entry = entries.get( key );
}
else if( pending != null && pending.createTemporaryDuringRead( key )){
addEmpty( key );
entry = entries.get( key );
temporary.add( key );
}
}
if( entry != null ){
Path current = settings.getCurrent( i );
Path old = null;
if( entry.dockable != null ){
M oldMode = getCurrentMode( entry.dockable );
if( oldMode != null ){
old = oldMode.getUniqueIdentifier();
}
}
if( current == null )
current = old;
entry.history.clear();
for( Path next : settings.getHistory( i ))
entry.history.add( next );
entry.properties = settings.getProperties( i );
if( (old == null && current != null) || (old != null && !old.equals( current ))){
applyDuringRead( key, old, current, entry.dockable );
}
}
}
// modes
for( ModeHandle handle : modes ){
if( handle.mode != null ){
ModeSetting<H> setting = settings.getSettings( handle.mode.getUniqueIdentifier() );
if( setting != null ){
handle.mode.readSetting( setting );
}
}
}
if( pending == null ){
return null;
}
else{
return new Runnable(){
public void run(){
for( String key : temporary ){
removeEmpty( key );
}
}
};
}
}
/**
* Adds all elements of <code>dockables</code> to the current
* {@link AffectedSet}.
* @param dockables the elements to add
*/
public void addAffected( Iterable<Dockable> dockables ){
openAffected();
for( Dockable element : dockables ){
affected.add( element );
}
closeAffected();
}
/**
* Opens the {@link ChangeSet} that collects {@link Dockable}s whose mode
* may have changed.
*/
private void openAffected(){
if( affectedCount == 0 ){
affected = new ChangeSet();
}
affectedCount++;
}
/**
* Closes the {@link ChangeSet} that collected {@link Dockable}s whose mode
* may have changed.
*/
private void closeAffected(){
affectedCount--;
if( affectedCount == 0 ){
ChangeSet old = affected;
affected = null;
old.finish();
}
}
@Override
public String toString(){
StringBuilder builder = new StringBuilder();
builder.append( getClass().getName() );
builder.append( "[" );
for( DockableHandle handle : entries.values() ){
builder.append( "\n\t" );
builder.append( handle.id );
for( Map.Entry<Path, H> entry : handle.properties.entrySet() ){
builder.append( "\n\t\t" );
builder.append( entry.getKey() );
builder.append( " -> " );
builder.append( entry.getValue() );
}
}
builder.append( "\n]" );
return builder.toString();
}
/**
* A wrapper around a mode, giving access to its properties. The mode
* inside this wrapper can be replaced any time.
* @author Benjamin Sigg
*/
private class ModeHandle{
private M mode;
public ModeHandle( M mode ){
this.mode = mode;
}
}
/**
* Describes all properties a {@link Dockable} has.
* @author Benjamin Sigg
*/
private class DockableHandle{
/** the {@link Dockable} for which the properties are stored */
public Dockable dockable;
/** a unique id associated with {@link #dockable} */
public String id;
/** the set of actions available for {@link #dockable} */
public MultiDockActionSource source;
/** a map that stores some properties mapped to the different modes */
public Map<Path, H> properties;
/** The modes this entry already visited. No mode is more than once in this list. */
private List<Path> history;
/** if <code>true</code>, then this entry is not deleted automatically */
private boolean empty = false;
/**
* Creates a new entry
* @param dockable the element whose properties are stores in this entry
* @param id the unique if of this entry
*/
public DockableHandle( Dockable dockable, String id ){
this.dockable = dockable;
this.id = id;
source = new MultiDockActionSource( new LocationHint( LocationHint.ACTION_GUARD, LocationHint.RIGHT ) );
properties = new HashMap<Path, H>();
history = new LinkedList<Path>();
}
/**
* Updates the action source of this manager.
*/
public void updateActionSource(){
if( dockable != null ){
source.removeAll();
M mode = getCurrentMode( dockable );
if( mode == null )
mode = getDefaultMode( dockable );
for( ModeHandle access : modes ){
DockActionSource next = access.mode.getActionsFor( dockable, mode );
if( next != null ){
source.add( next );
}
}
}
}
/**
* Stores <code>mode</code> in a stack that describes the history
* through which this entry moved. If <code>mode</code> is already
* in the stack, than it is moved to the top of the stack.
* @param mode the mode to store, <code>null</code> will be ignored
*/
public void putMode( ModeHandle mode ){
if( mode != null ){
ModeHandle oldMode = peekMode();
if( oldMode != mode ){
Path id = mode.mode.getUniqueIdentifier();
addToHistory( id, mode.mode.current( dockable ) );
rebuild( dockable );
fireModeChanged( dockable, oldMode == null ? null : oldMode.mode, mode.mode );
}
else{
rebuild( dockable );
}
}
}
/**
* Adds the mode <code>id</code> to the history.
* @param id the unique identifier of a mode
* @param data history data associated with mode <code>id</code>
*/
public void addToHistory( Path id, H data ){
history.remove( id );
history.add( id );
properties.put( id, data );
}
/**
* Gets the mode that was used previously to the current mode.
* If the history gets empty, then {@link ModeManager#getDefaultMode(Dockable)}
* is returned.
* @return the mode in which this entry was before the current mode
* was put onto the history
*/
public ModeHandle previousMode(){
if( history.size() < 2 )
return access( getDefaultMode( dockable ) );
else
return getAccess( history.get( history.size()-2 ) );
}
/**
* Gets the current mode of this entry.
* @return the mode or <code>null</code>
*/
public ModeHandle peekMode(){
if( history.isEmpty() )
return null;
else
return getAccess( history.get( history.size()-1 ) );
}
/**
* Gets the id of the current mode (if any).
* @return the id or <code>null</code>
*/
public Path getCurrent(){
if( dockable == null )
return null;
M mode = getCurrentMode( dockable );
if( mode == null )
return null;
return mode.getUniqueIdentifier();
}
}
/**
* Default implementation of {@link AffectedSet}. Linked to the enclosing
* {@link ModeManager}.
* @author Benjamin Sigg
*/
private class ChangeSet implements AffectedSet{
/** the changed elements */
private Set<Dockable> set = new HashSet<Dockable>();
/**
* Creates a new set
*/
public ChangeSet(){
// nothing
}
public void add( Dockable dockable ){
if( dockable != null ){
set.add( dockable );
DockStation station = dockable.asDockStation();
if( station != null ){
for( int i = 0, n = station.getDockableCount(); i<n; i++ ){
add( station.getDockable( i ));
}
}
}
}
/**
* Performs the clean up operations that are required after some
* <code>Dockable</code>s have changed their mode.<br>
* for each element known to this set.
*/
public void finish(){
for( Dockable dockable : set ){
refresh( dockable, false );
}
}
}
}