/* * Bibliothek - DockingFrames * Library built on Java/Swing, allows the user to "drag and drop" * panels containing any Swing-Co import java.awt.EventQueue; mponent the developer likes to add. * * Copyright (C) 2007 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.util; import java.awt.Component; import java.awt.Graphics; import java.awt.Image; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.UIManager; import bibliothek.gui.DockController; import bibliothek.gui.DockStation; import bibliothek.gui.DockTheme; import bibliothek.gui.DockUI; import bibliothek.gui.Dockable; import bibliothek.gui.dock.DockElement; import bibliothek.gui.dock.DockElementRepresentative; import bibliothek.gui.dock.layout.DockableProperty; import bibliothek.gui.dock.perspective.PerspectiveDockable; import bibliothek.gui.dock.perspective.PerspectiveElement; import bibliothek.gui.dock.perspective.PerspectiveStation; import bibliothek.gui.dock.station.LayoutLocked; import bibliothek.gui.dock.station.support.PlaceholderStrategy; import bibliothek.gui.dock.title.DockTitle; import bibliothek.util.Path; /** * A list of methods which can be used for different purposes. Methods * related to the {@link DockTheme} can be found in {@link DockUI}. * @author Benjamin Sigg */ public class DockUtilities { /** * A visitor used to visit the nodes of a dock-tree. * @author Benjamin Sigg */ public static abstract class DockVisitor{ /** * Invoked to visit <code>dockable</code>. * @param dockable the visited element */ public void handleDockable( Dockable dockable ){ /* do nothing */ } /** * Invoked to visit <code>station</code>. * @param station the visited element */ public void handleDockStation( DockStation station ){ /* do nothing */ } } /** whether {@link DockUtilities#checkLayoutLocked()} is enabled */ private static boolean checkLayoutLock = true; /** * Visits <code>dockable</code> and all its children. * @param dockable the first element to visit * @param visitor a delegate */ public static void visit( Dockable dockable, DockVisitor visitor ){ visitDockable( dockable, visitor ); } /** * Visits <code>station</code> and all its children. * @param station the first element to visit * @param visitor a delegate */ public static void visit( DockStation station, DockVisitor visitor ){ Dockable dockable = station.asDockable(); if( dockable != null ) visitDockable( dockable, visitor ); else visitStation( station, visitor ); } /** * Visits <code>element</code> and all its children. * @param element the first element to visit * @param visitor a delegate */ public static void visit( DockElement element, DockVisitor visitor ){ Dockable dockable = element.asDockable(); if( dockable != null ) visitDockable( dockable, visitor ); else{ DockStation station = element.asDockStation(); if( station != null ){ visitStation( station, visitor ); } } } /** * Visits <code>dockable</code> and all its children. * @param dockable the first element to visit * @param visitor a delegate */ private static void visitDockable( Dockable dockable, DockVisitor visitor ){ visitor.handleDockable( dockable ); DockStation station = dockable.asDockStation(); if( station != null ) visitStation( station, visitor ); } /** * Visits <code>station</code> and all its children. * @param station the first element to visit * @param visitor a delegate */ private static void visitStation( DockStation station, DockVisitor visitor ){ visitor.handleDockStation( station ); Dockable[] children = new Dockable[ station.getDockableCount() ]; for( int i = 0; i < children.length; i++ ){ children[i] = station.getDockable( i ); } for( Dockable child : children ){ visitDockable( child, visitor ); } } /** * Lists all {@link Dockable}s in the tree under <code>root</code>. * @param root the root of a tree of elements * @param includeRoot whether <code>root</code> should be in the resulting * list as well * @return the list of found {@link Dockable}s, might be empty but not <code>null</code> */ public static List<Dockable> listDockables( final DockElement root, final boolean includeRoot ){ final List<Dockable> list = new ArrayList<Dockable>(); visit( root, new DockVisitor(){ @Override public void handleDockable( Dockable dockable ) { if( includeRoot || dockable != root ){ list.add( dockable ); } } }); return list; } /** * Tells whether <code>child</code> is identical with <code>ancestor</code> * or a child of <code>ancestor</code>. * @param ancestor an element * @param child another element * @return <code>true</code> if <code>ancestor</code> is a parent of or * identical with <code>child</code>. */ public static boolean isAncestor( DockElement ancestor, DockElement child ){ if( ancestor == null ) throw new NullPointerException( "ancestor must not be null" ); if( child == null ) throw new NullPointerException( "child must not be null" ); Dockable dockable = child.asDockable(); DockStation station = null; while( dockable != null ){ if( ancestor == dockable ) return true; station = dockable.getDockParent(); dockable = station == null ? null : station.asDockable(); } return station == ancestor; } /** * Tells whether <code>child</code> is identical with <code>ancestor</code> * or a child of <code>ancestor</code>. * @param ancestor an element * @param child another element * @return <code>true</code> if <code>ancestor</code> is a parent of or * identical with <code>child</code>. */ public static boolean isAncestor( PerspectiveElement ancestor, PerspectiveElement child ){ if( ancestor == null ) throw new NullPointerException( "ancestor must not be null" ); if( child == null ) throw new NullPointerException( "child must not be null" ); PerspectiveDockable dockable = child.asDockable(); PerspectiveStation station = null; while( dockable != null ){ if( ancestor == dockable ) return true; station = dockable.getParent(); dockable = station == null ? null : station.asDockable(); } return station == ancestor; } /** * Searches the station which is an ancestor of <code>element</code> * and has no parent. * @param element the element whose oldest parent is searched * @return the root, may be <code>null</code> if element has no parent */ public static DockStation getRoot( DockElement element ){ Dockable dockable = element.asDockable(); if( dockable == null ) return element.asDockStation(); DockStation parent = dockable.getDockParent(); if( parent == null ) return element.asDockStation(); while( true ){ dockable = parent.asDockable(); if( dockable == null || dockable.getDockParent() == null ) return parent; parent = dockable.getDockParent(); } } /** * Searches the one {@link Dockable} that is either <code>subchild</code> or a parent * of <code>subchild</code> and whose parent is <code>parent</code>. * @param parent the parent of the result * @param subchild a direct or indirect child of <code>parent</code> * @return the child or <code>null</code> if subchild is no child of <code>parent</code> */ public static Dockable getDirectChild( DockStation parent, Dockable subchild ){ DockStation subparent = subchild.getDockParent(); while( subparent != null ){ if( subparent == parent ){ return subchild; } subchild = subparent.asDockable(); subparent = subchild == null ? null : subchild.getDockParent(); } return null; } /** * Creates a copy of <code>root</code> and sets <code>property</code> * as the successor of the very last element in the property chain beginning * at <code>root</code>. * @param root the root of the chain, can be <code>null</code> * @param property the new last element of the chain * @return the root of the new chain */ public static DockableProperty append( DockableProperty root, DockableProperty property ){ if( root == null ) return property; root = root.copy(); getLastProperty( root ).setSuccessor( property ); return root; } /** * Gets the last successor in the property chain beginning at <code>property</code>. * @param property the start of the chain * @return the end of the chain */ public static DockableProperty getLastProperty( DockableProperty property ){ while( property.getSuccessor() != null ) property = property.getSuccessor(); return property; } /** * Gets a {@link DockableProperty} which describes the path from the * {@link #getRoot(DockElement) root} to <code>dockable</code>. * @param dockable a Dockable whose location is searched * @return the properties or <code>null</code> if <code>dockable</code> * has no parent */ public static DockableProperty getPropertyChain( Dockable dockable ){ DockStation station = getRoot( dockable ); if( station == null || station == dockable ) return null; return getPropertyChain( station, dockable ); } /** * Creates a {@link DockableProperty} describing the path from * <code>ground</code> to <code>dockable</code>. * @param ground the base of the property * @param dockable an indirect child of <code>ground</code> * @return a property for the path <code>ground</code> to <code>dockable</code>. * @throws IllegalArgumentException if <code>ground</code> is not an * ancestor of <code>dockable</code> */ public static DockableProperty getPropertyChain( DockStation ground, Dockable dockable ){ if( ground == dockable ) throw new IllegalArgumentException( "ground and dockable are the same" ); DockStation parent = dockable.getDockParent(); DockableProperty property = parent.getDockableProperty( dockable, dockable ); Dockable child = dockable; while( true ){ if( parent == ground ) return property; child = parent.asDockable(); if( child == null ) throw new IllegalArgumentException( "The chain is not complete" ); parent = child.getDockParent(); if( parent == null ) throw new IllegalArgumentException( "The chain is not complete" ); DockableProperty temp = parent.getDockableProperty( child, dockable ); temp.setSuccessor( property ); property = temp; } } /** * Creates a {@link DockableProperty} describing the path from * <code>ground</code> to <code>dockable</code>. * @param ground the base of the property * @param dockable an indirect child of <code>ground</code> * @return a property for the path <code>ground</code> to <code>dockable</code>. * @throws IllegalArgumentException if <code>ground</code> is not an * ancestor of <code>dockable</code> */ public static DockableProperty getPropertyChain( PerspectiveStation ground, PerspectiveDockable dockable ){ if( ground == dockable ) throw new IllegalArgumentException( "ground and dockable are the same" ); PerspectiveStation parent = dockable.getParent(); DockableProperty property = parent.getDockableProperty( dockable, dockable ); PerspectiveDockable child = dockable; while( true ){ if( parent == ground ) return property; child = parent.asDockable(); if( child == null ) throw new IllegalArgumentException( "The chain is not complete" ); parent = child.getParent(); if( parent == null ) throw new IllegalArgumentException( "The chain is not complete" ); DockableProperty temp = parent.getDockableProperty( child, dockable ); temp.setSuccessor( property ); property = temp; } } /** * Searches a {@link Component} which is {@link Component#isShowing() showing} * and has something to do with <code>dockable</code>.<br> * This method first checks {@link Dockable} and {@link DockTitle}s, then it checks * all {@link DockElementRepresentative}s. * @param dockable a Dockable for which a Component has to be found * @return a showing component or <code>null</code> */ public static Component getShowingComponent( Dockable dockable ){ Component component = dockable.getComponent(); if( !component.isShowing() ){ for( DockTitle title : dockable.listBoundTitles() ){ component = title.getComponent(); if( component.isShowing() ) break; } if( !component.isShowing() ){ DockController controller = dockable.getController(); if( controller != null ){ for( DockElementRepresentative item : controller.getRepresentatives( dockable )){ if( item.getComponent().isShowing() ){ component = item.getComponent(); break; } } } } } if( component.isShowing() ) return component; else return null; } /** * Ensures that <code>newChild</code> has no parent, and that there will * be no cycle when <code>newChild</code> is added to <code>newParent</code> * @param newParent the element that becomes parent of <code>newChild</code> * @param newChild the element that becomes child of <code>newParent</code> * @throws NullPointerException if either <code>newParent</code> or <code>newChild</code> is <code>null</code> * @throws IllegalArgumentException if there would be a cycle introduced * @throws IllegalStateException if the old parent of <code>newChild</code> does not * allow to remove its child */ public static void ensureTreeValidity( DockStation newParent, Dockable newChild ){ if( newParent == null ) throw new NullPointerException( "parent must not be null" ); if( newChild == null ) throw new NullPointerException( "child must not be null" ); DockStation oldParent = newChild.getDockParent(); // check no self reference if( newChild == newParent ) throw new IllegalArgumentException( "child and parent are the same" ); // check no cycles if( isAncestor( newChild, newParent )){ throw new IllegalArgumentException( "can't create a cycle" ); } // remove old parent if( oldParent != null ){ if( oldParent != newParent && !oldParent.canDrag( newChild )) throw new IllegalStateException( "old parent of child does not want do release the child" ); oldParent.drag( newChild ); } } /** * Ensures that <code>newChild</code> has either no parent or <code>newParent</code> as parent, and that there will * be no cycle when <code>newChild</code> is added to <code>newParent</code> * @param newParent the element that becomes parent of <code>newChild</code> * @param newChild the element that becomes child of <code>newParent</code> * @throws NullPointerException if either <code>newParent</code> or <code>newChild</code> is <code>null</code> * @throws IllegalArgumentException if there would be a cycle introduced * @throws IllegalStateException if the old parent of <code>newChild</code> does not * allow to remove its child */ public static void ensureTreeValidity( PerspectiveStation newParent, PerspectiveDockable newChild ){ if( newParent == null ) throw new NullPointerException( "parent must not be null" ); if( newChild == null ) throw new NullPointerException( "child must not be null" ); PerspectiveStation oldParent = newChild.getParent(); // check no self reference if( newChild == newParent ) throw new IllegalArgumentException( "child and parent are the same" ); // check no cycles if( isAncestor( newChild, newParent )){ throw new IllegalArgumentException( "can't create a cycle" ); } // remove old parent if( oldParent != null && oldParent != newParent ){ oldParent.remove( newChild ); } } /** * Gets a "disabled" icon according to the current look and feel. * @param parent the component on which the icon will be painted, can be <code>null</code> * @param icon an icon or <code>null</code> * @return a disabled version of <code>icon</code> or <code>null</code> */ public static Icon disabledIcon( JComponent parent, Icon icon ){ if( icon == null ) return null; Icon result = UIManager.getLookAndFeel().getDisabledIcon( parent, icon ); if( result != null ) return result; if( parent != null ){ int width = icon.getIconWidth(); int height = icon.getIconHeight(); if( width > 0 && height > 0 ){ BufferedImage image = new BufferedImage( width, height, BufferedImage.TYPE_INT_ARGB ); Graphics g = image.createGraphics(); icon.paintIcon( parent, g, 0, 0 ); g.dispose(); icon = new ImageIcon( image ); result = UIManager.getLookAndFeel().getDisabledIcon( parent, icon ); } } if( result != null ) return result; return icon; } /** * Transforms <code>icon</code> into an image. * @param icon some icon * @return the image of the icon or <code>null</code> */ public static Image iconImage( Icon icon ){ if( icon instanceof ImageIcon ) return ((ImageIcon)icon).getImage(); return null; } /** * Loads a map of icons. * @param list a path to a property-file containing key-path-pairs. * @param path the base path to the icons, will be added before any * path of the property file, can be <code>null</code> * @param loader used to transform paths into urls. * @return the map of {@link Icon}s, the map can be empty if no icons were found * @see Properties#load(InputStream) */ public static Map<String, Icon> loadIcons( String list, String path, ClassLoader loader ){ return loadIcons( list, path, null, loader ); } /** * Loads a map of icons. * @param list a path to a property-file containing key-path-pairs. * @param path the base path to the icons, will be added before any * path of the property file, can be <code>null</code> * @param ignore keys that are already present in <code>ignore</code> are not loaded, can be <code>null</code> * @param loader used to transform paths into urls. * @return the map of {@link Icon}s, the map can be empty if no icons were found * @see Properties#load(InputStream) */ public static Map<String, Icon> loadIcons( String list, String path, Set<String> ignore, ClassLoader loader ){ try{ InputStream in = loader.getResourceAsStream( list ); if( in == null ) return new HashMap<String, Icon>(); Properties properties = new Properties(); properties.load( in ); in.close(); int index = list.lastIndexOf( '/' ); if( index > 0 ){ if( path == null ){ path = list.substring( 0, index+1 ); } else{ path = list.substring( 0, index+1 ) + path; } } Map<String, Icon> result = new HashMap<String, Icon>(); for( Map.Entry<Object, Object> entry : properties.entrySet() ){ String key = (String)entry.getKey(); if( ignore == null || !ignore.contains( key )){ String file = (String)entry.getValue(); if( path != null ) file = path + file; URL url = loader.getResource( file ); if( url == null ){ System.err.println( "Missing file: " + file ); } else{ ImageIcon icon = new ImageIcon( url ); result.put( key, icon ); } } } return result; } catch( IOException ex ){ ex.printStackTrace(); return new HashMap<String, Icon>(); } } /** * Merges the array <code>base</code> with the placeholder that is associated with <code>dockable</code>, but * only if that placeholder is not yet in <code>base</code>. * @param base some basic array, can be <code>null</code> * @param dockable the dockable whose placeholder is to be stored, can be <code>null</code> * @param strategy a strategy to find the placeholder of <code>dockable</code>, can be <code>null</code> * @return either a new and larger array than <code>base</code>, <code>base</code> itself, or <code>null</code> if * <code>base</code> was <code>null</code> and no additional placeholder was found */ public static Path[] mergePlaceholders( Path[] base, Dockable dockable, PlaceholderStrategy strategy ){ if( dockable == null || strategy == null ){ return base; } Path placeholder = strategy.getPlaceholderFor( dockable ); if( placeholder == null ){ return base; } if( base == null ){ return new Path[]{ placeholder }; } for( Path check : base ){ if( placeholder.equals( check )){ return base; } } Path[] result = new Path[ base.length+1 ]; System.arraycopy( base, 0, result, 0, base.length ); result[ base.length ] = placeholder; return result; } /** * Tells whether the {@link Dockable} <code>child</code> can be dropped over * <code>parent</code>. * @param parent the new parent * @param child the new child * @return <code>true</code> if the parent and the child accept each other */ public static boolean acceptable( DockStation parent, Dockable child ){ if( !parent.accept( child )){ return false; } if( !child.accept( parent )){ return false; } DockController controller = parent.getController(); if( controller == null ){ controller = child.getController(); } if( controller != null ){ return controller.getAcceptance().accept( parent, child ); } return true; } /** * Tells whether the {@link Dockable} <code>next</code> can be dropped over <code>old</code>. * @param parent the parent of <code>old</code> * @param old the existing child * @param next the new child * @return <code>true</code> if the parent and the child accept each other */ public static boolean acceptable( DockStation parent, Dockable old, Dockable next ){ if( !old.accept( parent, next )){ return false; } if( !next.accept( parent )){ return false; } DockController controller = parent.getController(); if( controller == null ){ controller = old.getController(); } if( controller == null ){ controller = next.getController(); } if( controller != null ){ return controller.getAcceptance().accept( parent, old, next ); } return true; } /** * Ensures that {@link #checkLayoutLocked()} never prints out any warnings. */ public static void disableCheckLayoutLocked(){ checkLayoutLock = false; } /** * Searches for a class or interface that is marked with {@link LayoutLocked} in the current * callstack and prints a warning if found. */ public static void checkLayoutLocked(){ if( checkLayoutLock ){ StackTraceElement[] elements = Thread.currentThread().getStackTrace(); Set<Class<?>> tested = new HashSet<Class<?>>(); for( StackTraceElement element : elements ){ try { Class<?> clazz = Class.forName( element.getClassName() ); if( checkLayoutLocked( clazz, tested ) ){ return; } } catch( ClassNotFoundException e ) { // ignore and continue } catch( SecurityException e ){ // ignore and continue } catch( RuntimeException e ){ // may happen if a ClassLoader is not happy about "forName". Not nice, but better // than crashing the application. } catch( Error e ){ // may happen if a ClassLoader is not happy about "forName". Not nice, but better // than crashing the application. } } } } private static boolean checkLayoutLocked( Class<?> clazz, Set<Class<?>> tested ){ if( clazz != null && tested.add( clazz )){ LayoutLocked locked = clazz.getAnnotation( LayoutLocked.class ); if( locked != null ){ if( locked.locked() ){ System.err.println( "Warning: layout should not be modified by subclasses of " + clazz.getName() ); System.err.println( " This is only an information, not an exception. If your code is actually safe you can:"); System.err.println( " - disabled the warning by calling DockUtilities.disableCheckLayoutLocked() )" ); System.err.println( " - mark your code as safe by setting the annotation 'LayoutLocked'" ); for( StackTraceElement item : Thread.currentThread().getStackTrace() ){ System.err.println( item ); } } return true; } boolean result = checkLayoutLocked( clazz.getSuperclass(), tested ); if( result ){ return result; } for( Class<?> interfaze : clazz.getInterfaces() ){ result = checkLayoutLocked( interfaze, tested ); if( result ){ return result; } } } return false; } }