Package bibliothek.gui.dock.support.mode

Source Code of bibliothek.gui.dock.support.mode.ModeManager$ChangeSet

/*
* 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>docakble</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();
   
    List<M> result = new ArrayList<M>();
    for( Path path : handle.history ){
      M mode = getMode( path );
      if( mode != null ){
        result.add( mode );
      }
    }
    return result;
  }
 
  /**
   * 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 everytime 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 managers 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 converstion tool from this managers 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 );
            }
        }
    }
}
TOP

Related Classes of bibliothek.gui.dock.support.mode.ModeManager$ChangeSet

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.