/* * 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.dockable; import java.awt.Component; import java.awt.Point; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import javax.swing.Icon; import javax.swing.event.MouseInputListener; import bibliothek.gui.DockController; import bibliothek.gui.DockStation; import bibliothek.gui.DockTheme; import bibliothek.gui.Dockable; import bibliothek.gui.dock.DockElement; import bibliothek.gui.dock.action.DockAction; import bibliothek.gui.dock.action.DockActionSource; import bibliothek.gui.dock.action.HierarchyDockActionSource; import bibliothek.gui.dock.component.DockComponentConfiguration; import bibliothek.gui.dock.component.DockComponentRootHandler; import bibliothek.gui.dock.displayer.DisplayerRequest; import bibliothek.gui.dock.displayer.DockableDisplayerHints; import bibliothek.gui.dock.event.DockHierarchyListener; import bibliothek.gui.dock.event.DockableListener; import bibliothek.gui.dock.event.KeyboardListener; import bibliothek.gui.dock.title.DockTitle; import bibliothek.gui.dock.title.DockTitleRequest; import bibliothek.gui.dock.util.DockProperties; import bibliothek.gui.dock.util.PropertyKey; import bibliothek.gui.dock.util.PropertyValue; import bibliothek.gui.dock.util.icon.DockIcon; import bibliothek.util.Todo; import bibliothek.util.Todo.Compatibility; import bibliothek.util.Todo.Priority; import bibliothek.util.Todo.Version; /** * An implementation of {@link Dockable} which deals with the simple things.<br> * Some of the capabilities of an AbstractDockable are: * <ul> * <li>add or remove a {@link DockableListener}, and fire an event</li> * <li>set the parent and the {@link DockController controller}</li> * <li>set the title and the icon</li> * <li>store a list of {@link DockAction DockActions}</li> * </ul> * @author Benjamin Sigg */ public abstract class AbstractDockable implements Dockable { /** the parent */ private DockStation parent; /** the controller used to get information like the {@link DockTheme} */ private DockController controller; /** a list of dockableListeners which will be informed when some properties changes */ private List<DockableListener> dockableListeners = new ArrayList<DockableListener>(); /** a listener to the hierarchy of the parent */ private DockHierarchyObserver hierarchyObserver; /** a listener for monitoring the location of this dockable */ private DockableStateListenerManager dockableStateListeners; /** the list of {@link KeyListener}s of this dockable */ private List<KeyListener> keyListeners = new ArrayList<KeyListener>(); /** the listener dispatching events to {@link #keyListeners} */ private KeyboardListener keyboardListener; /** the title of this dockable */ private PropertyValue<String> titleText; /** the icon of this dockable */ private DockIcon titleIcon; /** the current value of {@link #titleIcon} */ private Icon currentTitleIcon; /** how to react if a <code>null</code> {@link Icon} is set as title icon */ private IconHandling titleIconHandling = IconHandling.KEEP_NULL_ICON; /** the tooltip of this dockable */ private PropertyValue<String> titleToolTip; /** the DockTitles which are bound to this dockable */ private List<DockTitle> titles = new LinkedList<DockTitle>(); private DockableDisplayerHints hints; /** Informs the client about all the {@link Component}s that are present on this {@link Dockable} */ private DockComponentRootHandler rootHandler; /** * A modifiable list of {@link DockAction} which can be triggered and * will affect this dockable. */ private DockActionSource source; /** an unmodifiable list of actions used for this dockable */ private HierarchyDockActionSource globalSource; /** * Creates a new dockable. * @param titleText the key of the title, used to read in {@link DockProperties} * @param titleTooltip the key of the tooltip, used to read in {@link DockProperties} */ protected AbstractDockable( PropertyKey<String> titleText, PropertyKey<String> titleTooltip ){ this.titleText = new PropertyValue<String>( titleText ){ @Override protected void valueChanged( String oldValue, String newValue ){ if( oldValue == null ) oldValue = ""; if( newValue == null ) newValue = ""; fireTitleTextChanged( oldValue, newValue ); } }; this.titleToolTip = new PropertyValue<String>( titleTooltip ){ @Override protected void valueChanged( String oldValue, String newValue ) { fireTitleTooltipChanged( oldValue, newValue ); } }; dockableStateListeners = new DockableStateListenerManager( this ); hierarchyObserver = new DockHierarchyObserver( this ); globalSource = new HierarchyDockActionSource( this ); globalSource.bind(); } /** * Gets the {@link DockComponentRootHandler} which is responsible for keeping track of all the {@link Component}s of this * dockable. * @return the root handler, not <code>null</code> */ protected DockComponentRootHandler getRootHandler(){ if( rootHandler == null ){ rootHandler = createRootHandler(); rootHandler.addRoot( getComponent() ); } return rootHandler; } /** * Creates the {@link DockComponentRootHandler} which configures the {@link Component}s of this dockable. * @return the new handler, not <code>null</code> */ protected abstract DockComponentRootHandler createRootHandler(); /** * Creates the {@link DockIcon} which represents this {@link Dockable} or this {@link DockStation}. The * icon must call {@link #fireTitleIconChanged(Icon, Icon)} if the icon changes. * @return the default icon for this element */ protected abstract DockIcon createTitleIcon(); private DockIcon titleIcon(){ if( titleIcon == null ){ titleIcon = createTitleIcon(); titleIcon.setController( getController() ); } return titleIcon; } public void setDockParent( DockStation station ) { if( this.parent != station ){ parent = station; hierarchyObserver.update(); } } public DockStation getDockParent() { return parent; } public Dockable asDockable() { return this; } public void setController( DockController controller ) { getRootHandler().setController( null ); if( this.controller != null ){ if( keyboardListener != null ){ this.controller.getKeyboardController().removeListener( keyboardListener ); keyboardListener = null; } } this.controller = controller; titleIcon().setController( controller ); titleText.setProperties( controller ); hierarchyObserver.controllerChanged( controller ); if( !keyListeners.isEmpty() ){ registerKeyboardListener(); } getRootHandler().setController( controller ); } public DockController getController() { return controller; } public void setComponentConfiguration( DockComponentConfiguration configuration ) { getRootHandler().setConfiguration( configuration ); } public DockComponentConfiguration getComponentConfiguration() { return getRootHandler().getConfiguration(); } public boolean isDockableShowing(){ return isDockableVisible(); } @Deprecated @Todo( compatibility=Compatibility.BREAK_MAJOR, priority=Priority.ENHANCEMENT, target=Version.VERSION_1_1_3, description="remove this method" ) public boolean isDockableVisible(){ DockController controller = getController(); if( controller == null ){ return false; } DockStation parent = getDockParent(); if( parent != null ){ return parent.isVisible( this ); } return false; } public void addDockableListener( DockableListener listener ) { dockableListeners.add( listener ); } public void removeDockableListener( DockableListener listener ) { dockableListeners.remove( listener ); } public void addDockHierarchyListener( DockHierarchyListener listener ){ hierarchyObserver.addDockHierarchyListener( listener ); } public void removeDockHierarchyListener( DockHierarchyListener listener ){ hierarchyObserver.removeDockHierarchyListener( listener ); } public void addDockableStateListener( DockableStateListener listener ){ dockableStateListeners.addListener( listener ); } public void removeDockableStateListener( DockableStateListener listener ){ dockableStateListeners.removeListener( listener ); } /** * Gets the manager which is responsible for handling {@link DockableStateListener}s. * @return the manager, not <code>null</code> */ protected DockableStateListenerManager getDockableStateListeners(){ return dockableStateListeners; } /** * Access to the {@link DockableStateListenerManager} which can be used to fire {@link DockableStateEvent}s. This method * is intended to be used by subclasses that implement {@link DockStation}. * @return the listeners */ protected DockableStateListenerManager getDockElementObserver(){ return dockableStateListeners; } public void addMouseInputListener( MouseInputListener listener ) { getComponent().addMouseListener( listener ); getComponent().addMouseMotionListener( listener ); } public void removeMouseInputListener( MouseInputListener listener ) { getComponent().removeMouseListener( listener ); getComponent().removeMouseMotionListener( listener ); } /** * Adds a {@link KeyListener} to this {@link Dockable}. The listener * will be informed about any un-consumed {@link KeyEvent} that is * related to this {@link Dockable}, e.g. an event that is dispatched * on a {@link DockTitle}. * @param listener the new listener */ public void addKeyListener( KeyListener listener ){ keyListeners.add( listener ); registerKeyboardListener(); } /** * Removes <code>listener</code> from this element. * @param listener the listener to remove */ public void removeKeyListener( KeyListener listener ){ keyListeners.remove( listener ); if( keyboardListener != null && controller != null ){ controller.getKeyboardController().removeListener( keyboardListener ); keyboardListener = null; } } private KeyListener[] getKeyListeners(){ return keyListeners.toArray( new KeyListener[ keyListeners.size() ] ); } private void registerKeyboardListener(){ if( keyboardListener == null && controller != null ){ keyboardListener = new KeyboardListener() { public DockElement getTreeLocation(){ return AbstractDockable.this; } public boolean keyTyped( DockElement element, KeyEvent event ){ if( element == AbstractDockable.this ){ for( KeyListener listener : getKeyListeners() ){ listener.keyTyped( event ); } return event.isConsumed(); } else{ return false; } } public boolean keyReleased( DockElement element, KeyEvent event ){ if( element == AbstractDockable.this ){ for( KeyListener listener : getKeyListeners() ){ listener.keyReleased( event ); } return event.isConsumed(); } else{ return false; } } public boolean keyPressed( DockElement element, KeyEvent event ){ if( element == AbstractDockable.this ){ for( KeyListener listener : getKeyListeners() ){ listener.keyPressed( event ); } return event.isConsumed(); } else{ return false; } } }; controller.getKeyboardController().addListener( keyboardListener ); } } public DockElement getElement() { return this; } public boolean isUsedAsTitle() { return false; } public boolean shouldFocus(){ return true; } public boolean shouldTransfersFocus(){ return false; } public boolean accept( DockStation station ) { return true; } public boolean accept( DockStation base, Dockable neighbour ) { return true; } public String getTitleText() { String text = titleText.getValue(); if( text == null ) return ""; else return text; } /** * Sets the title of this dockable. All dockableListeners are informed about * the change. * @param titleText the title, <code>null</code> is replaced by the * empty string */ public void setTitleText( String titleText ) { this.titleText.setValue( titleText ); } public Icon getTitleIcon() { return currentTitleIcon; } /** * Sets the tooltip that will be shown on any title of this dockable. * @param titleToolTip the new tooltip, can be <code>null</code> */ public void setTitleToolTip( String titleToolTip ){ this.titleToolTip.setValue( titleToolTip ); } public String getTitleToolTip() { return titleToolTip.getValue(); } public Point getPopupLocation( Point click, boolean popupTrigger ) { if( popupTrigger ) return click; else return null; } /** * Sets the behavior of how the title icon is handled, whether it is replaced by the default * icon if <code>null</code> or simply not shown.<br> * Calling this method does not have any effect, rather the behavior of {@link #setTitleIcon(Icon)} * is changed. * @param titleIconHandling the new behavior, not <code>null</code> */ public void setTitleIconHandling( IconHandling titleIconHandling ){ if( titleIconHandling == null ){ throw new IllegalArgumentException( "titleIconHandling must not be null" ); } this.titleIconHandling = titleIconHandling; } /** * Tells how a <code>null</code> title icon is handled. * @return the behavior * @see #setTitleIconHandling(IconHandling) */ public IconHandling getTitleIconHandling(){ return titleIconHandling; } /** * Sets the icon of this dockable. All dockableListeners are informed about * the change.<br> * If <code>titleIcon</code> is <code>null</code>, then the exact behavior of this method * depends on the result of {@link #getTitleIconHandling()}. The method may either replace * the <code>null</code> {@link Icon} by the default icon, or simply not show any icon. * @param titleIcon the new icon, may be <code>null</code> */ public void setTitleIcon( Icon titleIcon ) { switch( getTitleIconHandling() ){ case KEEP_NULL_ICON: titleIcon().setValue( titleIcon, true ); break; case REPLACE_NULL_ICON: titleIcon().setValue( titleIcon, false ); break; default: throw new IllegalStateException( "unknown behavior: " + titleIconHandling ); } } /** * Resets the icon of this {@link Dockable}, the default icon is shown again. */ public void resetTitleIcon(){ titleIcon().setValue( null ); } /** * The default behavior of this method is to do nothing. */ public void requestDockTitle( DockTitleRequest request ){ // ignore } /** * The default behavior of this method is to do nothing. */ public void requestDisplayer( DisplayerRequest request ){ // ignore } public void bind( DockTitle title ) { if( titles.contains( title )) throw new IllegalArgumentException( "Title is already bound" ); titles.add( title ); fireTitleBound( title ); } public void unbind( DockTitle title ) { if( !titles.contains( title )) throw new IllegalArgumentException( "Title is unknown" ); titles.remove( title ); fireTitleUnbound( title ); } public DockTitle[] listBoundTitles(){ return titles.toArray( new DockTitle[ titles.size() ] ); } public DockActionSource getLocalActionOffers() { return source; } public DockActionSource getGlobalActionOffers(){ return globalSource; } /** * Sets the action-source of this {@link Dockable}. Other elements which * used {@link #getGlobalActionOffers()} will be informed about this change. * @param source The new source, may be <code>null</code> */ public void setActionOffers( DockActionSource source ){ this.source = source; globalSource.update(); } /** * Calls the {@link DockableListener#titleTextChanged(Dockable, String, String) titleTextChanged} * method of all registered {@link DockableListener}s. * @param oldTitle the old title * @param newTitle the new title */ protected void fireTitleTextChanged( String oldTitle, String newTitle ){ for( DockableListener listener : dockableListeners.toArray( new DockableListener[ dockableListeners.size()] )) listener.titleTextChanged( this, oldTitle, newTitle ); } /** * Called the {@link DockableListener#titleToolTipChanged(Dockable, String, String) titleTooltipChanged} * method of all registered {@link DockableListener}s. * @param oldTooltip the old value * @param newTooltip the new value */ protected void fireTitleTooltipChanged( String oldTooltip, String newTooltip ){ for( DockableListener listener : dockableListeners.toArray( new DockableListener[ dockableListeners.size()] )) listener.titleToolTipChanged( this, oldTooltip, newTooltip ); } /** * Calls the {@link DockableListener#titleIconChanged(Dockable, Icon, Icon) titleIconChanged} * method of all registered {@link DockableListener}. * @param oldIcon the old icon * @param newIcon the new icon */ protected void fireTitleIconChanged( Icon oldIcon, Icon newIcon ){ currentTitleIcon = newIcon; for( DockableListener listener : dockableListeners.toArray( new DockableListener[ dockableListeners.size()] )) listener.titleIconChanged( this, oldIcon, newIcon ); } /** * Informs all dockableListeners that <code>title</code> was bound to this dockable. * @param title the title which was bound */ protected void fireTitleBound( DockTitle title ){ for( DockableListener listener : dockableListeners.toArray( new DockableListener[ dockableListeners.size()] )) listener.titleBound( this, title ); } /** * Informs all dockableListeners that <code>title</code> was unbound from this dockable. * @param title the title which was unbound */ protected void fireTitleUnbound( DockTitle title ){ for( DockableListener listener : dockableListeners.toArray( new DockableListener[ dockableListeners.size()] )) listener.titleUnbound( this, title ); } /** * Informs all {@link DockableListener}s that <code>title</code> is no longer * considered to be a good title and should be exchanged. * @param title a title, can be <code>null</code> */ protected void fireTitleExchanged( DockTitle title ){ for( DockableListener listener : dockableListeners.toArray( new DockableListener[ dockableListeners.size()] )) listener.titleExchanged( this, title ); } /** * Informs all {@link DockableListener}s that all bound titles and the * <code>null</code> title are no longer considered good titles and * should be replaced */ protected void fireTitleExchanged(){ DockTitle[] bound = listBoundTitles(); for( DockTitle title : bound ) fireTitleExchanged( title ); fireTitleExchanged( null ); } public void configureDisplayerHints( DockableDisplayerHints hints ) { this.hints = hints; } /** * Gets the last {@link DockableDisplayerHints} that were given to * {@link #configureDisplayerHints(DockableDisplayerHints)}. * @return the current configurable hints, can be <code>null</code> */ protected DockableDisplayerHints getConfigurableDisplayerHints() { return hints; } }