/* * 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) 2010 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.frontend; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import bibliothek.gui.DockFrontend; import bibliothek.gui.DockStation; import bibliothek.gui.Dockable; import bibliothek.gui.DockFrontend.DockInfo; import bibliothek.gui.DockFrontend.RootInfo; import bibliothek.gui.dock.DockElement; import bibliothek.gui.dock.DockFactory; import bibliothek.gui.dock.layout.AdjacentDockFactory; import bibliothek.gui.dock.layout.DockLayoutComposition; import bibliothek.gui.dock.layout.DockSituation; import bibliothek.gui.dock.layout.DockSituationIgnore; import bibliothek.gui.dock.layout.DockableProperty; import bibliothek.gui.dock.layout.DockablePropertyFactory; import bibliothek.gui.dock.layout.PredefinedDockSituation; import bibliothek.gui.dock.layout.PropertyTransformer; import bibliothek.gui.dock.perspective.PerspectiveElement; import bibliothek.gui.dock.perspective.PerspectiveStation; import bibliothek.gui.dock.perspective.PredefinedMap; import bibliothek.gui.dock.perspective.PredefinedPerspective; import bibliothek.gui.dock.station.support.PlaceholderStrategy; import bibliothek.util.xml.XException; /** * This default implementation of a {@link LayoutChangeStrategy} heavily depends on the methods of {@link PredefinedDockSituation}. It * also offers a set of methods that may be interesting for subclasses which do not use a {@link PredefinedDockSituation}. * @author Benjamin Sigg */ public class DefaultLayoutChangeStrategy implements LayoutChangeStrategy{ private boolean updatingFullLayout = false; public boolean setLayout( DockFrontendInternals frontend, Setting setting, boolean entry ) throws IOException, XException{ try{ updatingFullLayout = true; return updateLayout( frontend, setting, entry ); } finally{ updatingFullLayout = false; } } public boolean shouldUpdateLayoutOnAdd( Dockable dockable ) { return !updatingFullLayout; } private boolean updateLayout( DockFrontendInternals frontend, Setting setting, boolean entry ) throws IOException, XException{ DockSituation situation = createSituation( frontend, entry, true ); DockSituationIgnore ignore = situation.getIgnore(); if( ignore == null ){ ignore = new DockSituationIgnore(){ public boolean ignoreChildren( DockStation station ) { return false; } public boolean ignoreElement( DockElement element ) { return false; } public boolean ignoreChildren( PerspectiveStation station ){ return false; } public boolean ignoreElement( PerspectiveElement element ){ return false; } }; } SettingAccess access = createAccess( frontend, setting ); // maybe cancel the operation if( approveClosing( frontend, situation, access ) == null ){ return false; } // split up all child parent relations frontend.clean( ignore ); // apply the new layout applyLayout( frontend, situation, access, entry ); applyInvisibleLayout( frontend, situation, access ); return true; } /** * Creates a wrapper around <code>setting</code> that allows the algorithm of this * {@link LayoutChangeStrategy} to access the setting. * @param frontend the caller of this method * @param setting the setting to hide * @return the wrapper */ protected SettingAccess createAccess( DockFrontendInternals frontend, Setting setting ){ return new SettingAccess( setting ); } /** * Forwards to {@link #createSituation(DockFrontendInternals, boolean, boolean)} with the * last argument set to <code>false</code>. */ public PredefinedDockSituation createSituation( DockFrontendInternals frontend, boolean entry ){ return createSituation( frontend, entry, false ); } /** * Creates a {@link DockSituation} which represents all the knowledge * <code>frontend</code> currently has. * @param frontend the frontend for which the situation is required * @param entry <code>true</code> if the situation is used for a regular setting, * <code>false</code> if the situation is used as the final setting which will * be loaded the next time the application starts. * @param onSetLayout whether this method is called by {@link #setLayout(DockFrontendInternals, Setting, boolean)} or not. If * <code>true</code> then the situation is used to apply some layout, otherwise it is used to store or read a layout * @return the situation, the default implementation always returns a {@link PredefinedDockSituation}, subclasses * may override and return other situations. */ @SuppressWarnings("unchecked") protected PredefinedDockSituation createSituation( final DockFrontendInternals frontend, final boolean entry, boolean onSetLayout ){ PredefinedDockSituation situation = new PredefinedDockSituation( frontend.getFrontend().getController() ){ @Override protected boolean shouldLayout( DockElement element ) { if( entry ){ Dockable dockable = element.asDockable(); if( dockable != null ){ DockInfo info = frontend.getInfo( dockable ); if( info != null ){ return info.isEntryLayout(); } } } return true; } @Override protected boolean shouldLayout( PerspectiveElement element, PredefinedPerspective perspective ) { if( entry ){ String key = perspective.get( element ); if( key != null ){ DockInfo info = frontend.getInfo( key ); if( info != null ){ return info.isEntryLayout(); } } } return true; } }; for( RootInfo info : frontend.getRoots() ){ situation.put( DockFrontend.ROOT_KEY_PREFIX + info.getName(), info.getStation() ); } for( DockInfo info : frontend.getDockables() ){ if( info.getDockable() != null && shouldPredefine( info.getDockable() )){ situation.put( DockFrontend.DOCKABLE_KEY_PREFIX + info.getKey(), info.getDockable() ); } } for( DockFactory<?,?,?> factory : frontend.getDockFactories() ){ situation.add( factory ); } for( DockFactory<?,?,?> backup : frontend.getBackupDockFactories() ){ situation.addBackup( new RegisteringDockFactory( frontend.getFrontend(), backup ) ); } for( AdjacentDockFactory<?> factory : frontend.getAdjacentDockFactories() ){ situation.addAdjacent( factory ); } if( entry ) situation.setIgnore( frontend.getFrontend().getIgnoreForEntry() ); else situation.setIgnore( frontend.getFrontend().getIgnoreForFinal() ); return situation; } /** * Tells whether the element <code>dockable</code> should be added as predefined element to the {@link PredefinedDockSituation} * that is created by {@link #createSituation(DockFrontendInternals, boolean, boolean)}. * @param dockable the element which may need to be predefined * @return <code>true</code> if <code>dockable</code> is to be predefined */ protected boolean shouldPredefine( Dockable dockable ){ return true; } public DockFrontendPerspective createPerspective( DockFrontendInternals frontend, boolean entry, final FrontendPerspectiveCache cache ){ PredefinedDockSituation situation = createSituation( frontend, entry ); PredefinedPerspective perspective = situation.createPerspective(); for( DockInfo info : frontend.getDockables() ){ if( info.getDockable() != null ){ PerspectiveElement element = cache.get( info.getKey(), info.getDockable(), false ); if( element != null ){ perspective.put( DockFrontend.DOCKABLE_KEY_PREFIX + info.getKey(), element ); } } } for( RootInfo info : frontend.getRoots() ){ PerspectiveElement element = cache.get( info.getName(), info.getStation(), true ); perspective.put( DockFrontend.ROOT_KEY_PREFIX + info.getName(), element ); } perspective.put( new PredefinedMap(){ public PerspectiveElement get( String id ){ if( id.startsWith( DockFrontend.DOCKABLE_KEY_PREFIX )){ return cache.get( id.substring( DockFrontend.DOCKABLE_KEY_PREFIX.length() ), false ); } else if( id.startsWith( DockFrontend.ROOT_KEY_PREFIX )){ return cache.get( id.substring( DockFrontend.ROOT_KEY_PREFIX.length() ), true ); } else{ return null; } } public String get( PerspectiveElement element ){ String id = cache.get( element ); if( id == null ){ return null; } if( element.asStation() != null && cache.isRootStation( element.asStation() )){ return DockFrontend.ROOT_KEY_PREFIX + id; } else{ return DockFrontend.DOCKABLE_KEY_PREFIX + id; } } }); return new DefaultDockFrontendPerspective( frontend.getFrontend(), perspective, entry ); } public PropertyTransformer createTransformer( DockFrontendInternals frontend ){ PropertyTransformer transformer = new PropertyTransformer(frontend.getFrontend().getController()); for( DockablePropertyFactory factory : frontend.getPropertyFactories() ) transformer.addFactory( factory ); return transformer; } /** * Applies the layout described in <code>setting</code> to the visible elements. * This implementation tries to estimate the location of missing dockables using * {@link #listEstimateLocations(DockSituation, DockLayoutComposition)}. * @param frontend the caller of this method * @param situation used to convert the layout * @param setting the new layout * @param entry whether the layout is a full or regular layout * @throws IOException if the layout cannot be converted * @throws XException if the layout cannot be converted */ protected void applyLayout( DockFrontendInternals frontend, DockSituation situation, SettingAccess setting, boolean entry ) throws IOException, XException{ DockFrontend dockFrontend = frontend.getFrontend(); MissingDockableStrategy missingDockable = frontend.getMissingDockableStrategy(); for( RootInfo info : frontend.getRoots() ){ DockLayoutComposition layout = setting.getRoot( info.getName() ); if( layout != null ){ layout = situation.fillMissing( layout ); Map<String, DockableProperty> missingLocations = listEstimateLocations( situation, layout ); if( missingLocations != null ){ for( Map.Entry<String, DockableProperty> missing : missingLocations.entrySet() ){ String key = missing.getKey(); DockInfo dockInfo = frontend.getInfo( key ); if( dockInfo == null && missingDockable.shouldStoreShown( key )){ dockFrontend.addEmpty( key ); dockInfo = frontend.getInfo( key ); } if( dockInfo != null ){ dockInfo.setLocation( info.getName(), missing.getValue() ); dockInfo.setShown( true ); } } } Map<String, DockLayoutComposition> missingLayouts = listLayouts( situation, layout ); if( missingLayouts != null ){ for( Map.Entry<String, DockLayoutComposition> missing : missingLayouts.entrySet() ){ String key = missing.getKey(); DockInfo dockInfo = frontend.getInfo( key ); if( dockInfo == null && missingDockable.shouldStoreShown( key )){ dockFrontend.addEmpty( key ); dockInfo = frontend.getInfo( key ); } if( dockInfo != null ){ dockInfo.setShown( true ); if( !entry || dockInfo.isEntryLayout() ){ dockInfo.setLayout( missing.getValue() ); } } } } situation.convert( layout ); } } } /** * Applies <code>setting</code> to the invisible elements. * @param frontend the caller of this method * @param situation to convert the layout * @param setting the new layout * @throws IOException if the layout cannot be converted * @throws XException if the layout cannot be converted */ protected void applyInvisibleLayout( DockFrontendInternals frontend, DockSituation situation, SettingAccess setting ) throws IOException, XException{ DockFrontend dockFrontend = frontend.getFrontend(); MissingDockableStrategy missingDockable = frontend.getMissingDockableStrategy(); for( int i = 0, n = setting.getInvisibleCount(); i<n; i++ ){ String key = setting.getInvisibleKey( i ); DockInfo info = frontend.getInfo( key ); if( info == null && missingDockable.shouldStoreHidden( key )){ dockFrontend.addEmpty( key ); info = frontend.getInfo( key ); } if( info != null ){ info.setShown( false ); info.setLocation( setting.getInvisibleRoot( i ), setting.getInvisibleLocation( i ) ); DockLayoutComposition layout = setting.getInvisibleLayout( i ); if( layout != null ){ layout = situation.fillMissing( layout ); if( info.getDockable() != null ){ situation.convert( layout ); layout = null; } info.setLayout( layout ); } } } } /** * Tries to estimate the layouts of missing {@link Dockable}s. The * default implementation works with any {@link PredefinedDockSituation}. * @param situation the situation to use for transforming information * @param layout the layout to analyze * @return a map with <code>Dockable</code>-names as key or <code>null</code> */ protected Map<String, DockLayoutComposition> listLayouts( DockSituation situation, DockLayoutComposition layout ){ if( situation instanceof PredefinedDockSituation ){ Map<String, DockLayoutComposition> map = ((PredefinedDockSituation)situation).listLayouts( layout, true ); Map<String, DockLayoutComposition> result = new HashMap<String, DockLayoutComposition>(); for( Map.Entry<String, DockLayoutComposition> entry : map.entrySet() ){ String key = entry.getKey(); if( key.startsWith( DockFrontend.DOCKABLE_KEY_PREFIX )) result.put( key.substring( DockFrontend.DOCKABLE_KEY_PREFIX.length() ), entry.getValue() ); else if( key.startsWith( DockFrontend.ROOT_KEY_PREFIX )) result.put( key.substring( DockFrontend.ROOT_KEY_PREFIX.length() ), entry.getValue() ); else result.put( key, entry.getValue() ); } return result; } return null; } /** * Tries to estimate the location of missing {@link Dockable}s. The * default implementation works with any {@link PredefinedDockSituation}. * @param situation the situation to use for transforming information * @param layout the layout to analyze * @return a map with <code>Dockable</code>-names as key or <code>null</code> */ protected Map<String, DockableProperty> listEstimateLocations( DockSituation situation, DockLayoutComposition layout ){ if( situation instanceof PredefinedDockSituation ){ Map<String, DockableProperty> map = ((PredefinedDockSituation)situation).listEstimatedLocations( layout, true ); Map<String, DockableProperty> result = new HashMap<String, DockableProperty>(); for( Map.Entry<String, DockableProperty> entry : map.entrySet() ){ String key = entry.getKey(); if( key.startsWith( DockFrontend.DOCKABLE_KEY_PREFIX )) result.put( key.substring( DockFrontend.DOCKABLE_KEY_PREFIX.length() ), entry.getValue() ); else if( key.startsWith( DockFrontend.ROOT_KEY_PREFIX )) result.put( key.substring( DockFrontend.ROOT_KEY_PREFIX.length() ), entry.getValue() ); else result.put( key, entry.getValue() ); } return result; } return null; } public void estimateLocations( DockFrontendInternals frontend, DockSituation situation, DockLayoutComposition layout ){ if( situation instanceof PredefinedDockSituation ){ ((PredefinedDockSituation)situation).estimateLocations( layout ); } } /** * Asks the {@link VetoManager} whether some {@link Dockable}s can be hidden. Only {@link Dockable}s that * are returned by {@link DockFrontendInternals#getDockables()} are checked by this method. * @param frontend the caller of this method * @param situation the situation that will convert the layout * @param setting the new layout * @return the set of elements for which closing was explicitly approved * or <code>null</code> if the operation should be canceled */ protected Collection<Dockable> approveClosing( DockFrontendInternals frontend, DockSituation situation, SettingAccess setting ){ // check whether some elements really should be closed Set<Dockable> remainingVisible = new HashSet<Dockable>(); for( RootInfo info : frontend.getRoots() ){ DockLayoutComposition layout = setting.getRoot( info.getName() ); if( layout != null ){ Set<Dockable> visible = estimateVisible( frontend, situation, layout ); if( visible != null ){ remainingVisible.addAll( visible ); } } } Collection<Dockable> closing = getClosingDockables( frontend, remainingVisible ); if( !closing.isEmpty() ){ if( !frontend.getVetos().expectToHide( closing, true ) ){ // cancel the operation return null; } } return closing; } /** * Creates a collection of all the {@link Dockable}s that are about to be closed. Subclasses * may override this method, they should at least check all the {@link Dockable}s that are * registered at <code>frontend</code>. Subclasses may need to override * {@link #estimateVisible(DockFrontendInternals, DockSituation, DockLayoutComposition) estimateVisible} as * well to get the correct set of <code>visible</code> elements. * @param frontend the caller of this method * @param visible the elements that remain visible as told by {@link #estimateVisible(DockFrontendInternals, DockSituation, DockLayoutComposition)}. * @return the set of dockables that is about to be closed, not <code>null</code>, may be empty */ protected Collection<Dockable> getClosingDockables( DockFrontendInternals frontend, Set<Dockable> visible ){ List<Dockable> closing = new ArrayList<Dockable>(); for( DockInfo info : frontend.getDockables() ){ Dockable dockable = info.getDockable(); if( dockable != null && info.isHideable() ){ if( !visible.contains( dockable )){ closing.add( info.getDockable() ); } } } return closing; } /** * Tries to estimate which of the currently visible {@link Dockable}s will * still be visible if <code>layout</code> is applied to <code>frontend</code>. The * default implementation assumes that <code>situation</code> is a {@link PredefinedDockSituation}.<br> * Subclasses may override this method and modify the result in any way they like * @param frontend the caller of this method * @param situation algorithm used to convert <code>layout</code> * @param layout the layout that will be applied * @return an estimation of the elements that will be made invisible or <code>null</code> */ protected Set<Dockable> estimateVisible( DockFrontendInternals frontend, DockSituation situation, DockLayoutComposition layout ){ if( situation instanceof PredefinedDockSituation ){ Set<Dockable> allDockables = new HashSet<Dockable>(); for( DockInfo info : frontend.getDockables() ){ Dockable dockable = info.getDockable(); if( dockable != null ){ allDockables.add( dockable ); } } PredefinedDockSituation predefined = (PredefinedDockSituation)situation; Set<Dockable> visible = predefined.listVisible( allDockables, layout ); return visible; } return null; } public PlaceholderStrategy getPlaceholderStrategy( DockFrontendInternals frontend ){ return null; } /** * A wrapper around a {@link Setting}, allows algorithms access to the settings * but also allows to modify the data without changing the setting. * @author Benjamin Sigg */ protected class SettingAccess{ private Setting setting; /** * Creates a new wrapper. * @param setting the source for all data, not <code>null</code> */ public SettingAccess( Setting setting ){ this.setting = setting; } /** * Gets the setting that is hidden by this wrapper. * @return the source of all data, not <code>null</code> */ public Setting getSetting(){ return setting; } /** * Gets the layout of a root. * @param root the root * @return the layout or <code>null</code> */ public DockLayoutComposition getRoot( String root ){ return setting.getRoot( root ); } /** * Gets the keys of all known roots. * @return the keys of the roots */ public String[] getRootKeys(){ return setting.getRootKeys(); } /** * Gets the number of stored invisible elements. * @return the number of elements */ public int getInvisibleCount(){ return setting.getInvisibleCount(); } /** * Gets the key of the index'th invisible element. * @param index the index of the element * @return the key */ public String getInvisibleKey( int index ){ return setting.getInvisibleKey( index ); } /** * Gets the preferred root of the index'th invisible element. * @param index the index of the element * @return the root */ public String getInvisibleRoot( int index ){ return setting.getInvisibleRoot( index ); } /** * Gets the location of the index'th invisible element. * @param index the index of the element * @return the location */ public DockableProperty getInvisibleLocation( int index ){ return setting.getInvisibleLocation( index ); } /** * Gets the layout of the index'th invisible element. * @param index the index of the layout * @return associated information, may be <code>null</code> */ public DockLayoutComposition getInvisibleLayout( int index ){ return setting.getInvisibleLayout( index ); } /** * Using the factories given by <code>situation</code>, this method tries * to fill any gaps in the layout. * @param situation a set of factories to use * @throws IllegalArgumentException if <code>situation</code> cannot handle * the content of this setting */ public void fillMissing( DockSituation situation ){ setting.fillMissing( situation ); } } }