| /* |
| * 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; |
| } |
| } |
| } |