/*
 * Created on Mar 14, 2005
 */
package org.flexdock.util;

import java.awt.Component;
import java.awt.Container;
import java.awt.Point;

import javax.swing.JComponent;
import javax.swing.JRootPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;

import org.flexdock.docking.Dockable;
import org.flexdock.docking.DockingConstants;
import org.flexdock.docking.DockingManager;
import org.flexdock.docking.DockingPort;
import org.flexdock.docking.defaults.DefaultRegionChecker;
import org.flexdock.docking.props.DockablePropertySet;
import org.flexdock.docking.state.DockingState;
import org.flexdock.docking.state.MinimizationManager;

/**
 * @author Christopher Butler
 */
public class DockingUtility implements DockingConstants {
    private DockingUtility() {
        // does nothing
    }

    /**
     * Returns the {@code DockingPort} that contains the specified
     * {@code Dockable}. If the {@code Dockable} is {@code null}, then a
     * {@code null} reference is returned.
     * <p>
     * This method will only return the immediate parent {@code DockingPort} of
     * the specified {@code Dockable} This means that a check is performed for
     * the {@code Component} returned by the {@code Dockable's}
     * {@code getComponent()} method. The {@code DockingPort} returned by this
     * method will not only be an ancestor {@code Container} of this
     * {@code Component}, but invoking the {@code DockingPort's}
     * {@code isParentDockingPort(Component comp)} with the this
     * {@code Component} will also return {@code true}. If both of these
     * conditions cannot be satisfied, then this method returns a {@code null}
     * reference.
     *
     * @param d
     *            the {@code Dockable} whose parent {@code DockingPort} is to be
     *            returned.
     * @return the imediate parent {@code DockingPort} that contains the
     *         specified {@code Dockable}.
     * @see #getParentDockingPort(Component)
     */
    public static DockingPort getParentDockingPort(Dockable d) {
        return d == null ? null : getParentDockingPort(d.getComponent());
    }

    /**
     * Returns the {@code DockingPort} that contains the specified
     * {@code Component}. If the {@code Component} is {@code null}, then a
     * {@code null} reference is returned.
     * <p>
     * This method will only return the immediate parent {@code DockingPort} of
     * the specified {@code Component} This means that the {@code DockingPort}
     * returned by this method will not only be an ancestor {@code Container} of
     * the specified {@code Component}, but invoking its
     * {@code isParentDockingPort(Component comp)} with the specified
     * {@code Component} will also return {@code true}. If both of these
     * conditions cannot be satisfied, then this method returns a {@code null}
     * reference.
     *
     * @param comp
     *            the {@code Component} whose parent {@code DockingPort} is to
     *            be returned.
     * @return the immediate parent {@code DockingPort} that contains the
     *         specified {@code Component}.
     */
    public static DockingPort getParentDockingPort(Component comp) {
        DockingPort port = comp == null ? null : (DockingPort) SwingUtilities
                           .getAncestorOfClass(DockingPort.class, comp);
        if (port == null)
            return null;

        return port.isParentDockingPort(comp) ? port : null;
    }

    /**
     * Returns {@code true} if the specified {@code DockingPort} has an ancestor
     * {@code DockingPort}; {@code false} otherwise. If the specified
     * {@code DockingPort} is {@code null}, then this method returns
     * {@code false}.
     *
     * @param dockingPort
     *            the {@code DockingPort} to check for an ancestor port
     * @return {@code true} if the specified {@code DockingPort} has an ancestor
     *         {@code DockingPort}; {@code false} otherwise.
     * @see SwingUtilities#getAncestorOfClass(java.lang.Class,
     *      java.awt.Component)
     */
    public static boolean isSubport(DockingPort dockingPort) {
        return dockingPort == null ? false : SwingUtilities.getAncestorOfClass(
                   DockingPort.class, (Component) dockingPort) != null;
    }

