/* * 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) 2012 Herve Guillaume, 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 * * Herve Guillaume * rvguillaume@hotmail.com * FR - France * * Benjamin Sigg * benjamin_sigg@gmx.ch * CH - Switzerland */ package bibliothek.gui.dock; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.LayoutManager; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.swing.JPanel; import javax.swing.SwingUtilities; import bibliothek.gui.DockController; import bibliothek.gui.DockStation; import bibliothek.gui.DockUI; import bibliothek.gui.Dockable; import bibliothek.gui.Orientation; import bibliothek.gui.dock.component.DefaultDockStationComponentRootHandler; import bibliothek.gui.dock.component.DockComponentRootHandler; import bibliothek.gui.dock.event.DockStationListener; import bibliothek.gui.dock.layout.DockableProperty; import bibliothek.gui.dock.layout.location.AsideAnswer; import bibliothek.gui.dock.layout.location.AsideRequest; import bibliothek.gui.dock.security.SecureContainer; import bibliothek.gui.dock.station.DisplayerCollection; import bibliothek.gui.dock.station.DisplayerFactory; import bibliothek.gui.dock.station.DockableDisplayer; import bibliothek.gui.dock.station.DockableDisplayerListener; import bibliothek.gui.dock.station.PlaceholderMapping; import bibliothek.gui.dock.station.StationChildHandle; import bibliothek.gui.dock.station.StationDropItem; import bibliothek.gui.dock.station.StationDropOperation; import bibliothek.gui.dock.station.layer.DockStationDropLayer; import bibliothek.gui.dock.station.support.ConvertedPlaceholderListItem; import bibliothek.gui.dock.station.support.PlaceholderList.Level; import bibliothek.gui.dock.station.support.PlaceholderMap; import bibliothek.gui.dock.station.support.PlaceholderStrategy; import bibliothek.gui.dock.station.toolbar.ToolbarGroupDockStationFactory; import bibliothek.gui.dock.station.toolbar.ToolbarGroupDockStationLayout; import bibliothek.gui.dock.station.toolbar.ToolbarStrategy; import bibliothek.gui.dock.station.toolbar.group.ColumnScrollBar; import bibliothek.gui.dock.station.toolbar.group.ColumnScrollBarFactory; import bibliothek.gui.dock.station.toolbar.group.DefaultToolbarGroupDividierStrategy; import bibliothek.gui.dock.station.toolbar.group.SlimScrollbar; import bibliothek.gui.dock.station.toolbar.group.ToolbarColumn; import bibliothek.gui.dock.station.toolbar.group.ToolbarColumnModel; import bibliothek.gui.dock.station.toolbar.group.ToolbarGroupDividerStrategy; import bibliothek.gui.dock.station.toolbar.group.ToolbarGroupDividerStrategyFactory; import bibliothek.gui.dock.station.toolbar.group.ToolbarGroupDropInfo; import bibliothek.gui.dock.station.toolbar.group.ToolbarGroupExpander; import bibliothek.gui.dock.station.toolbar.group.ToolbarGroupHeader; import bibliothek.gui.dock.station.toolbar.group.ToolbarGroupHeaderFactory; import bibliothek.gui.dock.station.toolbar.group.ToolbarGroupPlaceholderMapping; import bibliothek.gui.dock.station.toolbar.group.ToolbarGroupProperty; import bibliothek.gui.dock.station.toolbar.layer.ToolbarGroupInnerLayer; import bibliothek.gui.dock.station.toolbar.layer.ToolbarGroupOuterLayer; import bibliothek.gui.dock.station.toolbar.layout.DockablePlaceholderToolbarGrid; import bibliothek.gui.dock.station.toolbar.layout.PlaceholderToolbarGridConverter; import bibliothek.gui.dock.station.toolbar.layout.ToolbarGridLayoutManager; import bibliothek.gui.dock.station.toolbar.title.ColumnDockActionSource; import bibliothek.gui.dock.themes.DefaultDisplayerFactoryValue; import bibliothek.gui.dock.themes.DefaultStationPaintValue; import bibliothek.gui.dock.themes.ThemeManager; import bibliothek.gui.dock.themes.basic.BasicDockTitleFactory; import bibliothek.gui.dock.title.DockTitleFactory; import bibliothek.gui.dock.title.DockTitleVersion; import bibliothek.gui.dock.toolbar.expand.ExpandedState; import bibliothek.gui.dock.util.DockUtilities; import bibliothek.gui.dock.util.PropertyKey; import bibliothek.gui.dock.util.PropertyValue; import bibliothek.gui.dock.util.SilentPropertyValue; import bibliothek.gui.dock.util.extension.Extension; import bibliothek.gui.dock.util.property.ConstantPropertyFactory; import bibliothek.util.Path; import bibliothek.util.Todo; import bibliothek.util.Todo.Compatibility; import bibliothek.util.Todo.Priority; import bibliothek.util.Todo.Version; /** * A {@link Dockable} and a {@link DockStation} which stands a group of * {@link ToolbarDockStation}. As <code>Dockable</code> it can be put in * <code>DockStation</code> which implements marker interface * {@link ToolbarInterface} or in {@link ScreenDockStation}, so that a * <code>ToolbarDockStation</code> can be floattable. As * <code>DockStation</code>, it accepts a {@link ToolbarElementInterface}. If * the element is not a <code>ToolbarElementInterface</code>, it is wrapped in a * <code>ToolbarDockStation</code> before to be added. * * @author Herve Guillaume */ public class ToolbarGroupDockStation extends AbstractToolbarDockStation { /** the id of the {@link DockTitleFactory} which is used by this station */ public static final String TITLE_ID = "toolbar.group"; /** * This id is forwarded to {@link Extension}s which load additional * {@link DisplayerFactory}s */ public static final String DISPLAYER_ID = "toolbar.group"; /** Key for the factory that creates new {@link ColumnScrollBar}s */ public static final PropertyKey<ColumnScrollBarFactory> SCROLLBAR_FACTORY = new PropertyKey<ColumnScrollBarFactory>( "dock.scrollbarFactory", new ConstantPropertyFactory<ColumnScrollBarFactory>( SlimScrollbar.FACTORY ), true ); /** Key for a factory that creates new {@link ToolbarGroupHeader}s */ public static final PropertyKey<ToolbarGroupHeaderFactory> HEADER_FACTORY = new PropertyKey<ToolbarGroupHeaderFactory>( "dock.toolbarGroupHeaderFactory" ); /** Key for a strategy for painting borders between the {@link Dockable}s */ public static final PropertyKey<ToolbarGroupDividerStrategyFactory> DIVIDER_STRATEGY_FACTORY = new PropertyKey<ToolbarGroupDividerStrategyFactory>( "dock.toolbarGroupDividerStrategy", new ConstantPropertyFactory<ToolbarGroupDividerStrategyFactory>( DefaultToolbarGroupDividierStrategy.FACTORY ), true ); /** A list of all children organized in columns and lines */ private final DockablePlaceholderToolbarGrid<StationChildHandle> dockables = new DockablePlaceholderToolbarGrid<StationChildHandle>(); /** Responsible for managing the {@link ExpandedState} of the children */ private ToolbarGroupExpander expander; /** The {@link PlaceholderStrategy} that is used by {@link #dockables} */ private final PropertyValue<PlaceholderStrategy> placeholderStrategy = new PropertyValue<PlaceholderStrategy>( PlaceholderStrategy.PLACEHOLDER_STRATEGY ){ @Override protected void valueChanged( PlaceholderStrategy oldValue, PlaceholderStrategy newValue ){ dockables.setStrategy( newValue ); } }; /** factory creating the current divider strategy */ private PropertyValue<ToolbarGroupDividerStrategyFactory> dividerStrategyFactory = new PropertyValue<ToolbarGroupDividerStrategyFactory>( DIVIDER_STRATEGY_FACTORY ){ @Override protected void valueChanged( ToolbarGroupDividerStrategyFactory oldValue, ToolbarGroupDividerStrategyFactory newValue ){ if( newValue == null ){ setDividerStrategy( null ); } else{ setDividerStrategy( newValue.create( ToolbarGroupDockStation.this )); } } }; /** Responsible for painting a border between the {@link Dockable}s, can be <code>null</code> */ private ToolbarGroupDividerStrategy dividerStrategy; /** * The graphical representation of this station: the pane which contains * component */ private OverpaintablePanelBase mainPanel; /** * Size of the border outside this station where a {@link Dockable} will * still be considered to be dropped onto this station. Measured in pixel. */ private int borderSideSnapSize = 10; /** * Whether the bounds of this station are slightly bigger than the station * itself. Used together with {@link #borderSideSnapSize} to grab Dockables * "out of the sky". The default is <code>true</code>. */ private boolean allowSideSnap = true; /** * The {@link LayoutManager} that is currently used to set the location and * size of all children. Can be <code>null</code> if no * {@link #setOrientation(Orientation) orientation} is set. */ private ToolbarGridLayoutManager<StationChildHandle> layoutManager; /** * Information about the drop operation that is currently in progress */ private ToolbarGroupDropInfo dropInfo; /** the factory that creates new scrollbars */ private PropertyValue<ColumnScrollBarFactory> scrollbarFactory = new PropertyValue<ColumnScrollBarFactory>( SCROLLBAR_FACTORY ){ @Override protected void valueChanged( ColumnScrollBarFactory oldValue, ColumnScrollBarFactory newValue ){ resetScrollbars(); } }; /** the scrollbars that are currently shown on this station */ private Map<Integer, ColumnScrollBar> scrollbars = new HashMap<Integer, ColumnScrollBar>(); /** this listener is added to all {@link ColumnScrollBar}s and ensures an update of positions if the bar changes its value */ private AdjustmentListener adjustmentListener = new AdjustmentListener(){ @Override public void adjustmentValueChanged( AdjustmentEvent e ){ mainPanel.dockablePane.revalidate(); } }; public ToolbarGridLayoutManager<StationChildHandle> getLayoutManager(){ return layoutManager; } /** the factory used to create new headers for this station */ private PropertyValue<ToolbarGroupHeaderFactory> headerFactory = new PropertyValue<ToolbarGroupHeaderFactory>( HEADER_FACTORY ){ @Override protected void valueChanged( ToolbarGroupHeaderFactory oldValue, ToolbarGroupHeaderFactory newValue ){ ToolbarGroupHeader header = null; if( newValue != null ) { header = newValue.create( ToolbarGroupDockStation.this ); } if( groupHeader != null ) { mainPanel.removeHeaderCopmonent( groupHeader.getComponent() ); groupHeader.destroy(); } groupHeader = header; if( groupHeader != null ) { groupHeader.setOrientation( getOrientation() ); mainPanel.addHeaderComponent( groupHeader.getComponent() ); } } }; /** the current component at the top end of this station */ private ToolbarGroupHeader groupHeader; // ######################################################## // ############ Initialization Managing ################### // ######################################################## /** * Creates a new {@link ToolbarGroupDockStation}. */ public ToolbarGroupDockStation(){ init(); } protected void init(){ init( ThemeManager.BACKGROUND_PAINT + ".station.toolbar.group" ); mainPanel = new OverpaintablePanelBase(); paint = new DefaultStationPaintValue( ThemeManager.STATION_PAINT + ".toolbar.group", this ); setOrientation( getOrientation() ); displayerFactory = createDisplayerFactory(); displayers = new DisplayerCollection( this, displayerFactory, getDisplayerId() ); displayers.addDockableDisplayerListener( new DockableDisplayerListener(){ @Override public void discard( DockableDisplayer displayer ){ ToolbarGroupDockStation.this.discard( displayer ); } @Override public void moveableElementChanged( DockableDisplayer displayer ){ // ignore } } ); setTitleIcon( null ); expander = new ToolbarGroupExpander( this ); setDividerStrategy( dividerStrategyFactory.getValue().create( this ) ); } @Override protected DockComponentRootHandler createRootHandler() { return new DefaultDockStationComponentRootHandler( this, displayers ); } // ######################################################## // ################### Class Utilities #################### // ######################################################## /** * Gets access to a simplified view of the contents of this station. * * @return a model describing all the columns that are shown on this station */ public ToolbarColumnModel<Dockable,StationChildHandle> getColumnModel(){ return dockables.getModel(); } /** * Gets the column location of the <code>dockable</code>. * * @param dockable * the {@link Dockable} to search * @return the column location or -1 if the child was not found */ public int column( Dockable dockable ){ return dockables.getColumn( dockable ); } /** * Gets the line location of the <code>dockable</code>. * * @param dockable * the {@link Dockable} to search * @return the line location or -1 if the child was not found */ public int line( Dockable dockable ){ return dockables.getLine( dockable ); } /** * Gets the number of column of <code>this</code>. * * @return the number of column */ public int columnCount(){ return dockables.getColumnCount(); // column(getDockable(getDockableCount() // - 1)) + 1; } /** * Gets the number of lines in <code>column</code>. * * @param column * the column * @return the number of lines in <code>column</code> */ public int lineCount( int column ){ return dockables.getLineCount( column ); } /** * Gets the dockable at the specified <code>column</code> and * <code>line</code>. * * @param columnIndex * the column index * @param line * the line index * @return the dockable or <code>null</code> if there's no dockable at the * specified indexes. */ public Dockable getDockable( int columnIndex, int line ){ StationChildHandle handle = getHandle( columnIndex, line ); if( handle == null ) { return null; } return handle.asDockable(); } /** * Gets the {@link StationChildHandle} which is used at the given position. * * @param columnIndex * the column in which to search * @param line * the line in the column * @return the item or <code>null</code> if the indices are out of bounds */ public StationChildHandle getHandle( int columnIndex, int line ){ ToolbarColumnModel<Dockable,StationChildHandle> model = getColumnModel(); if( columnIndex < 0 || columnIndex >= model.getColumnCount() ) { return null; } ToolbarColumn<Dockable,StationChildHandle> column = model.getColumn( columnIndex ); if( line < 0 || line >= column.getDockableCount() ) { return null; } return column.getItem( line ); } /** * Gets the {@link StationChildHandle} which displays <code>dockable</code>. * @param dockable the item to search * @return the handle showing <code>dockable</code> or <code>null</code> if not found */ public StationChildHandle getHandle( Dockable dockable ){ ToolbarColumn<Dockable,StationChildHandle> column = getColumnModel().getColumn( dockable ); if( column == null ) { return null; } for( int i = 0, n = column.getDockableCount(); i < n; i++ ) { StationChildHandle handle = column.getItem( i ); if( handle.getDockable() == dockable ) { return handle; } } return null; } /** * Tells if <code>dockable</code> is the last dockable in its column. * * @param dockable * the dockable * @return true if the dockable it's the last in its column, false if it's * not or if it doesn't belong to <code>this</code> dockstation. */ public boolean isLastOfColumn( Dockable dockable ){ int index = indexOf( dockable ); int column = column( dockable ); if( index == (getDockableCount() - 1) ) { return true; } else if( column( getDockable( index + 1 ) ) != column ) { return true; } else { return false; } } // ######################################################## // ############ General DockStation Managing ############## // ######################################################## /** * Gets the {@link ToolbarStrategy} that is currently used by this station. * * @return the strategy, never <code>null</code> */ @Override public ToolbarStrategy getToolbarStrategy(){ DockController controller = getController(); DockStation parent = getDockParent(); while( controller == null && parent != null ){ controller = parent.getController(); Dockable parentDockable = parent.asDockable(); if( parentDockable == null ){ parent = null; } else{ parent = parentDockable.getDockParent(); } } final SilentPropertyValue<ToolbarStrategy> value = new SilentPropertyValue<ToolbarStrategy>( ToolbarStrategy.STRATEGY, controller ); final ToolbarStrategy result = value.getValue(); value.setProperties( (DockController) null ); return result; } @Override public Component getComponent(){ return mainPanel; } @Override public int getDockableCount(){ return dockables.size(); } @Override public Dockable getDockable( int index ){ return dockables.get( index ).asDockable(); } @Override public String getFactoryID(){ return ToolbarGroupDockStationFactory.ID; } /** * Gets a unique identifier used to get the {@link DisplayerFactory} for this station. * @return the unique identifier, not <code>null</code> */ protected String getDisplayerId(){ return DISPLAYER_ID; } /** * Sets whether {@link Dockable Dockables} which are dragged near the * station are captured and added to this station. * * @param allowSideSnap * <code>true</code> if the station can snap Dockables which are * near. * @see #setBorderSideSnapSize(int) */ public void setAllowSideSnap( boolean allowSideSnap ){ this.allowSideSnap = allowSideSnap; } /** * Tells whether the station can grab Dockables which are dragged near the * station. * * @return <code>true</code> if grabbing is allowed * @see #setAllowSideSnap(boolean) */ public boolean isAllowSideSnap(){ return allowSideSnap; } /** * There is an invisible border around the station. If a {@link Dockable} is * dragged inside this border, its considered to be on the station and will * be dropped into. * * @param borderSideSnapSize * the size of the border in pixel * @throws IllegalArgumentException * if the size is smaller than 0 */ public void setBorderSideSnapSize( int borderSideSnapSize ){ if( borderSideSnapSize < 0 ) { throw new IllegalArgumentException( "borderSideSnapeSize must not be less than 0" ); } this.borderSideSnapSize = borderSideSnapSize; } /** * Gets the size of the invisible border around the station where a dockable * can be dropped. * * @return the size in pixel * @see #setBorderSideSnapSize(int) */ public int getBorderSideSnapSize(){ return borderSideSnapSize; } @Override public void setController( DockController controller ){ if( getController() != controller ) { if( getController() != null ) { dockables.unbind(); } Iterator<StationChildHandle> iter = dockables.items(); while( iter.hasNext() ) { iter.next().setTitleRequest( null ); } super.setController( controller ); // if not set controller of the DefaultStationPaintValue, call to // DefaultStationPaintValue do nothing if( controller == null ) { title = null; } else { title = registerTitle( controller ); } paint.setController( controller ); expander.setController( controller ); placeholderStrategy.setProperties( controller ); displayerFactory.setController( controller ); displayers.setController( controller ); mainPanel.setController( controller ); layoutManager.setController( controller ); scrollbarFactory.setProperties( controller ); headerFactory.setProperties( controller ); dividerStrategyFactory.setProperties( controller ); if( getController() != null ) { dockables.bind(); } iter = dockables.items(); while( iter.hasNext() ) { iter.next().setTitleRequest( title, true ); } } } // ######################################################## // ############ Orientation Managing ###################### // ######################################################## @Override public void setOrientation( Orientation orientation ){ if( orientation == null ) { throw new IllegalArgumentException( "orientation must not be null" ); } // it's very important to change position and orientation of inside // dockables first, else doLayout() is done on wrong inside information this.orientation = orientation; fireOrientingEvent(); for( ColumnScrollBar scrollbar : scrollbars.values() ) { scrollbar.setOrientation( orientation ); } if( groupHeader != null ) { groupHeader.setOrientation( orientation ); mainPanel.removeHeaderCopmonent( groupHeader.getComponent() ); mainPanel.addHeaderComponent( groupHeader.getComponent() ); } mainPanel.updateAlignment(); mainPanel.revalidate(); } // ######################################################## // ############### Drop/Move Managing ##################### // ######################################################## @Override public DockStationDropLayer[] getLayers(){ return new DockStationDropLayer[]{ new ToolbarGroupInnerLayer( this, mainPanel.dockablePane ), new ToolbarGroupOuterLayer( this, mainPanel.dockablePane ) }; } @Override public boolean accept( Dockable child ){ return getToolbarStrategy().isToolbarGroupPart( child ); } @Override public boolean accept( DockStation station ){ return getToolbarStrategy().isToolbarGroupPartParent( station, this, false ); } public boolean accept( DockStation base, Dockable neighbor ){ return false; } @Override public StationDropOperation prepareDrop( StationDropItem item ){ // System.out.println(this.toString() + "## prepareDrop(...) ##"); final DockController controller = getController(); if( getExpandedState() == ExpandedState.EXPANDED ) { return null; } Dockable dockable = item.getDockable(); // check if the dockable and the station accept each other if( this.accept( dockable ) && dockable.accept( this ) ) { // check if controller exists and if the controller accepts that // the dockable becomes a child of this station if( controller != null ) { if( !controller.getAcceptance().accept( this, dockable ) ) { return null; } } Point mouse = new Point( item.getMouseX(), item.getMouseY() ); SwingUtilities.convertPointFromScreen( mouse, mainPanel.dockablePane ); int column = getColumnAt( mouse ); int line = -1; if( column >= 0 && column < columnCount() ) { Rectangle columnBounds = layoutManager.getBounds( column ); if( getOrientation() == Orientation.VERTICAL ) { int x = columnBounds.x; int width = columnBounds.width; if( x + width / 5 <= mouse.x && mouse.x <= x + width * 4 / 5 ) { line = layoutManager.getInsertionLineAt( column, mouse.y ); } else if( mouse.x >= x + width * 4 / 5 ) { column++; } } else { int y = columnBounds.y; int height = columnBounds.height; if( y + height / 5 <= mouse.y && mouse.y <= y + height * 4 / 5 ) { line = layoutManager.getInsertionLineAt( column, mouse.x ); } else if( mouse.y >= y + height * 4 / 5 ) { column++; } } } if( column == -1 ) { column = 0; } boolean effect = true; if( dockable.getDockParent() == this ){ int currentColumn = column( dockable ); int currentLine = line( dockable ); effect = currentColumn != column || (currentLine != line && currentLine != line-1 ); effect = effect && !(line == -1 && lineCount( currentColumn ) == 1 && (currentColumn == column || currentColumn == column-1)); } return new ToolbarGroupDropInfo( dockable, ToolbarGroupDockStation.this, column, line, effect ){ @Override public void execute(){ dropInfo = null; drop( this ); } // Note: draw() is called first by the Controller. It seems // destroy() is called after, after a new StationDropOperation // is created @Override public void destroy( StationDropOperation next ){ if( next == null || next.getTarget() != getTarget() ) { layoutManager.mutate(); } dropInfo = null; mainPanel.repaint(); } @Override public void draw(){ dropInfo = this; int column = getColumn(); int line = getLine(); if( hasEffect() ){ layoutManager.mutate( column, line ); } else{ layoutManager.mutate(); } mainPanel.repaint(); } }; } else { return null; } } /** * Tells which column covers the point <code>location</code>. If <code>location</code> is outside * this station, then a non-existing column <code>-1</code> or {@link #columnCount()} is returned. * @param location some point on this {@link Component} * @return the column covering <code>location</code>, can be <code>-1</code> or {@link #columnCount()} */ public int getColumnAt( Point location ){ int pos; int max; if( getOrientation() == Orientation.VERTICAL ) { pos = location.x; max = mainPanel.getWidth(); } else { pos = location.y; max = mainPanel.getHeight(); } if( pos < 0 ) { return -1; } if( pos >= max ) { return columnCount(); } return layoutManager.getColumnAt( pos ); } /** * Drops thanks to information collect by dropInfo. * * @param dropInfoGroup */ protected void drop( ToolbarGroupDropInfo dropInfoGroup ){ int line = dropInfoGroup.getLine(); if( line == -1 ) { drop( dropInfoGroup.getItem(), dropInfoGroup.getColumn() ); } else { drop( dropInfoGroup.getItem(), dropInfoGroup.getColumn(), line ); } } @Override public void drop( Dockable dockable ){ drop( dockable, 0, 0 ); } /** * Drops the <code>dockable</code> at the specified line and column. * * @param dockable * the dockable to insert * @param column * the column where insert * @param line * the line where insert * @return true if the dockable has been inserted, false otherwise */ public boolean drop( Dockable dockable, int column, int line ){ return drop( dockable, column, line, false ); } public boolean drop( Dockable dockable, int column, int line, boolean force ){ if( force || this.accept( dockable ) ) { if( !force ) { Dockable replacement = getToolbarStrategy().ensureToolbarLayer( this, dockable ); if( replacement == null ) { return false; } if( replacement != dockable ) { replacement.asDockStation().drop( dockable ); dockable = replacement; } } add( dockable, column, line ); return true; } return false; } private void add( Dockable dockable, int column, int line ){ DockUtilities.ensureTreeValidity( this, dockable ); DockUtilities.checkLayoutLocked(); Dockable replacement = getToolbarStrategy().ensureToolbarLayer( this, dockable ); if( replacement != dockable ) { replacement.asDockStation().drop( dockable ); dockable = replacement; } final DockHierarchyLock.Token token = DockHierarchyLock.acquireLinking( this, dockable ); try { listeners.fireDockableAdding( dockable ); dockable.setDockParent( this ); final StationChildHandle handle = createHandle( dockable ); // add in the list of dockable final int before = dockables.getColumnCount(); dockables.insert( column, line, handle ); // add in the main panel addComponent( handle ); listeners.fireDockableAdded( dockable ); fireDockablesRepositioned( dockable, before != dockables.getColumnCount() ); } finally { token.release(); } } /** * Creates a new {@link StationChildHandle} that wraps around * <code>dockable</code>. This method does not add the handle to any list or * fire any events, that is the callers responsibility. Callers should also * call {@link #addComponent(StationChildHandle)} with the new handle. * @param dockable the element that is to be wrapped * @return a new {@link StationChildHandle} for the element */ private StationChildHandle createHandle( Dockable dockable ){ final StationChildHandle handle = new StationChildHandle( this, displayers, dockable, title ); handle.updateDisplayer(); return handle; } /** * Adds <code>handle</code> to the {@link #mainPanel} of this station. Note * that this method only cares about the {@link Component}-{@link Container} * relationship, it does not store <code>handle</code> in the * {@link #dockables} list. * @param handle the handle to add */ private void addComponent( StationChildHandle handle ){ mainPanel.dockablePane.add( handle.getDisplayer().getComponent() ); mainPanel.getContentPane().revalidate(); } /** * Removes <code>handle</code> of the {@link #mainPanel} of this station. * Note that this method only cares about the {@link Component}- * {@link Container} relationship, it does not remove <code>handle</code> of * the {@link #dockables} list. * * @param handle * the handle to remove */ private void removeComponent( StationChildHandle handle ){ mainPanel.dockablePane.remove( handle.getDisplayer().getComponent() ); mainPanel.getContentPane().revalidate(); } /** * Drops the <code>dockable</code> in a new column. * * @param dockable * the dockable to insert * @param column * the column index to create * @return true if the dockable has been inserted, false otherwise */ public boolean drop( Dockable dockable, int column ){ return drop( dockable, column, false ); } public boolean drop( Dockable dockable, int column, boolean force ){ if( force || this.accept( dockable ) ) { if( !force ) { Dockable replacement = getToolbarStrategy().ensureToolbarLayer( this, dockable ); if( replacement == null ) { return false; } if( replacement != dockable ) { replacement.asDockStation().drop( dockable ); dockable = replacement; } } add( dockable, column ); return true; } return false; } private void add( Dockable dockable, int column ){ DockUtilities.ensureTreeValidity( this, dockable ); DockUtilities.checkLayoutLocked(); Dockable replacement = getToolbarStrategy().ensureToolbarLayer( this, dockable ); if( replacement != dockable ) { replacement.asDockStation().drop( dockable ); dockable = replacement; } final DockHierarchyLock.Token token = DockHierarchyLock.acquireLinking( this, dockable ); try { listeners.fireDockableAdding( dockable ); dockable.setDockParent( this ); final StationChildHandle handle = createHandle( dockable ); // add in the list of dockable final int before = dockables.getColumnCount(); dockables.insert( column, handle, false ); // add in the main panel addComponent( handle ); listeners.fireDockableAdded( dockable ); fireDockablesRepositioned( dockable, before != dockables.getColumnCount() ); } finally { token.release(); } } @Override public void drag( Dockable dockable ){ if( dockable.getDockParent() != this ) { throw new IllegalArgumentException( "not a child of this station: " + dockable ); } remove( dockable ); } @Override protected void remove( Dockable dockable ){ DockUtilities.checkLayoutLocked(); final int column = column( dockable ); final int before = dockables.getColumnCount(); final StationChildHandle handle = dockables.get( dockable ); final DockHierarchyLock.Token token = DockHierarchyLock.acquireUnlinking( this, dockable ); try { listeners.fireDockableRemoving( dockable ); dockables.remove( handle ); removeComponent( handle ); handle.destroy(); dockable.setDockParent( null ); listeners.fireDockableRemoved( dockable ); fireColumnRepositioned( column, before != dockables.getColumnCount() ); mainPanel.repaint(); // if we don't call repaint, the station is // not repaint when we remove a ToolbarDockStation in a column and // that // it doesn't affect the number of column } finally { token.release(); } } @Override public void replace( Dockable old, Dockable next ){ // TODO Auto-generated method stub DockUtilities.checkLayoutLocked(); final DockController controller = getController(); if( controller != null ) { controller.freezeLayout(); } final int column = dockables.getColumn( old ); final int line = dockables.getLine( old ); final int beforeCount = dockables.getColumnCount(); remove( old ); // if remove the old dockable delete a column we have to recreate it if( beforeCount != dockables.getColumnCount() ) { add( next, column ); } else { add( next, column, line ); } controller.meltLayout(); } @Override public boolean canReplace( Dockable old, Dockable next ){ return acceptable( next ) && getToolbarStrategy().isToolbarGroupPartParent( this, next, true ); } /** * Fires * {@link DockStationListener#dockablesRepositioned(DockStation, Dockable[])} * for all {@link Dockable}s that are in the same column as * <code>dockable</code>, including <code>dockable</code>. * * @param dockable * some item from a column that changed * @param all * whether there should be an event for all the columns after * <code>dockable</code> as well */ protected void fireDockablesRepositioned( Dockable dockable ){ fireDockablesRepositioned( dockable, false ); } /** * Fires * {@link DockStationListener#dockablesRepositioned(DockStation, Dockable[])} * for all {@link Dockable}s that are in the same column as * <code>dockable</code>, including <code>dockable</code>. * * @param dockable * some item from a column that changed * @param all * whether there should be an event for all the columns after the * column of the dockable as well */ protected void fireDockablesRepositioned( Dockable dockable, boolean all ){ fireColumnRepositioned( column( dockable ), all ); } /** * Fires * {@link DockStationListener#dockablesRepositioned(DockStation, Dockable[])} * for all {@link Dockable}s in the given <code>column</code>. * * @param column * the column for which to fire an event * @param all * whether there should be an event for all the columns after * <code>column</code> as well */ protected void fireColumnRepositioned( int column, boolean all ){ final List<Dockable> list = new ArrayList<Dockable>(); final int end = all ? dockables.getColumnCount() : column + 1; for( int i = column; i < end; i++ ) { final Iterator<StationChildHandle> items = dockables.getColumnContent( i ); while( items.hasNext() ) { list.add( items.next().getDockable() ); } } if( list.size() > 0 ) { listeners.fireDockablesRepositioned( list.toArray( new Dockable[list.size()] ) ); } } // ######################################################## // ###################### UI Managing ##################### // ######################################################## /** * Gets a {@link ColumnDockActionSource} which allows to modify the * {@link ExpandedState} of the children of this station. * * @return the source, not <code>null</code> */ public ColumnDockActionSource getExpandActionSource(){ return expander.getActions(); } /** * Gets the factory for the {@link #getHeaderComponent() header component} which is currently * in use. * @return the current factory, can be <code>null</code> * @see #HEADER_FACTORY */ public ToolbarGroupHeaderFactory getHeaderComponentFactory(){ return headerFactory.getValue(); } /** * Sets the factory for the {@link #getHeaderComponent() header component}. A value of <code>null</code> * will be interpreted as a request to reinstall the default factory. * @param factory the new factory, can be <code>null</code> in order to reinstall the default factory * @see #HEADER_FACTORY */ public void setHeaderComponentFactory( ToolbarGroupHeaderFactory factory ){ headerFactory.setValue( factory ); } /** * Gets the {@link Component} which is currently shown at the top of this station. * @return the component or <code>null</code> * @see #HEADER_FACTORY */ public ToolbarGroupHeader getHeaderComponent(){ return groupHeader; } @Override protected void callDockUiUpdateTheme() throws IOException{ DockUI.updateTheme( this, new ToolbarGroupDockStationFactory() ); } @Override protected DefaultDisplayerFactoryValue createDisplayerFactory(){ return new DefaultDisplayerFactoryValue( ThemeManager.DISPLAYER_FACTORY + ".toolbar.group", this ); } @Override protected DockTitleVersion registerTitle( DockController controller ){ return controller.getDockTitleManager().getVersion( TITLE_ID, BasicDockTitleFactory.FACTORY ); } /** * Replaces <code>displayer</code> with a new {@link DockableDisplayer}. * * @param displayer * the displayer to replace * @throws IllegalArgumentException * if <code>displayer</code> is not a child of this station */ @Override protected void discard( DockableDisplayer displayer ){ final Dockable dockable = displayer.getDockable(); final StationChildHandle handle = dockables.get( dockable ); if( handle == null ) { throw new IllegalArgumentException( "displayer is not child of this station: " + displayer ); } removeComponent( handle ); handle.updateDisplayer(); addComponent( handle ); } private void resetScrollbars(){ for( Map.Entry<Integer, ColumnScrollBar> entry : scrollbars.entrySet() ) { ColumnScrollBar replacement = scrollbarFactory.getValue().create( this ); mainPanel.dockablePane.remove( entry.getValue().getComponent() ); entry.setValue( replacement ); mainPanel.dockablePane.add( entry.getValue().getComponent() ); } } private void setDividerStrategy( ToolbarGroupDividerStrategy dividerStrategy ){ this.dividerStrategy = dividerStrategy; layoutManager.setDividerStrategy( dividerStrategy ); mainPanel.revalidate(); } public Rectangle getDropGapBoundaries(){ if( dropInfo == null ){ return null; } Component dockablePane = mainPanel.dockablePane; Point zero = new Point( 0, 0 ); zero = SwingUtilities.convertPoint( dockablePane, zero, mainPanel ); Rectangle gapBounds; if( dropInfo.getLine() == -1 ) { gapBounds = layoutManager.getGapBounds( dropInfo.getColumn(), false ); } else { gapBounds = layoutManager.getGapBounds( dropInfo.getColumn(), dropInfo.getLine() ); } gapBounds.x += zero.x; gapBounds.y += zero.y; return gapBounds; } /** * This panel is used as base of the station. All children of the station * have this panel as parent too. It allows to draw arbitrary figures over * the base panel * * @author Herve Guillaume */ protected class OverpaintablePanelBase extends SecureContainer { /** * Generated serial number */ private static final long serialVersionUID = -4399008463139189130L; /** * The direct parent {@link Container} of {@link Dockable}s. */ private final JPanel dockablePane = new JPanel(){ protected void paintComponent( Graphics g ){ super.paintComponent( g ); if( dividerStrategy != null ){ dividerStrategy.paint( this, g, layoutManager ); } } }; /** * The parent of {@link #dockablePane}, adds insets, borders and other * decorations. */ private JPanel decorationPane = createBackgroundPanel(); /** * Creates a new panel */ public OverpaintablePanelBase(){ setBasePane( decorationPane ); setSolid( false ); decorationPane.setOpaque( false ); dockablePane.setOpaque( false ); decorationPane.setLayout( new BorderLayout() ); decorationPane.add( dockablePane, BorderLayout.CENTER ); } public void addHeaderComponent( Component header ){ if( getOrientation() == Orientation.HORIZONTAL ) { decorationPane.add( header, BorderLayout.WEST ); } else { decorationPane.add( header, BorderLayout.NORTH ); } } public void removeHeaderCopmonent( Component header ){ decorationPane.remove( header ); } @Override public Dimension getPreferredSize(){ // this is a workaround because the boundaries of the children are required to // compute the preferred size of this components. It is not pretty... if( dockablePane != null && !dockablePane.isValid() ){ dockablePane.doLayout(); } return getBasePane().getPreferredSize(); } @Override public Dimension getMinimumSize(){ return getBasePane().getPreferredSize(); } @Override public Dimension getMaximumSize(){ return getBasePane().getPreferredSize(); } /** * Update alignment with regards to the current orientation of this * {@link ToolbarGroupDockStation} */ public void updateAlignment(){ final Orientation orientation = getOrientation(); if( orientation != null ) { if( layoutManager != null ) { layoutManager.setController( null ); } layoutManager = new ToolbarGridLayoutManager<StationChildHandle>( dockablePane, orientation, dockables, ToolbarGroupDockStation.this ){ @Override protected Component toComponent( StationChildHandle item ){ return item.getDisplayer().getComponent(); } @Override protected void setShowScrollbar( int column, boolean show ){ boolean change = false; if( show ) { if( !scrollbars.containsKey( column ) ) { ColumnScrollBar bar = scrollbarFactory.getValue().create( ToolbarGroupDockStation.this ); bar.setOrientation( getOrientation() ); scrollbars.put( column, bar ); dockablePane.add( bar.getComponent() ); bar.addAdjustmentListener( adjustmentListener ); change = true; } } else { ColumnScrollBar bar = scrollbars.remove( column ); if( bar != null ) { dockablePane.remove( bar.getComponent() ); bar.removeAdjustmentListener( adjustmentListener ); change = true; } } if( change ) { revalidateLater(); } } @Override protected int getScrollbarValue( int column, int required, int available ){ ColumnScrollBar scrollbar = scrollbars.get( column ); if( scrollbar == null ) { return 0; } scrollbar.setValues( required, available ); return scrollbar.getValue(); } @Override protected Component getScrollbar( int column ){ ColumnScrollBar scrollbar = scrollbars.get( column ); if( scrollbar == null ) { return null; } return scrollbar.getComponent(); } }; dockablePane.setLayout( layoutManager ); layoutManager.setDividerStrategy( dividerStrategy ); layoutManager.setController( getController() ); } mainPanel.revalidate(); } /** * Asynchronously revalidates {@link #decorationPane}, but only if its size did not change */ private void revalidateLater(){ if( orientation == Orientation.VERTICAL ) { final int height = decorationPane.getHeight(); EventQueue.invokeLater( new Runnable(){ @Override public void run(){ if( height == decorationPane.getHeight() ) { decorationPane.revalidate(); } } } ); } else { final int width = decorationPane.getWidth(); EventQueue.invokeLater( new Runnable(){ @Override public void run(){ if( width == decorationPane.getWidth() ) { decorationPane.revalidate(); } } } ); } } @Override protected void paintOverlay( Graphics g ){ final Graphics2D g2D = (Graphics2D) g; paintDrag( g ); if( dropInfo != null && dropInfo.hasEffect() ) { Point zero = new Point( 0, 0 ); zero = SwingUtilities.convertPoint( dockablePane, zero, this ); Rectangle gapBounds = getDropGapBoundaries(); paint.drawInsertion( g2D, new Rectangle( zero.x, zero.y, dockablePane.getWidth(), dockablePane.getHeight() ), gapBounds ); } } private void paintDrag( Graphics g ){ Dockable removal = getRemoval(); if( removal != null ) { StationChildHandle handle = getHandle( removal ); if( handle != null ) { Rectangle bounds = handle.getDisplayer().getComponent().getBounds(); Component dockablePane = mainPanel.dockablePane; Point zero = new Point( 0, 0 ); zero = SwingUtilities.convertPoint( dockablePane, zero, this ); bounds.x += zero.x; bounds.y += zero.y; getPaint().drawRemoval( g, bounds, bounds ); } } } @Override public String toString(){ return this.getClass().getSimpleName() + '@' + Integer.toHexString( hashCode() ); } } // ######################################################## // ############### PlaceHolder Managing ################### // ######################################################## @Override public PlaceholderMap getPlaceholders(){ PlaceholderMap map = dockables.toMap(); writeLayoutArguments( map ); return map; } /** * Converts this station into a {@link PlaceholderMap} using * <code>identifiers</code> to remember which {@link Dockable} was at which * location. * * @param identifiers * the identifiers to apply * @return <code>this</code> as map */ public PlaceholderMap getPlaceholders( Map<Dockable, Integer> identifiers ){ PlaceholderMap map = dockables.toMap( identifiers ); writeLayoutArguments( map ); return map; } @Override public PlaceholderMapping getPlaceholderMapping() { return new ToolbarGroupPlaceholderMapping( this, dockables ); } @Override public void setPlaceholders( PlaceholderMap placeholders ){ dockables.fromMap( placeholders ); readLayoutArguments( placeholders ); } public void setPlaceholders( PlaceholderMap placeholders, Map<Integer, Dockable> children ){ DockUtilities.checkLayoutLocked(); if( getDockableCount() > 0 ) { throw new IllegalStateException( "this station still has children" ); } final DockController controller = getController(); readLayoutArguments( placeholders ); try { if( controller != null ) { controller.freezeLayout(); } dockables.setStrategy( null ); dockables.unbind(); dockables.fromMap( placeholders, children, new PlaceholderToolbarGridConverter<Dockable, StationChildHandle>(){ @Override public StationChildHandle convert( Dockable dockable, ConvertedPlaceholderListItem item ){ listeners.fireDockableAdding( dockable ); dockable.setDockParent( ToolbarGroupDockStation.this ); final StationChildHandle handle = createHandle( dockable ); addComponent( handle ); return handle; } @Override public void added( StationChildHandle item ){ listeners.fireDockableAdded( item.getDockable() ); } } ); if( controller != null ) { dockables.bind(); } dockables.setStrategy( placeholderStrategy.getValue() ); } finally { if( controller != null ) { controller.meltLayout(); } } } private void writeLayoutArguments( PlaceholderMap map ){ ToolbarGroupDockStationLayout.writeOrientation( map, getOrientation() ); } private void readLayoutArguments( PlaceholderMap map ){ Orientation orientation = ToolbarGroupDockStationLayout.readOrientation( map ); if( orientation != null ){ setOrientation( orientation ); } } @Override public DockableProperty getDockableProperty( Dockable child, Dockable target ){ final int column = column( child ); final int line = line( child ); if( target == null ) { target = child; } final PlaceholderStrategy strategy = placeholderStrategy.getValue(); Path placeholder = null; if( strategy != null ) { placeholder = strategy.getPlaceholderFor( target ); if( (placeholder != null) && (column >= 0) && (line >= 0) ) { dockables.addPlaceholder( column, line, placeholder ); } } return new ToolbarGroupProperty( column, line, placeholder ); } @Override @Todo( compatibility=Compatibility.COMPATIBLE, priority=Priority.ENHANCEMENT, target=Version.VERSION_1_1_2, description="Implement this feature") public void aside( AsideRequest request ){ DockableProperty location = request.getLocation(); int column = -1; int line = -1; Path newPlaceholder = request.getPlaceholder(); if( location instanceof ToolbarGroupProperty ){ ToolbarGroupProperty groupLocation = (ToolbarGroupProperty)location; Path placeholder = groupLocation.getPlaceholder(); if( placeholder != null ){ column = dockables.getColumn( placeholder ); } if( column == -1 ){ placeholder = null; column = groupLocation.getColumn(); } int totalColumnCount = dockables.getTotalColumnCount(); column = Math.max( 0, Math.min( column, totalColumnCount ) ); if( placeholder != null && column < totalColumnCount ){ line = dockables.getLine( column, placeholder ); } else if( column == totalColumnCount ){ line = 0; } if( line == -1 ){ line = groupLocation.getLine(); line = Math.max( 0, Math.min( line, dockables.getLineCount( column ) ) ); } if( groupLocation.getSuccessor() == null ){ // insert if( newPlaceholder != null ){ dockables.insertPlaceholder( column, line, newPlaceholder ); } } else{ // add to existing location if( newPlaceholder != null ){ dockables.addPlaceholder( column, line, newPlaceholder ); Dockable existing = dockables.getModel().getColumn( column ).getDockable( line ); if( existing.asDockStation() != null ){ AsideAnswer answer = request.forward( existing.asDockStation() ); if( answer.isCanceled() ){ return; } } } } } else { column = 0; if( dockables.getColumnCount() > 0 ){ line = dockables.getLineCount( column ); } else{ line = 0; } } request.answer( new ToolbarGroupProperty( column, line, newPlaceholder ) ); } @Override public boolean drop( Dockable dockable, DockableProperty property ){ if( property instanceof ToolbarGroupProperty ) { return drop( dockable, (ToolbarGroupProperty) property ); } return false; } /** * Tries to drop <code>dockable</code> at <code>property</code>. * * @param dockable * the element to drop * @param property * the location of <code>dockable</code> * @return <code>true</code> if dropping was successful, <code>false</code> * otherwise */ public boolean drop( Dockable dockable, ToolbarGroupProperty property ){ final Path placeholder = property.getPlaceholder(); int column = property.getColumn(); int line = property.getLine(); if( placeholder != null ) { if( dockables.hasPlaceholder( placeholder ) ) { final StationChildHandle child = dockables.get( placeholder ); if( child == null ) { if( acceptable( dockable ) ) { Dockable replacement = getToolbarStrategy().ensureToolbarLayer( this, dockable ); if( replacement == null ) { return false; } DockController controller = getController(); if( controller != null ) { controller.freezeLayout(); } try { dropAt( replacement, placeholder ); if( replacement != dockable ) { if( property.getSuccessor() != null ) { if( !replacement.asDockStation().drop( dockable, property.getSuccessor() ) ) { replacement.asDockStation().drop( dockable ); } } else { replacement.asDockStation().drop( dockable ); } } } finally { if( controller != null ) { controller.meltLayout(); } } return true; } } else { if( drop( child, dockable, property ) ) { return true; } column = dockables.getColumn( child.getDockable() ); line = dockables.getLine( column, child.getDockable() ) + 1; } } } else { if( column >= 0 && column < columnCount() ) { if( line >= 0 && line < lineCount( column ) ) { DockStation child = getDockable( column, line ).asDockStation(); if( child != null && child.drop( dockable, property.getSuccessor() ) ) { return true; } } } } if( !acceptable( dockable ) ) { return false; } line = Math.max( 0, line ); // column = Math.max( 0, column ); return drop( dockable, column, line ); } /** * Drops <code>dockable</code> at the location described by * <code>placeholder</code>. This method must only be called if * <code>placeholder</code> points to a valid location. * * @param dockable * the dockable to drop * @param placeholder * the location of <code>dockable</code>, must be valid */ private void dropAt( Dockable dockable, Path placeholder ){ DockUtilities.checkLayoutLocked(); final DockHierarchyLock.Token token = DockHierarchyLock.acquireLinking( this, dockable ); try { DockUtilities.ensureTreeValidity( this, dockable ); listeners.fireDockableAdding( dockable ); final int before = dockables.getColumnCount(); dockable.setDockParent( this ); final StationChildHandle handle = createHandle( dockable ); dockables.put( placeholder, handle ); addComponent( handle ); listeners.fireDockableAdded( dockable ); fireDockablesRepositioned( dockable, before != dockables.getColumnCount() ); } finally { token.release(); } } @SuppressWarnings("static-method") private boolean drop( StationChildHandle parent, Dockable child, ToolbarGroupProperty property ){ if( property.getSuccessor() == null ) { return false; } final DockStation station = parent.getDockable().asDockStation(); if( station == null ) { return false; } return station.drop( child, property.getSuccessor() ); } @Override public void move( Dockable dockable, DockableProperty property ){ if( property instanceof ToolbarGroupProperty ) { move( dockable, (ToolbarGroupProperty) property ); } } private void move( Dockable dockable, ToolbarGroupProperty property ){ final int sourceColumn = column( dockable ); final int sourceLine = line( dockable ); boolean empty = false; int destinationColumn = property.getColumn(); int destinationLine = property.getLine(); final Path placeholder = property.getPlaceholder(); if( placeholder != null ) { final int column = dockables.getColumn( placeholder ); if( column != -1 ) { final int line = dockables.getLine( column, placeholder ); if( line != -1 ) { empty = true; destinationColumn = column; destinationLine = line; } } } if( !empty ) { // ensure destination valid destinationColumn = Math.min( destinationColumn, dockables.getColumnCount() ); if( (destinationColumn == dockables.getColumnCount()) || (destinationColumn == -1) ) { destinationLine = 0; } else { destinationLine = Math.min( destinationLine, dockables.getLineCount( destinationColumn ) ); } } Level level; if( empty ) { level = Level.BASE; } else { level = Level.DOCKABLE; } dockables.move( sourceColumn, sourceLine, destinationColumn, destinationLine, level ); mainPanel.getContentPane().revalidate(); } }