/* * 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) 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.station.stack.tab; import java.awt.Dimension; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import bibliothek.gui.DockController; import bibliothek.gui.Dockable; import bibliothek.gui.dock.station.stack.tab.layouting.TabPlacement; import bibliothek.gui.dock.util.PropertyValue; /** * An abstract implementation of {@link TabPane}. This class handles creation, * storage and destruction of {@link Tab}s and {@link TabMenu}s, it also stores * a list of currently visible {@link Dockable}s.<br> * Subclasses should call {@link #setController(DockController)} to make sure * this pane can use all available information. * @author Benjamin Sigg * @param <T> the kind of tabs this pane supports * @param <M> the kind of menus this pane supports * @param <I> the kind of info panel this pane supports */ public abstract class AbstractTabPane<T extends Tab, M extends TabMenu, I extends LonelyTabPaneComponent> implements TabPane{ /** the layout manager that is responsible of updating this pane */ private PropertyValue<TabLayoutManager> layoutManager = new PropertyValue<TabLayoutManager>( TabPane.LAYOUT_MANAGER ){ @Override protected void valueChanged( TabLayoutManager oldValue, TabLayoutManager newValue ){ if( oldValue != null ) oldValue.uninstall( AbstractTabPane.this ); if( newValue != null ) newValue.install( AbstractTabPane.this ); } }; /** the controller in whose realm this pane works */ private DockController controller; /** the children of this pane */ private List<Dockable> dockables = new ArrayList<Dockable>(); /** the current selection, can be <code>null</code> */ private Dockable selection; /** all the tabs, visible and invisible */ private Map<Dockable, T> tabs = new HashMap<Dockable, T>(); /** all the menus, visible and invisible */ private List<M> menus = new ArrayList<M>(); /** tells for each {@link Dockable} on which menu it is */ private Map<Dockable, M> menuPosition = new HashMap<Dockable, M>(); /** a list of listeners to be informed when the content or the selection changes */ private List<TabPaneListener> listeners = new ArrayList<TabPaneListener>(); /** additional information shown somewhere on this component */ private I info; /** where to place tabs */ private TabPlacement tabPlacement = TabPlacement.TOP_OF_DOCKABLE; /** * Connects this pane with <code>controller</code>. * @param controller the realm in which this pane works, may be <code>null</code> */ public void setController( DockController controller ){ this.controller = controller; layoutManager.setProperties( controller ); fireControllerChanged(); } public DockController getController(){ return controller; } /** * Tells this pane where to paint the tabs. * @param tabPlacement a side, not <code>null</code> */ public void setDockTabPlacement( TabPlacement tabPlacement ){ if( tabPlacement == null ) throw new IllegalArgumentException( "tab placement must not be null" ); this.tabPlacement = tabPlacement; revalidate(); } public TabPlacement getDockTabPlacement(){ return tabPlacement; } /** * Updates the layout of this pane, assuming a {@link TabLayoutManager} * is installed. */ public void doLayout(){ TabLayoutManager layout = layoutManager.getValue(); if( layout != null ){ layout.layout( this ); } } /** * Gets the minimal size required to have a big enough {@link #getAvailableArea()} to show * all content. * @return the minimal size */ public Dimension getMinimumSize(){ TabLayoutManager layout = layoutManager.getValue(); if( layout != null ){ return layout.getMinimumSize( this ); } return new Dimension( 1, 1 ); } /** * Gets the preferred size required to have a big enough {@link #getAvailableArea()} to show * all content. * @return the preferred size */ public Dimension getPreferredSize(){ TabLayoutManager layout = layoutManager.getValue(); if( layout != null ){ return layout.getPreferredSize( this ); } return new Dimension( 1, 1 ); } /** * Called when the layout of this pane has become invalid, the default * behavior is to call {@link #doLayout()}. Subclasses may override to * update the layout lazily. */ public void revalidate(){ doLayout(); } public void addTabPaneListener( TabPaneListener listener ){ listeners.add( listener ); } public void removeTabPaneListener( TabPaneListener listener ){ listeners.remove( listener ); } /** * Gets all the {@link TabPaneListener}s that are known to this {@link TabPane} * @return an array of listeners */ protected TabPaneListener[] listeners(){ return listeners.toArray( new TabPaneListener[ listeners.size() ] ); } /** * Informs all {@link TabPaneListener}s that the selection changed. */ protected void fireSelectionChanged(){ for( TabPaneListener listener : listeners() ){ listener.selectionChanged( this ); } } /** * Informs all {@link TabPaneListener}s that <code>dockable</code> * has been added. * @param dockable the new child */ protected void fireAdded( Dockable dockable ){ for( TabPaneListener listener : listeners() ){ listener.added( this, dockable ); } } /** * Informs all {@link TabPaneListener}s that <code>dockable</code> * has been removed. * @param dockable the removed child */ protected void fireRemoved( Dockable dockable ){ for( TabPaneListener listener : listeners() ){ listener.removed( this, dockable ); } } /** * Informs all {@link TabPaneListener}s that the info component has been * replaced. * @param oldInfo the old info component * @param newInfo the new info component */ protected void fireInfoComponentChanged( I oldInfo, I newInfo ){ for( TabPaneListener listener : listeners() ){ listener.infoComponentChanged( this, oldInfo, newInfo ); } } /** * Informs all {@link TabPaneListener} that the current {@link DockController} changed. */ protected void fireControllerChanged(){ for( TabPaneListener listener : listeners() ){ listener.controllerChanged( this, controller ); } } /** * Gets the layout manager that is currently used to layout the contents * of this pane. * @return the layout manager, may be <code>null</code> */ public TabLayoutManager getLayoutManager(){ return layoutManager.getValue(); } /** * Sets the layout manager that will layout the contents of this pane, a * value of <code>null</code> will reinstall the default layout manager. * @param layoutManager the new manager, may be <code>null</code> */ public void setLayoutManager( TabLayoutManager layoutManager ){ this.layoutManager.setValue( layoutManager ); } /** * Adds <code>dockable</code> as child to this tab-pane. * @param index the index of the new child * @param dockable the new child */ public void insert( int index, Dockable dockable ){ int size = getDockableCount(); dockables.add( index, dockable ); fireAdded( dockable ); if( size == 0 ){ setSelectedDockable( dockable ); } revalidate(); } /** * Moves the element at location <code>source</code> to <code>destination</code>. * @param source where to find the element to move * @param destination the target location */ public void move( int source, int destination ){ if( destination < 0 || destination >= getDockableCount() ){ throw new ArrayIndexOutOfBoundsException(); } Dockable dockable = dockables.remove( source ); cleanOut( dockable ); dockables.add( destination, dockable ); revalidate(); } /** * Removes the <code>index</code>'th element of this pane. * @param index the index of the element to remove */ public void remove( int index ){ Dockable dockable = dockables.remove( index ); boolean selected = false; if( selection == dockable ){ selected = true; setSelectedDockable( null ); } cleanOut( dockable ); fireRemoved( dockable ); // select other tab if( selected ){ if( index >= getDockableCount() ) index = getDockableCount()-1; if( index >= 0 ) setSelectedDockable( getDockable( index ) ); } revalidate(); } /** * Removes all elements from this pane. */ public void removeAll(){ for( T tab : tabs.values() ){ tab.setPaneVisible( false ); tabRemoved( tab ); } clearTabs(); for( Map.Entry<Dockable, M> item : menuPosition.entrySet() ){ removeFromMenu( item.getValue(), item.getKey() ); } menuPosition.clear(); for( Dockable dockable : dockables ){ fireRemoved( dockable ); } setSelectedDockable(null); dockables.clear(); revalidate(); } /** * Deletes all {@link Tab}s and {@link TabMenu}s of this {@link TabPane} * and rebuilds them. */ public void discardComponentsAndRebuild(){ for( T tab : tabs.values() ){ tab.setPaneVisible( false ); tabRemoved( tab ); } clearTabs(); for( Map.Entry<Dockable, M> item : menuPosition.entrySet() ){ removeFromMenu( item.getValue(), item.getKey() ); } menuPosition.clear(); doLayout(); } public Dockable getSelectedDockable(){ return selection; } /** * Selects the child <code>dockable</code> of this pane as the one visible * element. * @param dockable the newly selected element, can be <code>null</code> */ public void setSelectedDockable( Dockable dockable ){ if( this.selection != dockable ){ this.selection = dockable; revalidate(); fireSelectionChanged(); } } public Dockable[] getDockables(){ return dockables.toArray( new Dockable[ dockables.size() ] ); } /** * Gets the number of elements that are displayed on this pane. * @return the number of elements */ public int getDockableCount(){ return dockables.size(); } /** * Gets the <code>index</code>'th element of this pane. * @param index the index of an element * @return element at position <code>index</code> */ public Dockable getDockable( int index ){ return dockables.get( index ); } /** * Gets the index of <code>dockable</code> on this pane. * @param dockable the element to search * @return the index or -1 if <code>dockable</code> was not found */ public int indexOf( Dockable dockable ){ return dockables.indexOf( dockable ); } public Tab[] getTabs(){ List<Tab> list = new ArrayList<Tab>(); for( Tab tab : tabs.values() ){ if( tab.isPaneVisible() ){ list.add( tab ); } } return list.toArray( new Tab[ list.size() ] ); } /** * Returns the index of <code>tab</code> following the indices of * {@link #indexOf(Dockable) Dockables} but ignoring invisible tabs. * @param tab the tab to search * @return its index or -1 if not found or invisible */ public int indexOfVisible( Tab tab ){ int index = 0; for( int i = 0, n = getDockableCount(); i<n; i++ ){ Tab check = tabs.get( dockables.get( i ) ); if( check != null && check.isPaneVisible() ){ if( tab == check ) return index; index++; } } return -1; } /** * Gets the index'th visible tab. * @param index the index of some visible tab * @return the visible tab or <code>null</code> if <code>index</code> * is too big. * @see #indexOfVisible(Tab) * @throws IllegalArgumentException if <code>index</code> is smaller than <code>0</code>. */ public T getVisibleTab( int index ){ if( index < 0 ) throw new IllegalArgumentException( "index to small" ); for( int i = 0, n = getDockableCount(); i<n; i++ ){ T check = tabs.get( dockables.get( i ) ); if( check != null && check.isPaneVisible() ){ if( index == 0 ) return check; index--; } } return null; } /** * Gets the number of tabs that are currently visible. * @return the number of visible tabs * @see #getVisibleTab(int) */ public int getVisibleTabCount(){ int count = 0; for( T check : tabs.values() ){ if( check.isPaneVisible() ){ count++; } } return count; } /** * Gets all known tabs of this pane, including invisible tabs. The list is not ordered. * @return the tabs of this pane */ public List<T> getTabsList(){ return new ArrayList<T>( tabs.values() ); } /** * Gets the tab that is used to display <code>dockable</code>. * @param dockable the element to search * @return the tab or <code>null</code> */ public T getTab( Dockable dockable ){ return tabs.get( dockable ); } public T putOnTab( Dockable dockable ){ if( dockable == null ) throw new IllegalArgumentException( "dockable must not be null" ); if( !dockables.contains( dockable )) throw new IllegalArgumentException( "dockable not child of this pane" ); T tab = tabs.get( dockable ); if( tab == null ){ tab = newTab( dockable ); tab.setOrientation( getDockTabPlacement() ); putTab( dockable, tab ); } tab.setPaneVisible( true ); M menu = menuPosition.remove( dockable ); if( menu != null ){ removeFromMenu( menu, dockable ); } return tab; } public T getOnTab( Dockable dockable ){ if( dockable == null ) throw new IllegalArgumentException( "dockable must not be null" ); if( !dockables.contains( dockable )) throw new IllegalArgumentException( "dockable not child of this pane" ); T tab = tabs.get( dockable ); if( tab == null ){ tab = newTab( dockable ); tab.setOrientation( getDockTabPlacement() ); putTab( dockable, tab ); } return tab; } /** * Sets the info component. * @param info the new component, can be <code>null</code> * @see #getInfoComponent() */ public void setInfoComponent( I info ){ if( this.info != info ){ if( this.info != null ) this.info.setPaneVisible( false ); I oldInfo = this.info; this.info = info; if( this.info != null ) this.info.setPaneVisible( true ); fireInfoComponentChanged( oldInfo, info ); } } public I getInfoComponent(){ return info; } /** * Gets a list of all the menus of this pane, includes visible and * invisible menus * @return the list of menus */ public List<M> getMenuList(){ return new ArrayList<M>( menus ); } /** * Gets all the menus of this pane, visible and invisible * @return all the menus */ public TabMenu[] getMenus(){ return menus.toArray( new TabMenu[ menus.size() ] ); } /** * Gets the menu on which <code>dockable</code> is shown. * @param dockable some child of this pane * @return the menu or <code>null</code> */ public M getMenu( Dockable dockable ){ return menuPosition.get( dockable ); } @SuppressWarnings("unchecked") public void putInMenu( TabMenu menu, Dockable dockable ){ if( dockable == null ) throw new IllegalArgumentException( "dockables must not be null" ); if( !this.dockables.contains( dockable )) throw new IllegalArgumentException( "not child of this pane: " + dockable ); if( menu == null ) throw new IllegalArgumentException( "menu is null" ); if( !menus.contains( menu )) throw new IllegalArgumentException( "menu not created by this pane" ); // check current menu M currentMenu = menuPosition.get( dockable ); if( currentMenu == menu ) return; if( currentMenu != null ){ removeFromMenu( currentMenu, dockable ); } addToMenu( (M)menu, dockable ); menuPosition.put( dockable, (M)menu ); T tab = tabs.get( dockable ); if( tab != null ){ tab.setPaneVisible( false ); } } public TabMenu createMenu(){ M menu = newMenu(); menus.add( menu ); return menu; } @SuppressWarnings("unchecked") public void destroyMenu( TabMenu menu ){ if( menu == null ) throw new IllegalArgumentException( "menu is null" ); if( !menus.remove( menu )) throw new IllegalArgumentException( "menu not created by this pane" ); menu.setPaneVisible( false ); for( Dockable dockable : menu.getDockables() ){ menuPosition.remove( dockable ); } menuRemoved( (M)menu ); } /** * Adds <code>dockable</code> somewhere to <code>menu</code> * @param menu a menu of this pane * @param dockable a new child of <code>menu</code> */ protected abstract void addToMenu( M menu, Dockable dockable ); /** * Removes <code>dockable</code> from <code>menu</code>. * @param menu some menu of this pane * @param dockable a child of <code>menu</code> */ protected abstract void removeFromMenu( M menu, Dockable dockable ); /** * Removes <code>dockable</code> from all tabs and menus, also removes * tabs that are no longer needed. * @param dockable the element to remove */ private void cleanOut( Dockable dockable ){ // tab T tab = removeTab( dockable ); if( tab != null ){ tab.setPaneVisible( false ); tabRemoved( tab ); } // menus M menu = menuPosition.remove( dockable ); if( menu != null ){ removeFromMenu( menu, dockable ); } } /** * Associates <code>tab</code> with <code>dockable</code>. this method * modifies the internal data structure in order to store the change.<br> * Subclasses may override this method to be informed about the exact time when * a tab changes, but the overridden method must call the original method. * @param dockable the key for the tab-map * @param tab the value for the tab-map * @return the old tab at <code>dockable</code> */ protected T putTab( Dockable dockable, T tab ){ return tabs.put( dockable, tab ); } /** * Removes the tab of <code>dockable</code> from the internal data structure. * @param dockable the key of the element to be removed from the tab-map<br> * Subclasses may override this method to be informed about the exact time when * a tab changes, but the overridden method must call the original method. * @return the removed element */ protected T removeTab( Dockable dockable ){ return tabs.remove( dockable ); } /** * Removes all tabs from the internal data structure.<br> * Subclasses may override this method to be informed about the exact time when * a tab changes, but the overridden method must call the original method. */ protected void clearTabs(){ tabs.clear(); } /** * Creates a new {@link Tab} that has <code>this</code> as parent and * represents <code>dockable</code>. The new tab should not be stored in * any collection. * @param dockable the element for which a new tab is required * @return the new tab */ protected abstract T newTab( Dockable dockable ); /** * Creates a new {@link TabMenu} that has <code>this</code> as parent. * @return the new menu */ public abstract M newMenu(); /** * Informs this pane that <code>tab</code> will never be used again and * all resources associated with <code>tab</code> should be freed. This * method is only called if <code>tab</code> is invisible. * @param tab the tab to destroy */ protected abstract void tabRemoved( T tab ); /** * Informs this pane that <code>menu</code> will never be used again and * all resources associated with <code>menu</code> should be freed. This * method is only called if <code>menu</code> is invisible. * @param menu the destroyed menu */ protected abstract void menuRemoved( M menu ); }