    /**
     * Returns the deepest {@code DockingPort} within the specified
     * {@code Container} at the specified {@code location}. If either
     * {@code container} or {@code location} are {@code null}, then this method
     * returns {@code null}.
     * <p>
     * This method will find the deepest {@code Component} within the specified
     * container that the specified {@code Point} via
     * {@code SwingUtilities.getDeepestComponentAt(Component parent, int x, int y)}.
     * If no {@code Component} is resovled, then this method returns
     * {@code null}. If the resolved {@code Component} is a {@code DockingPort},
     * then it is returned. Otherwise, the {@code Component's}
     * {@code DockingPort} ancestor is resovled and returned from
     * {@code SwingUtilities.getAncestorOfClass(Class c, Component comp)},
     * passing {@code DockingPort.class} for the ancestor class parameter.
     *
     * @param container
     *            the {@code Container} within which to find a
     *            {@code DockingPort}.
     * @param location
     *            the point within the specified {@code Container} at which to
     *            search for a {@code DockingPort}.
     * @return the deepest {@code DockingPort} within the specified
     *         {@code Container} at the specified {@code location}.
     * @see SwingUtilities#getDeepestComponentAt(java.awt.Component, int, int)
     * @see SwingUtilities#getAncestorOfClass(java.lang.Class,
     *      java.awt.Component)
     */
    public static DockingPort findDockingPort(Container container,
            Point location) {
        if (container == null || location == null)
            return null;

        Component deepestComponent = SwingUtilities.getDeepestComponentAt(
                                         container, location.x, location.y);
        if (deepestComponent == null)
            return null;

        // we're assured here that the deepest component is both a Component and
        // DockingPort in
        // this case, so we're okay to return here.
        if (deepestComponent instanceof DockingPort)
            return (DockingPort) deepestComponent;

        // getAncestorOfClass() will either return a null or a Container that is
        // also an instance of
        // DockingPort. Since Container is a subclass of Component, we're fine
        // in returning both
        // cases.
        return (DockingPort) SwingUtilities.getAncestorOfClass(
                   DockingPort.class, deepestComponent);
    }

    /**
     * Returns the specified {@code region's} cross-axis equivalent region in
     * accordance with the orientation used by the specified {@code JSplitPane}.
     * If the {@code JSplitPane} is {@code null}, or the specified
     * {@code region} is invalid according to
     * {@code DockingManager.isValidDockingRegion(String region)}, then this
     * method returns {@code null}.
     *
     * {@code NORTH_REGION} and {@code SOUTH_REGION} are considered "vertical"
     * regions, while {@code WEST_REGION} and {@code EAST_REGION} are considered
     * horizontal regions. If the {@code JSplitPane} orientation matches the
     * specified {@code region} orientation, then the original {@code region}
     * value is returned. For instance, if the specified {@code region} is
     * {@code EAST_REGION}, and the {@code JSplitPane} is of a horizontal
     * orientation, then there is no need to translate the {@code region}
     * parameter across axes since its current axis is already horizontal. In
     * this case, {@code EAST_REGION} would be returned by this method.
     * <p>
     * If the axis of the specified {@code region} does not match the
     * orientation of the {@code JSplitPane}, then the region is translated to
     * its cross-axis equivalent and returns. In this case, {@code NORTH_REGION}
     * will be translated to {@code WEST_REGION}, {@code SOUTH_REGION} to
     * {@code EAST_REGION}, {@code WEST_REGION} to {@code NORTH_REGION}, and
     * {@code EAST_REGION} to {@code SOUTH_REGION}. {@code CENTER_REGION} is
     * never altered.
     *
     * @param splitPane
     *            the {@code JSplitPane} whose orientation is to be used as a
     *            target axis
     * @param region
     *            the docking region to translate to the target axis
     * @return the specified {@code region's} cross-axis equivalent region in
     *         accordance with the orientation used by the specified
     *         {@code JSplitPane}.
     * @see DockingManager#isValidDockingRegion(String)
     * @see JSplitPane#getOrientation()
     * @see #isAxisEquivalent(String, String)
     */
    public static String translateRegionAxis(JSplitPane splitPane, String region) {
        if (splitPane == null || !DockingManager.isValidDockingRegion(region))
            return null;

        boolean horizontal = splitPane.getOrientation() == JSplitPane.HORIZONTAL_SPLIT;
        if (horizontal) {
            if (NORTH_REGION.equals(region))
                region = WEST_REGION;
            else if (SOUTH_REGION.equals(region))
                region = EAST_REGION;
        } else {
            if (WEST_REGION.equals(region))
                region = NORTH_REGION;
            else if (EAST_REGION.equals(region))
                region = SOUTH_REGION;
        }
        return region;
    }

