org.flexdock.docking.defaults.DefaultDockingStrategy Maven / Gradle / Ivy
/*
* 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.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Map;
import javax.swing.JSplitPane;
import javax.swing.SwingUtilities;
import javax.swing.plaf.basic.BasicSplitPaneDivider;
import javax.swing.plaf.basic.BasicSplitPaneUI;
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.drag.DragManager;
import org.flexdock.docking.drag.DragOperation;
import org.flexdock.docking.event.DockingEvent;
import org.flexdock.docking.floating.frames.DockingFrame;
import org.flexdock.docking.floating.frames.FloatingDockingPort;
import org.flexdock.docking.state.FloatManager;
import org.flexdock.event.EventManager;
import org.flexdock.util.DockingUtility;
import org.flexdock.util.RootWindow;
import org.flexdock.util.SwingUtility;
/**
* @author Christopher Butler
*/
public class DefaultDockingStrategy implements DockingStrategy,
DockingConstants {
public static final String PREFERRED_PROPORTION = "DefaultDockingStrategy.PREFERRED_PROPORTION";
private static double defaultResizeWeight = -1;
private static boolean constantPercent;
/**
* Returns the specified {@code Dockable's} sibling {@code Dockable} within
* the current docking layout. This method checks the parent
* {@code DockingPort} of a given {@code Dockable} to see if it is split
* equally with another {@code Dockable}. If so, the immediate sibling
* {@code Dockable} is returned. If there are more than two
* {@code Dockables} within the split layout, then the closest sibling
* region is determined and this method dispatches to
* {@code getSibling(Dockable dockable, String region)}.
*
* If the specified {@code Dockable} is {@code null}, or there are no
* siblings available in the docking layout, then this methdo returns a
* {@code null} reference. If the specified {@code Dockable} is not
* currently docked within a {@code DockingPort}, then this method returns
* a {@code null} reference.
*
* @param dockable
* the {@code Dockable} whose sibling is to be returned
* @return the sibling of the specified {@code Dockable} within the current
* docking layout.
* @see Dockable#getDockingPort()
* @see #getSibling(Dockable, String)
*/
public static Dockable getSibling(Dockable dockable) {
if (dockable == null) {
return null;
}
DockingPort port = dockable.getDockingPort();
String startRegion = findRegion(dockable.getComponent());
String region = DockingUtility.flipRegion(startRegion);
Dockable sibling = findDockable(port, dockable.getComponent(), region,
startRegion);
return sibling;
}
/**
* Returns the sibling {@code Dockable} relative to the specified
* {@code Dockable}'s supplied region in the current docking layout. If
* {@code dockable} is {@code null} or {@code region} is either invalid or
* equal to {@code CENTER_REGION}, then this method returns a {@code null}
* reference.
*
* If the specified {@code Dockable} is in a {@code DockingPort} that
* equally splits the layout between two {@code Dockable}s in a fashion that
* matches up with the specified region, then the immediate sibling
* {@code Dockable} is returned. Otherwise, a fuzzy search is performed
* throughout the docking layout for a {@code Dockable} that "looks like" it
* is docked to the supplied region of the specified {@code Dockable} from a
* visual standpoint.
*
* For instance, a docking layout may consist of four quadrants {@code dockable1}
* (top-left), {@code dockable2} (top-right), {@code dockable3}
* (bottom-left) and {@code dockable4} (bottom-right). The layout is built
* by docking {@code dockable2} to the {@code EAST_REGION} of {@code dockable1},
* {@code dockable3} to the {@code SOUTH_REGION} of {@code dockable1}, and
* {@code dockable4} to the {@code SOUTH_REGION} of {@code dockable2}.
* Within this layout, {@code dockable1} and {@code dockable3} are immediate
* siblings, as are {@code dockable2} and {@code dockable4}. Thus,
* requesting sibling NORTH_REGION of {@code dockable3} will easily yield
* {@code dockable1}. However, {@code dockable3} has no immediate
* {@code EAST_REGION} sibling. In this case, a fuzzy search through the
* layout is performed to determine the visual sibling, and this method
* returns {@code dockable4}. Likewise, this method will return a
* {@code null} reference for the {@code WEST_REGION} sibling of
* {@code dockable3}, since there are no {@code Dockable}s in the visual layout
* to the west of this {@code Dockable}.
*
* @param dockable
* the {@code Dockable} whose sibling is to be returned
* @param region
* the region of the specified {@code Dockable} whose visual
* sibling is to be returned
* @return the {@code Dockable} in the supplied region relative to the
* specified {@code Dockable}
*/
public static Dockable getSibling(Dockable dockable, String region) {
if (dockable == null || !DockingManager.isValidDockingRegion(region)
|| CENTER_REGION.equals(region)) {
return null;
}
DockingPort port = dockable.getDockingPort();
String startRegion = findRegion(dockable.getComponent());
Dockable sibling = findDockable(port, dockable.getComponent(), region,
startRegion);
return sibling;
}
private static Dockable findDockable(DockingPort port, Component self,
String region, String startRegion) {
if (port == null) {
return null;
}
Component docked = port.getDockedComponent();
// if we're not a split port, then there is no concept of 'outer
// regions'.
// jump up a level to find the parent split port
if (!(docked instanceof JSplitPane)) {
DockingPort superPort = DockingManager
.getDockingPort((Component) port);
return findDockable(superPort, self, region, startRegion);
}
Component sibling = port.getComponent(region);
if (sibling == self) {
if (!(self instanceof JSplitPane)) {
DockingPort superPort = DockingManager
.getDockingPort((Component) port);
return findDockable(superPort, self, region, startRegion);
}
return null;
}
if (sibling instanceof JSplitPane) {
// go one level deeper
DockingPort subPort = DockingManager.getDockingPort(sibling);
Component other = port.getComponent(DockingUtility
.flipRegion(region));
String subRegion = findSubRegion((JSplitPane) sibling, other,
region, startRegion);
return findDockable(subPort, self, subRegion, startRegion);
}
// if we have no direct sibling in the specified region, the jump
// up a level.
if (sibling == null) {
DockingPort superPort = DockingManager
.getDockingPort((Component) port);
self = port.getDockedComponent();
return findDockable(superPort, self, region, startRegion);
}
return DockingManager.getDockable(sibling);
}
private static String findSubRegion(JSplitPane split, Component other,
String targetRegion, String baseRegion) {
String region = DockingUtility.translateRegionAxis(split, targetRegion);
if (!(other instanceof JSplitPane)) {
return region;
}
boolean translated = !targetRegion.equals(region);
if (translated && !DockingUtility.isAxisEquivalent(region, baseRegion)) {
region = DockingUtility.flipRegion(region);
}
return region;
}
/**
* Returns the docking region within the current split docking layout
* containing the specified {@code Component}. If {@code comp} is
* {@code null}, then a {@code null} reference is returned. If {@code comp}
* is not in a split layout, then {@code CENTER_REGION} is returned.
*
* This method resolves the associated {@code Dockable} and
* {@code DockingPort} for the specified {@code Component} and backtracks
* through the docking layout to find a split layout. If a split layout is
* found, then the region retured by this method is calculated relative to
* its sibling in the layout.
*
* @param comp
* the {@code Component} whose region is to be returned
* @return the region of the current split layout containing the specified
* {@code Dockable}
*/
public static String findRegion(Component comp) {
if (comp == null) {
return null;
}
DockingPort port = DockingManager.getDockingPort(comp);
Component docked = port.getDockedComponent();
if (!(docked instanceof JSplitPane)) {
// we didn't find a split pane, to check the grandparent dockingport
DockingPort superPort = DockingManager
.getDockingPort((Component) port);
// if there was no grandparent DockingPort, then we're stuck with
// the docked
// component we already found. this can happen on the root
// dockingport.
docked = superPort == null ? docked : superPort
.getDockedComponent();
}
if (!(docked instanceof JSplitPane)) {
return CENTER_REGION;
}
JSplitPane split = (JSplitPane) docked;
boolean horiz = split.getOrientation() == JSplitPane.HORIZONTAL_SPLIT;
Component left = split.getLeftComponent();
if (left == port) {
return horiz ? WEST_REGION : NORTH_REGION;
}
return horiz ? EAST_REGION : SOUTH_REGION;
}
/**
* Docks the specified {@code Dockable} into the supplied {@code region} of
* the specified {@code DockingPort}. This method is meant for programmatic
* docking, as opposed to realtime, event-based docking operations. As such,
* it defers processing to
* {@code dock(Dockable dockable, DockingPort port, String region, DragOperation token)},
* passing a {@code null} argument for the {@code DragOperation} parameter.
* This implies that there is no event-based drag operation currently in
* progress to control the semantics of the docking operation, only that an
* attempt should be made to dock the specified {@code Dockable} into the
* specified {@code DockingPort}.
*
* This method will return {@code false} if {@code dockable} or {@code port}
* are {@code null}, or if {@code region} is not a valid region according
* to the specified {@code DockingPort}. If a {@code Dockable} is currently
* docked within the specified {@code DockingPort}, then that
* {@code Dockable's} territorial properties are also checked and this
* method may return {@code false} if the territory is blocked. Finally,
* this method will return {@code false} if the specified {@code Dockable}
* is already docked within the supplied region of the specified
* {@code DockingPort}.
*
* @param dockable
* the {@code Dockable} we wish to dock
* @param port
* the {@code DockingPort} into which we wish to dock
* @param region
* the region of the specified {@code DockingPort} into which we
* wish to dock.
* @return {@code true} if the docking operation was successful,
* {@code false}. otherwise.
* @see #dock(Dockable, DockingPort, String, DragOperation)
* @see Dockable#getDockingProperties()
* @see org.flexdock.docking.props.DockablePropertySet#isTerritoryBlocked(String)
*/
@Override
public boolean dock(Dockable dockable, DockingPort port, String region) {
return dock(dockable, port, region, null);
}
/**
* Docks the specified {@code Dockable} into the supplied {@code region} of
* the specified {@code DockingPort}. This method is meant for realtime,
* event-based docking based on an in-progress drag operation. It is not
* recommended for developers to call this method programmatically, except
* to pass in a {@code null} {@code DragOperation} argument. *
*
* The {@code DragOperation} parameter, if present, will control the
* semantics of the docking operation based upon current mouse position,
* drag threshold, and a customizable drag context {@code Map}. For
* instance, the {@code DragOperation} may contain information regarding the
* {@code Dockable} over which the mouse is currently hovered, whether the
* user is attempting to drag a {@code Dockable} outside the bounds of any
* existing windows (perhaps in an attempt to float the {@code Dockable}),
* or whether the current distance offset from the original drag point
* sufficiently warrants a valid docking operation.
*
* If the {@code DragOperation} is {@code null}, then this method will
* attempt to programmatically dock the specified {@code Dockable} into the
* supplied {@code region} of the specified {@code DockingPort} without
* regard to external event-based criteria. This is in accordance with the
* behavior specified by
* {@code dock(Dockable dockable, DockingPort port, String region)}.
*
* This method will return {@code false} if {@code dockable} or {@code port}
* are {@code null}, or if {@code region} is not a valid region according
* to the specified {@code DockingPort}. If a {@code Dockable} is currently
* docked within the specified {@code DockingPort}, then that
* {@code Dockable's} territorial properties are also checked and this
* method may return {@code false} if the territory is blocked. If a
* {@code DragOperation} is present, then this method will return
* {@code false} if the required drag threshold has not been exceeded.
* Finally, this method will return {@code false} if the specified
* {@code Dockable} is already docked within the supplied region of the
* specified {@code DockingPort}.
*
* @param dockable
* the {@code Dockable} we wish to dock
* @param port
* the {@code DockingPort} into which we wish to dock
* @param region
* the region of the specified {@code DockingPort} into which we
* wish to dock.
* @return {@code true} if the docking operation was successful,
* {@code false}. otherwise.
* @see #dock(Dockable, DockingPort, String, DragOperation)
* @see Dockable#getDockingProperties()
* @see org.flexdock.docking.props.DockablePropertySet#isTerritoryBlocked(String)
*/
@Override
public boolean dock(Dockable dockable, DockingPort port, String region,
DragOperation operation) {
if (!isDockingPossible(dockable, port, region, operation)) {
return false;
}
if (!dragThresholdElapsed(operation)) {
return false;
}
// cache the old parent
DockingPort oldPort = dockable.getDockingPort();
// perform the drop operation.
DockingResults results = dropComponent(dockable, port, region,
operation);
// perform post-drag operations
DockingPort newPort = results.dropTarget;
int evtType = results.success ? DockingEvent.DOCKING_COMPLETE
: DockingEvent.DOCKING_CANCELED;
Map dragContext = DragManager.getDragContext(dockable);
DockingEvent evt = new DockingEvent(dockable, oldPort, newPort,
evtType, dragContext);
// populate DockingEvent status info
evt.setRegion(region);
evt.setOverWindow(operation == null ? true : operation.isOverWindow());
// notify the old docking port, new dockingport,and dockable
Object[] evtTargets = { oldPort, newPort, dockable };
EventManager.dispatch(evt, evtTargets);
return results.success;
}
protected boolean dragThresholdElapsed(DragOperation token) {
if (token == null || token.isPseudoDrag() || token.getStartTime() == -1) {
return true;
}
long elapsed = System.currentTimeMillis() - token.getStartTime();
// make sure the elapsed time of the drag is at least over .2 seconds.
// otherwise, we'll probably be responding to inadvertent clicks (maybe
// double-clicks)
return elapsed > 200;
}
protected boolean isDockingPossible(Dockable dockable, DockingPort port,
String region, DragOperation token) {
// superclass blocks docking if the 'port' or 'region' are null. If
// we've dragged outside
// the bounds of the parent frame, then both of these will be null. This
// is expected here and
// we intend to float in this case.
if (isFloatable(dockable, token)) {
return true;
}
// check to see if we're already floating and we're trying to drop into
// the
// same dialog.
DockingPort oldPort = DockingManager.getDockingPort(dockable);
if (oldPort instanceof FloatingDockingPort && oldPort == port) {
// only allow this situation if we're not the *last* dockable
// in the viewport. if we're removing the last dockable, then
// the dialog will disappear before we redock, and we don't want
// this
// to happen.
FloatingDockingPort floatingDockingPort = (FloatingDockingPort) oldPort;
if (floatingDockingPort.getDockableCount() == 1) {
return false;
}
}
if (dockable == null || dockable.getComponent() == null || port == null) {
return false;
}
if (!DockingManager.isValidDockingRegion(region)) {
return false;
}
Dockable docked = DockingManager.getDockable(port.getDockedComponent());
if (docked == null) {
return true;
}
// don't allow them to dock into this region if the territory there is
// blocked.
if (docked.getDockingProperties().isTerritoryBlocked(region)
.booleanValue()) {
return false;
}
// check to see if we're already docked into this region.
// get the parent dockingPort.
Container container = docked.getComponent().getParent();
// now get the grandparent dockingport
DockingPort grandparent = DockingManager.getDockingPort(container);
// if we don't share the grandparent dockingport, then we're definitely
// not split in the same dockingport
// across different region. in this case, it's ok to proceed with the
// dock
if (grandparent == null) {
return true;
}
Component currentlyInRegion = grandparent.getComponent(region);
// block docking if we're already the component docked within the
// specified region
if (currentlyInRegion == dockable.getComponent()) {
return false;
}
return true;
}
protected boolean isFloatable(Dockable dockable, DragOperation token) {
// can't float null objects
if (dockable == null || dockable.getComponent() == null
|| token == null) {
return false;
}
// can't float on a fake drag operation
if (token.isPseudoDrag()) {
return false;
}
// TODO: break this check out into a separate DropPolicy class.
// should be any customizable criteria, not hardcoded to checking
// for being outside the bounds of a window
if (token.isOverWindow()) {
return false;
}
return true;
}
protected DockingResults dropComponent(Dockable dockable,
DockingPort target, String region, DragOperation token) {
if (isFloatable(dockable, token)) {
return floatComponent(dockable, target, token);
}
DockingResults results = new DockingResults(target, false);
if (UNKNOWN_REGION.equals(region) || target == null) {
return results;
}
Component docked = target.getDockedComponent();
Component dockableCmp = dockable.getComponent();
if (dockableCmp != null && dockableCmp == docked) {
// don't allow docking the same component back into the same port
return results;
}
// obtain a reference to the content pane that holds the target
// DockingPort.
// MUST happen before undock(), in case the undock() operation removes
// the
// target DockingPort from the container tree.
Container contentPane = SwingUtility.getContentPane((Component) target);
Point contentPaneLocation = token == null ? null : token
.getCurrentMouse(contentPane);
// undock the current Dockable instance from it's current parent
// container
undock(dockable);
// when the original parent reevaluates its container tree after
// undocking, it checks to see how
// many immediate child components it has. split layouts and tabbed
// interfaces may be managed by
// intermediate wrapper components. When undock() is called, the docking
// port
// may decide that some of its intermedite wrapper components are no
// longer needed, and it may get
// rid of them. this isn't a hard rule, but it's possible for any given
// DockingPort implementation.
// In this case, the target we had resolved earlier may have been
// removed from the component tree
// and may no longer be valid. to be safe, we'll resolve the target
// docking port again and see if
// it has changed. if so, we'll adopt the resolved port as our new
// target.
if (contentPaneLocation != null && contentPane != null) {
results.dropTarget = DockingUtility.findDockingPort(contentPane,
contentPaneLocation);
target = results.dropTarget;
}
results.success = target.dock(dockableCmp, region);
SwingUtility.revalidate((Component) target);
return results;
}
/**
* Undocks the specified {@code Dockable} from it's parent
* {@code DockingPort}. If {@code dockable} is {@code null} or is not
* currently docked within a {@code DockingPort}, then this method returns
* {@code false}.
*
* @param dockable
* the {@code Dockable} to be undocked.
* @return {@code true} if the undocking operation was successful,
* {@code false} otherwise.
* @see #dock(Dockable, DockingPort, String)
*/
@Override
public boolean undock(Dockable dockable) {
if (dockable == null) {
return false;
}
Component dragSrc = dockable.getComponent();
Container parent = dragSrc.getParent();
RootWindow rootWin = RootWindow.getRootContainer(parent);
// if there's no parent container, then we really don't have anything
// from which to to
// undock this component, now do we?
if (parent == null) {
return false;
}
boolean success = false;
DockingPort dockingPort = DockingUtility.getParentDockingPort(dragSrc);
// notify that we are about to undock
Map dragContext = DragManager.getDragContext(dockable);
DockingEvent dockingEvent = new DockingEvent(dockable, dockingPort,
dockingPort, DockingEvent.UNDOCKING_STARTED, dragContext);
EventManager.dispatch(dockingEvent);
// if(dockingEvent.isConsumed())
// return false;
if (dockingPort != null) {
// if 'dragSrc' is currently docked, then undock it instead of using
// a
// simple remove(). this will allow the DockingPort to do any of its
// own
// cleanup operations associated with component removal.
success = dockingPort.undock(dragSrc);
} else {
// otherwise, just remove the component
parent.remove(dragSrc);
success = true;
}
if (rootWin != null) {
SwingUtility.revalidate(rootWin.getContentPane());
SwingUtility.repaint(rootWin.getContentPane());
}
if (success) {
dockingEvent = new DockingEvent(dockable, dockingPort, dockingPort,
DockingEvent.UNDOCKING_COMPLETE, dragContext);
// notify the docking port and dockable
Object[] evtTargets = { dockingPort, dockable };
EventManager.dispatch(dockingEvent, evtTargets);
}
return success;
}
protected DockingResults floatComponent(Dockable dockable,
DockingPort target, DragOperation token) {
// otherwise, setup a new DockingFrame and retarget to the CENTER region
DockingResults results = new DockingResults(target, false);
// determine the bounds of the new frame
Point screenLoc = token.getCurrentMouse(true);
SwingUtility.add(screenLoc, token.getMouseOffset());
Rectangle screenBounds = dockable.getComponent().getBounds();
screenBounds.setLocation(screenLoc);
// create the frame
FloatManager mgr = DockingManager.getFloatManager();
DockingFrame frame = mgr.floatDockable(dockable, dockable
.getComponent(), screenBounds);
// grab a reference to the frame's dockingPort for posterity
results.dropTarget = frame.getDockingPort();
results.success = true;
return results;
}
protected static class DockingResults {
public DockingResults(DockingPort port, boolean status) {
dropTarget = port;
success = status;
}
public DockingPort dropTarget;
public boolean success;
}
/**
* Returns a new {@code DefaultDockingPort} with characteristics similar to
* the specified base {@code DockingPort}. If the base {@code DockingPort}
* is a {@code DefaultDockingPort}, then the returned {@code DockingPort}
* will share the base {@code DockingPort's} border manager and tabbed
* drag-source flag. The returned {@code DockingPort's} {@code isRoot()}
* method will return {@code false}.
*
* @param base
* the {@code DockingPort} off of which to base the returned
* {@code DockingPort}
* @return a new {@code DefaultDockingPort} with characteristics similar to
* the specified base {@code DockingPort}.
* @see DefaultDockingPort#getBorderManager()
* @see DefaultDockingPort#setBorderManager(BorderManager)
* @see DefaultDockingPort#isTabsAsDragSource()
* @see DefaultDockingPort#setTabsAsDragSource(boolean)
* @see DefaultDockingPort#setRoot(boolean)
*/
@Override
public DockingPort createDockingPort(DockingPort base) {
DockingPort port = createDockingPortImpl(base);
if (port instanceof DefaultDockingPort
&& base instanceof DefaultDockingPort) {
DefaultDockingPort newPort = (DefaultDockingPort) port;
DefaultDockingPort ddp = (DefaultDockingPort) base;
newPort.setBorderManager(ddp.getBorderManager());
newPort.setTabsAsDragSource(ddp.isTabsAsDragSource());
newPort.setRoot(false);
}
return port;
}
protected DockingPort createDockingPortImpl(DockingPort base) {
return new DefaultDockingPort();
}
/**
* Returns a new {@code DockingSplitPane} based on the specified
* {@code DockingPort}. and region. Creation of the
* {@code DockingSplitPane} is deferred to an internal protected method to
* allow for overriding by subclasses. A client property is set on the
* returned split pane with the key DockingConstants.REGION to indicate the
* creation region of the split pane for non-{@code DockingSplitPanes}
* returned by overriding subclasses.
*
* This method determines the "elder" component of the split pane by
* checking whether the new creation region is in the TOP or LEFT
* (NORTH_REGION or WEST_REGION). If the creation region, representing where
* the new {@code Dockable} will be docked, is not in the top or
* left, then the elder {@code Component} in the split pane must be. This
* information is used to initialize the resize weight of the split pane,
* setting resize weight to {@code 1} if the elder is in the top or left of
* the split pane and {@code 0} if not. This gives the elder
* {@code Component} in the resulting split pane priority in the layout with
* resizing the split pane.
*
* If the creation region is {@code NORTH_REGION} or {@code SOUTH_REGION},
* the returned split pane is initialized with a {@code VERTICAL_SPLIT}
* orientation; otherwise a {@code HORIZONTAL_SPLIT} orientation is used.
*
* Before returning, the border is removed from the split pane, its divider
* size is set to 3, and if possible the border is removed from the split
* pane divider. This is to avoid an excessive compound border effect for
* embedded {@code Components} within the split pane that may have their own
* borders.
*
* @param base
* the {@code DockingPort} off of which the returned
* {@code JSplitPane} will be based.
* @param region
* the region within the base {@code DockingPort} used to
* determine the orientation of the returned {@code JSplitPane}.
* @return a new {@code DockingSplitPane} based on the specified
* {@code DockingPort}. and region.
* @see DockingSplitPane#DockingSplitPane(DockingPort, String)
* @see #createSplitPaneImpl(DockingPort, String)
* @see JSplitPane#setResizeWeight(double)
*/
@Override
public JSplitPane createSplitPane(DockingPort base, String region, float percent) {
JSplitPane split = createSplitPaneImpl(base, region);
split.setVisible(false);
// mark the creation region on the split pane
SwingUtility.putClientProperty(split, DockingConstants.REGION, region);
double resizeWeight;
if (defaultResizeWeight == -1) {
// the creation region represents the "new" region, not the elder
// region.
// so if the creation region is NOT in the top left, then the elder
// region is.
boolean elderInTopLeft = !DockingUtility.isRegionTopLeft(region);
resizeWeight = elderInTopLeft ? 1 : 0;
} else {
resizeWeight = defaultResizeWeight;
}
split.setResizeWeight(resizeWeight);
if (constantPercent && split instanceof DockingSplitPane) {
((DockingSplitPane) split).setConstantPercent(true);
}
// determine the orientation
int orientation = JSplitPane.HORIZONTAL_SPLIT;
if (NORTH_REGION.equals(region) || SOUTH_REGION.equals(region)) {
orientation = JSplitPane.VERTICAL_SPLIT;
}
split.setOrientation(orientation);
// remove the border from the split pane
split.setBorder(null);
// set the divider size for a more reasonable, less bulky look
split.setDividerSize(3);
split.setOneTouchExpandable(false); //zw
// check the UI. If we can't work with the UI any further, then
// exit here.
if (!(split.getUI() instanceof BasicSplitPaneUI)) {
return split;
}
// grab the divider from the UI and remove the border from it
final BasicSplitPaneDivider divider = ((BasicSplitPaneUI) split.getUI())
.getDivider();
if (divider != null) {
divider.setBorder(null);
divider.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)
&& e.getClickCount() == 2) {
// TODO should be not override, but placed logic here
((JSplitPane) divider.getParent())
.resetToPreferredSizes();
}
}
});
}
if (percent != -1) {
split.setDividerLocation(percent);
}
split.setVisible(true);
return split;
}
/**
* Returns a new {@code DockingSplitPane} based on the specified
* {@code DockingPort}. and region. Creation of the
* {@code DockingSplitPane} is deferred to an internal protected method to
* allow for overriding by subclasses. A client property is set on the
* returned split pane with the key DockingConstants.REGION to indicate the
* creation region of the split pane for non-{@code DockingSplitPanes}
* returned by overriding subclasses.
*
* This method determines the "elder" component of the split pane by
* checking whether the new creation region is in the TOP or LEFT
* (NORTH_REGION or WEST_REGION). If the creation region, representing where
* the new {@code Dockable} will be docked, is not in the top or
* left, then the elder {@code Component} in the split pane must be. This
* information is used to initialize the resize weight of the split pane,
* setting resize weight to {@code 1} if the elder is in the top or left of
* the split pane and {@code 0} if not. This gives the elder
* {@code Component} in the resulting split pane priority in the layout with
* resizing the split pane.
*
* If the creation region is {@code NORTH_REGION} or {@code SOUTH_REGION},
* the returned split pane is initialized with a {@code VERTICAL_SPLIT}
* orientation; otherwise a {@code HORIZONTAL_SPLIT} orientation is used.
*
* Before returning, the border is removed from the split pane, its divider
* size is set to 3, and if possible the border is removed from the split
* pane divider. This is to avoid an excessive compound border effect for
* embedded {@code Components} within the split pane that may have their own
* borders.
*
* @param base
* the {@code DockingPort} off of which the returned
* {@code JSplitPane} will be based.
* @param region
* the region within the base {@code DockingPort} used to
* determine the orientation of the returned {@code JSplitPane}.
* @return a new {@code DockingSplitPane} based on the specified
* {@code DockingPort}. and region.
* @see DockingSplitPane#DockingSplitPane(DockingPort, String)
* @see #createSplitPaneImpl(DockingPort, String)
* @see JSplitPane#setResizeWeight(double)
*/
@Override
public JSplitPane createSplitPane(DockingPort base, String region) {
return createSplitPane(base, region, -1f);
}
protected JSplitPane createSplitPaneImpl(DockingPort base, String region) {
return new DockingSplitPane(base, region);
}
/**
* Returns the initial divider location to be used by the specified
* {@code JSplitPane} when it is embedded within the specified
* {@code DockingPort}. It is assumed that the {@code JSplitPane} parameter
* is embedded within the specified {@code DockingPort}, is validated,
* visible, and its dimensions are non-zero.
*
* This method gets the "size" of the specified {@code DockingPort} based on
* the orientation of the split pane (width for horizontal split,
* height for vertical split) minus the {@code DockingPort's}
* insets. It then dispatches to
* {@code getDividerProportion(DockingPort port, JSplitPane splitPane)} to
* determine the preferred proportion of the split pane divider. The
* returned value for this method is the product of the {@code DockingPort}
* size and the split proportion.
*
* If either {@code port} or {@code splitPane} parameters are {@code null},
* then this method returns {@code 0}.
*
* @param port
* the {@code DockingPort} that contains the specified
* {@code JSplitPane}.
* @param splitPane
* the {@code JSplitPane} whose initial divider location is to be
* determined.
* @return the desired divider location of the supplied {@code JSplitPane}.
* @see DockingStrategy#getInitialDividerLocation(DockingPort, JSplitPane)
* @see #getDividerProportion(DockingPort, JSplitPane)
*/
@Override
public int getInitialDividerLocation(DockingPort port, JSplitPane splitPane) {
if (port == null || splitPane == null) {
return 0;
}
Container dockingPort = (Container) port;
Insets in = dockingPort.getInsets();
boolean vert = splitPane.getOrientation() == JSplitPane.VERTICAL_SPLIT;
int inset = vert ? in.top + in.bottom : in.left + in.right;
// get the dimensions of the DockingPort, minus the insets
int portSize = vert ? dockingPort.getHeight() : dockingPort.getWidth();
portSize -= inset;
// get the divider proportion for the split pane and multiply by the
// port size
double proportion = getDividerProportion(port, splitPane);
if (proportion < 0 || proportion > 1) {
proportion = 0.5d;
}
return (int) (portSize * proportion);
}
/**
* Returns the desired divider proportion of the specified
* {@code JSplitPane} after rendering. This method assumes that the
* {@code JSplitPane} parameter is, or will be embedded within the specified
* {@code DockingPort}. This method does not assume that the
* {@code JSplitPane} has been validated and that it's current dimensions
* are non-zero.
*
* If either {@code port} or {@code splitPane} parameters are {@code null},
* then this method returns the default value of
* {@code RegionChecker.DEFAULT_SIBLING_SIZE}. Otherwise the "elder"
* component within the {@code JSplitPane} is determined to see if it is
* contained within a sub-{@code DockingPort}. If the "elder"
* {@code Component} cannot be determined, or it is not contained within a
* sub-{@code DockingPort}, then the default value of
* {@code RegionChecker.DEFAULT_SIBLING_SIZE} is returned.
*
* If the "elder" {@code Component} is successfully resolved inside a sub-{@code DockingPort},
* then a check is done on the sub-port for the client property
* {@code DefaultDockingStrategy.PREFERRED_PROPORTION}. If this value is
* found, then the primitive float version of it is returned.
*
* Failing these checks, the {@code Dockable} is resolved for the "elder"
* {@code Component} in the specified {@code JSplitPane} via
* {@code DockingManager.getDockable(Component comp)}. If no
* {@code Dockable} can be found, then
* {@code RegionChecker.DEFAULT_SIBLING_SIZE} is returned.
*
* Otherwise, the {@code DockingPortPropertySet} is retrieved from the
* specified {@code DockingPort} and its {@code getRegionChecker()} method
* is called. {@code getSiblingSize(Component c, String region)} is invoked
* on the returned {@code RegionChecker} passing the "elder"
* {@code Component} in the split pane and the creation region resolved for
* the specified {@code JSplitPane}. This resolves the preferred sibling
* size for the elder {@code Dockable} component. If the elder
* {@code Component} is in the top/left of the split pane, then
* {@code 1F-prefSize} is returned. Otherwise, the preferred sibling size is
* returned.
*
* @param port
* the {@code DockingPort} that contains, or will contain the
* specified {@code JSplitPane}.
* @param splitPane
* the {@code JSplitPane} whose initial divider location is to be
* determined.
* @return the desired divider proportion of the supplied {@code JSplitPane}.
* @see RegionChecker#DEFAULT_SIBLING_SIZE
* @see #PREFERRED_PROPORTION
* @see DockingManager#getDockable(Component)
* @see RegionChecker#getSiblingSize(Component, String)
*/
@Override
public double getDividerProportion(DockingPort port, JSplitPane splitPane) {
if (port == null || splitPane == null) {
return DockingManager.getDefaultSiblingSize();
}
Component elder = getElderComponent(splitPane);
if (elder == null) {
return DockingManager.getDefaultSiblingSize();
}
Float prefProp = getPreferredProportion(splitPane, elder);
if (prefProp != null) {
return prefProp.doubleValue();
}
if (elder instanceof DockingSplitPane) {
elder = ((DockingSplitPane) elder).getElderComponent();
}
Dockable dockable = DockingManager.getDockable(elder);
if (dockable != null) {
// DockingSplitPane splitter = (DockingSplitPane)splitPane;
RegionChecker rc = port.getDockingProperties().getRegionChecker();
float prefSize = rc.getSiblingSize(dockable.getComponent(),
getCreationRegion(splitPane));
return isElderTopLeft(splitPane) ? 1f - prefSize : prefSize;
// return prefSize;
}
return DockingManager.getDefaultSiblingSize();
}
protected String getCreationRegion(JSplitPane splitPane) {
if (splitPane instanceof DockingSplitPane) {
return ((DockingSplitPane) splitPane).getRegion();
}
return (String) SwingUtility.getClientProperty(splitPane,
DockingConstants.REGION);
}
protected boolean isElderTopLeft(JSplitPane splitPane) {
if (splitPane instanceof DockingSplitPane) {
return ((DockingSplitPane) splitPane).isElderTopLeft();
}
String region = getCreationRegion(splitPane);
// creation region represents the "new" region, not the "elder" region.
// so if the "new" region is NOT the topLeft, then the "elder" is.
return !DockingUtility.isRegionTopLeft(region);
}
protected Float getPreferredProportion(JSplitPane splitPane,
Component controller) {
// 'controller' is inside a dockingPort. re-reference to the parent
// dockingPort.
Container controllerPort = controller.getParent();
return getPreferredProportion(controllerPort);
}
protected Component getElderComponent(JSplitPane splitPane) {
if (splitPane instanceof DockingSplitPane) {
return ((DockingSplitPane) splitPane).getElderComponent();
}
boolean inTopLeft = isElderTopLeft(splitPane);
Component comp = inTopLeft ? splitPane.getLeftComponent() : splitPane
.getRightComponent();
if (comp instanceof DockingPort) {
comp = ((DockingPort) comp).getDockedComponent();
}
return comp;
}
protected static Float getPreferredProportion(Component c) {
return c == null ? null : (Float) SwingUtility.getClientProperty(c,
PREFERRED_PROPORTION);
}
public static void setDefaultResizeWeight(double rw) {
defaultResizeWeight = rw;
}
public static void keepConstantPercentage(boolean cstPercent) {
constantPercent = cstPercent;
}
}