/*
 * Copyright (c) 2004 Christopher M Butler
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.flexdock.docking.defaults;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.LayoutManager2;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.WeakHashMap;

import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;

import org.flexdock.docking.Dockable;
import org.flexdock.docking.DockingConstants;
import org.flexdock.docking.DockingManager;
import org.flexdock.docking.DockingPort;
import org.flexdock.docking.DockingStrategy;
import org.flexdock.docking.RegionChecker;
import org.flexdock.docking.activation.ActiveDockableTracker;
import org.flexdock.docking.event.DockingEvent;
import org.flexdock.docking.event.DockingListener;
import org.flexdock.docking.event.DockingMonitor;
import org.flexdock.docking.event.TabbedDragListener;
import org.flexdock.docking.event.hierarchy.DockingPortTracker;
import org.flexdock.docking.props.DockingPortPropertySet;
import org.flexdock.docking.props.PropertyChangeListenerFactory;
import org.flexdock.docking.props.PropertyManager;
import org.flexdock.docking.state.LayoutNode;
import org.flexdock.docking.state.tree.DockableNode;
import org.flexdock.docking.state.tree.DockingPortNode;
import org.flexdock.docking.state.tree.SplitNode;
import org.flexdock.util.DockingUtility;
import org.flexdock.util.LookAndFeelSettings;
import org.flexdock.util.SwingUtility;
import org.flexdock.util.UUID;
import org.flexdock.util.Utilities;

/**
 * This is a {@code Container} that implements the {@code DockingPort}
 * interface. It provides a default implementation of {@code DockingPort} to
 * allow ease of development within docking-enabled applications.
 * <p>
 * The {@code DefaultDockingPort} handles docking in one of three ways. If the
 * port is empty, then all incoming {@code Dockables} are docked to the CENTER
 * region. If the port is not empty, then all incoming {@code Dockables} docked
 * to the CENTER region are embedded within a {@code JTabbedPane}. All incoming
 * {@code Dockables} docked to an outer region (NORTH, SOUTH, EAST, and WEST) of
 * a non-empty port are placed into a split layout using a {@code JSplitPane}.
 * <p>
 * For centrally docked {@code Components}, the immediate child of the
 * {@code DefaultDockingPort} may or may not be a {@code JTabbedPane}. If
 * {@code isSingleTabAllowed()} returns {@code true} for the current
 * {@code DefaultDockingPort}, then the immediate child returned by
 * {@code getDockedComponent()} will return a {@code JTabbedPane} instance even
 * if there is only one {@code Dockable} embedded within the port. If there is a
 * single {@code Dockable} in the port, but {@code isSingleTabAllowed()} returns
 * {@code false}, then {@code getDockedComponent()} will return the
 * {@code Component} that backs the currently docked {@code Dockable}, returned
 * by the {@code Dockable's} {@code getComponent()} method.
 * {@code isSingleTabAllowed()} is a scoped property that may apply to this
 * port, all ports across the JVM, or all ports within a user defined scope.
 * {@code getDockedComponent()} will return a {@code JTabbedPane} at all times
 * if there is more than one centrally docked {@code Dockable} within the port,
 * and all docked {@code Components} will reside within the tabbed pane.
 * <p>
 * Components that are docked in the NORTH, SOUTH, EAST, or WEST regions are
 * placed in a {@code JSplitPane} splitting the layout of the
 * {@code DockingPort} between child components. Each region of the
 * {@code JSplitPane} contains a new {@code DefaultDockingPort}, which, in
 * turn, contains the docked components. In this situation,
 * {@code getDockedComponent()} will return a {@code JSplitPane} reference.
 * <p>
 * A key concept that drives the {@code DefaultDockingPort}, then, is the
 * notion that this {@code DockingPort} implementation may only ever have one
 * single child component, which may or may not be a wrapper for other child
 * components. Because {@code JSplitPane} contains child
 * {@code DefaultDockingPorts}, each of those {@code DefaultDockingPorts} is
 * available for further sub-docking operations.
 * <p>
 * Since a {@code DefaultDockingPort} may only contain one child component,
 * there is a container hierarchy to manage tabbed interfaces, split layouts,
 * and sub-docking. As components are removed from this hierarchy, the hierarchy
 * itself must be reevaluated. Removing a component from a child
 * {@code DefaultDockingPort} within a {@code JSplitPane} renders the child
 * {@code DefaultDockingPort} unnecessary, which, in turn, renders the notion of
 * splitting the layout with a {@code JSplitPane} unnecessary (since there are
 * no longer two components to split the layout between). Likewise, removing a
 * child component from a {@code JTabbedPane} such that there is only one child
 * left within the {@code JTabbedPane} removes the need for a tabbed interface
 * to begin with.
 * <p>
 * When the {@code DockingManager} removes a component from a
 * {@code DockingPort} via {@code DockingManager.undock(Dockable dockable)} it
 * uses a call to {@code undock()} on the current {@code DockingPort}.
 * {@code undock()} automatically handles the reevaluation of the container
 * hierarchy to keep wrapper-container usage at a minimum. Since
 * {@code DockingManager} makes this callback automatic, developers normally
 * will not need to call this method explicitly. However, when removing a
 * component from a {@code DefaultDockingPort} using application code,
 * developers should keep in mind to use {@code undock()} instead of
 * {@code remove()}.
 * 
 * Border management after docking and undocking operations are accomplished
 * using a {@code BorderManager}. {@code setBorderManager()} may be used to set
 * the border manager instance and customize border management.
 * 
 * @author Christopher Butler
 * 
 */