    /**
     * Returns the opposite docking region of the specified {@code region}. For
     * {@code NORTH_REGION}, this method returns {@code SOUTH_REGION}. For
     * {@code SOUTH_REGION}, this method returns {@code NORTH_REGION}. For
     * {@code EAST_REGION}, this method returns {@code WEST_REGION}. For
     * {@code WEST_REGION}, this method returns {@code EAST_REGION}. For
     * {@code CENTER_REGION} or an invalid region, as specified by
     * {@code DockingManager.isValidDockingRegion(String region)}, this method
     * return {@code CENTER_REGION}.
     *
     * @param region
     *            the region whose opposite is to be returned.
     * @return the opposite docking region of the specified {@code region}.
     * @see DockingManager#isValidDockingRegion(String)
     */
    public static String flipRegion(String region) {
        if (!DockingManager.isValidDockingRegion(region)
                || CENTER_REGION.equals(region))
            return CENTER_REGION;

        if (NORTH_REGION.equals(region))
            return SOUTH_REGION;

        if (SOUTH_REGION.equals(region))
            return NORTH_REGION;

        if (EAST_REGION.equals(region))
            return WEST_REGION;

        return EAST_REGION;
    }

    /**
     * Tests for region equivalency between the specified region parameters
     * across horizontal and vertical axes. If either {@code region} or
     * {@code otherRegion} are {@code null} or invalid according to
     * {@code DockingManager.isValidDockingRegion(String region)}, then this
     * method returns {@code false}.
     * <p>
     * Equivalency within the same axis means that the two specified regions are
     * the same value, as each region is unique within its axis. Thus, this
     * method returns {@code true} if {@code region.equals(otherRegion)} returns
     * {@code true}. This includes {@code CENTER_REGION}, which is axis
     * independent.
     * <p>
     * {@code CENTER_REGION} is not an axis equivalent to any region other than
     * itself since it is the only docking region that does not correspond to a
     * horizontal or vertical axis. If either the specified {@code region} or
     * {@code otherRegion} is {@code CENTER_REGION} and the other is not, then
     * this method returns {@code false}.
     * <p>
     * Equivalancy across axes follows a top-to-left and bottom-to-right
     * mapping. In this fashion, {@code NORTH_REGION} and {@code WEST_REGION}
     * are equivalent and {@code SOUTH_REGION} and {@code EAST_REGION} are
     * equivalent. These combination will return {@code true} for this method.
     * All other region combinatinos will cause this method to return
     * {@code false}.
     *
     * @param region
     *            the first region to check for equivalency
     * @param otherRegion
     *            the second region to check for equivalency
     * @return {@code true} if the two specified regions are equal or cross-axis
     *         equivalents, {@code false} otherwise.
     * @see DockingManager#isValidDockingRegion(String)
     */
    public static boolean isAxisEquivalent(String region, String otherRegion) {
        if (!DockingManager.isValidDockingRegion(region)
                || !DockingManager.isValidDockingRegion(otherRegion))
            return false;

        if (region.equals(otherRegion))
            return true;

        if (CENTER_REGION.equals(region))
            return false;

        if (NORTH_REGION.equals(region))
            return WEST_REGION.equals(otherRegion);
        if (SOUTH_REGION.equals(region))
            return EAST_REGION.equals(otherRegion);
        if (EAST_REGION.equals(region))
            return SOUTH_REGION.equals(otherRegion);
        if (WEST_REGION.equals(region))
            return NORTH_REGION.equals(otherRegion);

        return false;
    }

