/* * 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.common.mode; import java.util.HashMap; import java.util.List; import java.util.Map; import bibliothek.gui.DockStation; import bibliothek.gui.Dockable; import bibliothek.gui.dock.common.CLocation; import bibliothek.gui.dock.common.CStation; import bibliothek.gui.dock.common.intern.CControlAccess; import bibliothek.gui.dock.common.intern.CDockFrontend; import bibliothek.gui.dock.common.intern.CDockFrontendListener; import bibliothek.gui.dock.common.intern.CDockable; import bibliothek.gui.dock.common.intern.CDockableAccess; import bibliothek.gui.dock.common.intern.CSetting; import bibliothek.gui.dock.common.intern.CommonDockable; import bibliothek.gui.dock.facile.mode.CLocationModeSettings; import bibliothek.gui.dock.facile.mode.Location; import bibliothek.gui.dock.facile.mode.LocationMode; import bibliothek.gui.dock.facile.mode.LocationModeManager; 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.ModeManager; import bibliothek.gui.dock.support.mode.ModeManagerListener; import bibliothek.gui.dock.support.mode.ModeSettings; import bibliothek.gui.dock.support.mode.ModeSettingsConverter; import bibliothek.gui.dock.support.mode.UndoableModeSettings; import bibliothek.gui.dock.util.DockUtilities; import bibliothek.gui.dock.util.IconManager; import bibliothek.util.Path; import bibliothek.util.container.Single; /** * {@link LocationModeManager} providing additional methods for working with * {@link CLocation}s, {@link CommonDockable}s and other items specific to the * <code>common</code> project. * @author Benjamin Sigg * */ public class CLocationModeManager extends LocationModeManager<CLocationMode>{ /** the key used for the {@link IconManager} to read the {@link javax.swing.Icon} for the "minimize"-action */ public static final String ICON_MANAGER_KEY_MINIMIZE = "locationmanager.minimize"; /** the key used for the {@link IconManager} to read the {@link javax.swing.Icon} for the "maximize"-action */ public static final String ICON_MANAGER_KEY_MAXIMIZE = "locationmanager.maximize"; /** the key used for the {@link IconManager} to read the {@link javax.swing.Icon} for the "normalize"-action */ public static final String ICON_MANAGER_KEY_NORMALIZE = "locationmanager.normalize"; /** the key used for the {@link IconManager} to read the {@link javax.swing.Icon} for the "externalize"-action */ public static final String ICON_MANAGER_KEY_EXTERNALIZE = "locationmanager.externalize"; /** the key used for the {@link IconManager} to read the {@link javax.swing.Icon} for the "unexternalize"-action */ public static final String ICON_MANAGER_KEY_UNEXTERNALIZE = "locationmanager.unexternalize"; /** the key used for the {@link IconManager} to read the {@link javax.swing.Icon} for the "unmaximize externalized"-action */ public static final String ICON_MANAGER_KEY_UNMAXIMIZE_EXTERNALIZED = "locationmanager.unmaximize_externalized"; private CControlAccess control; private CNormalMode normalMode; private CMaximizedMode maximizedMode; private CMinimizedMode minimizedMode; private CExternalizedMode externalizedMode; /** * Creates a new manager. * @param control the control in whose realm this manager works */ public CLocationModeManager( CControlAccess control ){ super( control.getOwner().intern().getController() ); this.control = control; setDoubleClickStrategy( new PreviousModeDoubleClickStrategy( this ) ); minimizedMode = new CMinimizedMode( control.getOwner() ); maximizedMode = new CMaximizedMode( control.getOwner() ); normalMode = new CNormalMode( control.getOwner() ); externalizedMode = new CExternalizedMode( control.getOwner() ); putMode( minimizedMode ); putMode( externalizedMode ); putMode( normalMode ); putMode( maximizedMode ); addModeManagerListener(new ModeManagerListener<Location, LocationMode>(){ public void dockableAdded( ModeManager<? extends Location, ? extends LocationMode> manager, Dockable dockable ){ // ignore } public void dockableRemoved( ModeManager<? extends Location, ? extends LocationMode> manager, Dockable dockable ){ // ignore } public void modeAdded( ModeManager<? extends Location, ? extends LocationMode> manager, LocationMode mode ){ // ignore } public void modeChanged( ModeManager<? extends Location, ? extends LocationMode> manager, Dockable dockable, LocationMode oldMode, LocationMode newMode ){ CDockableAccess access = CLocationModeManager.this.control.access( ((CommonDockable)dockable).getDockable() ); if( access != null ){ ExtendedMode mode = getMode(dockable); access.informMode( mode ); } } public void modeRemoved( ModeManager<? extends Location, ? extends LocationMode> manager, LocationMode mode ){ // ignore } }); } /** * Direct access to the mode handling "normal" {@link Dockable}s. * @return the mode */ public CNormalMode getNormalMode(){ return normalMode; } /** * Direct access to the mode handling "maximized" {@link Dockable}s. * @return the mode */ public CMaximizedMode getMaximizedMode(){ return maximizedMode; } /** * Direct access to the mode handling "minimized" {@link Dockable}s. * @return the mode */ public CMinimizedMode getMinimizedMode(){ return minimizedMode; } /** * Direct access to the mode handling "externalized" {@link Dockable}s. * @return the mode */ public CExternalizedMode getExternalizedMode(){ return externalizedMode; } @Override protected boolean createEntryDuringRead( String key ){ return control.shouldStore( key ); } @Override public void remove( Dockable dockable ){ if( dockable instanceof CommonDockable ){ CDockable cdockable = ((CommonDockable)dockable).getDockable(); String key = control.shouldStore( cdockable ); if( key != null ){ addEmpty( key ); } } super.remove( dockable ); } @Override public <B> ModeSettings<Location, B> createModeSettings( ModeSettingsConverter<Location, B> converter ){ return new CLocationModeSettings<B>( converter ); } @Override public void readSettings( ModeSettings<Location, ?> settings ){ UndoableModeSettings undoable = new UndoableModeSettings(){ public boolean createTemporaryDuringRead( String key ){ return control.getRegister().isMultiId( key ); } }; final Runnable temporary = readSettings( settings, undoable ); control.getOwner().intern().addListener( new CDockFrontendListener(){ public void loading( CDockFrontend frontend, CSetting setting ){ // ignore } public void loaded( CDockFrontend frontend, CSetting setting ){ temporary.run(); frontend.removeListener( this ); } }); } @Override public Runnable readSettings( ModeSettings<Location, ?> settings, UndoableModeSettings pending ){ Runnable result = super.readSettings( settings, pending ); if( settings instanceof CLocationModeSettings<?> ){ CLocationModeSettings<?> locationSettings = (CLocationModeSettings<?>)settings; locationSettings.rescue( getMaximizedMode() ); } return result; } /** * Tries to set the location of <code>dockable</code>. Does nothing if * <code>location</code> is invalid or requires information that is * not available. If <code>dockable</code> is a {@link CommonDockable} and * the {@link CLocationMode mode} respects {@link CLocationMode#respectWorkingAreas(DockStation) working-areas}, * then the working-area is set or removed depending on the value of {@link CStation#isWorkingArea()}. * @param dockable the element to move * @param location the new location of <code>dockable</code> */ public void setLocation( Dockable dockable, CLocation location ){ ExtendedMode mode = location.findMode(); if( mode == null ) return; CLocationMode newMode = getMode( mode.getModeIdentifier() ); if( newMode == null ) return; String root = location.findRoot(); if( root != null ){ if( dockable instanceof CommonDockable ){ CStation<?> station = control.getOwner().getStation( root ); if( station != null ){ if( newMode.respectWorkingAreas( station.getStation() )){ if( station.isWorkingArea() ){ ((CommonDockable)dockable).getDockable().setWorkingArea( station ); } else{ ((CommonDockable)dockable).getDockable().setWorkingArea( null ); } } } } // easy solution: set the location, then change the mode setProperties( newMode, dockable, new Location( mode.getModeIdentifier(), root, location.findProperty(), true ) ); apply( dockable, newMode, true ); } else{ apply( dockable, newMode, false ); } } /** * Sets the default location of <code>dockable</code> when going into * <code>mode</code>. The properties set here will be overridden * as soon as the user drags <code>dockable</code> to another location (within * the same mode) or <code>dockable</code> is removed permanently.<br> * This method has no effect if <code>dockable</code> is already in * <code>mode</code>. There is also no effect if <code>dockable</code> * has not been registered at the {@link CLocationModeManager}.<br> * Note: it is the clients responsibility to ensure that <code>location</code> * and <code>mode</code> belong to each other. * @param dockable the element whose location will be set * @param mode the mode for which the location is to be set or <code>null</code> * @param location the new location * @throws IllegalArgumentException if either argument is <code>null</code> or * if {@link CLocation#findRoot()} or {@link CLocation#findProperty()} returns * <code>null</code> */ public void setLocation( Dockable dockable, ExtendedMode mode, CLocation location ){ if( dockable == null ) throw new IllegalArgumentException( "dockable must not be null" ); if( mode == null ) throw new IllegalArgumentException( "mode must not be null" ); if( location != null ){ String root = location.findRoot(); if( root == null ) throw new IllegalArgumentException( "the location is not sufficient to find the root station" ); DockableProperty property = location.findProperty(); if( property == null ) throw new IllegalArgumentException( "the location does not carry enough information to find the location of dockable" ); ExtendedMode locationMode = location.findMode(); if( locationMode == null ){ throw new IllegalArgumentException( "the location does not carry enough information to find the mode of dockable" ); } if( !mode.getModeIdentifier().equals( locationMode.getModeIdentifier() )) throw new IllegalArgumentException( "location and mode do not belong together, they do not have the same identifier" ); setProperties( getMode( mode.getModeIdentifier() ), dockable, new Location( mode.getModeIdentifier(), root, property, true ) ); } else{ setProperties( getMode( mode.getModeIdentifier() ), dockable, null ); } } /** * Gets an element describing the location of <code>dockable</code> as * good as possible. * @param dockable the element whose location should be searched * @return the location or <code>null</code> if no location was found */ public CLocation getLocation( Dockable dockable ){ CLocationMode mode = getCurrentMode( dockable ); if( mode == null ) return null; return mode.getCLocation( dockable ); } /** * Assuming that <code>dockable</code> is currently not in mode <code>mode</code>, * then this method searches for the previously stored location of <code>dockable</code>. * Note that this method can't tell where <code>dockable</code> would be * shown if it never was in that mode and the client never specified the * location. * @param dockable the dockable whose location is searched * @param mode the mode which might be taken by <code>dockable</code> * @return the location or <code>null</code> * @throws IllegalArgumentException if any argument is <code>null</code> */ public CLocation getLocation( Dockable dockable, ExtendedMode mode ){ if( dockable == null ) throw new IllegalArgumentException( "dockable must not be null" ); if( mode == null ) throw new IllegalArgumentException( "mode must not be null" ); CLocationMode cmode = getMode( mode.getModeIdentifier() ); Location location = getProperties( cmode, dockable ); if( location == null ) return null; return cmode.getCLocation( dockable, location ); } /** * Tries to find the "optimal spot" where to put a new child onto <code>station</code>. In this * case the optimal spot is {@link CLocation#aside()} the latest focused child of <code>station</code>. * @param station the station where a {@link CDockable} is about to be dropped onto * @return the preferred location of the new child */ public CLocation getDropLocation( CStation<?> station ){ Dockable[] history = control.getOwner().getController().getFocusHistory().getHistory(); for( int i = history.length-1; i >= 0; i-- ){ Dockable next = history[i]; if( next instanceof CommonDockable && next.asDockStation() != station.getStation() ){ CDockable cnext = ((CommonDockable)next).getDockable(); if( DockUtilities.isAncestor( station.getStation(), next )){ boolean valid; if( station.isWorkingArea() ){ valid = cnext.getWorkingArea() == station; } else{ valid = cnext.getWorkingArea() == null; } if( valid ){ CLocation location = cnext.getBaseLocation(); if( location != null ){ return location.aside(); } } } if( cnext.getWorkingArea() == station ){ CLocation location = cnext.getBaseLocation(); if( location != null ){ return location.aside(); } } } } return station.getStationLocation(); } @Override public void ensureValidLocation( Dockable dockable ){ if( dockable instanceof CommonDockable ){ ensureValidLocation( ((CommonDockable)dockable).getDockable() ); } } /** * This method compares the current mode of <code>dockable</code> with its * availability set. If the current mode is not available, then <code>dockable</code> * is put into another mode (usually the {@link #getNormalMode() normal mode}).<br> * This method also checks the working area, provided that the current mode respects * the working-area settings.<br> * This method returns immediately if in {@link #isLayouting() layouting mode} * @param dockable the element whose mode is to be checked */ public void ensureValidLocation( CDockable dockable ){ if( isLayouting() ) return; ExtendedMode mode = getMode( dockable.intern() ); if( mode == ExtendedMode.NORMALIZED ){ CStation<?> preferredArea = dockable.getWorkingArea(); CStation<?> currentArea = findFirstParentWorkingArea( dockable.intern() ); if( preferredArea != currentArea ){ if( preferredArea == null ){ // the dockable is on a working-area, but should not be there CLocation defaultLocation = getNormalMode().getDefaultLocation(); dockable.setLocation( defaultLocation ); } else{ // reset the location dockable.setLocation( preferredArea.getStationLocation() ); } } mode = getMode( dockable.intern() ); } // normalize the element if its current mode is not valid if( !isModeAvailable( dockable.intern(), mode )){ dockable.setExtendedMode( ExtendedMode.NORMALIZED ); } } /** * Ensures that all dockables are in a basic mode.<br> * This method returns immediately if in {@link #isLayouting() layouting mode} * @return <code>true</code> if at least one element was affected by changes, * <code>false</code> if nothing happened. */ public boolean ensureBasicModes(){ if( isLayouting() ) return false; final Single<Boolean> result = new Single<Boolean>( false ); runTransaction( new AffectingRunnable() { public void run( AffectedSet set ){ for( Dockable dockable : listDockables() ){ CLocationMode current = getCurrentMode( dockable ); if( current != null && !current.isBasicMode() ){ List<CLocationMode> modes = getModeHistory( dockable ); CLocationMode next = null; for( int i = modes.size()-1; i >= 0 && next == null; i-- ){ CLocationMode mode = modes.get( i ); if( mode.isBasicMode() && isModeAvailable( dockable, mode.getExtendedMode() )){ next = mode; } } if( next == null ){ next = getNormalMode(); } result.setA( true ); setMode( dockable, next.getExtendedMode() ); } } } }); return result.getA(); } /** * Updates the location of all dockables that should be on a working-area * and that are currently in a mode that does not support working-areas. The history * of the elements is searched for the first mode which supports working-areas. If no * such mode is found, then the normal-mode is applied. */ public void resetWorkingAreaChildren(){ runTransaction( new AffectingRunnable() { public void run( AffectedSet set ){ for( Dockable dockable : listDockables() ){ if( dockable instanceof CommonDockable ){ CDockable cdockable = ((CommonDockable)dockable).getDockable(); resetWorkingArea( cdockable, set ); } } } }); } private void resetWorkingArea( CDockable dockable, AffectedSet set ){ if( dockable.getWorkingArea() == null ) return; DockStation parent = dockable.intern().getDockParent(); if( parent == null ) return; CLocationMode current = getCurrentMode( dockable.intern() ); if( current == null ) return; if( current.respectWorkingAreas( parent )) return; // need to reset List<Location> history = getPropertyHistory( dockable.intern() ); CLocationMode next = null; for( int i = history.size()-1; i >= 0 && next == null; i-- ){ Location check = history.get( i ); Path path = check.getMode(); String root = check.getRoot(); if( path != null && root != null ){ CLocationMode mode = getMode( path ); if( mode != null ){ CStation<?> station = control.getOwner().getStation( root ); if( station != null ){ if( mode.respectWorkingAreas( station.getStation() ) && mode.isRepresenting( station.getStation() )){ if( isModeAvailable( dockable.intern(), mode.getExtendedMode() )){ next = mode; } } } } } } if( next == null ){ next = getNormalMode(); } apply( dockable.intern(), next, set, false ); } /** * Guesses the result of {@link #getCurrentMode(Dockable)} once a {@link Dockable} is * dropped onto {@link DockStation}. If more than one {@link LocationMode mode} is using * <code>parent</code>, then the guess might not always be correct. * @param parent some station * @return the mode its children are in, or <code>null</code> if no guess can be made */ public ExtendedMode childsExtendedMode( DockStation parent ){ while( parent != null ){ CLocationMode mode = getRepresentingMode( parent ); if( mode != null ){ return mode.getExtendedMode(); } Dockable dockable = parent.asDockable(); if( dockable == null ) return null; parent = dockable.getDockParent(); } return null; } /** * Finds the first {@link CStation} in the path up to the root from * <code>dockable</code> wich is a working area. * @param dockable the element which might have a {@link CStation} * as parent. * @return the first found {@link CStation}. */ private CStation<?> findFirstParentWorkingArea( Dockable dockable ){ DockStation station = dockable.getDockParent(); dockable = station == null ? null : station.asDockable(); if( dockable != null ) return getAreaOf( dockable ); else return null; } /** * Searches <code>dockable</code> and its parent for the first {@link CStation} * that is a working area. * @param dockable the element whose working area is searched * @return the first working area or <code>null</code> */ protected CStation<?> getAreaOf( Dockable dockable ){ Map<DockStation, CStation<?>> stations = new HashMap<DockStation, CStation<?>>(); for( CStation<?> station : control.getOwner().getStations() ){ if( station.isWorkingArea() ){ stations.put( station.getStation(), station ); } } if( dockable.asDockStation() != null ){ CStation<?> station = stations.get( dockable.asDockStation() ); if( station != null ) return station; } Dockable check = dockable; while( check != null ){ DockStation parent = check.getDockParent(); if( parent == null ) check = null; else check = parent.asDockable(); CStation<?> station = stations.get( parent ); if( station != null ) return station; } return null; } /** * Searches the {@link CLocationMode mode} which represents the mode of * the children of <code>station</code>. This method calls * {@link LocationMode#isRepresenting(DockStation)} with <code>station</code>, * but does not check the parents of <code>station</code>. Basic modes are preferred * over non-basic modes by this method. * @param station some station * @return the mode or <code>null</code> if nothing found */ private CLocationMode getRepresentingMode( DockStation station ){ Iterable<CLocationMode> modes = modes(); CLocationMode first = null; for( CLocationMode mode : modes ){ if( mode.isRepresenting( station )){ if( mode.isBasicMode() ) return mode; if( first == null ) first = mode; } } return first; } }