/* * Bibliothek - DockingFrames * Library built on Java/Swing, allows the user to "drag and drop" * panels containing any Swing-Component the developer likes to add. * * Copyright (C) 2009 Benjamin Sigg * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Benjamin Sigg * benjamin_sigg@gmx.ch * CH - Switzerland */ package bibliothek.gui.dock.facile.mode; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.swing.KeyStroke; import bibliothek.gui.DockController; import bibliothek.gui.DockStation; import bibliothek.gui.Dockable; import bibliothek.gui.dock.DockElement; import bibliothek.gui.dock.common.CControl; import bibliothek.gui.dock.common.action.predefined.CMaximizeAction; import bibliothek.gui.dock.common.mode.CLocationModeManager; import bibliothek.gui.dock.common.mode.ExtendedMode; import bibliothek.gui.dock.event.DockRegisterAdapter; import bibliothek.gui.dock.event.KeyboardListener; import bibliothek.gui.dock.facile.mode.action.MaximizedModeAction; import bibliothek.gui.dock.layout.DockableProperty; import bibliothek.gui.dock.support.mode.AffectedSet; import bibliothek.gui.dock.support.mode.AffectingRunnable; import bibliothek.gui.dock.support.mode.Mode; import bibliothek.gui.dock.support.mode.ModeManager; import bibliothek.gui.dock.support.mode.ModeManagerListener; import bibliothek.gui.dock.support.mode.ModeSetting; import bibliothek.gui.dock.support.mode.ModeSettingFactory; import bibliothek.gui.dock.util.DockProperties; import bibliothek.gui.dock.util.DockUtilities; import bibliothek.gui.dock.util.IconManager; import bibliothek.gui.dock.util.PropertyValue; import bibliothek.util.Path; /** * {@link Dockable}s are maximized if they take up the whole space a frame * or a screen offers. * @author Benjamin Sigg * @param <M> the kind of areas this mode handles */ public class MaximizedMode<M extends MaximizedModeArea> extends AbstractLocationMode<M>{ /** unique identifier for this mode */ public static final Path IDENTIFIER = new Path( "dock.mode.maximized" ); /** the key used for the {@link IconManager} to read the {@link javax.swing.Icon} for the "maximize"-action */ public static final String ICON_IDENTIFIER = CLocationModeManager.ICON_MANAGER_KEY_MAXIMIZE; /** the mode in which some dockable with id=key was before maximizing */ private Map<String, Path> lastMaximizedMode = new HashMap<String, Path>(); /** the location some dockable had before maximizing */ private Map<String, Location> lastMaximizedLocation = new HashMap<String, Location>(); /** the listener responsible for detecting apply-events on other modes */ private Listener listener = new Listener(); /** all the {@link KeyHook}s that are currently used */ private List<KeyHook> hooks = new LinkedList<KeyHook>(); /** * {@link KeyStroke} used to go into, or go out from the maximized state. */ private PropertyValue<KeyStroke> keyStrokeMaximizeChange = new PropertyValue<KeyStroke>( CControl.KEY_MAXIMIZE_CHANGE ){ @Override protected void valueChanged( KeyStroke oldValue, KeyStroke newValue ) { // ignore } }; /** * Empty default constructor. Subclasses should call * {@link #setActionProvider(LocationModeActionProvider)} to complete * initialization of this mode. */ protected MaximizedMode(){ // nothing } /** * Creates a new mode * @param control the control in whose realm this mode will work */ public MaximizedMode( CControl control ){ setActionProvider( new DefaultLocationModeActionProvider( new CMaximizeAction( control ) ) ); } /** * Creates a new mode. * @param controller the owner of this mode */ public MaximizedMode( DockController controller ){ setActionProvider( new DefaultLocationModeActionProvider( new MaximizedModeAction( controller, this ) ) ); } @Override public void setManager( LocationModeManager<?> manager ){ for( KeyHook hook : hooks ){ hook.destroy( false ); } hooks.clear(); LocationModeManager<?> old = getManager(); listener.replaceManager( old, manager ); if( manager == null ) keyStrokeMaximizeChange.setProperties( (DockProperties)null ); else keyStrokeMaximizeChange.setProperties( manager.getController() ); super.setManager( manager ); } public Path getUniqueIdentifier(){ return IDENTIFIER; } public ExtendedMode getExtendedMode(){ return ExtendedMode.MAXIMIZED; } public boolean runApply( Dockable dockable, Location history, AffectedSet set ){ MaximizedModeArea area = getMaximizeArea( dockable, history ); if( area == null ) area = getDefaultArea(); area.prepareApply( dockable, history, set ); return maximize( area, dockable, history, set ); } public Location current( Dockable dockable ){ MaximizedModeArea area = get( dockable ); if( area == null ) return null; DockableProperty location = area.getLocation( dockable ); return new Location( getUniqueIdentifier(), area.getUniqueId(), location, false ); } public boolean isCurrentMode( Dockable dockable ){ for( MaximizedModeArea area : this ){ if( area.isChild( dockable ) ) return true; } return false; } public boolean isDefaultMode( Dockable dockable ){ return false; } @Override public boolean isRepresenting( DockStation station ){ if( super.isRepresenting( station )){ return true; } Dockable dockable = station.asDockable(); if( dockable == null ){ return false; } for( MaximizedModeArea area : this ){ if( area.isChild( dockable ) ){ return true; } } return false; } /** * Assuming <code>dockable</code> is a maximized element, tells which * mode would be the preferred mode for unmaximization. * @param dockable some child * @return the preferred unmaximized mode, can be <code>null</code> */ public LocationMode getUnmaximizedMode( Dockable dockable ){ while( dockable != null ){ for( MaximizedModeArea area : this ){ if( area.isChild( dockable ) ){ return area.getUnmaximizedMode(); } } DockStation parent = dockable.getDockParent(); dockable = parent == null ? null : parent.asDockable(); } return null; } /** * Ensures that <code>dockable</code> is maximized. * @param area the future parent of <code>dockable</code>, can be <code>null</code> * @param dockable the element that should be made maximized * @param set a set of <code>Dockable</code>s which will be filled by the * elements that change their mode because of this method */ public void maximize( MaximizedModeArea area, Dockable dockable, AffectedSet set ){ maximize( area, dockable, null, set ); } /** * Ensures that <code>dockable</code> is maximized. * @param area the future parent of <code>dockable</code>, can be <code>null</code> * @param dockable the element that should be made maximized * @param history the expected location of <code>dockable</code> after this method has finished, can be <code>null</code>. * No guarantees are given that the final location matches <code>history</code>. * @param set a set of <code>Dockable</code>s which will be filled by the * elements that change their mode because of this method * @return whether the operation was a success */ public boolean maximize( MaximizedModeArea area, Dockable dockable, Location history, AffectedSet set ){ Dockable maximizing = getMaximizingElement( dockable ); if( maximizing != dockable ) getManager().store( maximizing ); if( area == null ) area = getMaximizeArea( maximizing ); if( area == null ) area = getDefaultArea(); String id = getManager().getKey( maximizing ); LocationMode current = getManager().getCurrentMode( maximizing ); if( id == null && current == null ){ throw new IllegalStateException( "an unidentified dockable without location has been found, all dockables except true root-station must have a location, true root-stations can never be used in this method." ); } if( id == null && current != null ){ lastMaximizedLocation.put( area.getUniqueId(), current.current( maximizing ) ); lastMaximizedMode.put( area.getUniqueId(), current.getUniqueIdentifier() ); } else{ getManager().store( dockable ); } List<Dockable> oldMaximized = getMaximized( area ); area.setMaximized( maximizing, true, history, set ); if( !(id == null && current == null )){ for( Dockable newMaximized : area.getMaximized() ){ if( newMaximized != maximizing ){ if( DockUtilities.isAncestor( newMaximized, maximizing )){ // the maximizing element was put on a DockStation if( !oldMaximized.contains( newMaximized )){ // and the DockStation was created by this action for( Dockable replaced : oldMaximized ){ if( DockUtilities.isAncestor( newMaximized, replaced )){ storeLastMaximizedLocation( area, replaced ); break; } } } } } } } set.add( maximizing ); return true; } private List<Dockable> getMaximized( MaximizedModeArea area ){ Dockable[] children = area.getMaximized(); if( children == null ){ return Collections.emptyList(); } else{ return Arrays.asList( children ); } } private void storeLastMaximizedLocation( MaximizedModeArea area, Dockable dockable ){ LocationMode previousMode = getManager().getPreviousMode( dockable ); if( previousMode != null ){ Location previousLocation = getManager().getHistory( dockable, previousMode.getUniqueIdentifier() ); if( previousLocation != null ){ lastMaximizedLocation.put( area.getUniqueId(), previousLocation ); lastMaximizedMode.put( area.getUniqueId(), previousMode.getUniqueIdentifier() ); } } } /** * Ensures that <code>dockable</code> is not maximized. Does nothing if the parent * {@link MaximizedModeArea} of <code>dockable</code> has not maximized <code>dockable</code> * or if the {@link LocationModeManager} does not know <code>dockable</code>. * @param dockable the element that might be maximized currently * @param set a set of <code>Dockable</code>s which will be filled by the * elements that change their mode because of this method */ public void unmaximize( Dockable dockable, AffectedSet set ){ final MaximizedModeArea area = getMaximizeArea( dockable ); if( area != null && area.getMaximized() != null ){ Dockable[] maximized = area.getMaximized(); if( maximized != null ){ for( Dockable check : maximized ){ if( DockUtilities.isAncestor( check, dockable )){ set.add( dockable ); dockable = check; final Dockable element = dockable; final LocationModeManager<?> manager = getManager(); manager.runTransaction( new AffectingRunnable() { public void run( AffectedSet set ){ area.setMaximized( element, false, null, set ); String key = area.getUniqueId(); boolean done = false; // try to apply the last mode if( lastMaximizedLocation.get( key ) != null ){ done = getManager().apply( element, lastMaximizedMode.remove( key ), lastMaximizedLocation.remove( key ), set ); } if( !done ){ applyOldLocation( element, set ); } } }, true ); manager.store( dockable ); return; } } } } } /** * Recursively searches through the tree of {@link DockElement}s and applies old locations * on those {@link Dockable}s which are known to have a location. Branches are not visited * if the a parent element has been found with an old location. * @param element the root of the tree to search through * @param set a set to be filled with all the {@link Dockable}s whose location changed. */ private void applyOldLocation( Dockable element, AffectedSet set ){ LocationModeManager<?> manager = getManager(); if( manager.isRegistered( element ) ){ LocationMode mode = manager.getPreviousMode( element ); if( mode == null || mode == MaximizedMode.this ) mode = manager.getMode( NormalMode.IDENTIFIER ); manager.apply( element, mode.getUniqueIdentifier(), set, true ); } else if( element.asDockStation() != null ){ DockStation station = element.asDockStation(); Dockable[] children = new Dockable[ station.getDockableCount() ]; for( int i = 0; i < children.length; i++ ){ children[i] = station.getDockable( i ); } for( Dockable child : children ){ applyOldLocation( child, set ); } } } /** * Searches the {@link MaximizedModeArea} which either represents * <code>station</code> or its nearest parent. * @param station some station * @return the nearest area or <code>null</code> */ public MaximizedModeArea getNextMaximizeArea( DockStation station ){ while( station != null ){ MaximizedModeArea area = getMaximizeArea( station ); if( area != null ){ return area; } Dockable dockable = station.asDockable(); if( dockable == null ) return null; station = dockable.getDockParent(); } return null; } /** * Ensures that either the {@link MaximizedModeArea} <code>station</code> or its * nearest parent does not show a maximized element. * @param station an area or a child of an area * @param affected elements whose mode changes will be added to this set */ public void unmaximize( DockStation station, AffectedSet affected ){ MaximizedModeArea area = getNextMaximizeArea( station ); if( area != null ){ Dockable[] dockables = area.getMaximized(); if( dockables != null ){ for( Dockable dockable : dockables ){ unmaximize( dockable, affected ); } } } } /** * Ensures that <code>area</code> has no maximized child. * @param area some area * @param affected the element whose mode might change */ public void unmaximize( MaximizedModeArea area, AffectedSet affected ){ Dockable[] dockables = area.getMaximized(); if( dockables != null ){ for( Dockable dockable : dockables ){ unmaximize( dockable, affected ); } } } public void ensureNotHidden( final Dockable dockable ){ getManager().runTransaction( new AffectingRunnable() { public void run( AffectedSet set ){ Dockable mutableDockable = dockable; DockStation parent = mutableDockable.getDockParent(); Dockable element = getMaximizingElement( mutableDockable ); while( parent != null ){ MaximizedModeArea area = getMaximizeArea( parent ); if( area != null ){ Dockable[] maximized = area.getMaximized(); if( maximized != null ){ for( Dockable check : maximized ){ if( maximized != null && check != mutableDockable && check != element ){ unmaximize( check, set ); } } } } mutableDockable = parent.asDockable(); parent = mutableDockable == null ? null : mutableDockable.getDockParent(); } } }); } /** * Gets the area to which <code>dockable</code> should be maximized. This can be * {@link #getMaximizeArea(Dockable)}, or some other station. * @param dockable the element that is maximized * @param history the history of the last place where <code>dockable</code> was maximized, might be <code>null</code> * @return the preferred area to maximize <code>dockable</code> */ public MaximizedModeArea getMaximizeArea( Dockable dockable, Location history ){ return getMaximizeArea( dockable ); } /** * Searches the first {@link MaximizedModeArea} which is a parent * of <code>dockable</code>. This method will never return * <code>dockable</code> itself. * @param dockable the element whose maximize area is searched * @return the area or <code>null</code> */ public MaximizedModeArea getMaximizeArea( Dockable dockable ){ DockStation parent = dockable.getDockParent(); while( parent != null ){ MaximizedModeArea area = getMaximizeArea( parent ); if( area != null ) return area; dockable = parent.asDockable(); if( dockable == null ){ parent = null; } else{ parent = dockable.getDockParent(); } } return null; } /** * Searches the one {@link MaximizedModeArea} whose station is * <code>station</code>. * @param station the station whose area is searched * @return the area or <code>null</code> if not found */ public MaximizedModeArea getMaximizeArea( DockStation station ){ for( MaximizedModeArea area : this ){ if( area.isRepresenting( station ) ){ return area; } } return null; } /** * Gets the element which must be maximized when the user requests that * <code>dockable</code> is maximized. * @param dockable some element, not <code>null</code> * @return the element that must be maximized, might be <code>dockable</code> * itself, not <code>null</code> */ public Dockable getMaximizingElement( Dockable dockable ){ return getManager().getGroupBehavior().getGroupElement( getManager(), dockable, getExtendedMode() ); } /** * Gets the element which would be maximized if <code>old</code> is currently * maximized, and <code>dockable</code> is or will not be maximized. * @param old some element * @param dockable some element, might be <code>old</code> * @return the element which would be maximized if <code>dockable</code> is * no longer maximized, can be <code>null</code> */ public Dockable getMaximizingElement( Dockable old, Dockable dockable ){ return getManager().getGroupBehavior().getReplaceElement( getManager(), old, dockable, getExtendedMode() ); } protected void applyStarting( LocationModeEvent event ){ List<Runnable> runs = new ArrayList<Runnable>(); for( MaximizedModeArea area : this ){ Runnable run = area.onApply( event ); if( run != null ){ runs.add( run ); } } Dockable dockable = event.getDockable(); final MaximizedModeArea maxiarea = getMaximizeArea( dockable ); if( maxiarea == null ) return; Dockable[] maximizedNow = maxiarea.getMaximized(); if( maximizedNow == null ) return; Dockable maximized = null; for( int i = 0; i < maximizedNow.length; i++ ){ if( DockUtilities.isAncestor( maximizedNow[i], dockable )){ maximized = getMaximizingElement( maximizedNow[i], dockable ); break; } } Runnable run = maxiarea.onApply( event, maximized ); if( run != null ){ runs.add( run ); } if( !runs.isEmpty() ){ event.setClientObject( listener, runs ); } } @SuppressWarnings("unchecked") protected void applyDone( LocationModeEvent event ){ List<Runnable> runs = (List<Runnable>)event.getClientObject( listener ); if( runs != null ){ for( Runnable run : runs ){ run.run(); } } } public ModeSettingFactory<Location> getSettingFactory(){ return MaximizedModeSetting.FACTORY; } public void writeSetting( ModeSetting<Location> setting ){ if( setting instanceof MaximizedModeSetting ){ MaximizedModeSetting modeSetting = (MaximizedModeSetting)setting; modeSetting.setLastMaximizedLocation( lastMaximizedLocation ); modeSetting.setLastMaximizedMode( lastMaximizedMode ); } } public void readSetting( ModeSetting<Location> setting ){ if( setting instanceof MaximizedModeSetting ){ MaximizedModeSetting modeSetting = (MaximizedModeSetting)setting; lastMaximizedLocation = new HashMap<String, Location>( modeSetting.getLastMaximizedLocation() ); lastMaximizedMode = new HashMap<String, Path>( modeSetting.getLastMaximizedMode() ); } } /** * Invoked whenever a key is pressed, released or typed. * @param dockable the element to which the event belongs * @param event the event * @return <code>true</code> if the event has been processed, <code>false</code> * if the event was not used up. */ protected boolean process( Dockable dockable, KeyEvent event ){ KeyStroke stroke = KeyStroke.getKeyStrokeForEvent( event ); if( stroke.equals( keyStrokeMaximizeChange.getValue() )){ return switchMode( dockable ); } return false; } /** * Tries to switch the current mode of <code>dockable</code> to or from * the maximized mode. * @param dockable the element whose mode is to be changed * @return whether the operation was successful */ public boolean switchMode( Dockable dockable ){ LocationModeManager<?> manager = getManager(); LocationMode current = manager.getCurrentMode( dockable ); if( current == this ){ LocationMode mode = manager.getPreviousMode( dockable ); if( mode != null ){ if( manager.isModeAvailable( dockable, mode.getExtendedMode() )){ manager.setMode( dockable, mode.getExtendedMode() ); manager.ensureValidLocation( dockable ); return true; } } } else{ if( manager.isModeAvailable( dockable, getExtendedMode() )){ manager.setMode( dockable, getExtendedMode() ); manager.ensureValidLocation( dockable ); return true; } } return false; } /** * A hook recording key-events for a specific {@link Dockable}. The hook will remove * itself from the {@link Dockable} automatically once the element is removed from * the controller. * @author Benjamin Sigg */ private class KeyHook extends DockRegisterAdapter implements KeyboardListener{ /** the Dockable which is observed by this hook */ private Dockable dockable; /** the controller on which this hook has registered its listeners */ private DockController controller; /** * Creates a new hook * @param dockable the element which will be observed until it is removed * from the {@link DockController}. */ public KeyHook( Dockable dockable ){ this.dockable = dockable; controller = getController(); controller.getKeyboardController().addListener( this ); controller.getRegister().addDockRegisterListener( this ); hooks.add( this ); } @Override public void dockableUnregistered( DockController controller, Dockable dockable ) { if( this.dockable == dockable ){ destroy( true ); } } /** * Removes this hook from the controller * @param complete whether to remove <code>this</code> from {@link MaximizedMode#hooks} */ public void destroy( boolean complete ){ controller.getKeyboardController().removeListener( this ); controller.getRegister().removeDockRegisterListener( this ); if( complete ){ hooks.remove( this ); } } public DockElement getTreeLocation() { return dockable; } public boolean keyPressed( DockElement element, KeyEvent event ) { return process( dockable, event ); } public boolean keyReleased( DockElement element, KeyEvent event ) { return process( dockable, event ); } public boolean keyTyped( DockElement element, KeyEvent event ) { return process( dockable, event ); } } /** * A listener that adds itself to all {@link LocationMode}s a {@link LocationModeManager} has. * Calls to the {@link Mode#apply(Dockable, Object, AffectedSet) apply} method is forwarded * to the enclosing {@link MaximizedMode}. * @author Benjamin Sigg */ private class Listener implements ModeManagerListener<Location, LocationMode>, LocationModeListener { /** * Removes this listener from <code>oldManager</code> and adds this to <code>newManager</code>. * @param oldManager the old manager, can be <code>null</code> * @param newManager the new manager, can be <code>null</code> */ public void replaceManager( LocationModeManager<?> oldManager, LocationModeManager<?> newManager ){ if( oldManager != null ){ oldManager.removeModeManagerListener( this ); for( LocationMode mode : oldManager.modes() ){ modeRemoved( oldManager, mode ); } } if( newManager != null ){ newManager.addModeManagerListener( this ); for( LocationMode mode : newManager.modes() ){ modeAdded( newManager, mode ); } for( Dockable dockable : newManager.listDockables() ){ new KeyHook( dockable ); } } } public void dockableAdded( ModeManager<? extends Location, ? extends LocationMode> manager, Dockable dockable ){ new KeyHook( dockable ); } public void dockableRemoved( ModeManager<? extends Location, ? extends LocationMode> manager, Dockable dockable ){ // ignore } public void modeAdded( ModeManager<? extends Location, ? extends LocationMode> manager, LocationMode mode ){ if( mode != MaximizedMode.this ){ mode.addLocationModeListener( this ); } } public void modeChanged( ModeManager<? extends Location, ? extends LocationMode> manager, Dockable dockable, LocationMode oldMode, LocationMode newMode ){ // ignore } public void modeRemoved( ModeManager<? extends Location, ? extends LocationMode> manager, LocationMode mode ){ mode.removeLocationModeListener( this ); } public void applyDone( LocationModeEvent event ){ MaximizedMode.this.applyDone( event ); } public void applyStarting( LocationModeEvent event ){ MaximizedMode.this.applyStarting( event ); } } }