    /**
     * Returns {@code true} if the specified {@code region} is equal to either
     * {@code NORTH_REGION} or {@code WEST_REGION}. Returns {@code false}
     * otherwise.
     *
     * @param region
     *            the {@code region} to check for top or left equivalency
     * @return {@code true} if the specified {@code region} is equal to either
     *         {@code NORTH_REGION} or {@code WEST_REGION}; {@code false}
     *         otherwise.
     * @see DockingConstants#NORTH_REGION
     * @see DockingConstants#WEST_REGION
     */
    public static boolean isRegionTopLeft(String region) {
        return NORTH_REGION.equals(region) || WEST_REGION.equals(region);
    }

    /**
     * Returns the {@code String} docking region for the specified orientation
     * constant. {@code LEFT} maps to {@code WEST_REGION}, {@code RIGHT} maps
     * to {@code EAST_REGION}, {@code TOP} maps to {@code NORTH_REGION},
     * {@code BOTTOM} maps to {@code SOUTH_REGION}, and {@code CENTER} maps to
     * {@code CENTER_REGION}. All other integer values will cause this method
     * to return {@code UNKNOWN_REGION}.
     * <p>
     * All constants, both integer an {@code String} values, can be found on the
     * {@code DockingConstants} interface.
     *
     * @param regionType
     *            the orientation constant to translate into a docking region
     * @return the {@code String} docking region for the specified orientation
     *         constant.
     * @see DockingConstants#LEFT
     * @see DockingConstants#RIGHT
     * @see DockingConstants#TOP
     * @see DockingConstants#BOTTOM
     * @see DockingConstants#CENTER
     * @see DockingConstants#WEST_REGION
     * @see DockingConstants#EAST_REGION
     * @see DockingConstants#NORTH_REGION
     * @see DockingConstants#SOUTH_REGION
     * @see DockingConstants#CENTER_REGION
     * @see DockingConstants#UNKNOWN_REGION
     */
    public static String getRegion(int regionType) {
        switch (regionType) {
        case LEFT:
            return WEST_REGION;
        case RIGHT:
            return EAST_REGION;
        case TOP:
            return NORTH_REGION;
        case BOTTOM:
            return SOUTH_REGION;
        case CENTER:
            return CENTER_REGION;
        default:
            return UNKNOWN_REGION;
        }
    }

    /**
     * Returns {@code true} if the specified {@code Dockable} is currently
     * minimized; {@code false} otherwise. If the {@code Dockable} is
     * {@code null}, then this method returns {@code false}.
     * <p>
     * This method retrieves the current {@code DockingState} instance
     * associated with the {@code Dockable} and calls it's {@code isMinimized()}
     * method to return. {@code DockingState} for the specified {@code Dockable}
     * is queried by calling {@code getDockingState(Dockable dockable)} on the
     * {@code DockingManager's} currently installed {@code LayoutManager}.
     *
     * @param dockable
     *            the {@code Dockable} whose minimized state is to be returned
     * @return {@code true} if the specified {@code Dockable} is currently
     *         minimized; {@code false} otherwise.
     * @see DockingState#isMinimized()
     * @see DockingManager#getLayoutManager()
     * @see org.flexdock.docking.state.LayoutManager#getDockingState(Dockable)
     */
    public static boolean isMinimized(Dockable dockable) {
        if (dockable == null)
            return false;

        DockingState info = getDockingState(dockable);
        return info == null ? false : info.isMinimized();
    }

