org.flexdock.docking.defaults.DockingSplitPane 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.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.AbstractAction;
import javax.swing.JSplitPane;
import javax.swing.plaf.basic.BasicSplitPaneUI;
import org.flexdock.docking.DockingConstants;
import org.flexdock.docking.DockingManager;
import org.flexdock.docking.DockingPort;
import org.flexdock.util.DockingUtility;
import org.flexdock.util.SwingUtility;
/**
* @author Christopher Butler
*/
@SuppressWarnings(value = { "serial" })
public class DockingSplitPane extends JSplitPane implements DockingConstants {
protected DockingPort dockingPort;
protected String region;
protected boolean dividerLocDetermined;
protected boolean controllerInTopLeft;
protected double initialDividerRatio = .5;
protected double percent = -1;
private int dividerHashCode = -1;
private boolean constantPercent;
/**
* Creates a new {@code DockingSplitPane} for the specified
* {@code DockingPort} with the understanding that the resulting
* {@code DockingSplitPane} will be used for docking a {@code Dockable} into
* the {@code DockingPort's} specified {@code region}. Neither {@code port}
* or {@code region} may be {@code null}. {@code region} must be a valid
* docking region as defined by {@code isValidDockingRegion(String region)}.
*
* @param port
* the {@code DockingPort} for which this
* {@code DockingSplitPane} is to be created.
* @param region
* the region within the specified {@code DockingPort} for which
* this {@code DockingSplitPane} is to be created.
* @throws IllegalArgumentException
* if either {@code port} is {@code null} or }region} is
* {@code null} or invalid.
* @see DockingManager#isValidDockingRegion(String)
*/
public DockingSplitPane(DockingPort port, String region) {
if (port == null) {
throw new IllegalArgumentException("'port' cannot be null.");
}
if (!DockingManager.isValidDockingRegion(region)) {
throw new IllegalArgumentException("'" + region
+ "' is not a valid region.");
}
this.region = region;
this.dockingPort = port;
// the controlling item is in the topLeft if our new item (represented
// by the "region" string) is NOT in the topLeft.
controllerInTopLeft = !DockingUtility.isRegionTopLeft(region);
// set the proper resize weight
int weight = controllerInTopLeft ? 1 : 0;
setResizeWeight(weight);
addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent pce) {
if (constantPercent && getUI() instanceof BasicSplitPaneUI) {
BasicSplitPaneUI ui = (BasicSplitPaneUI) getUI();
if (dividerHashCode != ui.getDivider().hashCode()) {
dividerHashCode = ui.getDivider().hashCode();
ui.getDivider().addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
DockingSplitPane.this.percent = SwingUtility.getDividerProportion(DockingSplitPane.this);
DockingSplitPane.this.setResizeWeight(percent);
}
});
}
}
}
});
getActionMap().put("toggleFocus", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
SwingUtility.toggleFocus(+1);
}
});
}
@Override
public void setBounds(int x, int y, int w, int h) {
super.setBounds(x, y, w, h);
if (constantPercent) {
setResizeWeight(percent);
super.setDividerLocation((int) (percent * getSplitSize()));
}
}
public void setConstantPercent(boolean cstPercent) {
if (cstPercent != constantPercent) {
constantPercent = cstPercent;
}
}
@Override
public void resetToPreferredSizes() {
Insets i = getInsets();
if (getOrientation() == VERTICAL_SPLIT) {
int h = getHeight() - i.top - i.bottom - getDividerSize();
int topH = getTopComponent().getPreferredSize().height;
int bottomH = getBottomComponent().getPreferredSize().height;
int extraSpace = h - topH - bottomH;
// we have more space than necessary; resize to give each at least
// preferred size
if (extraSpace >= 0) {
setDividerLocation(i.top + topH
+ ((int) (extraSpace * getResizeWeight() + .5)));
}
// TODO implement shrinking excess space to ensure that one has
// preferred and nothing more
} else {
int w = getWidth() - i.left - i.right - getDividerSize();
int leftH = getLeftComponent().getPreferredSize().width;
int rightH = getRightComponent().getPreferredSize().width;
int extraSpace = w - leftH - rightH;
// we have more space than necessary; resize to give each at least
// preferred size
if (extraSpace >= 0) {
setDividerLocation(i.left + leftH
+ ((int) (extraSpace * getResizeWeight() + .5)));
}
// TODO implement shrinking excess space to ensure that one has
// preferred and nothing more
}
}
private int getSplitSize() {
return getOrientation() == JSplitPane.HORIZONTAL_SPLIT ? getWidth() : getHeight();
}
@Override
public void setDividerLocation(double percent) {
this.percent = percent;
super.setDividerLocation(percent);
setResizeWeight(percent);
}
public double getPercent() {
if (constantPercent) {
return percent;
}
return -1;
}
protected boolean isDividerSizeProperlyDetermined() {
if (getDividerLocation() != 0) {
return true;
}
return dividerLocDetermined;
}
/**
* Returns the 'oldest' {@code Component} to have been added to this
* {@code DockingSplitPane} as a result of a docking operation. A
* {@code DockingSplitPane} is created based upon the need to share space
* within a {@code DockingPort} between two {@code Dockables}. This happens
* when a new {@code Dockable} is introduced into an outer region of a
* {@code DockingPort} that already contains a {@code Dockable}. The
* {@code Dockable} that was in the {@code DockingPort} prior to splitting
* the layout is the 'elder' {@code Component} and, in many circumstances,
* may be used to control initial divider location and resize weight.
*
* If this split pane contains {@code DockingPorts} as its child components,
* then this method will return the {@code Component} determined by calling
* {@code getDockedComponent()} for the {@code DockingPort} in this split
* pane's elder region.
*
* The elder region of this {@code DockingSplitPane} is determined using the
* value returned from {@code getRegion()}, where {@code getRegion()}
* indicates the docking region of the 'new' {@code Dockable} for this
* {@code DockingSplitPane}.
*
* @return the 'oldest' {@code Component} to have been added to this
* {@code DockingSplitPane} as a result of a docking operation.
* @see #getRegion()
* @see DockingPort#getDockedComponent()
*/
public Component getElderComponent() {
Component c = controllerInTopLeft ? getLeftComponent()
: getRightComponent();
if (c instanceof DockingPort) {
c = ((DockingPort) c).getDockedComponent();
}
return c;
}
/**
* Returns the docking region for which this {@code DockingSplitPane} was
* created. A {@code DockingSplitPane} is created based upon the need to
* share space within a {@code DockingPort} between two {@code Dockables}.
* This happens when a new {@code Dockable} is introduced into an outer
* region of a {@code DockingPort} that already contains a {@code Dockable}.
* This method returns that outer region for which this
* {@code DockingSplitPane} was created and may be used to control the
* orientation of the split pane. The region returned by this method will be
* the same passed into the {@code DockingSplitPane} constructor on
* instantiation.
*
* @return the docking region for which this {@code DockingSplitPane} was
* created.
* @see #DockingSplitPane(DockingPort, String)
*/
public String getRegion() {
return region;
}
/**
* Indicates whether the 'oldest' {@code Component} to have been added to
* this {@code DockingSplitPane} as a result of a docking operation is in
* the TOP or LEFT side of the split pane. A {@code DockingSplitPane} is
* created based upon the need to share space within a {@code DockingPort}
* between two {@code Dockables}. This happens when a new {@code Dockable}
* is introduced into an outer region of a {@code DockingPort} that already
* contains a {@code Dockable}. The {@code Dockable} that was in the
* {@code DockingPort} prior to splitting the layout is the 'elder'
* {@code Component} and is returned by {@code getElderComponent()}. This
* method indicates whether or not that {@code Component} is in the TOP or
* LEFT side of this {@code DockingSplitPane}.
*
* The elder region of this {@code DockingSplitPane} is determined using the
* value returned from {@code getRegion()}, where {@code getRegion()}
* indicates the docking region of the 'new' {@code Dockable} for this
* {@code DockingSplitPane}.
*
* @return {@code true} if the 'oldest' {@code Component} to have been added
* to this {@code DockingSplitPane} is in the TOP or LEFT side of
* the split pane; {@code false} otherwise.
* @see #getElderComponent()
* @see #getRegion()
*/
public boolean isElderTopLeft() {
return controllerInTopLeft;
}
/**
* Overridden to ensure proper divider location on initial rendering.
* Sometimes, a split divider location is set as a proportion before the
* split pane itself has been fully realized in the container hierarchy.
* This results in a layout calculation based on a proportion of zero width
* or height, rather than the desired proportion of width or height after
* the split pane has been fully rendered. This method ensures that default
* {@code JSplitPane} layout behavior is deferred until after the initial
* dimensions of this split pane have been properly determined.
*
* @see java.awt.Container#doLayout()
* @see JSplitPane#setDividerLocation(double)
*/
@Override
public void doLayout() {
// if they setup the docking configuration while the application
// was first starting up, then the dividerLocation was calculated before
// the container tree was visible, sized, validated, etc, so it'll be
// stuck at zero. in that case, redetermine the divider location now
// that we have valid container bounds with which to work.
if (!isDividerSizeProperlyDetermined()) {
// make sure this can only run once so we don't get a StackOverflow
dividerLocDetermined = true;
setDividerLocation(initialDividerRatio);
}
// continue the layout
super.doLayout();
}
/**
* Releases any internal references to external objects to aid garbage
* collection. This method is {@code public} and may be invoked manually for
* proactive memory management. Otherwise, this method is invoked by this
* {@code DockingSplitPane's} {@code finalize()} method.
*/
public void cleanup() {
dockingPort = null;
}
/**
* Sets the initial divider ration for creating split panes. The default
* value is {@code 0.5}.
*
* @exception IllegalArgumentException
* if {@code ratio} is less than 0.0 or greater than 1.0.
* @param ratio
* a ratio for determining weighting between the two sides of a
* split pane.
*/
public void setInitialDividerRatio(double ratio) {
if (ratio < 0.0 || ratio > 1.0) {
throw new IllegalArgumentException("ratio (" + ratio
+ ") must be between [0.0,1,0] inclusive");
}
initialDividerRatio = ratio;
}
@Override
protected void finalize() throws Throwable {
cleanup();
super.finalize();
}
}