public class DefaultDockingPort extends JPanel implements DockingPort,
        DockingConstants {
    protected class PortLayout implements LayoutManager2, Serializable {
        /**
         * Returns the amount of space the layout would like to have.
         * 
         * @param parent
         *            the Container for which this layout manager is being used
         * @return a Dimension object containing the layout's preferred size
         */
        public Dimension preferredLayoutSize(Container parent) {
            Dimension dd;
            Insets i = getInsets();

            if (dockedComponent != null) {
                dd = dockedComponent.getPreferredSize();
            } else {
                dd = parent.getSize();
            }

            return new Dimension(dd.width + i.left + i.right, dd.height + i.top
                    + i.bottom);
        }

        /**
         * Returns the minimum amount of space the layout needs.
         * 
         * @param parent
         *            the Container for which this layout manager is being used
         * @return a Dimension object containing the layout's minimum size
         */
        public Dimension minimumLayoutSize(Container parent) {
            Dimension dd;
            Insets i = getInsets();

            if (dockedComponent != null) {
                dd = dockedComponent.getMinimumSize();
            } else {
                dd = parent.getSize();
            }

            return new Dimension(dd.width + i.left + i.right, dd.height + i.top
                    + i.bottom);
        }

        /**
         * Returns the maximum amount of space the layout can use.
         * 
         * @param target
         *            the Container for which this layout manager is being used
         * @return a Dimension object containing the layout's maximum size
         */
        public Dimension maximumLayoutSize(Container target) {
            Dimension dd;
            Insets i = getInsets();

            if (dockedComponent != null) {
                dd = dockedComponent.getMaximumSize();
            } else {
                // This is silly, but should stop an overflow error
                dd = new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE - i.top
                        - i.bottom);
            }

            return new Dimension(dd.width + i.left + i.right, dd.height + i.top
                    + i.bottom);
        }

        /**
         * Instructs the layout manager to perform the layout for the specified
         * container.
         * 
         * @param parent
         *            the Container for which this layout manager is being used
         */
        public void layoutContainer(Container parent) {
            Rectangle b = getBounds();
            Insets i = getInsets();
            int w = b.width - i.right - i.left;
            int h = b.height - i.top - i.bottom;

            if (dockedComponent != null) {
                dockedComponent.setBounds(i.left, i.top, w, h);
            }
        }

        public void addLayoutComponent(String name, Component comp) {
        }

        public void removeLayoutComponent(Component comp) {
        }

        public void addLayoutComponent(Component comp, Object constraints) {
        }

        public float getLayoutAlignmentX(Container target) {
            return 0.0f;
        }

        public float getLayoutAlignmentY(Container target) {
            return 0.0f;
        }

        public void invalidateLayout(Container target) {
        }
    }

    private static final WeakHashMap COMPONENT_TITLES = new WeakHashMap();

    protected ArrayList dockingListeners;

    private Component dockedComponent;

    private BorderManager borderManager;

    private String persistentId;

    private boolean tabsAsDragSource;

    private boolean rootPort;

    private BufferedImage dragImage;

    static {
        // setup PropertyChangeListenerFactory to respond to
        // DefaultDockingPort-specific
        // events
        PropertyChangeListenerFactory
                .addFactory(new DockablePropertyChangeHandler.Factory());
    }

    /**
     * Creates a new {@code DefaultDockingPort} with a persistent ID equal to
     * the {@code String} value of this a random UUID.
     * 
     * @see org.flexdock.util.UUID
     */
    public DefaultDockingPort() {
        this(UUID.randomUUID().toString());
    }

    /**
     * Creates a new {@code DefaultDockingPort} with the specified persistent
     * ID. If {@code id} is {@code null}, then the {@code String} value of this
     * {@code Object's} hash code is used. The persistent ID will be the same
     * value returned by invoking {@code getPersistentId()} for this
     * {@code DefaultDockingPort}.
     * 
     * @param id
     *            the persistent ID for the new {@code DefaultDockingPort}
     *            instance.
     */
    public DefaultDockingPort(String id) {
        setPersistentId(id);
        dockingListeners = new ArrayList(2);
        addDockingListener(this);

        DockingPortPropertySet props = getDockingProperties();
        props.setRegionChecker(new DefaultRegionChecker());

        // check container hierarchy to track root dockingports
        addHierarchyListener(DockingPortTracker.getInstance());

        // start out as a root dockingport
        rootPort = true;

        // configure layout
        setLayout(createLayout());
        
        //configure the default border manager
        setBorderManager(createBorderManager());
    }

    protected LayoutManager createLayout() {
        return new PortLayout();
    }

    /**
     * Creates a standard border manager for this docking port.
     * <p>
     * This method is called from the constructor.
     * 
     * @return the border manager for this docking port.
     */
    protected BorderManager createBorderManager() {
        return new StandardBorderManager(new EmptyBorder(0, 0, 0, 0));
    }
    
    /**
     * Overridden to set the currently docked component. Should not be called by
     * application code.
     * 
     * @param comp
     *            the component to be added
     */
    public Component add(Component comp) {
        return setComponent(comp);
    }

    /**
     * Overridden to set the currently docked component. Should not be called by
     * application code.
     * 
     * @param comp
     *            the component to be added
     * @param index
     *            the position at which to insert the component, or {@code -1}
     *            to append the component to the end
     */
    public Component add(Component comp, int index) {
        return setComponent(comp);
    }

    /**
     * Overridden to set the currently docked component. Should not be called by
     * application code.
     * 
     * @param comp
     *            the component to be added
     * @param constraints
     *            an object expressing layout contraints for this component
     */
    public void add(Component comp, Object constraints) {
        setComponent(comp);
    }

    /**
     * Overridden to set the currently docked component. Should not be called by
     * application code.
     * 
     * @param comp
     *            the component to be added
     * @param constraints
     *            an object expressing layout contraints for this
     * @param index
     *            the position in the container's list at which to insert the
     *            component; {@code -1} means insert at the end
     */
    public void add(Component comp, Object constraints, int index) {
        setComponent(comp);
    }

    /**
     * Overridden to set the currently docked component. Should not be called by
     * application code.
     * 
     * @param name
     *            the name of the {@code Component} to be added.
     * @param comp
     *            the {@code Component} to add.
     */
    public Component add(String name, Component comp) {
        return setComponent(comp);
    }

    private void addCmp(DockingPort port, Component c) {
        if (port instanceof Container)
            ((Container) port).add(c);
    }

    private void dockCmp(DockingPort port, Component c) {
        port.dock(c, CENTER_REGION);
    }

    /**
     * Returns {@code true} if docking is allowed for the specified
     * {@code Component} within the supplied {@code region}, {@code false}
     * otherwise. It is important to note that success of a docking operation
     * relies on many factors and a return value of {@code true} from this
     * method does not necessarily guarantee that a call to {@code dock()} will
     * succeed. This method merely indicates that the current
     * {@code DockingPort} does not have any outstanding reason to block a
     * docking operation with respect to the specified {@code Component} and
     * {@code region}.
     * <p>
     * If {@code comp} is {@code null} or {@code region} is invalid according to
     * {@code DockingManager.isValidDockingRegion(String region)}, then this
     * method returns {@code false}.
     * <p>
     * If this {@code DockingPort} is not already the parent {@code DockingPort}
     * for the specified {@code Component}, then this method returns
     * {@code true}.
     * <p>
     * If this {@code DockingPort} is already the parent {@code DockingPort} for
     * the specified {@code Component}, then a check is performed to see if
     * there is a tabbed layout. Tabbed layouts may contain multiple
     * {@code Dockables}, and thus the tab ordering may be rearranged, or
     * shifted into a split layout. If {@code comp} is the only docked
     * {@code Component} within this {@code DockingPort}, then this method
     * returns {@code false} since the layout cannot be rearranged. Otherwise,
     * this method returns {@code true}.
     * 
     * @param comp
     *            the {@code Component} whose docking availability is to be
     *            checked
     * @param region
     *            the region to be checked for docking availability for the
     *            specified {@code Component}.
     * @return {@code true} if docking is allowed for the specified
     *         {@code Component} within the supplied {@code region},
     *         {@code false} otherwise.
     * @see DockingPort#isDockingAllowed(Component, String)
     * @see DockingManager#isValidDockingRegion(String)
     * @see #isParentDockingPort(Component)
     */
    public boolean isDockingAllowed(Component comp, String region) {
        if (comp == null || !isValidDockingRegion(region))
            return false;

        // allow any valid region if we're not already the parent
        // of the component we're checking
        if (!isParentDockingPort(comp))
            return true;

        // we already contain 'comp', so we're either a tabbed-layout, or
        // we contain 'comp' directly. If we contain 'comp' directly, then we
        // cannot logically move 'comp' to some other region within us, as it
        // already fills up our entire space.
        Component docked = getDockedComponent();
        if (!(docked instanceof JTabbedPane))
            // not a tabbed-layout, so we contain 'c' directly
            return false;

        JTabbedPane tabs = (JTabbedPane) docked;
        // if there is only 1 tab, then we already fill up the entire
        // dockingport space and cannot be moved elsewhere
        if (tabs.getTabCount() < 2)
            return false;

        // there is more than 1 tab present, so re-ordering is possible,
        // as well as changing regions
        return true;
    }

    /**
     * Checks the current state of the {@code DockingPort} and, if present,
     * issues the appropriate call to the assigned {@code BorderManager}
     * instance describing the container state. This method will issue a call to
     * 1 of the 4 following methods on the assigned {@code BorderManager}
     * instance, passing {@code this} as the method argument:
     * <p>
     * {@code managePortNullChild(DockingPort port)}
     * {@code managePortSimpleChild(DockingPort port)}
     * {@code managePortSplitChild(DockingPort port)}
     * {@code managePortTabbedChild(DockingPort port)}
     */
    final void evaluateDockingBorderStatus() {
        if (borderManager == null)
            return;

        Component docked = getDockedComponent();
        // check for the null-case
        if (docked == null)
            borderManager.managePortNullChild(this);
        // check for a split layout
        else if (docked instanceof JSplitPane)
            borderManager.managePortSplitChild(this);
        // check for a tabbed layout
        else if (docked instanceof JTabbedPane)
            borderManager.managePortTabbedChild(this);
        // otherwise, we have a simple case of a regular component docked within
        // us
        else
            borderManager.managePortSimpleChild(this);
    }

    /**
     * Returns the docking region within this {@code DockingPort} that contains
     * the specified {@code Point}. Valid return values are those regions
     * defined in {@code DockingConstants} and include {@code NORTH_REGION},
     * {@code SOUTH_REGION}, {@code EAST_REGION}, {@code WEST_REGION},
     * {@code CENTER_REGION}, and {@code UNKNOWN_REGION}.
     * <p>
     * If {@code location} is {@code null}, then {@code UNKNOWN_REGION} is
     * returned.
     * <p>
     * This method gets the {@code RegionChecker} for this {@code DockingPort}
     * by calling {@code getRegionChecker()}. It then attempts to locate the
     * {@code Dockable} at the specified {@code location} by calling
     * {@code getDockableAt(Point location)}.
     * <p>
     * This method defers processing to {@code getRegion(Component c, Point p)}
     * for the current {@code RegionChecker}. If a {@code Dockable} was found
     * at the specified {@code Point}, then the location of the {@code Point}
     * is translated to the coordinate system of the {@code Component} for the
     * embedded {@code Dockable} and that {@code Component} and modified
     * {@code Point} are passed into {@code getRegion(Component c, Point p)}}
     * for the current {@code RegionChecker}. If no {@code Dockable} was found,
     * then the specified {@code Point} is left unmodified and this
     * {@code DockingPort} and the supplied {@code Point} are passed to
     * {@code getRegion(Component c, Point p)}} for the current
     * {@code RegionChecker}.
     * 
     * @param location
     *            the location within this {@code DockingPort} to examine for a
     *            docking region.
     * @return the docking region within this {@code DockingPort} that contains
     *         the specified {@code Point}
     * @see #getRegionChecker()
     * @see #getDockableAt(Point)
     * @see Dockable#getComponent()
     * @see RegionChecker#getRegion(Component, Point)
     */
    public String getRegion(Point location) {
        if (location == null)
            return UNKNOWN_REGION;

        RegionChecker regionChecker = getRegionChecker();
        Dockable d = getDockableAt(location);
        Component regionTest = this;

        if (d != null) {
            regionTest = d.getComponent();
            location = SwingUtilities.convertPoint(this, location, regionTest);
        }

        return regionChecker.getRegion(regionTest, location);
    }

    /**
     * Returns the {@code RegionChecker} currently used by this
     * {@code DockingPort}. This method retrieves the
     * {@code DockingPortPropertySet} instance for this {@code DockingPort} by
     * calling {@code getDockingProperties()}. It then returns by invoking
     * {@code getRegionChecker()} on the resolved {@code DockingPortPropertySet}.
     * 
     * @return the {@code RegionChecker} currently used by this
     *         {@code DockingPort}.
     * @see #getDockingProperties()
     * @see DockingPortPropertySet#getRegionChecker()
     */
    public RegionChecker getRegionChecker() {
        return getDockingProperties().getRegionChecker();
    }

    /**
     * Returns the direct child {@code Dockable} located at the specified
     * {@code Point}. If {@code location} is {@code null}, or this
     * {@code DockingPort} is empty, then a {@code null} reference is returned.
     * <p>
     * If this {@code DockingPort} contains a split layout, then any nested
     * {@code Dockables} will be within a sub-{@code DockingPort} and not a
     * direct child of this {@code DockingPort}. Therefore, if
     * {@code getDockedComponent()} returns a {@code JSplitPane}, then this
     * method will return a {@code null} reference.
     * <p>
     * If this {@code DockingPort} contains a tabbed layout, then the
     * {@code JTabbedPane} returned by {@code getDockedComponent()} will be
     * checked for a {@code Dockable} at the specified {@code Point}.
     * 
     * @param location
     *            the location within the {@code DockingPort} to test for a
     *            {@code Dockable}.
     * @return the direct child {@code Dockable} located at the specified
     *         {@code Point}.
     * @see #getDockedComponent()
     * @see DockingManager#getDockable(Component)
     * @see JTabbedPane#getComponentAt(int x, int y)
     */
    public Dockable getDockableAt(Point location) {
        if (location == null)
            return null;

        Component docked = getDockedComponent();
        if (docked == null || docked instanceof JSplitPane)
            return null;

        if (docked instanceof JTabbedPane) {
            JTabbedPane tabs = (JTabbedPane) docked;
            Component c = tabs.getComponentAt(location.x, location.y);
            return c instanceof Dockable ? (Dockable) c : DockingManager
                    .getDockable(c);
        }

        return DockingManager.getDockable(docked);
    }

    /**
     * Returns the {@code Component} currently docked within the specified
     * {@code region}.
     * <p>
     * If this {@code DockingPort} has either a single child {@code Dockable} or
     * a tabbed layout, then the supplied region must be {@code CENTER_REGION}
     * or this method will return a {@code null} reference. If there is a single
     * child {@code Dockable}, then this method will return the same
     * {@code Component} as returned by {@code getDockedComponent()}. If there
     * is a tabbed layout, then this method will return the {@code Component} in
     * the currently selected tab.
     * <p>
     * If this {@code DockingPort} has a split layout, then a check for
     * {@code CENTER_REGION} will return a {@code null} reference. For outer
     * regions ({@code NORTH_REGION}, {@code SOUTH_REGION},
     * {@code EAST_REGION}, or {@code WEST_REGION}), the supplied region
     * parameter must match the orientation of the embedded {@code JSplitPane}.
     * Thus for a vertically oriented split pane, checks for {@code EAST_REGION}
     * and {@code WEST_REGION} will return a {@code null} reference. Likewise,
     * for a horizontally oriented split pane, checks for {@code NORTH_REGION}
     * and {@code SOUTH_REGION} will return a {@code null} reference.
     * <p>
     * Outer regions are mapped to corresponding split pane regions.
     * {@code NORTH_REGION} maps to the split pane's top component,
     * {@code SOUTH_REGION} maps to the bottom, {@code EAST_REGION} maps to the
     * right, and {@code WEST_REGION} maps to the left. The sub-{@code DockingPort}
     * for the split pane region that corresponds to the specified
     * {@code region} parameter will be resolved and this method will return
     * that {@code Component} retrieved by calling its
     * {@code getDockedComponent()} method. <i>Note that the
     * {@code getDockedComponent()} call to a sub- {@code DockingPort} implies
     * that the {@code JTabbedPane} or {@code JSplitPane} for the sub-port may
     * be returned if the sub-port contains multiple {@code Dockables}.</i>
     * <p>
     * If this {@code DockingPort} is empty, then this method returns a
     * {@code null} reference.
     * 
     * @param region
     *            the region to be checked for a docked {@code Component}
     * @return the {@code Component} docked within the specified region.
     * @see DockingPort#getComponent(String)
     * @see #getDockedComponent()
     */
    public Component getComponent(String region) {
        Component docked = getDockedComponent();
        if (docked == null)
            return null;

        if (docked instanceof JTabbedPane) {
            // they can only get tabbed dockables if they were checking the
            // CENTER region.
            if (!CENTER_REGION.equals(region))
                return null;

            JTabbedPane tabs = (JTabbedPane) docked;
            return tabs.getSelectedComponent();
        }

        if (docked instanceof JSplitPane) {
            // they can only get split dockables if they were checking an outer
            // region.
            if (CENTER_REGION.equals(region))
                return null;

            JSplitPane split = (JSplitPane) docked;

            // make sure the supplied regions correspond to the current
            // splitpane orientation
            boolean horizontal = split.getOrientation() == JSplitPane.HORIZONTAL_SPLIT;
            if (horizontal) {
                if (NORTH_REGION.equals(region) || SOUTH_REGION.equals(region))
                    return null;
            } else {
                if (EAST_REGION.equals(region) || WEST_REGION.equals(region))
                    return null;
            }

            boolean left = NORTH_REGION.equals(region)
                    || WEST_REGION.equals(region);
            Component c = left ? split.getLeftComponent() : split
                    .getRightComponent();
            // split panes only contain sub-dockingports. if 'c' is not a
            // sub-dockingport,
            // then something is really screwed up.
            if (!(c instanceof DockingPort))
                return null;

            // get the dockable contained in the sub-dockingport
            return ((DockingPort) c).getDockedComponent();
        }

        // we already checked the tabbed layout and split layout. all that's
        // left is the direct-child component itself. this will only ever
        // exist in the CENTER, so return it if they requested the CENTER
        // region.
        return CENTER_REGION.equals(region) ? docked : null;
    }

    /**
     * Returns the {@code Dockable} currently docked within the specified
     * {@code region}. This method dispatches to
     * {@code getComponent(String region)} to retrieve the {@code Component}
     * docked within the specified region and returns its associated
     * {@code Dockable} via {@code DockingManager.getDockable(Component comp)}.
     * <p>
     * There are somewhat strict semantics associated with retrieving the
     * {@code Component} in a particular docking region. API documentation for
     * {@code getComponent(String region)} should be referenced for a listing of
     * the rule set. If {@code region} is invalid according to
     * {@code DockingManager.isValidDockingRegion(String region)}, then this
     * method returns a {@code null} reference.
     * 
     * @param region
     *            the region to be checked for a docked {@code Dockable}
     * @return the {@code Dockable} docked within the specified region.
     * @see DockingPort#getDockable(String)
     * @see #getComponent(String)
     * @see #getDockedComponent()
     * @see DockingManager#getDockable(Component)
     * @see DockingManager#isValidDockingRegion(String)
     */
    public Dockable getDockable(String region) {
        Component c = getComponent(region);
        return DockingManager.getDockable(c);
    }

    /**
     * If this method returns {@code null}, implementations may throw
     * NullPointerExceptions. Do not expect NPE checking.
     * 
     * @return a valid JTabbedPane.
     */
    protected JTabbedPane createTabbedPane() {
        Insets oldInsets = UIManager
                .getInsets(LookAndFeelSettings.TAB_PANE_BORDER_INSETS);
        int tabPlacement = getInitTabPlacement();

        int edgeInset = LookAndFeelSettings.getTabEdgeInset(tabPlacement);

        Insets newInsets = new Insets(0, 0, 0, 0);
        switch (tabPlacement) {
        case JTabbedPane.TOP:
            newInsets.top = edgeInset >= 0 ? edgeInset : oldInsets.top;
            break;
        case JTabbedPane.LEFT:
            newInsets.left = edgeInset >= 0 ? edgeInset : oldInsets.left;
            break;
        case JTabbedPane.BOTTOM:
            newInsets.bottom = edgeInset >= 0 ? edgeInset : oldInsets.bottom;
            break;
        case JTabbedPane.RIGHT:
            newInsets.right = edgeInset >= 0 ? edgeInset : oldInsets.right;
            break;
        }

        UIManager.put(LookAndFeelSettings.TAB_PANE_BORDER_INSETS, newInsets);
        JTabbedPane pane = new JTabbedPane();
        pane.setTabPlacement(tabPlacement);
        UIManager.put(LookAndFeelSettings.TAB_PANE_BORDER_INSETS, oldInsets);

        TabbedDragListener tdl = new TabbedDragListener();
        pane.addMouseListener(tdl);
        pane.addMouseMotionListener(tdl);
        return pane;
    }

    protected void updateTab(Dockable dockable) {
        Component docked = getDockedComponent();
        if (docked instanceof JTabbedPane) {
            JTabbedPane tabs = (JTabbedPane) docked;
            int index = tabs.indexOfComponent(dockable.getComponent());
            if (index > -1) {
                tabs.setIconAt(index, dockable.getDockingProperties()
                        .getTabIcon());
                tabs.setTitleAt(index, dockable.getDockingProperties()
                        .getDockableDesc());
            }
        }
    }

    /**
     * Returns the {@code DockingStrategy} used by this {@code DockingPort}.
     * This method dispatches to {@code getDockingStrategy(Object obj)},
     * passing {@code this} as an argument. By default,
     * {@code DefaultDockingStrategy} is used unless a different
     * {@code DockingStrategy} has been assigned by the end user for
     * {@code DefaultDockingPort}.
     * 
     * @return the {@code DockingStrategy} used by this {@code DockingPort}.
     * @see DockingPort#getDockingStrategy()
     * @see DockingManager#getDockingStrategy(Object)
     */
    public DockingStrategy getDockingStrategy() {
        return DockingManager.getDockingStrategy(this);
    }

    /**
     * Removes all {@code Dockables} from this {@code DockingPort}. Internally,
     * this method dispatches to {@code removeAll()}. This ensures that not
     * only docked {@code Components} are removed, that that all wrapper
     * containers such as {@code JTabbedPanes}, {@code JSplitPanes}, and sub-{@code DockingPorts}
     * are removed as well.
     * 
     * @see DockingPort#clear()
     * @see #removeAll()
     */
    public void clear() {
        removeAll();
    }

    /**
     * Docks the specified component within the specified region. This method
     * attempts to resolve the {@code Dockable} associated with the specified
     * {@code Component} by invoking
     * {@code DockingManager.getDockable(Component comp)}. Processing is then
     * dispatched to {@code dock(Dockable dockable, String region)}.
     * <p>
     * If no {@code Dockable} is resolved for the specified {@code Component},
     * then this method attempts to register the {@code Component} as a
     * {@code Dockable} automatically by calling
     * {@code DockingManager.registerDockable(Component comp)}.
     * <p>
     * If either {@code comp} or {@code region} region are {@code null}, then
     * this method returns {@code false}. Otherwise, this method returns a
     * boolean indicating the success of the docking operation based upon
     * {@code dock(Dockable dockable, String region)}.
     * 
     * @param comp
     *            the {@code Component} to be docked within this
     *            {@code DockingPort}
     * @param region
     *            the region within this {@code DockingPort} to dock the
     *            specified {@code Component}
     * @return {@code true} if the docking operation was successful,
     *         {@code false} otherwise.
     * @see DockingPort#dock(Component, String)
     * @see #dock(Dockable, String)
     * @see DockingManager#getDockable(Component)
     * @see DockingManager#registerDockable(Component)
     */
    public boolean dock(Component comp, String region) {
        if (comp == null || region == null)
            return false;

        Dockable dockable = DockingManager.getDockable(comp);
        if (dockable == null)
            dockable = DockingManager.registerDockable(comp);

        return dock(dockable, region);
    }

    /**
     * Docks the specified {@code Dockable} within the specified region. The
     * {@code Component} used for docking is returned by calling
     * {@code getComponent()} on the specified {@code Dockable}. This method
     * returns {@code false} immediately if the specified {@code Dockable} is
     * {@code null} or if
     * {@code isDockingAllowed(Component comp, String region)} returns
     * {@code false}.
     * <p>
     * If this {@code DockingPort} is currently empty, then the {@code Dockable}
     * is docked into the {@code CENTER_REGION}, regardless of the supplied
     * {@code region} parameter's value.
     * <p>
     * If {@code isSingleTabAllowed()} returns {@code false} and the
     * {@code DockingPort} is emtpy, then the {@code Dockable} will be added
     * directly to the {@code DockingPort} and will take up all available space
     * within the {@code DockingPort}. In this case, subsequent calls to
     * {@code getDockedComponent()} will return the dockable {@code Component}.
     * <p>
     * If {@code isSingleTabAllowed()} returns {@code true} and the
     * {@code DockingPort} is emtpy, then a {@code JTabbedPane} will be added
     * directly to the {@code DockingPort} and will take up all available space
     * within the {@code DockingPort}. The dockable {@code Component} will be
     * added as a tab within the tabbed pane. In this case, subsequent calls to
     * {@code getDockedComponent()} will return the {@code JTabbedPane}.
     * <p>
     * If the {@code DockingPort} is <b>not</b> empty, and the specified region
     * is {@code CENTER_REGION}, then the dockable {@code Component} will be
     * added to the {@code JTabbedPane} returned by {@code getDockedComponent()}.
     * If this {@code DockingPort} only contained a single dockable
     * {@code Component} without a tabbed pane, then the currently docked
     * {@code Component} is removed, a {@code JTabbedPane} is created and added,
     * and both the old {@code Component} and the new one are added to the
     * {@code JTabbedPane}. In this case, subsequent calls to
     * {@code getDockedComponent()} will return the {@code JTabbedPane}.
     * <p>
     * If the {@code DockingPort} is <b>not</b> empty, and the specified region
     * is {@code NORTH_REGION}, {@code SOUTH_REGION}, {@code EAST_REGION}, or
     * {@code WEST_REGION}, then the currently docked {@code Component} is
     * removed and replaced with a {@code JSplitPane}. Two new
     * {@code DefaultDockingPorts} are created as sub-ports and are added to
     * each side of the {@code JSplitPane}. The previously docked
     * {@code Component} is docked to the CENTER_REGION of one of the sub-ports
     * and the new {@code Component} is added to the other. In this case,
     * subsequent calls to {@code getDockedComponent()} will return the
     * {@code JSplitPane}. In this fasion, the sub-ports will now be capable of
     * handling further sub-docking within the layout.
     * <p>
     * {@code JSplitPane} and sub-{@code DockingPort} creation are delegated to
     * the {@code DockingStrategy} returned by {@code getDockingStrategy()}.
     * Initial splitpane divider location is also controlled by this
     * {@code DockingStrategy}.
     * 
     * @param dockable
     *            the {@code Dockable} to be docked within this
     *            {@code DockingPort}
     * @param region
     *            the region within this {@code DockingPort} to dock the
     *            specified {@code Dockable}
     * @return {@code true} if the docking operation was successful,
     *         {@code false} otherwise.
     * @see DockingPort#dock(Dockable, String)
     * @see #isDockingAllowed(Component, String)
     * @see #getDockedComponent()
     * @see #getDockingStrategy()
     * @see DockingStrategy#createDockingPort(DockingPort)
     * @see DockingStrategy#createSplitPane(DockingPort, String)
     * @see DockingStrategy#getInitialDividerLocation(DockingPort, JSplitPane)
     * @see DockingStrategy#getDividerProportion(DockingPort, JSplitPane)
     */
    public boolean dock(Dockable dockable, String region) {
        if (dockable == null)
            return false;

        Component comp = dockable.getComponent();
        if (comp == null || !isDockingAllowed(comp, region))
            return false;

        // can't dock the same component twice. This will also keep them from
        // moving CENTER to NORTH and that sort of thing, which would just be a
        // headache to manage anyway.
        Component docked = getDockedComponent();
        if (comp == docked)
            return false;

        // if there is nothing currently in the docking port, then we can only
        // dock into the CENTER region.
        if (docked == null)
            region = CENTER_REGION;

        String tabTitle = DockingUtility.getTabText(dockable);
        COMPONENT_TITLES.put(comp, tabTitle);

        if (!isSingleTabAllowed() && docked == null) {
            setComponent(comp);
            evaluateDockingBorderStatus();
            return true;
        }

        boolean success = CENTER_REGION.equals(region) ? dockInCenterRegion(comp)
                : dockInOuterRegion(comp, region);

        if (success) {
            evaluateDockingBorderStatus();
            // if we docked in an outer region, then there is a new JSplitPane.
            // We'll want to divide it in half. this is done after
            // evaluateDockingBorderStatus(), so we'll know any border
            // modification that took place has already happened, and we can be
            // relatively safe about assumptions regarding our current insets.
            if (!CENTER_REGION.equals(region))
                resetSplitDividerLocation();
        }
        return success;
    }

    private void resetSplitDividerLocation() {
        Component c = getDockedComponent();
        if (c instanceof JSplitPane)
            deferSplitDividerReset((JSplitPane) c);
    }

    private void deferSplitDividerReset(final JSplitPane splitPane) {
        applySplitDividerLocation(splitPane);
        // we don't need to defer split divider location reset until after
        // a DockingSplitPane has rendered, since that class is able to figure
        // out its proper divider location by itself.
        if (splitPane instanceof DockingSplitPane) {
            return;
        }

        // check to see if we've rendered
        int size = SwingUtility.getSplitPaneSize(splitPane);
        if (splitPane.isVisible() && size > 0 && EventQueue.isDispatchThread()) {
            // if so, apply the split divider location and return
            applySplitDividerLocation(splitPane);
            splitPane.validate();
            return;
        }

        // otherwise, defer applying the divider location reset until
        // the split pane is rendered.
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                deferSplitDividerReset(splitPane);
            }
        });
    }

    private void applySplitDividerLocation(JSplitPane splitPane) {
        DockingStrategy strategy = DockingManager.getDockingStrategy(this);
        int loc = strategy.getInitialDividerLocation(this, splitPane);
        splitPane.setDividerLocation(loc);
    }

    private boolean dockInCenterRegion(Component comp) {
        Component docked = getDockedComponent();
        JTabbedPane tabs = null;

        if (docked instanceof JTabbedPane) {
            tabs = (JTabbedPane) docked;
            addTab(tabs, comp);
            tabs.revalidate();
            tabs.setSelectedIndex(tabs.getTabCount() - 1);
            return true;
        }

        tabs = createTabbedPane();
        // createTabbedPane() is protected and may be overridden, so we'll have
        // to check for a possible null case here. Though why anyone would
        // return a null, I don't know. Maybe we should throw a
        // NullPointerException instead.
        if (tabs == null)
            return false;

        // remove the currently docked component and add it to the tabbed pane
        if (docked != null) {
            remove(docked);
            addTab(tabs, docked);
        }

        // add the new component to the tabbed pane
        addTab(tabs, comp);

        // now add the tabbed pane back to the main container
        setComponent(tabs);
        tabs.setSelectedIndex(tabs.getTabCount() - 1);
        return true;
    }

    private void addTab(JTabbedPane tabs, Component comp) {
        String tabText = getValidTabTitle(tabs, comp);
        tabs.add(comp, tabText);
        Dockable d = DockingManager.getDockable(comp);
        if (d == null)
            return;

        Icon icon = d.getDockingProperties().getTabIcon();
        int indx = tabs.getTabCount() - 1;
        tabs.setIconAt(indx, icon);
    }

    private boolean dockInOuterRegion(Component comp, String region) {
        // cache the current size and cut it in half for later in the method.
        Dimension halfSize = getSize();
        halfSize.width /= 2;
        halfSize.height /= 2;

        // remove the old docked content. we'll be adding it to another
        // dockingPort.
        Component docked = getDockedComponent();
        remove(docked);

        // add the components to their new parents.
        DockingStrategy strategy = getDockingStrategy();
        DockingPort oldContent = strategy.createDockingPort(this);
        DockingPort newContent = strategy.createDockingPort(this);
        addCmp(oldContent, docked);
        dockCmp(newContent, comp);

        JSplitPane newDockedContent = strategy.createSplitPane(this, region);

        // put the ports in the correct order and add them to a new wrapper
        // panel
        DockingPort[] ports = putPortsInOrder(oldContent, newContent, region);

        if (ports[0] instanceof JComponent) {
            ((JComponent) ports[0]).setMinimumSize(new Dimension(0, 0));
        }
        if (ports[1] instanceof JComponent) {
            ((JComponent) ports[1]).setMinimumSize(new Dimension(0, 0));
        }

        if (ports[0] instanceof Component)
            newDockedContent.setLeftComponent((Component) ports[0]);
        if (ports[1] instanceof Component)
            newDockedContent.setRightComponent((Component) ports[1]);

        // set the split in the middle
        double ratio = .5;

        if (docked instanceof Dockable
                && newDockedContent instanceof DockingSplitPane) {
            Float siblingRatio = ((Dockable) docked).getDockingProperties()
                    .getSiblingSize(region);
            if (siblingRatio != null) {
                ratio = siblingRatio.doubleValue();
            }

            ((DockingSplitPane) newDockedContent).setInitialDividerRatio(ratio);
        }
        newDockedContent.setDividerLocation(ratio);

        // now set the wrapper panel as the currently docked component
        setComponent(newDockedContent);
        // if we're currently showing, then we can exit now
        if (isShowing())
            return true;

        // otherwise, we have unrealized components whose sizes cannot be
        // determined until after we're visible. cache the desired size
        // values now for use later during rendering.
        double proportion = strategy.getDividerProportion(this,
                newDockedContent);
        SwingUtility.putClientProperty((Component) oldContent,
                DefaultDockingStrategy.PREFERRED_PROPORTION, new Float(
                        proportion));
        SwingUtility.putClientProperty((Component) newContent,
                DefaultDockingStrategy.PREFERRED_PROPORTION, new Float(
                        1f - proportion));

        return true;
    }

    /**
     * Returns the child {@code Component} currently embedded within with
     * {@code DockingPort}. If the {@code DockingPort} is empty, then this
     * method returns a {@code null} reference. If there is a single
     * {@code Dockable} docked within it with no tabbed layout, then the
     * {@code Component} for that {@code Dockable} is returned per its
     * {@code getComponent()} method. If there is a tabbed layout present, then
     * a {@code JTabbedPane} is returned. If there is a split layout present,
     * then a {@code JSplitPane} is returned.
     * 
     * @see DockingPort#getDockedComponent()
     */
    public Component getDockedComponent() {
        return dockedComponent;
    }

    // private JSplitPane getDockedSplitPane() {
    // Component docked = getDockedComponent();
    // return docked instanceof JSplitPane? (JSplitPane)docked: null;
    // }

    /**
     * Returns a {@code String} identifier that is unique to
     * {@code DockingPorts} within a JVM instance, but persistent across JVM
     * instances. This is used for configuration mangement, allowing the JVM to
     * recognize a {@code DockingPort} instance within an application instance,
     * persist the ID, and recall it in later application instances. The ID
     * should be unique within an appliation instance so that there are no
     * collisions with other {@code DockingPort} instances, but it should also
     * be consistent from JVM to JVM so that the association between a
     * {@code DockingPort} instance and its ID can be remembered from session to
     * session.
     * <p>
     * The value returned by this method will come from the most recent call to
     * {@code setPersistentId(String id)}. If
     * {@code setPersistentId(String id)} was invoked with a {@code null}
     * argument, then the {@code String} verion of this {@code DockingPort's}
     * hash code is used. Therefore, this method will never return a
     * {@code null} reference.
     * 
     * @return the persistent ID for this {@code DockingPort}
     * @see DockingPort#getPersistentId()
     * @see #setPersistentId(String)
     * @see DockingManager#getDockingPort(String)
     */
    public String getPersistentId() {
        return persistentId;
    }

    /**
     * Sets the persisent ID to be used for this {@code DockingPort}. If
     * {@code id} is {@code null}, then the {@code String} value of this
     * {@code DockingPort's} hash code is used.
     * <p>
     * {@code DockingPorts} are tracked by persistent ID within
     * {@code DockingManager}. Whenever this method is called, the
     * {@code DockingManager's} tracking mechanism is automatically upated for
     * this {@code DockingPort}.
     * 
     * @param id
     *            the persistent ID to be used for this {@code DockingPort}
     * @see #getPersistentId()
     * @see DockingManager#getDockingPort(String)
     * @see DockingPortTracker#updateIndex(DockingPort)
     */
    public void setPersistentId(String id) {
        if (id == null) {
            id = UUID.randomUUID().toString();
        }
        persistentId = id;
        DockingPortTracker.updateIndex(this);
    }

    private String getValidTabTitle(JTabbedPane tabs, Component comp) {
        String title = (String) COMPONENT_TITLES.get(comp);
        if (title == null || title.trim().length() == 0)
            title = "null";

        int tc = tabs.getTabCount();
        int occurrances = 0;
        HashSet titles = new HashSet();
        String tmp = null;
        for (int i = 0; i < tc; i++) {
            tmp = tabs.getTitleAt(i).toLowerCase();
            titles.add(tmp);
            if (tmp.startsWith(title.toLowerCase()))
                occurrances++;
        }

        if (titles.contains(title) && occurrances > 0)
            title += occurrances;

        COMPONENT_TITLES.put(comp, title);
        return title;
    }

    /**
     * Returns {@code true} if single tabs are allowed within this
     * {@code DockingPort}, {@code false} otherwise.
     * <p>
     * Generally the tabbed interface does not appear until two or more
     * {@code Dockables} are docked to the {@code CENTER_REGION} of the
     * {@code DockingPort} and tabs are required to switch between them. When
     * there is only a single {@code Dockable} within the {@code DockingPort},
     * the default behavior for the dockable {@code Component} to take up all of
     * the space within the {@code DockingPort}.
     * <p>
     * If this method returns {@code true}, then a single {@code Dockable}
     * within this {@code DockingPort} will reside within a tabbed layout that
     * contains only one tab.
     * <p>
     * The value returned by this method is a scoped property. This means there
     * may be many different "scopes" at which the single-tab property may be
     * set. For instance, a "global" setting may override the individual setting
     * for this {@code DockingPort}, and this {@code DockingPort's} particular
     * setting may override the global default setting.
     * {@code org.flexdock.docking.props.PropertyManager} should be referenced
     * for further information on scoped properties.
     * 
     * @return {@code true} if single tabs are allowed within this
     *         {@code DockingPort}, {@code false} otherwise.
     * @see #setSingleTabAllowed(boolean)
     * @see DockingManager#isSingleTabsAllowed()
     * @see DockingManager#setSingleTabsAllowed(boolean)
     * @see PropertyManager
     * @see DockingPortPropertySet#isSingleTabsAllowed()
     * @see DockingPortPropertySet#setSingleTabsAllowed(boolean)
     */
    public boolean isSingleTabAllowed() {
        return getDockingProperties().isSingleTabsAllowed().booleanValue();
    }

    /**
     * Sets the "single tab" property for this {@code DockingPort}, allowing or
     * disallowing a single {@code Dockable} within the {@code DockingPort} to
     * appear within a tabbed layout.
     * <p>
     * Generally the tabbed interface does not appear until two or more
     * {@code Dockables} are docked to the {@code CENTER_REGION} of the
     * {@code DockingPort} and tabs are required to switch between them. When
     * there is only a single {@code Dockable} within the {@code DockingPort},
     * the default behavior for the dockable {@code Component} to take up all of
     * the space within the {@code DockingPort}.
     * <p>
     * If the single tab property is set to {@code true}, then a single
     * {@code Dockable} within this {@code DockingPort} will reside within a
     * tabbed layout that contains only one tab.
     * <p>
     * The single tab property is a scoped property. This means there may be
     * many different "scopes" at which the single-tab property may be set. For
     * instance, a "global" setting may override the individual setting for this
     * {@code DockingPort}, and this {@code DockingPort's} particular setting
     * may override the global default setting. <b>This method applied a value
     * only to the local scope for this particular {@code DockingPort}.</b>
     * {@code org.flexdock.docking.props.PropertyManager} should be referenced
     * for further information on scoped properties.
     * 
     * @param allowed
     *            {@code true} if a single-tabbed layout should be allowed,
     *            {@code false} otherwise
     * @see #isSingleTabAllowed()
     * @see DockingManager#setSingleTabsAllowed(boolean)
     * @see DockingManager#isSingleTabsAllowed()
     * @see PropertyManager
     * @see DockingPortPropertySet#setSingleTabsAllowed(boolean)
     * @see DockingPortPropertySet#isSingleTabsAllowed()
     */
    public void setSingleTabAllowed(boolean allowed) {
        getDockingProperties().setSingleTabsAllowed(allowed);
    }

    /**
     * Indicates whether or not the specified component is docked somewhere
     * within this {@code DefaultDockingPort}. This method returns {@code true}
     * if the specified {@code Component} is a direct child of the
     * {@code DefaultDockingPort} or is a direct child of a {@code JTabbedPane}
     * or {@code JSplitPane}that is currently the {@code DefaultDockingPort's}docked
     * component. Otherwise, this method returns {@code false}. If {@code comp}
     * is {@code null}, then then this method return {@code false}
     * 
     * @param comp
     *            the Component to be tested.
     * @return a boolean indicating whether or not the specified component is
     *         docked somewhere within this {@code DefaultDockingPort}.
     * @see DockingPort#isParentDockingPort(java.awt.Component)
     * @see Component#getParent()
     * @see #getDockedComponent()
     */
    public boolean isParentDockingPort(Component comp) {
        if (comp == null)
            return false;

        Container parent = comp.getParent();
        // if the component has no parent, then it can't be docked within us
        if (parent == null)
            return false;

        // if we're the direct parent of this component, then we're the parent
        // docking port
        if (parent == this)
            return true;

        // if the component is directly inside our docked component, then we're
        // also considered its parent dockingPort
        return parent == getDockedComponent();
    }

    protected boolean isValidDockingRegion(String region) {
        return DockingManager.isValidDockingRegion(region);
    }

    private boolean isSingleComponentDocked() {
        Component c = getDockedComponent();
        // we have no docked component
        if (c == null)
            return false;

        // we do have a docked component. It'll be a splitpane, a tabbedpane,
        // or something else.

        // if it's a splitpane, then we definitely have more than one component
        // docked
        if (c instanceof JSplitPane)
            return false;

        // if it's a tabbed pane, then check the number of tabs on the pane
        if (c instanceof JTabbedPane) {
            return ((JTabbedPane) c).getTabCount() == 1;
        }

        // splitpane and tabbed pane are the only two subcontainers that signify
        // more than one docked component. if neither, then we only have one
        // component docked.
        return true;
    }

    protected Dockable getCenterDockable() {
        // can't have a CENTER dockable if there's nothing in the center
        if (!isSingleComponentDocked())
            return null;

        // get the component in the CENTER
        Component c = getDockedComponent();
        if (c instanceof JTabbedPane) {
            // if in a tabbed pane, get the first component in there.
            // (there will only be 1, since we've already passed the
            // isSingleComponentDocked() test)
            c = ((JTabbedPane) c).getComponent(0);
        }
        // return the Dockable instance associated with this component
        return DockingManager.getDockable(c);
    }

    private DockingPort[] putPortsInOrder(DockingPort oldPort,
            DockingPort newPort, String region) {
        if (NORTH_REGION.equals(region) || WEST_REGION.equals(region))
            return new DockingPort[] { newPort, oldPort };
        return new DockingPort[] { oldPort, newPort };
    }

    /**
     * This method completes with a call to
     * {@code evaluateDockingBorderStatus()} to allow any installed
     * {@code DefaultDockingStrategy} to handle container-state-related
     * behavior.
     */
    private void reevaluateContainerTree() {
        reevaluateDockingWrapper();
        reevaluateTabbedPane();

        evaluateDockingBorderStatus();
    }

    private void reevaluateDockingWrapper() {
        Component docked = getDockedComponent();
        Container parent = getParent();
        Container grandParent = parent == null ? null : parent.getParent();

        // added grandparent check up here so we will be able to legally embed a
        // DefaultDockingPort within a plain JSplitPane without triggering an
        // unnecessary remove()
        if (docked == null && parent instanceof JSplitPane
                && grandParent instanceof DefaultDockingPort) {
            // in this case, the docked component has disappeared (removed) and
            // our parent component is a wrapper for us and our child so that we
            // can share the root docking port with another component. since our
            // child is gone, there's no point in our being here anymore and our
            // sibling component shouldn't have to share screen real estate with
            // us anymore. we'll remove ourselves and notify the root docking
            // port that the component tree has been modified.
            parent.remove(this);
            ((DefaultDockingPort) grandParent).reevaluateContainerTree(); // LABEL
            // 1
        } else if (docked instanceof JSplitPane) {
            // in this case, we're the parent of a docking wrapper. this implies
            // that we're splitting our real estate between two components. (in
            // practice, we're actually the parent that was called above at
            // LABEL
            // 1).
            JSplitPane wrapper = (JSplitPane) docked;
            Component left = wrapper.getLeftComponent();
            Component right = wrapper.getRightComponent();

            // first, check to make sure we do in fact have 2 components. if so,
            // then we don't have
            // to go any further.
            if (left != null && right != null)
                return;

            // check to see if we have zero components. if so, remove everything
            // and return.
            if (left == right) {
                removeAll();
                return;
            }

            // if we got here, then one of our components has been removed (i.e.
            // LABEL 1). In this
            // case, we want to pull the remaining content out of its
            // split-wrapper and add it
            // as a direct child to ourselves.
            Component comp = left == null ? right : left;
            wrapper.remove(comp);

            // do some cleanup on the wrapper before removing it
            if (wrapper instanceof DockingSplitPane) {
                ((DockingSplitPane) wrapper).cleanup();
            }
            super.remove(wrapper);

            if (comp instanceof DefaultDockingPort)
                comp = ((DefaultDockingPort) comp).getDockedComponent();

            if (comp != null)
                setComponent(comp);
        }
    }

    private void reevaluateTabbedPane() {
        Component docked = getDockedComponent();
        if (!(docked instanceof JTabbedPane))
            return;

        JTabbedPane tabs = (JTabbedPane) docked;
        int tabCount = tabs.getTabCount();
        // we don't have to do anything special here if there is more than the
        // minimum number of allowable tabs
        int minTabs = isSingleTabAllowed() ? 0 : 1;
        if (tabCount > minTabs) {
            return;
        }

        // otherwise, pull out the component in the remaining tab (if it
        // exists), and add it to ourselves as a direct child (ditching the
        // JTabbedPane).
        Component comp = tabCount == 1 ? tabs.getComponentAt(0) : null;
        removeAll();
        if (comp != null)
            setComponent(comp);

        Container parent = getParent();
        Container grandParent = parent == null ? null : parent.getParent();
        // if our TabbedPane's last component was removed, then the TabbedPane
        // itself has now been removed. if we're a child port within a
        // JSplitPane
        // within another DockingPort, then we ourselved need to be removed from
        // the component tree, since we don't have any content.
        if (comp == null && parent instanceof JSplitPane
                && grandParent instanceof DefaultDockingPort) {
            parent.remove(this);
            ((DefaultDockingPort) grandParent).reevaluateContainerTree();
        }
    }

    /**
     * Overridden to decorate superclass method, keeping track of internal
     * docked-component reference.
     * 
     * @param index
     *            the index of the component to be removed.
     * @see Container#remove(int)
     */
    public void remove(int index) {
        Component docked = getDockedComponent();
        Component comp = getComponent(index);
        super.remove(index);
        if (docked == comp)
            dockedComponent = null;
    }

    /**
     * Overridden to decorate superclass method, keeping track of internal
     * docked-component reference.
     * 
     * @see Container#removeAll()
     */
    public void removeAll() {
        super.removeAll();
        dockedComponent = null;
    }

    /**
     * Sets the currently installed {@code BorderManager}. This method provides
     * a means of customizing border managment following any successful call to
     * {@code dock(Dockable dockable, String region)} or
     * {@code undock(Component comp)}, allowing cleanup of borders for nested
     * {@code Components} within the docking layout. {@code null} values are
     * allowed.
     * 
     * @param mgr
     *            the {@code BorderManager} assigned to to manage docked
     *            component borders.
     * @see #getBorderManager()
     * @see BorderManager
     */
    public void setBorderManager(BorderManager mgr) {
        borderManager = mgr;
    }

    /**
     * Returns the currently intalled {@code BorderManager}. The
     * {@code BorderManager} is used any time a successful call to
     * {@code dock(Dockable dockable, String region)} or
     * {@code undock(Component comp)} has been issued to clean up borders for
     * nested {@code Components} within the docking layout. This method will
     * return a {@code null} reference if there is no {@code BorderManager}
     * installed.
     * 
     * @return the currently installed {@code BorderManager}.
     * @see #setBorderManager(BorderManager)
     * @see BorderManager
     */
    public BorderManager getBorderManager() {
        return borderManager;
    }

    private Component setComponent(Component c) {
        if (getDockedComponent() != null)
            removeAll();

        dockedComponent = c;
        Component ret = super.add(dockedComponent);

        // calling doLayout here to properly set the component's size
        // validate throws an error
        doLayout();

        return ret;
    }

    /**
     * Undocks the specified {@code Component} and returns a boolean indicating
     * the success of the operation.
     * <p>
     * Since {@code DefaultDockingPort} may only contain one child component,
     * there i s a container hierarchy to manage tabbed interfaces, split
     * layouts, and sub-docking. As components are removed from this hierarchy,
     * the hierarchy itself must be reevaluated. Removing a component from a
     * child code>DefaultDockingPort} within a {@code JSplitPane} renders the
     * child {@code DefaultDockingPort} unnecessary, which, in turn, renders the
     * notion of splitting the layout with a {@code JSplitPane} unnecessary
     * (since there are no longer two components to split the layout between).
     * Likewise, removing a child component from a {@code JTabbedPane} such that
     * there is only one child left within the {@code JTabbedPane} removes the
     * need for a tabbed interface to begin with.
     * <p>
     * This method automatically handles the reevaluation of the container
     * hierarchy to keep wrapper-container usage at a minimum. Since
     * {@code DockingManager} makes this callback automatic, developers normally
     * will not need to call this method explicitly. However, when removing a
     * component from a {@code DefaultDockingPort} using application code,
     * developers should keep in mind to use this method instead of
     * {@code remove()}.
     * 
     * @param comp
     *            the {@code Component} to be undocked.
     * @return a boolean indicating the success of the operation
     * @see DockingPort#undock(Component comp)
     * @see DockingManager#undock(Dockable)
     */
    public boolean undock(Component comp) {
        // can't undock a component that isn't already docked within us
        if (!isParentDockingPort(comp))
            return false;

        // remove the component
        comp.getParent().remove(comp);

        // reevaluate the container tree.
        reevaluateContainerTree();

        return true;
    }

    /**
     * Returns all {@code Dockables} docked within this {@code DockingPort} and
     * all sub-{@code DockingPorts}. The returned {@code Set} will contain
     * {@code Dockable} instances. If there are no {@code Dockables} present, an
     * empty {@code Set} will be returned. This method will never return a
     * {@code null} reference.
     * 
     * @return all {@code Dockables} docked within this {@code DockingPort} and
     *         all sub-{@code DockingPorts}.
     * @see DockingPort#getDockables()
     */
    public Set getDockables() {
        // return ALL dockables, recursing to maximum depth
        return getDockableSet(-1, 0, null);
    }

    protected Set getDockableSet(int depth, int level, Class desiredClass) {
        Component c = getDockedComponent();

        if (c instanceof JTabbedPane) {
            JTabbedPane tabs = (JTabbedPane) c;
            int len = tabs.getTabCount();
            HashSet set = new HashSet(len);
            for (int i = 0; i < len; i++) {
                c = tabs.getComponentAt(i);
                if (isValidDockableChild(c, desiredClass)) {
                    if (c instanceof Dockable)
                        set.add(c);
                    else
                        set.add(DockingManager.getDockable(c));
                }
            }
            return set;
        }

        HashSet set = new HashSet(1);

        // if we have a split-layout, then we need to decide whether to get the
        // child viewSets. If 'depth' is less then zero, then it's implied we
        // want to recurse to get ALL child viewsets no matter how deep. If
        // 'depth' is greater than or equal to zero, we only want to go as deep
        // as the specified depth.
        if (c instanceof JSplitPane && (depth < 0 || level <= depth)) {
            JSplitPane pane = (JSplitPane) c;
            Component sub1 = pane.getLeftComponent();
            Component sub2 = pane.getRightComponent();

            if (sub1 instanceof DefaultDockingPort)
                set.addAll(((DefaultDockingPort) sub1).getDockableSet(depth,
                        level + 1, desiredClass));

            if (sub2 instanceof DefaultDockingPort)
                set.addAll(((DefaultDockingPort) sub2).getDockableSet(depth,
                        level + 1, desiredClass));
        }

        if (isValidDockableChild(c, desiredClass)) {
            if (c instanceof Dockable)
                set.add(c);
            else
                set.add(DockingManager.getDockable(c));
        }
        return set;
    }

    protected boolean isValidDockableChild(Component c, Class desiredClass) {
        return desiredClass == null ? DockingManager.getDockable(c) != null
                : desiredClass.isAssignableFrom(c.getClass());
    }

    /**
     * Adds a {@code DockingListener} to observe docking events for this
     * {@code DockingPort}. {@code null} arguments are ignored.
     * 
     * @param listener
     *            the {@code DockingListener} to add to this {@code DockingPort}.
     * @see DockingMonitor#addDockingListener(DockingListener)
     * @see #getDockingListeners()
     * @see #removeDockingListener(DockingListener)
     */
    public void addDockingListener(DockingListener listener) {
        if (listener != null)
            dockingListeners.add(listener);
    }

    /**
     * Returns an array of all {@code DockingListeners} added to this
     * {@code DockingPort}. If there are no listeners present for this
     * {@code DockingPort}, then a zero-length array is returned.
     * 
     * @return an array of all {@code DockingListeners} added to this
     *         {@code DockingPort}.
     * @see DockingMonitor#getDockingListeners()
     * @see #addDockingListener(DockingListener)
     * @see #removeDockingListener(DockingListener)
     */
    public DockingListener[] getDockingListeners() {
        return (DockingListener[]) dockingListeners
                .toArray(new DockingListener[0]);
    }

    /**
     * Removes the specified {@code DockingListener} from this
     * {@code DockingPort}. If the specified {@code DockingListener} is
     * {@code null}, or the listener has not previously been added to this
     * {@code DockingPort}, then no {@code Exception} is thrown and no action
     * is taken.
     * 
     * @param listener
     *            the {@code DockingListener} to remove from this
     *            {@code DockingPort}
     * @see DockingMonitor#removeDockingListener(DockingListener)
     * @see #addDockingListener(DockingListener)
     * @see #getDockingListeners()
     */
    public void removeDockingListener(DockingListener listener) {
        if (listener != null)
            dockingListeners.remove(listener);
    }

    /**
     * No operation. Provided as a method stub to fulfull the
     * {@code DockingListener} interface contract.
     * 
     * @param evt
     *            the {@code DockingEvent} to respond to.
     * @see DockingListener#dockingCanceled(DockingEvent)
     */
    public void dockingCanceled(DockingEvent evt) {
    }

    /**
     * Requests activation for the newly docked Dockable.
     * 
     * @param evt
     *            the {@code DockingEvent} to respond to.
     * @see DockingListener#dockingComplete(DockingEvent)
     */
    public void dockingComplete(DockingEvent evt) {
        Dockable dockable = evt.getDockable();
        if (dockable == null || !isShowing() || evt.getNewDockingPort() != this)
            return;

        ActiveDockableTracker
                .requestDockableActivation(dockable.getComponent());
    }

    /**
     * No operation. Provided as a method stub to fulfull the
     * {@code DockingListener} interface contract.
     * 
     * @param evt
     *            the {@code DockingEvent} to respond to.
     * @see DockingListener#dragStarted(DockingEvent)
     */
    public void dragStarted(DockingEvent evt) {
    }

    /**
     * No operation. Provided as a method stub to fulfull the
     * {@code DockingListener} interface contract.
     * 
     * @param evt
     *            the {@code DockingEvent} to respond to.
     * @see DockingListener#dropStarted(DockingEvent)
     */
    public void dropStarted(DockingEvent evt) {
    }

    /**
     * No operation. Provided as a method stub to fulfull the
     * {@code DockingListener} interface contract.
     * 
     * @param evt
     *            the {@code DockingEvent} to respond to.
     * @see DockingListener#undockingComplete(DockingEvent)
     */
    public void undockingComplete(DockingEvent evt) {
    }

    /**
     * No operation. Provided as a method stub to fulfull the
     * {@code DockingListener} interface contract.
     * 
     * @param evt
     *            the {@code DockingEvent} to respond to.
     * @see DockingListener#undockingStarted(DockingEvent)
     */
    public void undockingStarted(DockingEvent evt) {
    }

    /**
     * Returns a {@code DockingPortPropertySet} instance associated with this
     * {@code DockingPort}. This method returns the default implementation
     * supplied by the framework by invoking
     * {@code getDockingPortPropertySet(DockingPort port)} on
     * {@code org.flexdock.docking.props.PropertyManager} and supplying an
     * argument of {@code this}.
     * 
     * @return the {@code DockingPortPropertySet} associated with this
     *         {@code DockingPort}. This method will not return a {@code null}
     *         reference.
     * @see DockingPortPropertySet
     * @see DockingPort#getDockingProperties()
     * @see org.flexdock.docking.props.PropertyManager#getDockingPortPropertySet(DockingPort)
     */
    public DockingPortPropertySet getDockingProperties() {
        return PropertyManager.getDockingPortPropertySet(this);
    }

    /**
     * Enables or disables drag support for docking operations on the tabs used
     * within an embedded tabbed layout. If tab-drag-source is enabled, then the
     * tab that corresponds to a {@code Dockable} within an embedded tabbed
     * layout will respond to drag events as if the tab were a component
     * included within the {@code List} returned by calling
     * {@code getDragSources()} on the {@code Dockable}. This allows dragging a
     * tab to initiate drag-to-dock operations.
     * 
     * @param enabled
     *            {@code true} if drag-to-dock support should be enabled for
     *            tabs and their associated {@code Dockables}, {@code false}
     *            otherwise.
     * @see #isTabsAsDragSource()
     * @see Dockable#getDragSources()
     */
    public void setTabsAsDragSource(boolean enabled) {
        tabsAsDragSource = enabled;
    }

    /**
     * Returns {@code true} if drag-to-dock support is enabled for tabs and
     * their associated {@code Dockables}, {@code false} otherwise. If
     * tab-drag-source is enabled, then the tab that corresponds to a
     * {@code Dockable} within an embedded tabbed layout will respond to drag
     * events as if the tab were a component included within the {@code List}
     * returned by calling {@code getDragSources()} on the {@code Dockable}.
     * This allows dragging a tab to initiate drag-to-dock operations.
     * 
     * @return {@code true} if drag-to-dock support is enabled for tabs and
     *         their associated {@code Dockables}, {@code false} otherwise.
     * @see #setTabsAsDragSource(boolean)
     * @see Dockable#getDragSources()
     */
    public boolean isTabsAsDragSource() {
        return tabsAsDragSource;
    }

    protected int getInitTabPlacement() {
        return getDockingProperties().getTabPlacement().intValue();
    }

    /**
     * Returns a boolean indicating whether or not this {@code DockingPort} is
     * nested within another {@code DockingPort}. If there are no other
     * {@code DockingPorts} within this {@code DockingPort's} container ancestor
     * hierarchy, then this method will return {@code true}. Otherwise, this
     * method will return {@code false}. If the this {@code DockingPort} is not
     * validated and/or is not part of a container hierarchy, this method should
     * return {@code true}.
     * 
     * @return {@code false} if this {@code DockingPort} is nested within
     *         another {@code DockingPort}, {@code true} otherwise.
     * @see DockingPort#isRoot()
     */
    public boolean isRoot() {
        return rootPort;
    }

    /**
     * This method is used internally by the framework to notify
     * {@code DefaultDockingPorts} whether they are "root" {@code DockingPorts}
     * according to the rules specified by {@code isRoot()} on the
     * {@code DockingPort} interface. <b>This method should not be called by
     * application-level developers.</b> It will most likely be removed in
     * future versions and the logic contained herein will be managed by some
     * type of change listener.
     * 
     * @param root
     *            {@code true} if this is a "root" {@code DockingPort},
     *            {@code false} otherwise.
     * @see #isRoot()
     * @see DockingPort#isRoot()
     */
    public void setRoot(boolean root) {
        this.rootPort = root;
    }

    /**
     * This method is used internally by the framework to notify
     * {@code DefaultDockingPorts} whether a drag operation is or is not
     * currently in progress and should not be called by application-level
     * developers. It will most likely be removed in future versions and the
     * logic contained herein will be managed by some type of change listener.
     * 
     * @param inProgress
     *            {@code true} if a drag operation involving this
     *            {@code DockingPort} is currently in progress, {@code false}
     *            otherwise.
     */
    public void setDragInProgress(boolean inProgress) {
        if (inProgress && dragImage != null)
            return;

        if (!inProgress && dragImage == null)
            return;

        if (inProgress) {
            dragImage = SwingUtility.createImage(this);
        } else {
            dragImage = null;
        }
        repaint();
    }

    /**
     * Overridden to provide enhancements during drag operations. Some
     * {@code DragPreview} implementations may by able to supply a
     * {@code BufferedImage} for this {@code DockingPort} to use for painting
     * operations. This may be useful for cases in which the dimensions of
     * docked {@code Components} are altered in realtime during the drag
     * operation to provide a "ghost" image for the {@code DragPreview}. In
     * this case, visual feedback for altered subcomponents within this
     * {@code DockingPort} may be blocked in favor of a temporary
     * {@code BufferedImage} for the life of the drag operation.
     * 
     * @param g
     *            the {@code Graphics} context in which to paint
     * @see JComponent#paint(java.awt.Graphics)
     */
    public void paint(Graphics g) {
        if (dragImage == null) {
            super.paint(g);
            return;
        }

        g.drawImage(dragImage, 0, 0, this);
    }

    /**
     * Returns a {@code LayoutNode} containing metadata that describes the
     * current layout contained within this {@code DefaultDockingPort}. The
     * {@code LayoutNode} returned by this method will be a
     * {@code DockingPortNode} that constitutes the root of a tree structure
     * containing various {@code DockingNode} implementations; specifically
     * {@code SplitNode}, {@code DockableNode}, and {@code DockingPortNode}.
     * Each of these nodes is {@code Serializable}, implying the
     * {@code LayoutNode} itself may be written to external storage and later
     * reloaded into this {@code DockingPort} via
     * {@code importLayout(LayoutNode node)}.
     * 
     * @return a {@code LayoutNode} representing the current layout state within
     *         this {@code DockingPort}
     * @see DockingPort#importLayout(LayoutNode)
     * @see #importLayout(LayoutNode)
     * @see org.flexdock.docking.state.LayoutManager#createLayout(DockingPort)
     * @see LayoutNode
     * @see org.flexdock.docking.state.tree.DockingNode
     * @see DockingPortNode
     * @see SplitNode
     * @see DockableNode
     */
    public LayoutNode exportLayout() {
        return DockingManager.getLayoutManager().createLayout(this);
    }

    /**
     * Clears out the existing layout within this {@code DockingPort} and
     * reconstructs a new layout based upon the specified {@code LayoutNode}.
     * <p>
     * At present, this method can only handle {@code LayoutNodes} that have
     * been generated by {@code DefaultDockingPort's} {@code exportLayout()}
     * method. If the specified {@code LayoutNode} is {@code null} or is
     * otherwise <i>not</i> an instance of {@code DockingPortNode}, then this
     * method returns immediately with no action taken.
     * <p>
     * Otherwise, the necessary {@code Dockables} are docked within this
     * {@code DockingPort} and all subsequently generated sub-{@code DockingPorts}
     * in a visual configuration mandated by the tree structure modeled by the
     * specified {@code LayoutNode}.
     * 
     * @param node
     *            the {@code LayoutNode} whose layout is to be instantiated
     *            within this {@code DockingPort}
     * @see DockingPort#importLayout(LayoutNode)
     * @see #exportLayout()
     * @see LayoutNode
     * @see org.flexdock.docking.state.tree.DockingNode
     * @see DockingPortNode
     * @see SplitNode
     * @see DockableNode
     */
    public void importLayout(LayoutNode node) {
        if (!(node instanceof DockingPortNode))
            return;

        node.setUserObject(this);
        ArrayList splitPaneResizeList = new ArrayList();
        constructLayout(node, splitPaneResizeList);
        deferSplitPaneValidation(splitPaneResizeList);
        revalidate();
    }

    private void constructLayout(LayoutNode node, ArrayList splitPaneResizeList) {
        // load the user object. this object isn't used here, but
        // LayoutNode should have a lazy-load mechanism for loading of
        // userObject at runtime. we just want to make sure the userObject has
        // been loaded before we proceed.
        Object obj = node.getUserObject();
        if (node instanceof SplitNode)
            splitPaneResizeList.add(node);

        for (Enumeration en = node.children(); en.hasMoreElements();) {
            LayoutNode child = (LayoutNode) en.nextElement();
            constructLayout(child, splitPaneResizeList);
        }

        if (node instanceof SplitNode)
            reconstruct((SplitNode) node);
        else if (node instanceof DockingPortNode)
            reconstruct((DockingPortNode) node);
    }

    private void reconstruct(DockingPortNode node) {
        DefaultDockingPort port = (DefaultDockingPort) node.getDockingPort();

        if (node.isSplit()) {
            SplitNode child = (SplitNode) node.getChildAt(0);
            JSplitPane split = child.getSplitPane();
            // float percentage = child.getPercentage();
            port.setComponent(split);
            port.evaluateDockingBorderStatus();
            return;
        }

        for (Enumeration en = node.children(); en.hasMoreElements();) {
            LayoutNode child = (LayoutNode) en.nextElement();
            if (child instanceof DockableNode) {
                Dockable dockable = ((DockableNode) child).getDockable();
                port.dock(dockable, CENTER_REGION);
            }
        }
    }

    private void reconstruct(SplitNode node) {
        JSplitPane split = node.getSplitPane();
        Component left = node.getLeftComponent();
        Component right = node.getRightComponent();
        split.setLeftComponent(left);
        split.setRightComponent(right);
    }

    private void deferSplitPaneValidation(final ArrayList splitNodes) {
        Thread t = new Thread() {
            public void run() {
                // poor man's yield(), intended for finer grained control across
                // platforms
                Utilities.sleep(15);
                Runnable r = new Runnable() {
                    public void run() {
                        processImportedSplitPaneValidation(splitNodes);
                    }
                };
                EventQueue.invokeLater(r);
            }
        };
        t.start();
    }

    private void processImportedSplitPaneValidation(ArrayList splitNodes) {
        int len = splitNodes.size();
        if (len == 0)
            return;

        // first, check to see if we're ready for rendering
        SplitNode node = (SplitNode) splitNodes.get(0);
        JSplitPane split = node.getSplitPane();
        int size = split.getOrientation() == JSplitPane.HORIZONTAL_SPLIT ? split
                .getWidth()
                : split.getHeight();
        // if we're not ready to render, then defer processing again until later
        if (!split.isValid() || !split.isVisible() || size == 0) {
            // try to validate first
            if (!split.isValid())
                split.validate();
            // now redispatch
            deferSplitPaneValidation(splitNodes);
            return;
        }

        // if we're ready to render, then loop through all the splitNodes and
        // set the split dividers to their appropriate locations.
        for (int i = 0; i < len; i++) {
            node = (SplitNode) splitNodes.get(i);
            split = node.getSplitPane();
            size = split.getOrientation() == JSplitPane.HORIZONTAL_SPLIT ? split
                    .getWidth()
                    : split.getHeight();
            float percent = node.getPercentage();
            int divLoc = (int) ((float) size * percent);
            // System.err.println("percent: " + percent + ", divLoc: " +
            // divLoc);
            split.setDividerLocation(divLoc);

            // make sure to invoke the installed BorderManager how that we have
            // a hierarchy of DockingPorts. otherwise, we may end up with some
            // ugly nested borders.
            DockingPort port = DockingUtility.getParentDockingPort(split);
            if (port instanceof DefaultDockingPort)
                ((DefaultDockingPort) port).evaluateDockingBorderStatus();

            split.validate();
        }
    }

    // --- maximization

    // remember information to restore state after maximized dockable has left
    // us again
    private MaximizationInstallInfo maximizationInstallInfo;

    public void installMaximizedDockable(Dockable dockable) {
        if (maximizationInstallInfo != null) {
            throw new IllegalStateException("Already maximized");
        }
        maximizationInstallInfo = new MaximizationInstallInfo(
                getDockedComponent(), getBorder());

        Component newComponent = dockable.getComponent();
        setComponent(newComponent);
        evaluateDockingBorderStatus();
        revalidate();
    }

    public void uninstallMaximizedDockable() {
        if (maximizationInstallInfo == null) {
            throw new IllegalStateException("No dockable maximized.");
        }

        setComponent(maximizationInstallInfo.getContent());
        setBorder(maximizationInstallInfo.getBorder());
        maximizationInstallInfo = null;
        revalidate();
    }

    // remember necessary information to restore state after maximized dockable
    // returns
    private MaximizationReleaseInfo maximizationReleaseInfo;

    public void releaseForMaximization(Dockable dockable) {
        if (maximizationReleaseInfo != null) {
            throw new IllegalStateException(
                    "Already released a Dockable for maximization.");
        }

        Component comp = dockable.getComponent();
        Border border = null;
        if (comp instanceof JComponent) {
            border = ((JComponent) comp).getBorder();
        }
        maximizationReleaseInfo = new MaximizationReleaseInfo(comp, border);

        Component docked = getDockedComponent();
        if (docked == null) {
            throw new IllegalStateException("DefaultDockingPort is empty.");
        } else if (docked instanceof JSplitPane) {
            // this should never happen since in that case this DockingPort
            // can't
            // be the direct parent of any dockable requesting maximization
            throw new IllegalStateException(
                    "DefaultDockingPort does not directly contain a Dockable");
        } else if (docked instanceof JTabbedPane) {
            // this is the tricky case, we have to store layout of tabbed pane
            // to restore later
            JTabbedPane tabs = (JTabbedPane) docked;
            maximizationReleaseInfo.setTabIndex(getTabIndex(tabs, comp));
            tabs.remove(comp);
        } else {
            // check if our component is the one that requested maximization
            if (comp != docked) {
                throw new IllegalStateException(
                        "Dockable requesting maximization is not the one docked in this DefaultDockingPort.");
            }
            remove(comp);
        }

    }

    private int getTabIndex(JTabbedPane tabs, Component comp) {
        int tabCount = tabs.getTabCount();
        for (int i = 0; i < tabCount; i++) {
            if (tabs.getComponentAt(i) == comp) {
                return i;
            }
        }
        return -1;
    }

    public void returnFromMaximization() {

        Component comp = maximizationReleaseInfo.getContent();
        if (comp instanceof JComponent) {
            ((JComponent) comp).setBorder(maximizationReleaseInfo.getBorder());
        }
        int tabIndex = maximizationReleaseInfo.getTabIndex();
        maximizationReleaseInfo = null;

        Component docked = getDockedComponent();
        if (docked != null && docked instanceof JTabbedPane) {
            // return dockable to its original (TODO) position in the
            // JTabbedPane
            JTabbedPane tabs = (JTabbedPane) docked;
            tabs.add(comp, getValidTabTitle(tabs, comp), tabIndex);
            tabs.setSelectedIndex(tabIndex);
        } else {
            // return dockable as direct child
            setComponent(comp);
        }

        revalidate();
    }

    private static class MaximizationInstallInfo {
        private final Component content;

        private final Border border;

        public MaximizationInstallInfo(Component content, Border border) {
            this.content = content;
            this.border = border;
        }

        public Border getBorder() {
            return border;
        }

        public Component getContent() {
            return content;
        }
    }

    private static class MaximizationReleaseInfo extends
            MaximizationInstallInfo {
        private int tabIndex;

        public MaximizationReleaseInfo(Component content, Border border) {
            super(content, border);
        }

        public int getTabIndex() {
            return tabIndex;
        }

        public void setTabIndex(int tabIndex) {
            this.tabIndex = tabIndex;
        }
    }
}