    /**
     * Returns an {@code int} value representing the current minimization
     * constraint for the specified {@code Dockable}. If the {@code Dockable}
     * is {@code null}, then this method returns
     * {@code MinimizationManager.UNSPECIFIED_LAYOUT_CONSTRAINT}.
     * <p>
     * This method retrieves the current {@code DockingState} instance
     * associated with the {@code Dockable} and calls it's
     * {@code getMinimizedConstraint()} method to return. {@code DockingState}
     * for the specified {@code Dockable} is queried by calling
     * {@code getDockingState(Dockable dockable)} on the
     * {@code DockingManager's} currently installed {@code LayoutManager}.
     *
     * @param dockable
     *            the {@code Dockable} whose minimized constraint is to be
     *            returned
     * @return an {@code int} value representing the current minimization
     *         constraint for the specified {@code Dockable}
     * @see MinimizationManager#UNSPECIFIED_LAYOUT_CONSTRAINT
     * @see DockingState#getMinimizedConstraint()()
     * @see DockingManager#getLayoutManager()
     * @see org.flexdock.docking.state.LayoutManager#getDockingState(Dockable)
     */
    public static int getMinimizedConstraint(Dockable dockable) {
        int defaultConstraint = MinimizationManager.UNSPECIFIED_LAYOUT_CONSTRAINT;
        DockingState info = dockable == null ? null : getDockingState(dockable);
        return info == null ? defaultConstraint : info.getMinimizedConstraint();
    }

    private static DockingState getDockingState(Dockable dockable) {
        return DockingManager.getLayoutManager().getDockingState(dockable);
    }

    /**
     * Docks the specified {@code Dockable} relative to another already-docked
     * {@code Dockable} in the specified region. The "parent" {@code Dockable}
     * must currently be docked. If not, this method will return {@code false}.
     * Otherwise, its parent {@code DockingPort} will be resolved and the new
     * {@code Dockable} will be docked into the {@code DockingPort} relative to
     * the "parent" {@code Dockable}. This method defers processing to
     * {@code dockRelative(Dockable dockable, Dockable parent, String relativeRegion, float ratio)}
     * passing {@code UNSPECIFIED_SIBLING_PREF} for the {@code ratio} parameter.
     * <p>
     * This method returns {@code false} if any of the input parameters are
     * {@code null} or if the specified {@code region} is invalid according to
     * {@code DockingManager.isValidDockingRegion(String region)}. If the
     * specified region is other than CENTER, then a split layout should result.
     *
     * @param dockable
     *            the {@code Dockable} to be docked
     * @param parent
     *            the {@code Dockable} used as a reference point for docking
     * @param relativeRegion
     *            the docking region into which {@code dockable} will be docked
     * @return {@code true} if the docking operation was successful;
     *         {@code false} otherwise.
     * @see #dockRelative(Dockable, Dockable, String, float)
     * @see DockingManager#isValidDockingRegion(String)
     * @see Dockable#getDockingPort()
     * @see DockingManager#dock(Dockable, DockingPort, String)
     */
    public static boolean dockRelative(Dockable dockable, Dockable parent,
                                       String relativeRegion) {
        return dockRelative(parent, dockable, relativeRegion,
                            UNSPECIFIED_SIBLING_PREF);
    }

