blob: 71a0267025631ee41d7c255c537c3f3951da7634 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.ui;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.wm.FocusWatcher;
import com.intellij.ui.ClickListener;
import com.intellij.ui.UIBundle;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
/**
* @author Vladimir Kondratyev
*/
public class Splitter extends JPanel {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.ui.Splitter");
@NonNls public static final String PROP_PROPORTION = "proportion";
@NonNls public static final String PROP_ORIENTATION = "orientation";
private int myDividerWidth;
/**
* /------/
* | 1 |
* This is vertical split |------|
* | 2 |
* /------/
* <p/>
* /-------/
* | | |
* This is horizontal split | 1 | 2 |
* | | |
* /-------/
*/
private boolean myVerticalSplit;
private boolean myHonorMinimumSize = false;
private final float myMinProp;
private final float myMaxProp;
protected float myProportion;// first size divided by total size
private final Divider myDivider;
private JComponent mySecondComponent;
private JComponent myFirstComponent;
private final FocusWatcher myFocusWatcher;
private boolean myShowDividerIcon;
private boolean myShowDividerControls;
private boolean mySkipNextLayouting;
private static final Rectangle myNullBounds = new Rectangle();
/**
* Creates horizontal split (with components which are side by side) with proportion equals to .5f
*/
public Splitter() {
this(false);
}
/**
* Creates split with specified orientation and proportion equals to .5f
*
* @param vertical If true, components are displayed above one another. If false, components are displayed side by side.
*/
public Splitter(boolean vertical) {
this(vertical, .5f);
}
/**
* Creates split with specified orientation and proportion.
*
* @param vertical If true, components are displayed above one another. If false, components are displayed side by side.
* @param proportion The initial proportion of the splitter (between 0.0f and 1.0f).
*/
public Splitter(boolean vertical, float proportion) {
this(vertical, proportion, 0.0f, 1.0f);
}
public Splitter(boolean vertical, float proportion, float minProp, float maxProp) {
myMinProp = minProp;
myMaxProp = maxProp;
LOG.assertTrue(minProp >= 0.0f);
LOG.assertTrue(maxProp <= 1.0f);
LOG.assertTrue(minProp <= maxProp);
myVerticalSplit = vertical;
myShowDividerControls = false;
myShowDividerIcon = true;
myHonorMinimumSize = true;
myDivider = createDivider();
setProportion(proportion);
myDividerWidth = 7;
super.add(myDivider);
myFocusWatcher = new FocusWatcher();
myFocusWatcher.install(this);
setOpaque(false);
}
public void setShowDividerControls(boolean showDividerControls) {
myShowDividerControls = showDividerControls;
setOrientation(myVerticalSplit);
}
public void setShowDividerIcon(boolean showDividerIcon) {
myShowDividerIcon = showDividerIcon;
setOrientation(myVerticalSplit);
}
public void setResizeEnabled(final boolean value) {
myDivider.setResizeEnabled(value);
}
public void setAllowSwitchOrientationByMouseClick(boolean enabled) {
myDivider.setSwitchOrientationEnabled(enabled);
}
public boolean isShowDividerIcon() {
return myShowDividerIcon;
}
public boolean isShowDividerControls() {
return myShowDividerControls;
}
public boolean isHonorMinimumSize() {
return myHonorMinimumSize;
}
public void setHonorComponentsMinimumSize(boolean honorMinimumSize) {
myHonorMinimumSize = honorMinimumSize;
}
/**
* This is temporary solution for UIDesigner. <b>DO NOT</b> use it from code.
*
* @see #setFirstComponent(JComponent)
* @see #setSecondComponent(JComponent)
* @deprecated
*/
@Override
public Component add(Component comp) {
final int childCount = getComponentCount();
LOG.assertTrue(childCount >= 1);
if (childCount > 3) {
throw new IllegalStateException("" + childCount);
}
LOG.assertTrue(childCount <= 3);
if (childCount == 1) {
setFirstComponent((JComponent)comp);
}
else {
setSecondComponent((JComponent)comp);
}
return comp;
}
public void dispose() {
myFocusWatcher.deinstall(this);
}
protected Divider createDivider() {
return new DividerImpl();
}
@Override
public boolean isVisible() {
return super.isVisible() &&
(myFirstComponent != null && myFirstComponent.isVisible() || mySecondComponent != null && mySecondComponent.isVisible());
}
@Override
public Dimension getMinimumSize() {
final int dividerWidth = getDividerWidth();
if (myFirstComponent != null && myFirstComponent.isVisible() && mySecondComponent != null && mySecondComponent.isVisible()) {
final Dimension firstMinSize = myFirstComponent.getMinimumSize();
final Dimension secondMinSize = mySecondComponent.getMinimumSize();
return isVertical()
? new Dimension(Math.max(firstMinSize.width, secondMinSize.width), firstMinSize.height + dividerWidth + secondMinSize.height)
: new Dimension(firstMinSize.width + dividerWidth + secondMinSize.width, Math.max(firstMinSize.height, secondMinSize.height));
}
if (myFirstComponent != null && myFirstComponent.isVisible()) { // only first component is visible
return myFirstComponent.getMinimumSize();
}
if (mySecondComponent != null && mySecondComponent.isVisible()) { // only second component is visible
return mySecondComponent.getMinimumSize();
}
return super.getMinimumSize();
}
@Override
public Dimension getPreferredSize() {
final int dividerWidth = getDividerWidth();
if (myFirstComponent != null && myFirstComponent.isVisible() && mySecondComponent != null && mySecondComponent.isVisible()) {
final Dimension firstPrefSize = myFirstComponent.getPreferredSize();
final Dimension secondPrefSize = mySecondComponent.getPreferredSize();
return isVertical()
? new Dimension(Math.max(firstPrefSize.width, secondPrefSize.width),
firstPrefSize.height + dividerWidth + secondPrefSize.height)
: new Dimension(firstPrefSize.width + dividerWidth + secondPrefSize.width,
Math.max(firstPrefSize.height, secondPrefSize.height));
}
if (myFirstComponent != null && myFirstComponent.isVisible()) { // only first component is visible
return myFirstComponent.getPreferredSize();
}
if (mySecondComponent != null && mySecondComponent.isVisible()) { // only second component is visible
return mySecondComponent.getPreferredSize();
}
return super.getPreferredSize();
}
public void skipNextLayouting() {
mySkipNextLayouting = true;
}
@Override
public void doLayout() {
if (mySkipNextLayouting) {
mySkipNextLayouting = false;
return;
}
int width = getWidth();
int height = getHeight();
int total = isVertical() ? height : width;
if (total <= 0) return;
if (!isNull(myFirstComponent) && myFirstComponent.isVisible() && !isNull(mySecondComponent) && mySecondComponent.isVisible()) {
// both first and second components are visible
Rectangle firstRect = new Rectangle();
Rectangle dividerRect = new Rectangle();
Rectangle secondRect = new Rectangle();
int d = getDividerWidth();
double size1;
if (total <= d) {
size1 = 0;
d = total;
}
else {
size1 = myProportion * total;
double size2 = total - size1 - d;
if (isHonorMinimumSize()) {
double mSize1 = isVertical() ? myFirstComponent.getMinimumSize().getHeight() : myFirstComponent.getMinimumSize().getWidth();
double mSize2 = isVertical() ? mySecondComponent.getMinimumSize().getHeight() : mySecondComponent.getMinimumSize().getWidth();
if (size1 + size2 < mSize1 + mSize2) {
double proportion = mSize1 / (mSize1 + mSize2);
size1 = proportion * total;
}
else {
if (size1 < mSize1) {
size1 = mSize1;
}
else if (size2 < mSize2) {
size2 = mSize2;
size1 = total - size2 - d;
}
}
}
}
int iSize1 = (int)Math.round(Math.floor(size1));
int iSize2 = (int)Math.round(total - size1 - d);
if (isVertical()) {
firstRect.setBounds(0, 0, width, iSize1);
dividerRect.setBounds(0, iSize1, width, d);
secondRect.setBounds(0, iSize1 + d, width, iSize2);
}
else {
firstRect.setBounds(0, 0, iSize1, height);
dividerRect.setBounds(iSize1, 0, d, height);
secondRect.setBounds((iSize1 + d), 0, iSize2, height);
}
myDivider.setVisible(true);
myFirstComponent.setBounds(firstRect);
myDivider.setBounds(dividerRect);
mySecondComponent.setBounds(secondRect);
myFirstComponent.revalidate();
mySecondComponent.revalidate();
}
else if (!isNull(myFirstComponent) && myFirstComponent.isVisible()) { // only first component is visible
hideNull(mySecondComponent);
myDivider.setVisible(false);
myFirstComponent.setBounds(0, 0, width, height);
myFirstComponent.revalidate();
}
else if (!isNull(mySecondComponent) && mySecondComponent.isVisible()) { // only second component is visible
hideNull(myFirstComponent);
myDivider.setVisible(false);
mySecondComponent.setBounds(0, 0, width, height);
mySecondComponent.revalidate();
}
else { // both components are null or invisible
myDivider.setVisible(false);
if (myFirstComponent != null) {
myFirstComponent.setBounds(0, 0, 0, 0);
myFirstComponent.revalidate();
}
else {
hideNull(myFirstComponent);
}
if (mySecondComponent != null) {
mySecondComponent.setBounds(0, 0, 0, 0);
mySecondComponent.revalidate();
}
else {
hideNull(mySecondComponent);
}
}
myDivider.revalidate();
}
static boolean isNull(Component component) {
return NullableComponent.Check.isNull(component);
}
static void hideNull(Component component) {
if (component instanceof NullableComponent) {
if (!component.getBounds().equals(myNullBounds)) {
component.setBounds(myNullBounds);
component.validate();
}
}
}
public int getDividerWidth() {
return myDividerWidth;
}
public void setDividerWidth(int width) {
if (width <= 0) {
throw new IllegalArgumentException("Wrong divider width: " + width);
}
if (myDividerWidth != width) {
myDividerWidth = width;
revalidate();
repaint();
}
}
public float getProportion() {
return myProportion;
}
public void setProportion(float proportion) {
if (myProportion == proportion) {
return;
}
if (proportion < .0f || proportion > 1.0f) {
throw new IllegalArgumentException("Wrong proportion: " + proportion);
}
if (proportion < myMinProp) proportion = myMinProp;
if (proportion > myMaxProp) proportion = myMaxProp;
float oldProportion = myProportion;
myProportion = proportion;
firePropertyChange(PROP_PROPORTION, new Float(oldProportion), new Float(myProportion));
revalidate();
repaint();
}
/**
* Swaps components.
*/
public void swapComponents() {
JComponent tmp = myFirstComponent;
myFirstComponent = mySecondComponent;
mySecondComponent = tmp;
revalidate();
repaint();
}
/**
* @return <code>true</code> if splitter has vertical orientation, <code>false</code> otherwise
*/
public boolean getOrientation() {
return myVerticalSplit;
}
/**
* @return true if |-|
*/
public boolean isVertical() {
return myVerticalSplit;
}
/**
* @param verticalSplit <code>true</code> means that splitter will have vertical split
*/
public void setOrientation(boolean verticalSplit) {
if (myVerticalSplit == verticalSplit) return;
myVerticalSplit = verticalSplit;
myDivider.setOrientation(verticalSplit);
firePropertyChange(PROP_ORIENTATION, !myVerticalSplit, myVerticalSplit);
revalidate();
repaint();
}
public JComponent getFirstComponent() {
return myFirstComponent;
}
/**
* Sets component which is located as the "first" splitted area. The method doesn't validate and
* repaint the splitter. If there is already
*
* @param component
*/
public void setFirstComponent(@Nullable JComponent component) {
if (myFirstComponent != component) {
if (myFirstComponent != null) {
remove(myFirstComponent);
}
myFirstComponent = component;
if (myFirstComponent != null) {
super.add(myFirstComponent);
}
revalidate();
repaint();
}
}
public JComponent getSecondComponent() {
return mySecondComponent;
}
public JComponent getOtherComponent(final Component comp) {
if (comp.equals(getFirstComponent())) return getSecondComponent();
if (comp.equals(getSecondComponent())) return getFirstComponent();
LOG.error("invalid component");
return getFirstComponent();
}
/**
* Sets component which is located as the "second" splitted area. The method doesn't validate and
* repaint the splitter.
*
* @param component
*/
public void setSecondComponent(@Nullable JComponent component) {
if (mySecondComponent != component) {
if (mySecondComponent != null) {
remove(mySecondComponent);
}
mySecondComponent = component;
if (mySecondComponent != null) {
super.add(mySecondComponent);
}
revalidate();
repaint();
}
}
public JPanel getDivider() {
return myDivider;
}
public class DividerImpl extends Divider {
private boolean myResizeEnabled;
private boolean mySwitchOrientationEnabled;
protected Point myPoint;
public DividerImpl() {
super(new GridBagLayout());
myResizeEnabled = true;
mySwitchOrientationEnabled = false;
setFocusable(false);
enableEvents(MouseEvent.MOUSE_EVENT_MASK | MouseEvent.MOUSE_MOTION_EVENT_MASK);
//setOpaque(false);
setOrientation(myVerticalSplit);
}
public void setOrientation(boolean isVerticalSplit) {
removeAll();
setCursor(isVertical() ?
Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR) :
Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
if (!myShowDividerControls && !myShowDividerIcon) {
return;
}
Icon glueIcon = isVerticalSplit ? AllIcons.General.SplitGlueV : AllIcons.General.SplitGlueH;
add(new JLabel(glueIcon), new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE,
new Insets(0, 0, 0, 0), 0, 0));
if (myShowDividerControls && false) {
int glueFill = isVerticalSplit ? GridBagConstraints.VERTICAL : GridBagConstraints.HORIZONTAL;
int xMask = isVerticalSplit ? 1 : 0;
int yMask = isVerticalSplit ? 0 : 1;
int leftInsetArrow = 0;
int leftInsetIcon = 1;
JLabel splitDownlabel = new JLabel(isVerticalSplit ? AllIcons.General.SplitDown : AllIcons.General.SplitRight);
splitDownlabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
splitDownlabel.setToolTipText(isVerticalSplit ? UIBundle.message("splitter.down.tooltip.text") : UIBundle
.message("splitter.right.tooltip.text"));
new ClickListener() {
@Override
public boolean onClick(@NotNull MouseEvent e, int clickCount) {
setProportion(1.0f - getMinProportion(mySecondComponent));
return true;
}
}.installOn(splitDownlabel);
add(splitDownlabel, new GridBagConstraints(isVerticalSplit ? 1 : 0, isVerticalSplit ? 0 : 5, 1, 1, 0, 0, GridBagConstraints.CENTER,
GridBagConstraints.NONE, new Insets(0, leftInsetArrow, 0, 0), 0, 0));
//
add(new JLabel(glueIcon),
new GridBagConstraints(2 * xMask, 2 * yMask, 1, 1, 0, 0, GridBagConstraints.CENTER, glueFill,
new Insets(0, leftInsetIcon, 0, 0), 0, 0));
JLabel splitCenterlabel =
new JLabel(isVerticalSplit ? AllIcons.General.SplitCenterV : AllIcons.General.SplitCenterH);
splitCenterlabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
splitCenterlabel.setToolTipText(UIBundle.message("splitter.center.tooltip.text"));
new ClickListener() {
@Override
public boolean onClick(@NotNull MouseEvent e, int clickCount) {
setProportion(.5f);
return true;
}
}.installOn(splitCenterlabel);
add(splitCenterlabel, new GridBagConstraints(3 * xMask, 3 * yMask, 1, 1, 0, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE,
new Insets(0, leftInsetArrow, 0, 0), 0, 0));
add(new JLabel(glueIcon),
new GridBagConstraints(4 * xMask, 4 * yMask, 1, 1, 0, 0, GridBagConstraints.CENTER, glueFill,
new Insets(0, leftInsetIcon, 0, 0), 0, 0));
//
JLabel splitUpLabel = new JLabel(isVerticalSplit ? AllIcons.General.SplitUp : AllIcons.General.SplitLeft);
splitUpLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
splitUpLabel.setToolTipText(isVerticalSplit ? UIBundle.message("splitter.up.tooltip.text") : UIBundle
.message("splitter.left.tooltip.text"));
new ClickListener() {
@Override
public boolean onClick(@NotNull MouseEvent e, int clickCount) {
setProportion(getMinProportion(myFirstComponent));
return true;
}
}.installOn(splitUpLabel);
add(splitUpLabel, new GridBagConstraints(isVerticalSplit ? 5 : 0, isVerticalSplit ? 0 : 1, 1, 1, 0, 0, GridBagConstraints.CENTER,
GridBagConstraints.NONE, new Insets(0, leftInsetArrow, 0, 0), 0, 0));
add(new JLabel(glueIcon), new GridBagConstraints(6 * xMask, 6 * yMask, 1, 1, 0, 0,
isVerticalSplit ? GridBagConstraints.WEST : GridBagConstraints.SOUTH, glueFill,
new Insets(0, leftInsetIcon, 0, 0), 0, 0));
}
revalidate();
repaint();
}
@Override
protected void processMouseMotionEvent(MouseEvent e) {
super.processMouseMotionEvent(e);
if (!myResizeEnabled) return;
if (MouseEvent.MOUSE_DRAGGED == e.getID()) {
myPoint = SwingUtilities.convertPoint(this, e.getPoint(), Splitter.this);
float proportion;
if (isVertical()) {
if (getHeight() > 0) {
proportion = Math.min(1.0f, Math.max(.0f, Math
.min(Math.max(getMinProportion(myFirstComponent), (float)myPoint.y / (float)Splitter.this.getHeight()),
1 - getMinProportion(mySecondComponent))));
setProportion(proportion);
}
}
else {
if (getWidth() > 0) {
proportion = Math.min(1.0f, Math.max(.0f, Math
.min(Math.max(getMinProportion(myFirstComponent), (float)myPoint.x / (float)Splitter.this.getWidth()),
1 - getMinProportion(mySecondComponent))));
setProportion(proportion);
}
}
}
}
private float getMinProportion(JComponent component) {
if (isHonorMinimumSize()) {
if (component != null && myFirstComponent != null && myFirstComponent.isVisible() && mySecondComponent != null &&
mySecondComponent.isVisible()) {
if (isVertical()) {
return (float)component.getMinimumSize().height / (float)(Splitter.this.getHeight() - getDividerWidth());
}
else {
return (float)component.getMinimumSize().width / (float)(Splitter.this.getWidth() - getDividerWidth());
}
}
}
return 0.0f;
}
@Override
protected void processMouseEvent(MouseEvent e) {
super.processMouseEvent(e);
if (e.getID() == MouseEvent.MOUSE_CLICKED) {
if (mySwitchOrientationEnabled
&& e.getClickCount() == 1
&& SwingUtilities.isLeftMouseButton(e) && (SystemInfo.isMac ? e.isMetaDown() : e.isControlDown())) {
Splitter.this.setOrientation(!Splitter.this.getOrientation());
}
if (myResizeEnabled && e.getClickCount() == 2) {
Splitter.this.setProportion(.5f);
}
}
}
public void setResizeEnabled(boolean resizeEnabled) {
myResizeEnabled = resizeEnabled;
if (!myResizeEnabled) {
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
else {
setCursor(isVertical() ?
Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR) :
Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
}
}
public void setSwitchOrientationEnabled(boolean switchOrientationEnabled) {
mySwitchOrientationEnabled = switchOrientationEnabled;
}
}
}