/* * 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; import java.awt.Component; import java.awt.Dialog; import java.awt.EventQueue; import java.awt.Frame; import java.awt.Window; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.ResourceBundle; import java.util.Set; import javax.swing.Icon; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import bibliothek.gui.dock.DockElement; import bibliothek.gui.dock.DockElementRepresentative; import bibliothek.gui.dock.DockHierarchyLock; import bibliothek.gui.dock.SplitDockStation; import bibliothek.gui.dock.accept.DockAcceptance; import bibliothek.gui.dock.accept.MultiDockAcceptance; import bibliothek.gui.dock.action.ActionGuard; import bibliothek.gui.dock.action.ActionOffer; import bibliothek.gui.dock.action.ActionPopupSuppressor; import bibliothek.gui.dock.action.DockAction; import bibliothek.gui.dock.action.DockActionSource; import bibliothek.gui.dock.action.popup.ActionPopupMenuFactory; import bibliothek.gui.dock.action.view.ActionViewConverter; import bibliothek.gui.dock.component.DockComponentManager; import bibliothek.gui.dock.component.DockComponentRoot; import bibliothek.gui.dock.control.ComponentHierarchyObserver; import bibliothek.gui.dock.control.ControllerSetupCollection; import bibliothek.gui.dock.control.DefaultDockControllerFactory; import bibliothek.gui.dock.control.DockControllerFactory; import bibliothek.gui.dock.control.DockRegister; import bibliothek.gui.dock.control.DockRelocator; import bibliothek.gui.dock.control.DockRelocatorMode; import bibliothek.gui.dock.control.DockableSelector; import bibliothek.gui.dock.control.DoubleClickController; import bibliothek.gui.dock.control.GlobalMouseDispatcher; import bibliothek.gui.dock.control.KeyboardController; import bibliothek.gui.dock.control.PopupController; import bibliothek.gui.dock.control.SingleParentRemover; import bibliothek.gui.dock.control.focus.DefaultFocusRequest; import bibliothek.gui.dock.control.focus.FocusController; import bibliothek.gui.dock.control.focus.FocusHistory; import bibliothek.gui.dock.control.focus.FocusRequest; import bibliothek.gui.dock.control.focus.MouseFocusObserver; import bibliothek.gui.dock.event.ControllerSetupListener; import bibliothek.gui.dock.event.DockControllerRepresentativeListener; import bibliothek.gui.dock.event.DockRegisterAdapter; import bibliothek.gui.dock.event.DockRegisterListener; import bibliothek.gui.dock.event.DockStationAdapter; import bibliothek.gui.dock.event.DockStationListener; import bibliothek.gui.dock.event.DockTitleBindingListener; import bibliothek.gui.dock.event.DockableAdapter; import bibliothek.gui.dock.event.DockableFocusEvent; import bibliothek.gui.dock.event.DockableFocusListener; import bibliothek.gui.dock.event.DockableListener; import bibliothek.gui.dock.event.DockableSelectionEvent; import bibliothek.gui.dock.event.DockableSelectionListener; import bibliothek.gui.dock.themes.ThemeManager; import bibliothek.gui.dock.title.ActivityDockTitleEvent; import bibliothek.gui.dock.title.DockTitle; import bibliothek.gui.dock.title.DockTitleManager; import bibliothek.gui.dock.util.CoreWarningDialog; import bibliothek.gui.dock.util.DirectWindowProvider; import bibliothek.gui.dock.util.DockProperties; import bibliothek.gui.dock.util.DockUtilities; import bibliothek.gui.dock.util.IconManager; import bibliothek.gui.dock.util.Priority; import bibliothek.gui.dock.util.PropertyKey; import bibliothek.gui.dock.util.TextManager; import bibliothek.gui.dock.util.UIScheme; import bibliothek.gui.dock.util.WindowProvider; import bibliothek.gui.dock.util.WindowProviderListener; import bibliothek.gui.dock.util.WindowProviderWrapper; import bibliothek.gui.dock.util.color.ColorManager; import bibliothek.gui.dock.util.extension.ExtensionManager; import bibliothek.gui.dock.util.font.FontManager; import bibliothek.gui.dock.util.icon.DefaultIconScheme; import bibliothek.gui.dock.util.icon.DockIcon; import bibliothek.gui.dock.util.icon.DockIconBridge; import bibliothek.gui.dock.util.property.ConstantPropertyFactory; import bibliothek.gui.dock.util.text.DefaultTextScheme; import bibliothek.gui.dock.util.text.TextBridge; import bibliothek.gui.dock.util.text.TextValue; import bibliothek.util.Todo; import bibliothek.util.Todo.Compatibility; import bibliothek.util.Todo.Version; import bibliothek.util.Workarounds; /** * A controller connects all the {@link DockStation}s, {@link Dockable}s and * other objects that play together in this framework. This class also serves * as low-level access point for clients. When using this framework in general, * or {@link DockController} in particular, several rules have * to be obeyed: * <ul> * <li>{@link DockStation}s and {@link Dockable}s build trees. The roots * of these trees need to be registered using {@link #add(DockStation)}.</li> * <li>Each <code>DockController</code> builds its own realm, normally only * objects within such a realm can interact with each other. Drag and drop * operations cannot move a {@link Dockable} from one realm to another.</li> * <li>Most of the interesting actions are only available for {@link Dockable}s * that are within a realm (like drag and drop).</li> * <li>Normally clients do not work with the trees of stations and <code>Dockable</code>s. * If they need to work directly in the tree they should call {@link #freezeLayout()} * and later {@link #meltLayout()} to temporarily disable automatic actions (like * the fact that a <code>DockStation</code> with only one child gets removed).</li> * <li>If a <code>DockController</code> is no longer needed then the method * {@link #kill()} should be called. This method will ensure that the * object can be reclaimed by the garbage collector. </li> * </ul> * * @author Benjamin Sigg */ public class DockController { /** property telling whether this application runs in a restricted environment or not, the default value is the result of {@link DockUI#isSecureEnvironment()} */ public static final PropertyKey<Boolean> RESTRICTED_ENVIRONMENT = new PropertyKey<Boolean>( "dock.restricted_environment", new ConstantPropertyFactory<Boolean>( DockUI.getDefaultDockUI().isSecureEnvironment() ), true ); /** the known dockables and DockStations */ private DockRegister register; /** the known {@link Component}s in the realm of this controller */ private ComponentHierarchyObserver componentHierarchyObserver; /** a manager handling drag and drop */ private DockRelocator relocator; /** the controller that manages global double clicks */ private DoubleClickController doubleClickController; /** the controller that manages global key-events */ private KeyboardController keyboardController; /** selector allows to select {@link Dockable} using the mouse or the keyboard */ private DockableSelector dockableSelector; /** Listeners observing the selected {@link Dockable}s */ private List<DockableSelectionListener> dockableSelectionListeners = new ArrayList<DockableSelectionListener>(); /** Listeners observing the bound-state of {@link DockTitle}s */ private List<DockTitleBindingListener> dockTitleBindingListeners = new ArrayList<DockTitleBindingListener>(); /** a special controller listening to AWT-events and changing the focused dockable */ private MouseFocusObserver focusObserver; /** central collection of {@link MouseEvent}s */ private GlobalMouseDispatcher mouseDispatcher; /** class managing focus transfer between {@link Dockable}s */ private FocusController focusController; /** class telling the order in which {@link Dockable}s had the focus */ private FocusHistory focusHistory; /** an observer of the bound {@link DockTitle}s */ private DockTitleObserver dockTitleObserver = new DockTitleObserver(); /** mapping tells which titles are currently active */ private Map<DockTitle, Dockable> activeTitles = new HashMap<DockTitle, Dockable>(); /** a source for {@link DockTitle} */ private DockTitleManager dockTitles; /** keeps track of all the {@link DockComponentRoot}s in the realm of this controller */ private DockComponentManager dockComponentManager; /** the set of icons used with this controller */ private IconManager icons; /** the set of strings used by this controller */ private TextManager texts; /** map of colors that are used through the realm of this controller */ private ColorManager colors; /** map of fonts that are used through the realm of this controller */ private FontManager fonts; /** extensions to this controller */ private ExtensionManager extensions; /** A list of sources for a {@link DockActionSource} */ private List<ActionOffer> actionOffers = new ArrayList<ActionOffer>(); /** A list of sources for {@link DockActionSource DockActionSources} */ private List<ActionGuard> guards = new ArrayList<ActionGuard>(); /** The default source for a {@link DockActionSource} */ private ActionOffer defaultActionOffer; /** A converter used to transform {@link DockAction actions} into views */ private ActionViewConverter actionViewConverter; /** behavior which dockable can be dropped over which station */ private MultiDockAcceptance acceptance = new MultiDockAcceptance(); /** controls the popup menus */ private PopupController popupController; /** remover of stations with none or one child */ private SingleParentRemover remover; /** a theme describing the look of the stations */ private ThemeManager theme; /** a set of properties */ private DockProperties properties; /** the factory that creates new parts of this controller */ private DockControllerFactory factory; /** tells which {@link Component} represents which {@link DockElement} */ private Map<Component, DockElementRepresentative> componentToDockElements = new HashMap<Component, DockElementRepresentative>(); /** a list of listeners listening for changes in {@link #componentToDockElements} */ private List<DockControllerRepresentativeListener> componentToDockElementsListeners = new ArrayList<DockControllerRepresentativeListener>(); /** the root window of the application */ private WindowProviderWrapper rootWindowProvider; /** the current root window, can be <code>null</code> */ private Window rootWindow; /** ensurance against concurrent modifications */ private DockHierarchyLock lock = new DockHierarchyLock(); /** whether {@link #showCoreWarning()} actually opens a dialog */ private static boolean showCoreWarning = true; /** * Creates a new controller. */ public DockController(){ this( new DefaultDockControllerFactory() ); } /** * Creates a new controller but does not initiate the properties of this * controller if not wished. Clients should call the method * {@link #initiate(DockControllerFactory,ControllerSetupCollection)} * if they pass <code>null</code> to this constructor. * Otherwise the behavior of this controller is unspecified. * @param factory the factory creating elements of this controller or * <code>null</code> if {@link #initiate(DockControllerFactory,ControllerSetupCollection)} will be * called later */ public DockController( DockControllerFactory factory ){ if( factory != null ){ initiate( factory, null ); } showCoreWarning(); } /** * Disables the dialog warning from using the Core API, that pops up when creating a {@link DockController}. * @deprecated it really is not a good idea using the Core API, then there is the Common API */ @Deprecated public static void disableCoreWarning(){ showCoreWarning = false; } /** * Opens an annoying dialog warning the developer that he is using the Core API, when he should be using * the Common API. This warning can be disabled by calling {@link #disableCoreWarning()}. */ protected void showCoreWarning(){ if( showCoreWarning ){ EventQueue.invokeLater( new Runnable(){ public void run(){ CoreWarningDialog.showDialog(); } }); } } /** * Initializes all properties of this controller. This method should be * called only once. This method can be called by a subclass if the * subclass used {@link #DockController(DockControllerFactory)} with an argument <code>null</code>. * @param factory a factory used to create various sub-controls * @param setup the collection of {@link ControllerSetupListener}s that will be invoked * when setup is finished. If this parameter is set, then all {@link ControllerSetupListener}s * will be added to <code>setup</code>. If this parameter is <code>null</code>, then * a new collection will be created, and the event will be fired as soon as * this method is finished. */ protected final void initiate( DockControllerFactory factory, ControllerSetupCollection setup ){ if( this.factory != null ) throw new IllegalStateException( "DockController already initialized" ); if( factory == null ) throw new IllegalArgumentException( "Factory must not be null" ); extensions = factory.createExtensionManager( this, setup ); properties = new DockProperties( this ); theme = new ThemeManager( this ); icons = new IconManager( this ); icons.setScheme( Priority.DEFAULT, createDefaultIconScheme() ); colors = new ColorManager( this ); fonts = new FontManager( this ); dockTitles = new DockTitleManager( this ); texts = new TextManager( this ); texts.setScheme( Priority.DEFAULT, createDefaultTextScheme() ); theme.init(); rootWindowProvider = new WindowProviderWrapper(); rootWindowProvider.addWindowProviderListener( new WindowProviderListener(){ public void windowChanged( WindowProvider provider, Window window ) { Window oldWindow = rootWindow; rootWindow = window; rootWindowChanged( oldWindow, window ); } public void visibilityChanged( WindowProvider provider, boolean showing ){ // ignore } }); final List<ControllerSetupListener> setupListeners = new LinkedList<ControllerSetupListener>(); if( setup == null ){ setup = new ControllerSetupCollection(){ public void add( ControllerSetupListener listener ) { if( listener == null ) throw new NullPointerException( "listener must not be null" ); setupListeners.add( listener ); } }; } this.factory = factory; register = factory.createRegister( this, setup ); DockRegisterListener focus = factory.createVisibilityFocusObserver( this, setup ); if( focus != null ) register.addDockRegisterListener( focus ); popupController = factory.createPopupController( this, setup ); DockRegisterListener binder = factory.createActionBinder( this, setup ); if( binder != null ) register.addDockRegisterListener( binder ); register.addDockRegisterListener( dockTitleObserver ); addDockTitleBindingListener( dockTitleObserver ); register.addDockRegisterListener( new DockableSelectionObserver() ); relocator = factory.createRelocator( this, setup ); defaultActionOffer = factory.createDefaultActionOffer( this, setup ); focusObserver = factory.createMouseFocusObserver( this, setup ); focusController = factory.createFocusController( this, setup ); focusHistory = factory.createFocusHistory( this, setup ); actionViewConverter = factory.createActionViewConverter( this, setup ); doubleClickController = factory.createDoubleClickController( this, setup ); keyboardController = factory.createKeyboardController( this, setup ); dockableSelector = factory.createDockableSelector( this, setup ); mouseDispatcher = factory.createGlobalMouseDispatcher( this, setup ); dockComponentManager = factory.createDockComponentManager( this, setup ); extensions.init(); setTheme( DockUI.getDefaultDockUI().getDefaultTheme().create( this ) ); relocator.addMode( DockRelocatorMode.SCREEN_ONLY ); relocator.addMode( DockRelocatorMode.NO_COMBINATION ); // set properties here, allows the keys not to have a default value and // allows to have the properties present properties.set( SplitDockStation.MAXIMIZE_ACCELERATOR, KeyStroke.getKeyStroke( KeyEvent.VK_M, InputEvent.CTRL_DOWN_MASK ) ); properties.set( DockFrontend.HIDE_ACCELERATOR, KeyStroke.getKeyStroke( KeyEvent.VK_F4, InputEvent.CTRL_DOWN_MASK ) ); properties.set( DockableSelector.INIT_SELECTION, KeyStroke.getKeyStroke( KeyEvent.VK_E, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK ) ); setSingleParentRemover( factory.createSingleParentRemover( this, setup ) ); focusController.addDockableFocusListener( new FocusControllerObserver() ); for( ControllerSetupListener listener : setupListeners ) listener.done( this ); Workarounds.getDefault().setup( this ); } /** * Creates the default {@link UIScheme} for the {@link IconManager}. * @return the default {@link UIScheme}, should not be <code>null</code> */ protected UIScheme<Icon, DockIcon, DockIconBridge> createDefaultIconScheme(){ DefaultIconScheme scheme = new DefaultIconScheme( "data/bibliothek/gui/dock/core/icons.ini", this ); scheme.link( PropertyKey.DOCKABLE_ICON, "dockable.default" ); scheme.link( PropertyKey.DOCK_STATION_ICON, "dockStation.default" ); return scheme; } /** * Creates the default {@link UIScheme} for the {@link TextManager}. * @return the default {@link UIScheme}, should not be <code>null</code> */ protected UIScheme<String, TextValue, TextBridge> createDefaultTextScheme(){ ResourceBundle bundle = ResourceBundle.getBundle( "data.bibliothek.gui.dock.core.locale.text", Locale.getDefault(), DockController.class.getClassLoader() ); List<ResourceBundle> list = texts.loadExtensionBundles( Locale.getDefault() ); ResourceBundle[] bundles = list.toArray( new ResourceBundle[ list.size()+1] ); bundles[ bundles.length-1 ] = bundle; return new DefaultTextScheme( bundles ); } /** * Removes listeners and frees resources. This method should be called * if this controller is no longer needed. This method should be called * only once. */ public void kill(){ setRootWindowProvider( null ); focusObserver.kill(); register.kill(); keyboardController.kill(); theme.kill(); extensions.kill(); mouseDispatcher.kill(); } /** * Tells this controller whether this application runs in a restricted environment or not. Calling this * method is equivalent of setting the property {@link #RESTRICTED_ENVIRONMENT}.<br> * Please note that setting this property to <code>false</code> in a restricted environment will lead * to {@link SecurityException}s and ultimately to unspecified behavior. * @param restricted whether restricted algorithms have to be used */ public void setRestrictedEnvironment( boolean restricted ){ getProperties().set( RESTRICTED_ENVIRONMENT, restricted ); } /** * Tells whether this controller uses restricted algorithms for a restricted environment. * @return whether restricted algorithms have to be used */ public boolean isRestrictedEnvironment(){ return getProperties().get( RESTRICTED_ENVIRONMENT ); } /** * Gets the current focus manager that tracks the mouse. * @return the controller */ public MouseFocusObserver getMouseFocusObserver() { return focusObserver; } /** * Gets the manager which is responsible for transferring focus between {@link Dockable}s. * @return the manager, not <code>null</code> */ public FocusController getFocusController(){ return focusController; } /** * Gets the history of the focused {@link Dockable}s. * @return the history, not <code>null</code> */ public FocusHistory getFocusHistory(){ return focusHistory; } /** * Grants access to the {@link GlobalMouseDispatcher} which is responsible for collecting and * distributing global {@link MouseEvent}s. Clients may use the dispatcher to listen for * {@link MouseEvent}s. * @return the dispatcher, not <code>null</code> */ public GlobalMouseDispatcher getGlobalMouseDispatcher(){ return mouseDispatcher; } /** * Gets the set of {@link Dockable Dockables} and {@link DockStation DockStations} * known to this controller. * @return the set of elements */ public DockRegister getRegister(){ return register; } /** * Gets a list of all {@link Component}s which are used on the {@link Dockable}s * known to this controller. * @return the list of <code>Component</code>s. */ public ComponentHierarchyObserver getComponentHierarchyObserver() { if( componentHierarchyObserver == null ){ componentHierarchyObserver = new ComponentHierarchyObserver( this ); if( rootWindow != null ) componentHierarchyObserver.add( rootWindow ); } return componentHierarchyObserver; } /** * Gets the manager for handling drag and drop operations. * @return the manager */ public DockRelocator getRelocator(){ return relocator; } /** * Gets the manager for handling global double clicks of the mouse. * @return the manager */ public DoubleClickController getDoubleClickController() { return doubleClickController; } /** * Gets the manager that handles all global KeyEvents. * @return the handler */ public KeyboardController getKeyboardController(){ return keyboardController; } /** * Gets the manager that is responsible to convert {@link DockAction}s to * some kind of {@link Component}. * @return the converter */ public ActionViewConverter getActionViewConverter(){ return actionViewConverter; } /** * Gets the handler used to remove stations with only one or none * children. * @return the handler or <code>null</code>. * @see #setSingleParentRemover(SingleParentRemover) */ public SingleParentRemover getSingleParentRemover() { return remover; } /** * Exchanges the handler that removes stations with only one or none children. * @param remover the new handler, can be <code>null</code> to disable the * feature. */ public void setSingleParentRemover( SingleParentRemover remover ){ if( this.remover != null ){ this.remover.uninstall( this ); } this.remover = remover; if( this.remover != null ){ this.remover.install( this ); this.remover.testAll( this ); } } /** * Gets a lock that prevents concurrent modification of the child-parent relationship * of {@link Dockable}s and {@link DockStation}s. This lock should only be acquired by * {@link DockStation}s. * @return the lock */ public DockHierarchyLock getHierarchyLock(){ return lock; } /** * Freezes the layout. Normally if a client makes a change in the layout * (e.g. remove a {@link Dockable} from its parent) additional actions * can be triggered (e.g. remove the parent because it has no children * left). If the layout is frozen then these implicit actions are not * triggered. This method can be called more than once and the layout * will remain frozen until {@link #meltLayout()} is called as often * as {@link #freezeLayout()}.<br> * A side note: internally this method disables the {@link DockRegisterListener}s. * Events during the time where the listeners are disabled are collected, * conflicting events will cancel each other out, remaining events will be * distributed once {@link #meltLayout()} is called. The effect of this method is * equal to the effect when calling {@link DockRegister#setStalled(boolean)}. * @return <code>true</code> if the layout was already frozen, * <code>false</code> if it was not frozen * @see #meltLayout() */ public boolean freezeLayout(){ DockRegister register = getRegister(); boolean frozen = register.isStalled(); getRegister().setStalled( true ); return frozen; } /** * Tells whether the layout is frozen, see {@link #freezeLayout()}. * @return <code>true</code> if the layout is frozen */ public boolean isLayoutFrozen(){ return getRegister().isStalled(); } /** * Melts a frozen layout (see {@link #freezeLayout()}). * @return <code>true</code> if the layout remains frozen, <code>false</code> * if the layout has melted. * @throws IllegalStateException if the layout is not {@link #isLayoutFrozen() frozen} * @see #meltLayout() */ public boolean meltLayout(){ if( !isLayoutFrozen() ) throw new IllegalStateException( "the layout is not frozen" ); DockRegister register = getRegister(); register.setStalled( false ); return register.isStalled(); } /** * Gets the behavior that tells which stations can have which children. * @return the behavior * @see #addAcceptance(DockAcceptance) * @see #removeAcceptance(DockAcceptance) */ public MultiDockAcceptance getAcceptance() { return acceptance; } /** * Adds a rule that decides which station can have which children. * The <code>acceptance</code> does not override the * <code>accept</code>-methods of {@link Dockable#accept(DockStation) Dockable} * and {@link DockStation#accept(Dockable) DockStation}. * @param acceptance the additional rule */ public void addAcceptance( DockAcceptance acceptance ) { this.acceptance.add( acceptance ); } /** * Removes a rule that decided which station could have which children. * @param acceptance the rule to remove */ public void removeAcceptance( DockAcceptance acceptance ){ this.acceptance.remove( acceptance ); } /** * Gets the guard which decides, which popups should be allowed. * @return the guard * @see #setPopupSuppressor(ActionPopupSuppressor) */ public ActionPopupSuppressor getPopupSuppressor() { return popupController.getPopupSuppressor(); } /** * Sets the guard which decides, which popups with {@link DockAction DockActions} * are allowed to show up, and which popups will be suppressed. * @param popupSuppressor the guard */ public void setPopupSuppressor( ActionPopupSuppressor popupSuppressor ) { popupController.setPopupSuppressor( popupSuppressor ); } /** * Gets the factory which creates new popup menus. * @return the factory for creating popup menus, never <code>null</code> */ public ActionPopupMenuFactory getPopupMenuFactory(){ return popupController.getPopupMenuFactory(); } /** * Sets the factory which creates new popup menus. * @param factory the factory, not <code>null</code> */ public void setPopupMenuFactory( ActionPopupMenuFactory factory ){ popupController.setPopupMenuFactory( factory ); } /** * Gets the {@link PopupController} which is responsible for managing the * popup menus. * @return the controller, never <code>null</code> */ public PopupController getPopupController(){ return popupController; } /** * Gets the factory for a {@link DockActionSource} which is used * if no other offer was {@link ActionOffer#interested(Dockable) interested} * in a {@link Dockable}. * @return the default offer */ public ActionOffer getDefaultActionOffer() { return defaultActionOffer; } /** * Sets the factory for a {@link DockActionSource} which is used * if no other offer was {@link ActionOffer#interested(Dockable) interested} * in a {@link Dockable}. * @param defaultActionOffer the offer, not <code>null</code> */ public void setDefaultActionOffer( ActionOffer defaultActionOffer ) { if( defaultActionOffer == null ) throw new IllegalArgumentException(); this.defaultActionOffer = defaultActionOffer; } /** * Adds a factory for a {@link DockActionSource}. The factory will * create a source if it is the first offer which is * {@link ActionOffer#interested(Dockable) interested} in a {@link Dockable}. * @param offer the algorithm */ public void addActionOffer( ActionOffer offer ){ if( offer == null ) throw new IllegalArgumentException(); actionOffers.add( offer ); } /** * Removes an earlier added offer. * @param offer the factory to remove */ public void removeActionOffer( ActionOffer offer ){ actionOffers.remove( offer ); } /** * Searches the {@link ActionOffer} for <code>dockable</code>. * @param dockable the element whose offer is searched * @return the offer */ public ActionOffer getActionOffer( Dockable dockable ){ for( ActionOffer offer : actionOffers ){ if( offer.interested( dockable )) return offer; } return getDefaultActionOffer(); } /** * Sets the theme of this controller. This method ensures that all * registered stations know also the new theme. * @param theme the new theme */ public void setTheme( DockTheme theme ){ this.theme.setTheme( theme ); } /** * Gets the current theme of this controller. * @return the theme */ public DockTheme getTheme() { return theme.getTheme(); } /** * Gets the manager that is responsible for handling the current {@link DockTheme} and * distributing its properties. * @return the manager */ public ThemeManager getThemeManager(){ return theme; } /** * A set of properties that can be used at any place. * @return the set of properties */ public DockProperties getProperties(){ return properties; } /** * Gets a manager which keeps track of all the {@link DockComponentRoot}s, and hence of all the {@link Component}s * that are known to this {@link DockController}. * @return the manager, not <code>null</code> */ public DockComponentManager getDockComponentManager() { return dockComponentManager; } /** * Adds a listener to this controller, <code>listener</code> will be informed * when the map of {@link DockElement}s and the {@link Component}s which * represent them changes. * @param listener the new listener, not <code>null</code> */ public void addRepresentativeListener( DockControllerRepresentativeListener listener ){ if( listener == null ) throw new IllegalArgumentException( "listener must not be null" ); componentToDockElementsListeners.add( listener ); } /** * Removes <code>listener</code> from this controller. * @param listener the listener to remove */ public void removeRepresentativeListener( DockControllerRepresentativeListener listener ){ componentToDockElementsListeners.remove( listener ); } /** * Informs this controller about a new representative for a {@link DockElement}. * Note that each {@link DockElementRepresentative} of this {@link DockController} * must have another {@link DockElementRepresentative#getComponent() component}. * @param representative the new representative * @see #searchElement(Component) */ public void addRepresentative( DockElementRepresentative representative ) { DockControllerRepresentativeListener[] listeners = componentToDockElementsListeners .toArray( new DockControllerRepresentativeListener[componentToDockElementsListeners.size()] ); DockElementRepresentative old = componentToDockElements.put( representative.getComponent(), representative ); if( old != null ){ for( DockControllerRepresentativeListener listener : listeners ){ listener.representativeRemoved( this, old ); } } for( DockControllerRepresentativeListener listener : listeners ){ listener.representativeAdded( this, representative ); } } /** * Removes <code>representative</code> from this controller. * @param representative the element to remove * @see #addRepresentative(DockElementRepresentative) */ public void removeRepresentative( DockElementRepresentative representative ){ if( componentToDockElements.remove( representative.getComponent() ) != null ){ DockControllerRepresentativeListener[] listeners = componentToDockElementsListeners .toArray( new DockControllerRepresentativeListener[componentToDockElementsListeners.size()] ); for( DockControllerRepresentativeListener listener : listeners ){ listener.representativeRemoved( this, representative ); } } } /** * Searches the element which is parent or equal to <code>representative</code>. * This method searches through all elements given by {@link #addRepresentative(DockElementRepresentative)}. * This also includes all {@link Dockable}s and all {@link DockTitle}s. * @param representative some component * @return the parent or <code>null</code> * @see #addRepresentative(DockElementRepresentative) */ public DockElementRepresentative searchElement( Component representative ){ while( representative != null ){ DockElementRepresentative element = componentToDockElements.get( representative ); if( element != null ){ if( element.getElement().getController() == this ) return element; } representative = representative.getParent(); } return null; } /** * Searches all registered {@link DockElementRepresentative} whose element is <code>element</code>. * @param element the element whose {@link DockElementRepresentative} are searched * @return the representatives, may include <code>element</code> as well */ public DockElementRepresentative[] getRepresentatives( DockElement element ){ List<DockElementRepresentative> result = new ArrayList<DockElementRepresentative>(); for( DockElementRepresentative representative : componentToDockElements.values() ){ if( representative.getElement() == element ){ result.add( representative ); } } return result.toArray( new DockElementRepresentative[ result.size() ] ); } /** * Adds a station to this controller. The controller allows the user to * drag and drop children from and to <code>station</code>. If * the children of <code>station</code> are stations itself, then * they will be added automatically. The station will be treated as root-station, meaning * that <code>station</code> remains registered until it is explicitly removed from the * {@link DockRegister}. On the other hand child stations may be removed automatically at any time.<br> * Even if <code>station</code> is already known to this controller or a child of a root-station, then * <code>station</code> is promoted to root-station. * @param station the new station */ public void add( DockStation station ){ register.add( station ); register.setProtected( station, true ); } /** * Removes a station which was managed by this controller. * @param station the station to remove */ public void remove( DockStation station ){ register.remove( station ); } /** * Gets the number of stations registered at this controller. * @return the number of stations * @see #add(DockStation) */ public int getStationCount(){ return register.getStationCount(); } /** * Gets the station at the specified position. * @param index the location * @return the station */ public DockStation getStation( int index ){ return register.getStation( index ); } /** * Tells whether one of the methods which change the focus is currently * running, or not. If the result is <code>true</code>, no-one should * change the focus. * @return <code>true</code> if the focus is currently changing */ public boolean isOnFocusing() { return focusController.isOnFocusing(); } /** * Sets the focused {@link Dockable}. Nothing happens if <code>focusedDockable</code> * is a station and one of its children already has the focus. * @param focusedDockable the element which should have the focus * @param component the {@link Component} which should receive the focus, can be <code>null</code>. * See {@link FocusController#setFocusedDockable(DockElementRepresentative, Component, boolean, boolean, boolean)}. * @see #isOnFocusing() */ public void setAtLeastFocusedDockable( Dockable focusedDockable, Component component ) { Dockable current = getFocusedDockable(); if( current == null ){ setFocusedDockable( new DefaultFocusRequest( focusedDockable, component, false ) ); } else if( !DockUtilities.isAncestor( focusedDockable, current )){ setFocusedDockable( new DefaultFocusRequest( focusedDockable, component, false ) ); } } /** * Sets the {@link Dockable} which should have the focus. This is identical of calling * {@link #setFocusedDockable(FocusRequest)} with a new {@link DefaultFocusRequest}. * @param focusedDockable the element with the focus or <code>null</code> * @param force <code>true</code> if this controller must ensure * that all properties are correct, <code>false</code> if some * optimizations are allowed. Clients normally can set this argument * to <code>false</code>. */ public void setFocusedDockable( Dockable focusedDockable, boolean force ) { setFocusedDockable( new DefaultFocusRequest( focusedDockable, force ) ); } /** * Sets the {@link Dockable} which should have the focus. * @param focusedDockable the element with the focus or <code>null</code> * @param component the {@link Component} which should receive the focus, can be <code>null</code>. * See {@link FocusController#setFocusedDockable(DockElementRepresentative, Component, boolean, boolean, boolean)}. * @param force <code>true</code> if this controller must ensure * that all properties are correct, <code>false</code> if some * optimizations are allowed. Clients normally can set this argument * to <code>false</code>. * @deprecated clients should use {@link #setFocusedDockable(FocusRequest)} instead */ @Deprecated @Todo( compatibility=Compatibility.BREAK_MAJOR, description="remove this method", priority=Todo.Priority.ENHANCEMENT, target=Version.VERSION_1_1_3) public void setFocusedDockable( Dockable focusedDockable, Component component, boolean force ) { setFocusedDockable( new DefaultFocusRequest( focusedDockable, component, force ) ); } /** * Sets the {@link Dockable} which should have the focus. * @param focusedDockable the element with the focus or <code>null</code> * @param component the {@link Component} which should receive the focus, can be <code>null</code>. * See {@link FocusController#setFocusedDockable(DockElementRepresentative, Component, boolean, boolean, boolean)}. * @param force <code>true</code> if this controller must ensure * that all properties are correct, <code>false</code> if some * optimizations are allowed. Clients normally can set this argument * to <code>false</code>. * @param ensureFocusSet if <code>true</code>, then this method should make sure that either <code>focusedDockable</code> * itself or one of its {@link DockElementRepresentative} is the focus owner * @param ensureDockableFocused if <code>true</code>, then this method should make sure that <code>focusedDockable</code> * is the focus owner. This parameter is stronger that <code>ensureFocusSet</code> * @deprecated clients should use {@link #setFocusedDockable(FocusRequest)} instead */ @Deprecated @Todo( compatibility=Compatibility.BREAK_MAJOR, description="remove this method", priority=Todo.Priority.ENHANCEMENT, target=Version.VERSION_1_1_3) public void setFocusedDockable( Dockable focusedDockable, Component component, boolean force, boolean ensureFocusSet, boolean ensureDockableFocused ) { setFocusedDockable( new DefaultFocusRequest( focusedDockable, component, force, ensureFocusSet, ensureDockableFocused ) ); } /** * Starts a request to set the focused {@link Dockable}. * @param request the request to execute, not <code>null</code> */ public void setFocusedDockable( FocusRequest request ){ focusController.focus( request ); } /** * Tells whether <code>dockable</code> or one of its children * has currently the focus. * @param dockable the element which may have the focus * @return <code>true</code> if <code>dockable</code> or * one of its children is focused */ public boolean isFocused( Dockable dockable ){ Dockable temp = getFocusedDockable(); while( temp != null ){ if( temp == dockable ) return true; DockStation station = temp.getDockParent(); temp = station == null ? null : station.asDockable(); } return false; } /** * Tells whether <code>title</code> is bound to its dockable or not. The * behavior is unspecified if the dockable of <code>title</code> is * unknown to this controller. * @param title the title which might be bound * @return <code>true</code> if the title is bound * @see Dockable#bind(DockTitle) */ public boolean isBound( DockTitle title ){ return dockTitleObserver.isBound( title ); } /** * Ensures that a title or a {@link Component} of the currently * {@link #getFocusedDockable() focused Dockable} really * has the focus. */ public void ensureFocusSet(){ focusController.ensureFocusSet( false ); } /** * Gets the {@link Dockable} which is currently focused. * @return the focused element or <code>null</code> */ public Dockable getFocusedDockable() { return focusController.getFocusedDockable(); } /** * Gets the selector which can show a popup window such that the user * can use the keyboard or the mouse to focus a {@link Dockable}. * @return the selector */ public DockableSelector getDockableSelector() { return dockableSelector; } /** * Gets the manager of all titles on this controller * @return the manager */ public DockTitleManager getDockTitleManager() { return dockTitles; } /** * Gets the set of icons which are used by this controller. * @return the set of icons */ public IconManager getIcons() { return icons; } /** * Gets the set of strings which are used by this controller. * @return the set of texts */ public TextManager getTexts(){ return texts; } /** * Gets the map of colors which are used by this controller. * @return the map of colors */ public ColorManager getColors() { return colors; } /** * Gets the map of fonts which are used by this controller. * @return the map of fonts */ public FontManager getFonts() { return fonts; } /** * Gets all extensions that are used by this controller. * @return all available extensions */ public ExtensionManager getExtensions(){ return extensions; } /** * Sets the window that is used when dialogs have to be shown. * @param window the root window, can be <code>null</code> * @see #findRootWindow() * @see #setRootWindowProvider(WindowProvider) */ public void setRootWindow( Window window ){ if( window == null ) setRootWindowProvider( null ); else setRootWindowProvider( new DirectWindowProvider( window ) ); } /** * Sets the provider which will be used to find a root window * for this controller. The root window is used as owner for dialogs. * @param window the new provider, can be <code>null</code> */ public void setRootWindowProvider( WindowProvider window ){ rootWindowProvider.setDelegate( window ); } /** * Gets the provider which will be used to find a root window for this * controller. Note that this is not the same provider as given to * {@link #setRootWindowProvider(WindowProvider)}, but one that will * always return the same result as the provider set by the client. This * method always returns the same object. * @return the root window provider, never <code>null</code> */ public WindowProviderWrapper getRootWindowProvider() { return rootWindowProvider; } /** * Called whenever the root window of this controller changed. * @param oldWindow the old root window * @param newWindow the new root window */ protected void rootWindowChanged( Window oldWindow, Window newWindow ){ if( componentHierarchyObserver != null ){ if( oldWindow != null ) componentHierarchyObserver.remove( oldWindow ); if( newWindow != null ) componentHierarchyObserver.add( newWindow ); } } /** * Searches the root-window of the application. Assuming the window is not yet known: * uses all {@link DockElement}s known to this controller to search * the root window. This method first tries to find a {@link Frame}, * then a {@link Dialog} and finally returns every {@link Window} * that it finds. * @return the root window or <code>null</code> * @see #setRootWindow(Window) */ public Window findRootWindow(){ if( rootWindow != null ) return rootWindow; Window window = null; Dialog dialog = null; for( DockStation station : getRegister().listRoots() ){ Dockable dockable = station.asDockable(); if( dockable != null ){ Component component = dockable.getComponent(); Window ancestor = SwingUtilities.getWindowAncestor( component ); if( ancestor != null ){ window = ancestor; if( ancestor instanceof Frame ){ return ancestor; } else if( ancestor instanceof Dialog ){ dialog = (Dialog)ancestor; } } } } for( Dockable dockable : getRegister().listDockables() ){ Component component = dockable.getComponent(); Window ancestor = SwingUtilities.getWindowAncestor( component ); if( ancestor != null ){ window = ancestor; if( ancestor instanceof Frame ){ return ancestor; } else if( ancestor instanceof Dialog ){ dialog = (Dialog)ancestor; } } } if( dialog != null ) return dialog; return window; } /** * Adds <code>guard</code> to this controller. The new * {@link ActionGuard} has no influence on * {@link DockActionSource DockActionSources} which are already * created. * @param guard the new guard */ public void addActionGuard( ActionGuard guard ){ if( guard == null ) throw new IllegalArgumentException( "guard must not be null" ); guards.add( guard ); } /** * Removes <code>guard</code> from this controller. * @param guard the element to remove */ public void removeActionGuard( ActionGuard guard ){ guards.remove( guard ); } /** * Creates a list of {@link DockAction DockActions} which can * affect {@link Dockable}.<br> * Clients might rather use {@link Dockable#getGlobalActionOffers()} to * get a list of actions for a specific Dockable. This method only uses * the local information to compute a new source. * @param dockable a Dockable whose actions are demanded * @return a list of actions */ public DockActionSource listOffers( Dockable dockable ){ List<DockActionSource> guards = new ArrayList<DockActionSource>(); List<DockActionSource> parents = new ArrayList<DockActionSource>(); DockStation station = dockable.getDockParent(); while( station != null ){ parents.add( station.getIndirectActionOffers( dockable ) ); Dockable transform = station.asDockable(); if( transform != null ) station = transform.getDockParent(); else station = null; } for( ActionGuard guard : this.guards ){ if( guard.react( dockable )) guards.add( guard.getSource( dockable ) ); } ActionOffer offer = getActionOffer( dockable ); DockActionSource parentSource = null; if( dockable.getDockParent() != null ) parentSource = dockable.getDockParent().getDirectActionOffers( dockable ); return offer.getSource( dockable, dockable.getLocalActionOffers(), guards.toArray( new DockActionSource[guards.size()] ), parentSource, parents.toArray( new DockActionSource[ parents.size() ] )); } /** * Adds a listener to this controller, the listener will receive events when * a {@link DockTitle} is bound or unbound. * @param listener the new listener */ public void addDockTitleBindingListener( DockTitleBindingListener listener ){ if( listener == null ) throw new NullPointerException( "listener must not be null" ); dockTitleBindingListeners.add( listener ); } /** * Removes the observer <code>listener</code> from this controller. * @param listener the listener to remove */ public void removeDockTitleBindingListener( DockTitleBindingListener listener ){ if( listener == null ) throw new NullPointerException( "listener must not be null" ); dockTitleBindingListeners.remove( listener ); } /** * Gets an array of all {@link DockTitleBindingListener} that are currently * registered at this controller. * @return the modifiable array */ protected DockTitleBindingListener[] dockTitleBindingListeners(){ return dockTitleBindingListeners.toArray( new DockTitleBindingListener[ dockTitleBindingListeners.size() ] ); } /** * Adds a listener to this controller, the listener will be informed when * the focused {@link Dockable} changes. * @param listener the new listener */ public void addDockableFocusListener( DockableFocusListener listener ){ focusController.addDockableFocusListener( listener ); } /**d * Removes a listener from this controller. * @param listener the listener to remove */ public void removeDockableFocusListener( DockableFocusListener listener ){ focusController.removeDockableFocusListener( listener ); } /** * Adds a listener to this controller, the listener will be informed when * a selected {@link Dockable} changes. A selected {@link Dockable} shown * in a special way by its parent {@link DockStation}. * @param listener the new listener */ public void addDockableSelectionListener( DockableSelectionListener listener ){ if( listener == null ) throw new NullPointerException( "listener must not be null" ); dockableSelectionListeners.add( listener ); } /** * Removes a listener from this controller. * @param listener the listener to remove */ public void removeDockableSelectionListener( DockableSelectionListener listener ){ dockableSelectionListeners.remove( listener ); } /** * Gets an array of currently registered {@link DockableSelectionListener}s. * @return the modifiable array */ protected DockableSelectionListener[] dockableSelectionListeners(){ return dockableSelectionListeners.toArray( new DockableSelectionListener[ dockableSelectionListeners.size() ] ); } /** * Informs all listeners that <code>title</code> has been bound * to <code>dockable</code>. * @param title the bound title * @param dockable the owner of <code>title</code> */ protected void fireTitleBound( DockTitle title, Dockable dockable ){ for( DockTitleBindingListener listener : dockTitleBindingListeners() ) listener.titleBound( this, title, dockable ); } /** * Informs all listeners that <code>title</code> is no longer bound * to <code>dockable</code>. * @param title the unbound title * @param dockable the former owner of <code>title</code> */ protected void fireTitleUnbound( DockTitle title, Dockable dockable ){ for( DockTitleBindingListener listener : dockTitleBindingListeners() ) listener.titleUnbound( this, title, dockable ); } /** * Informs all listeners that <code>dockable</code> has been selected * by <code>station</code>. * @param station some {@link DockStation} * @param oldSelected the element which was selected earlier * @param newSelected the selected element of <code>station</code> */ protected void fireDockableSelected( DockStation station, Dockable oldSelected, Dockable newSelected){ DockableSelectionEvent event = new DockableSelectionEvent( this, station, oldSelected, newSelected ); for( DockableSelectionListener listener : dockableSelectionListeners() ) listener.dockableSelected( event ); } /** * An observer of the register and all {@link DockStation}s, informs when * a {@link DockStation} changes its selected {@link Dockable}. * @author Benjamin Sigg */ private class DockableSelectionObserver extends DockRegisterAdapter{ /** listener added to all {@link DockStation}s */ private DockStationListener listener = new DockStationAdapter(){ @Override public void dockableSelected( DockStation station, Dockable oldSelected, Dockable newSelected ) { fireDockableSelected( station, oldSelected, newSelected ); } }; @Override public void dockStationRegistered( DockController controller, DockStation station ) { station.addDockStationListener( listener ); } @Override public void dockStationUnregistered( DockController controller, DockStation station ) { station.removeDockStationListener( listener ); } } /** * Added to the current {@link FocusController} to track the active titles. */ private class FocusControllerObserver implements DockableFocusListener{ public void dockableFocused( DockableFocusEvent event ){ for( Map.Entry<DockTitle, Dockable> title : activeTitles.entrySet() ){ DockStation parent = title.getValue().getDockParent(); if( parent != null ) parent.changed( title.getValue(), title.getKey(), false ); else title.getKey().changed( new ActivityDockTitleEvent( title.getValue(), false )); } activeTitles.clear(); Dockable dockable = event.getNewFocusOwner(); while( dockable != null ){ DockStation station = dockable.getDockParent(); if( station != null ){ DockTitle[] titles = dockable.listBoundTitles(); for( DockTitle title : titles ){ station.changed( dockable, title, true ); activeTitles.put( title, dockable ); } station.setFrontDockable( dockable ); dockable = station.asDockable(); } else dockable = null; } } } /** * Observers the {@link DockRegister}, adds listeners to new {@link Dockable}s * and {@link DockTitle}s, and collects the components of these elements */ private class DockTitleObserver extends DockRegisterAdapter implements DockTitleBindingListener{ /** a set of all known titles */ private Set<DockTitle> titles = new HashSet<DockTitle>(); /** a listener added to each {@link Dockable} */ private DockableListener dockableListener = new DockableAdapter(){ @Override public void titleBound( Dockable dockable, DockTitle title ) { titles.add( title ); handleAddedTitle( dockable, title ); } @Override public void titleUnbound( Dockable dockable, DockTitle title ) { titles.remove( title ); handleRemovedTitle( dockable, title ); } }; /** * Tells whether title is bound to its {@link Dockable} or not. * @param title the title whose state is searched * @return the state */ public boolean isBound( DockTitle title ){ return titles.contains( title ); } public void titleBound( DockController controller, DockTitle title, Dockable dockable ) { addRepresentative( title ); } public void titleUnbound( DockController controller, DockTitle title, Dockable dockable ) { removeRepresentative( title ); activeTitles.remove( title ); DockStation parent = dockable.getDockParent(); if( parent != null ) parent.changed( dockable, title, false ); else title.changed( new ActivityDockTitleEvent( dockable, false )); } private void handleAddedTitle( Dockable dockable, DockTitle title ){ title.bind(); fireTitleBound( title, dockable ); DockStation station = dockable.getDockParent(); boolean focused = false; Dockable temp = getFocusedDockable(); while( !focused && temp != null ){ focused = temp == dockable; DockStation parent = temp.getDockParent(); temp = parent == null ? null : parent.asDockable(); } if( station == null ) title.changed( new ActivityDockTitleEvent( dockable, focused )); else station.changed( dockable, title, focused ); if( focused ) activeTitles.put( title, dockable ); } private void handleRemovedTitle( Dockable dockable, DockTitle title ){ title.unbind(); fireTitleUnbound( title, dockable ); } @Override public void dockableRegistering( DockController controller, Dockable dockable ){ dockable.addDockableListener( dockableListener ); } @Override public void dockableRegistered( DockController controller, Dockable dockable ) { addRepresentative( dockable ); DockTitle[] titles = dockable.listBoundTitles(); for( DockTitle title : titles ){ if( this.titles.add( title )){ handleAddedTitle( dockable, title ); } } } @Override public void dockableUnregistered( DockController controller, Dockable dockable ) { dockable.removeDockableListener( dockableListener ); removeRepresentative( dockable ); DockTitle[] titles = dockable.listBoundTitles(); for( DockTitle title : titles ){ if( this.titles.remove( title ) ){ handleRemovedTitle( dockable, title ); } } } } }