    /**
     * Docks the specified {@code Dockable} relative to another already-docked
     * {@code Dockable} in the specified region with the specified split
     * proportion. The "parent" {@code Dockable} must currently be docked. If
     * not, this method will return {@code false}. Otherwise, its parent
     * {@code DockingPort} will be resolved and the new {@code Dockable} will be
     * docked into the {@code DockingPort} relative to the "parent"
     * {@code Dockable}. If the specified region is CENTER, then the
     * {@code proportion} parameter is ignored. Otherwise, a split layout should
     * result with the proportional space specified in the {@code proportion}
     * parameter allotted to the {@code dockable} argument.
     * <p>
     * This method returns {@code false} if any of the input parameters are
     * {@code null} or if the specified {@code region} is invalid according to
     * {@code DockingManager.isValidDockingRegion(String region)}.
     *
     * @param dockable
     *            the {@code Dockable} to be docked
     * @param parent
     *            the {@code Dockable} used as a reference point for docking
     * @param relativeRegion
     *            the docking region into which {@code dockable} will be docked
     * @param ratio
     *            the proportional space to allot the {@code dockable} argument
     *            if the docking operation results in a split layout.
     * @return {@code true} if the docking operation was successful;
     *         {@code false} otherwise.
     * @see DockingManager#isValidDockingRegion(String)
     * @see Dockable#getDockingPort()
     * @see DockingManager#dock(Dockable, DockingPort, String)
     */
    public static boolean dockRelative(Dockable dockable, Dockable parent,
                                       String relativeRegion, float ratio) {
        if (parent == null || dockable == null
                || !DockingManager.isValidDockingRegion(relativeRegion))
            return false;

        // set the sibling preference
        setSiblingPreference(parent, relativeRegion, ratio);

        DockingPort port = parent.getDockingPort();
        if (port != null)
            return DockingManager.dock(dockable, port, relativeRegion);

        return false;
    }

    private static void setSiblingPreference(Dockable src, String region,
            float size) {
        if (size == UNSPECIFIED_SIBLING_PREF || CENTER_REGION.equals(region)
                || !DockingManager.isValidDockingRegion(region))
            return;

        size = DefaultRegionChecker.validateSiblingSize(size);
        src.getDockingProperties().setSiblingSize(region, size);
    }

    /**
     * Returns {@code true} if the specified {@code Dockable} is currently
     * docked within a floating dialog. This method returns {@code false} if the
     * {@code Dockable} is presently, minimized, hidden, docked within the main
     * application layout, or if the {@code Dockable} parameter is {@code null}.
     * <p>
     * This method retrieves the current {@code DockingState} instance
     * associated with the {@code Dockable} and calls it's {@code isFloating()}
     * method to return. {@code DockingState} for the specified {@code Dockable}
     * is queried by calling {@code getDockingState(Dockable dockable)} on the
     * {@code DockingManager's} currently installed {@code LayoutManager}.
     *
     * @param dockable
     *            the {@code Dockable} whose floating state is to be returned
     * @return {@code true} if the specified {@code Dockable} is currently
     *         floating; {@code false} otherwise.
     * @see DockingState#isFloating()
     * @see DockingManager#getLayoutManager()
     * @see org.flexdock.docking.state.LayoutManager#getDockingState(Dockable)
     */
    public static boolean isFloating(Dockable dockable) {
        DockingState info = getDockingState(dockable);
        return info == null ? false : info.isFloating();
    }

    /**
     * Returns {@code true} if the specified {@code Dockable} is currently
     * docked within a {@code DockingPort}. This method returns {@code false}
     * if the {@code Dockable} is presently floating, minimized, hidden, or if
     * the {@code Dockable} parameter is {@code null}.
     *
     * @param dockable
     *            the {@code Dockable} whose embedded state is to be returned
     * @return {@code true} if the specified {@code Dockable} is currently
     *         docked within a {@code DockingPort}; {@code false} otherwise.
     * @see DockingManager#isDocked(Dockable)
     * @see #isFloating(Dockable)
     */
    public static boolean isEmbedded(Dockable dockable) {
        return dockable == null ? false : DockingManager.isDocked(dockable)
               && !isFloating(dockable);
    }

    /**
     * Sets the divider location of the split layout embedded within the
     * specified {@code DockingPort}. This method differs from both
     * {@code setSplitProportion(Dockable dockable, float proportion)} in that
     * this method resolves the split layout embedded <b>within</b> the
     * specified {@code DockingPort}, whereas the other method modifies the
     * split layout <b>containing</b> its respective {@code Dockable}
     * parameter.
     * <p>
     * The resulting divider location will be a percentage of the split layout
     * size based upon the {@code proportion} parameter. Valid values for
     * {@code proportion} range from {@code 0.0F{@code  to {@code 1.0F}. For
     * example, a {@code proportion} of {@code 0.3F} will move the divider to
     * 30% of the "size" (<i>width</i> for horizontal split, <i>height</i>
     * for vertical split) of the split container embedded within the specified
     * {@code DockingPort}. If a {@code proportion} of less than {@code 0.0F}
     * is supplied, the value }0.0F} is used. If a {@code proportion} greater
     * than {@code 1.0F} is supplied, the value }1.0F} is used.
     * <p>
     * This method should be effective regardless of whether the split layout in
     * question has been fully realized and is currently visible on the screen.
     * This should alleviate common problems associated with setting percentages
     * of unrealized {@code Component} dimensions, which are initially
     * {@code 0x0} before the {@code Component} has been rendered to the screen.
     * <p>
     * If the specified {@code DockingPort} is {@code null}, then no
     * {@code Exception} is thrown and no action is taken. Identical behavior
     * occurs if the {@code DockingPort} does not contain split layout.
     *
     * @param port
     *            the {@code DockingPort} containing the split layout is to be
     *            resized.
     * @param proportion
     *            the percentage of split layout size to which the split divider
     *            should be set.
     * @see SwingUtility#setSplitDivider(JSplitPane, float)
     */
    public static void setSplitProportion(DockingPort port, float proportion) {
        if (port == null)
            return;

        Component comp = port.getDockedComponent();
        if (comp instanceof JSplitPane)
            SwingUtility.setSplitDivider((JSplitPane) comp, proportion);
    }

    /**
     * Sets the divider location of the split layout containing the specified
     * dockable {@code Component}.
     * <p>
     * The resulting divider location will be a percentage of the split layout
     * size based upon the {@code proportion} parameter. Valid values for
     * {@code proportion} range from {@code 0.0F{@code  to {@code 1.0F}. For
     * example, a {@code proportion} of {@code 0.3F} will move the divider to
     * 30% of the "size" (<i>width</i> for horizontal split, <i>height</i>
     * for vertical split) of the split container that contains the specified
     * {@code Dockable}. If a {@code proportion} of less than {@code 0.0F} is
     * supplied, the value }0.0F} is used. If a {@code proportion} greater than
     * {@code 1.0F} is supplied, the value }1.0F} is used.
     * <p>
     * It is important to note that the split divider location is only a
     * percentage of the container size from left to right or top to bottom. A
     * {@code proportion} of {@code 0.3F} does not imply that {@code dockable}
     * itself will be allotted 30% of the available space. The split divider
     * will be moved to the 30% position of the split container regardless of
     * the region in which the specified {@code Dockable} resides (which may
     * possibly result in {@code dockable} being allotted 70% of the available
     * space).
     * <p>
     * This method should be effective regardless of whether the split layout in
     * question has been fully realized and is currently visible on the screen.
     * This should alleviate common problems associated with setting percentages
     * of unrealized {@code Component} dimensions, which are initially
     * {@code 0x0} before the {@code Component} has been rendered to the screen.
     * <p>
     * If the specified {@code Dockable} is {@code null}, then no
     * {@code Exception} is thrown and no action is taken. Identical behavior
     * occurs if the {@code Dockable} does not reside within a split layout.
     * <p>
     * If the {@code Dockable} resides within a tabbed layout, a check is done
     * to see if the tabbed layout resides within a parent split layout. If so,
     * the resolved split layout is resized. Otherwise no action is taken.
     *
     * @param dockable
     *            the {@code Dockable} whose containing split layout is to be
     *            resized.
     * @param proportion
     *            the percentage of containing split layout size to which the
     *            split divider should be set.
     * @see SwingUtility#setSplitDivider(JSplitPane, float)
     */
    public static void setSplitProportion(Dockable dockable, float proportion) {
        if (dockable == null)
            return;

        Component comp = dockable.getComponent();
        Container parent = comp.getParent();
        if (parent instanceof JTabbedPane) {
            parent = parent.getParent();
        }
        if (!(parent instanceof DockingPort))
            return;

        Container grandParent = parent.getParent();
        if (grandParent instanceof JSplitPane)
            SwingUtility.setSplitDivider((JSplitPane) grandParent, proportion);
    }

    /**
     * Returns the text to be used by a {@code Dockable} as a tab label within a
     * tabbed layout. This method retrieves the associated
     * {@code DockablePropertySet} by calling {@code getDockingProperties()} on
     * the specified {@code Dockable}. It then returns the value retrieved from
     * calling {@code getDockableDesc()} on the {@code DockablePropertySet}
     * instance. If the specified {@code Dockable} is {@code null}, then this
     * method returns {@code null}.
     *
     * @param dockable
     *            the {@code Dockable} whose tab-text is to be returned
     * @return the text to be used by a {@code Dockable} as a tab label within a
     *         tabbed layout.
     * @see Dockable#getDockingProperties()
     * @see DockablePropertySet#getDockableDesc()
     */
    public static String getTabText(Dockable dockable) {
        DockablePropertySet props = dockable == null ? null : dockable
                                    .getDockingProperties();
        return props == null ? null : props.getDockableDesc();
    }

    /**
     * Returns {@code true} if the specific {@code Object} is a {@code Dockable}.
     * If {@code obj instanceof Dockable} is {@code true}, then this method
     * returns {@code true}. A {@code null} parameter will cause this method to
     * return {@code false}.
     * <p>
     * Registered {@code Dockable} components, if they are {@code JComponents},
     * will also have a {@code Boolean} client property present with the key
     * {@code Dockable.DOCKABLE_INDICATOR}, used by dockable
     * {@code JComponents} that don't implement the {@code Dockable} interface
     * directly, but acquire docking capabilities through a separate wrapper
     * {@code Dockable} implementation. For these components, the
     * {@code instanceof} check is insufficient since the valid {@code Dockable}
     * is implemented by a separate class. Therefore, if the {@code instanceof}
     * check fails, and the supplied {@code Object} parameter is a
     * {@code JComponent}, a client property with the key
     * {@code Dockable.DOCKABLE_INDICATOR} is checked for a value of
     * {@code Boolean.TRUE}. If the client property is present, then this
     * method returns {@code true}.
     *
     * @param obj
     *            the {@code Object} to be checked to see if it represents a
     *            valid {@code Dockable}
     * @return {@code true} if the specific {@code Object} is a {@code Dockable}
     * @see Dockable#DOCKABLE_INDICATOR
     * @see Boolean#TRUE
     * @see javax.swing.JComponent#getClientProperty(java.lang.Object)
     */
    public static boolean isDockable(Object obj) {
        if (obj == null)
            return false;

        // if the object directly implements Dockable, then we can return from
        // here.
        if (obj instanceof Dockable)
            return true;

        // if the object is a JComponent, but not a Dockable implementation,
        // then check its
        // client property indicator
        if (obj instanceof JComponent) {
            Component comp = (Component) obj;
            return SwingUtility.getClientProperty(comp,
                                                  Dockable.DOCKABLE_INDICATOR) == Boolean.TRUE;
        }

        // they may have a heavyweight Component that does not directly
        // implement Dockable.
        // in this case, Component does not have client properties we can check.
        // we'll have to
        // check directly with the DockingManager.
        if (obj instanceof Component) {
            Component comp = (Component) obj;
            return DockingManager.getDockable(comp) != null;
        }

        return false;
    }

    public static Dockable getAncestorDockable(Component comp) {
        if (comp == null)
            return null;

        if (isDockable(comp))
            return DockingManager.getDockable(comp);

        Container parent = comp.getParent();
        while (parent != null && !(parent instanceof JRootPane)) {
            if (isDockable(parent))
                return DockingManager.getDockable(parent);
            parent = parent.getParent();
        }
        return null;
    }

    public static boolean isActive(Dockable dockable) {
        if (dockable == null)
            return false;
        return dockable.getDockingProperties().isActive().booleanValue();
    }
}
