2 * Copyright 2000-2015 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.intellij.openapi.ui;
18 import com.intellij.icons.AllIcons;
19 import com.intellij.openapi.Disposable;
20 import com.intellij.openapi.util.Disposer;
21 import com.intellij.openapi.util.Weighted;
22 import com.intellij.openapi.wm.IdeGlassPane;
23 import com.intellij.openapi.wm.IdeGlassPaneUtil;
24 import com.intellij.ui.ClickListener;
25 import com.intellij.ui.UIBundle;
26 import com.intellij.util.ui.JBUI;
27 import com.intellij.util.ui.update.Activatable;
28 import com.intellij.util.ui.update.UiNotifyConnector;
29 import org.jetbrains.annotations.NotNull;
30 import org.jetbrains.annotations.Nullable;
34 import java.awt.event.MouseAdapter;
35 import java.awt.event.MouseEvent;
38 * @author Vladimir Kondratyev
40 public class ThreeComponentsSplitter extends JPanel implements Disposable {
41 private int myDividerWidth;
45 * This is vertical split |------|
51 * This is horizontal split | 1 | 2 |
55 private boolean myVerticalSplit;
56 private boolean myHonorMinimumSize = false;
58 private final Divider myFirstDivider;
59 private final Divider myLastDivider;
61 @Nullable private JComponent myFirstComponent;
62 @Nullable private JComponent myInnerComponent;
63 @Nullable private JComponent myLastComponent;
65 private int myFirstSize = 0;
66 private int myLastSize = 0;
67 private int myMinSize = 0;
69 private boolean myShowDividerControls;
70 private int myDividerZone;
74 * Creates horizontal split with proportion equals to .5f
76 public ThreeComponentsSplitter() {
81 public ThreeComponentsSplitter(boolean vertical) {
82 myVerticalSplit = vertical;
83 myShowDividerControls = false;
84 myFirstDivider = new Divider(true);
85 Disposer.register(this, myFirstDivider);
86 myLastDivider = new Divider(false);
87 Disposer.register(this, myLastDivider);
95 public void setShowDividerControls(boolean showDividerControls) {
96 myShowDividerControls = showDividerControls;
97 setOrientation(myVerticalSplit);
100 public void setDividerMouseZoneSize(int size) {
101 myDividerZone = size;
104 public boolean isHonorMinimumSize() {
105 return myHonorMinimumSize;
108 public void setHonorComponentsMinimumSize(boolean honorMinimumSize) {
109 myHonorMinimumSize = honorMinimumSize;
112 public boolean isVisible() {
113 return super.isVisible() && (firstVisible() || innerVisible() || lastVisible());
116 private boolean lastVisible() {
117 return !Splitter.isNull(myLastComponent) && myLastComponent.isVisible();
120 private boolean innerVisible() {
121 return !Splitter.isNull(myInnerComponent) && myInnerComponent.isVisible();
124 private boolean firstVisible() {
125 return !Splitter.isNull(myFirstComponent) && myFirstComponent.isVisible();
128 private int visibleDividersCount() {
130 if (firstDividerVisible()) count++;
131 if (lastDividerVisible()) count++;
135 private boolean firstDividerVisible() {
136 return firstVisible() && innerVisible() || firstVisible() && lastVisible() && !innerVisible();
139 private boolean lastDividerVisible() {
140 return innerVisible() && lastVisible();
143 public Dimension getMinimumSize() {
144 if (isHonorMinimumSize()) {
145 final int dividerWidth = getDividerWidth();
146 final Dimension firstSize = myFirstComponent != null ? myFirstComponent.getMinimumSize() : JBUI.emptySize();
147 final Dimension lastSize = myLastComponent != null ? myLastComponent.getMinimumSize() : JBUI.emptySize();
148 final Dimension innerSize = myInnerComponent != null ? myInnerComponent.getMinimumSize() : JBUI.emptySize();
149 if (getOrientation()) {
150 int width = Math.max(firstSize.width, Math.max(lastSize.width, innerSize.width));
151 int height = visibleDividersCount() * dividerWidth;
152 height += firstSize.height;
153 height += lastSize.height;
154 height += innerSize.height;
155 return new Dimension(width, height);
158 int heigth = Math.max(firstSize.height, Math.max(lastSize.height, innerSize.height));
159 int width = visibleDividersCount() * dividerWidth;
160 width += firstSize.width;
161 width += lastSize.width;
162 width += innerSize.width;
163 return new Dimension(width, heigth);
166 return super.getMinimumSize();
169 public void doLayout() {
170 final int width = getWidth();
171 final int height = getHeight();
173 Rectangle firstRect = new Rectangle();
174 Rectangle firstDividerRect = new Rectangle();
175 Rectangle lastDividerRect = new Rectangle();
176 Rectangle lastRect = new Rectangle();
177 Rectangle innerRect = new Rectangle();
178 final int componentSize = getOrientation() ? height : width;
179 int dividerWidth = getDividerWidth();
180 int dividersCount = visibleDividersCount();
182 int firstCompontSize;
183 int lastComponentSize;
184 int innerComponentSize;
185 if(componentSize <= dividersCount * dividerWidth) {
186 firstCompontSize = 0;
187 lastComponentSize = 0;
188 innerComponentSize = 0;
189 dividerWidth = componentSize;
192 firstCompontSize = getFirstSize();
193 lastComponentSize = getLastSize();
194 int sizeLack = firstCompontSize + lastComponentSize - (componentSize - dividersCount * dividerWidth - myMinSize);
196 // Lacking size. Reduce first & last component's size, inner -> MIN_SIZE
197 double firstSizeRatio = (double)firstCompontSize / (firstCompontSize + lastComponentSize);
198 if (firstCompontSize > 0) {
199 firstCompontSize -= sizeLack * firstSizeRatio;
200 firstCompontSize = Math.max(myMinSize, firstCompontSize);
202 if (lastComponentSize > 0) {
203 lastComponentSize -= sizeLack * (1 - firstSizeRatio);
204 lastComponentSize = Math.max(myMinSize, lastComponentSize);
206 innerComponentSize = getMinSize(myInnerComponent);
209 innerComponentSize = Math.max(getMinSize(myInnerComponent), componentSize - dividersCount * dividerWidth - getFirstSize() - getLastSize());
212 if (!innerVisible()) {
213 lastComponentSize += innerComponentSize;
214 innerComponentSize = 0;
215 if (!lastVisible()) {
216 firstCompontSize = componentSize;
221 if (getOrientation()) {
222 int space = firstCompontSize;
223 firstRect.setBounds(0, 0, width, firstCompontSize);
224 if (firstDividerVisible()) {
225 firstDividerRect.setBounds(0, space, width, dividerWidth);
226 space += dividerWidth;
229 innerRect.setBounds(0, space, width, innerComponentSize);
230 space += innerComponentSize;
232 if (lastDividerVisible()) {
233 lastDividerRect.setBounds(0, space, width, dividerWidth);
234 space += dividerWidth;
237 lastRect.setBounds(0, space, width, lastComponentSize);
240 int space = firstCompontSize;
241 firstRect.setBounds(0, 0, firstCompontSize, height);
243 if (firstDividerVisible()) {
244 firstDividerRect.setBounds(space, 0, dividerWidth, height);
245 space += dividerWidth;
248 innerRect.setBounds(space, 0, innerComponentSize, height);
249 space += innerComponentSize;
251 if (lastDividerVisible()) {
252 lastDividerRect.setBounds(space, 0, dividerWidth, height);
253 space += dividerWidth;
256 lastRect.setBounds(space, 0, lastComponentSize, height);
259 myFirstDivider.setVisible(firstDividerVisible());
260 myFirstDivider.setBounds(firstDividerRect);
261 myFirstDivider.doLayout();
263 myLastDivider.setVisible(lastDividerVisible());
264 myLastDivider.setBounds(lastDividerRect);
265 myLastDivider.doLayout();
267 validateIfNeeded(myFirstComponent, firstRect);
268 validateIfNeeded(myInnerComponent, innerRect);
269 validateIfNeeded(myLastComponent, lastRect);
272 private static void validateIfNeeded(final JComponent c, final Rectangle rect) {
273 if (!Splitter.isNull(c)) {
274 if (!c.getBounds().equals(rect)) {
279 Splitter.hideNull(c);
284 public int getDividerWidth() {
285 return myDividerWidth;
288 public void setDividerWidth(int width) {
290 throw new IllegalArgumentException("Wrong divider width: " + width);
292 if (myDividerWidth != width) {
293 myDividerWidth = width;
300 * @return <code>true</code> if splitter has vertical orientation, <code>false</code> otherwise
302 public boolean getOrientation() {
303 return myVerticalSplit;
307 * @param verticalSplit <code>true</code> means that splitter will have vertical split
309 public void setOrientation(boolean verticalSplit) {
310 myVerticalSplit = verticalSplit;
311 myFirstDivider.setOrientation(verticalSplit);
312 myLastDivider.setOrientation(verticalSplit);
318 public JComponent getFirstComponent() {
319 return myFirstComponent;
323 * Sets component which is located as the "first" splitted area. The method doesn't validate and
324 * repaint the splitter. If there is already
327 public void setFirstComponent(@Nullable JComponent component) {
328 if (myFirstComponent != component) {
329 if (myFirstComponent != null) {
330 remove(myFirstComponent);
332 myFirstComponent = component;
333 if (myFirstComponent != null) {
334 add(myFirstComponent);
335 myFirstComponent.invalidate();
341 public JComponent getLastComponent() {
342 return myLastComponent;
347 * Sets component which is located as the "secont" splitted area. The method doesn't validate and
348 * repaint the splitter.
351 public void setLastComponent(@Nullable JComponent component) {
352 if (myLastComponent != component) {
353 if (myLastComponent != null) {
354 remove(myLastComponent);
356 myLastComponent = component;
357 if (myLastComponent != null) {
358 add(myLastComponent);
359 myLastComponent.invalidate();
365 public JComponent getInnerComponent() {
366 return myInnerComponent;
371 * Sets component which is located as the "inner" splitted area. The method doesn't validate and
372 * repaint the splitter.
375 public void setInnerComponent(@Nullable JComponent component) {
376 if (myInnerComponent != component) {
377 if (myInnerComponent != null) {
378 remove(myInnerComponent);
380 myInnerComponent = component;
381 if (myInnerComponent != null) {
382 add(myInnerComponent);
383 myInnerComponent.invalidate();
387 public void setMinSize(int minSize) {
388 myMinSize = Math.max(0, minSize);
394 public void setFirstSize(final int size) {
400 public void setLastSize(final int size) {
406 public int getFirstSize() {
407 return firstVisible() ? myFirstSize : 0;
410 public int getLastSize() {
411 return lastVisible() ? myLastSize : 0;
414 public int getMinSize(boolean first) {
415 return getMinSize(first? myFirstComponent : myLastComponent);
418 public int getMaxSize(boolean first) {
419 final int size = getOrientation() ? this.getHeight() : this.getWidth();
420 return size - (first? myLastSize: myFirstSize) - myMinSize;
423 private int getMinSize(JComponent component) {
424 if (isHonorMinimumSize()) {
425 if (component != null && myFirstComponent != null && myFirstComponent.isVisible() && myLastComponent != null && myLastComponent.isVisible()) {
426 if (getOrientation()) {
427 return component.getMinimumSize().height;
430 return component.getMinimumSize().width;
438 public void dispose() {
439 myLastComponent = null;
440 myFirstComponent = null;
441 myInnerComponent = null;
443 Container container = getParent();
444 if (container != null) {
445 container.remove(this);
449 private class Divider extends JPanel implements Disposable {
450 protected boolean myDragging;
451 protected Point myPoint;
452 private final boolean myIsFirst;
454 private IdeGlassPane myGlassPane;
456 private class MyMouseAdapter extends MouseAdapter implements Weighted {
458 public void mousePressed(MouseEvent e) {
459 _processMouseEvent(e);
463 public void mouseReleased(MouseEvent e) {
464 _processMouseEvent(e);
468 public void mouseMoved(MouseEvent e) {
469 _processMouseMotionEvent(e);
473 public void mouseDragged(MouseEvent e) {
474 _processMouseMotionEvent(e);
477 public double getWeight() {
480 private void _processMouseMotionEvent(MouseEvent e) {
481 MouseEvent event = getTargetEvent(e);
483 myGlassPane.setCursor(null, myListener);
487 processMouseMotionEvent(event);
488 if (event.isConsumed()) {
493 private void _processMouseEvent(MouseEvent e) {
494 MouseEvent event = getTargetEvent(e);
496 myGlassPane.setCursor(null, myListener);
500 processMouseEvent(event);
501 if (event.isConsumed()) {
507 private final MouseAdapter myListener = new MyMouseAdapter();
510 private MouseEvent getTargetEvent(MouseEvent e) {
511 return SwingUtilities.convertMouseEvent(e.getComponent(), e, this);
514 private boolean myWasPressedOnMe;
516 public Divider(boolean isFirst) {
517 super(new GridBagLayout());
519 enableEvents(MouseEvent.MOUSE_EVENT_MASK | MouseEvent.MOUSE_MOTION_EVENT_MASK);
521 setOrientation(myVerticalSplit);
523 new UiNotifyConnector.Once(this, new Activatable.Adapter() {
525 public void showNotify() {
531 private boolean isInside(Point p) {
532 if (!isVisible()) return false;
534 if (myVerticalSplit) {
535 if (p.x >= 0 && p.x < getWidth()) {
536 if (getHeight() > 0) {
537 return p.y >= 0 && p.y < getHeight();
540 return p.y >= -myDividerZone / 2 && p.y <= myDividerZone / 2;
545 if (p.y >= 0 && p.y < getHeight()) {
546 if (getWidth() > 0) {
547 return p.x >= 0 && p.x < getWidth();
550 return p.x >= -myDividerZone / 2 && p.x <= myDividerZone / 2;
558 private void init() {
559 myGlassPane = IdeGlassPaneUtil.find(this);
560 myGlassPane.addMouseMotionPreprocessor(myListener, this);
561 myGlassPane.addMousePreprocessor(myListener, this);
564 public void dispose() {
567 private void setOrientation(boolean isVerticalSplit) {
570 if (!myShowDividerControls) {
574 int xMask = isVerticalSplit ? 1 : 0;
575 int yMask = isVerticalSplit ? 0 : 1;
577 Icon glueIcon = isVerticalSplit ? AllIcons.General.SplitGlueV : AllIcons.General.SplitCenterH;
578 int glueFill = isVerticalSplit ? GridBagConstraints.VERTICAL : GridBagConstraints.HORIZONTAL;
579 add(new JLabel(glueIcon),
580 new GridBagConstraints(0, 0, 1, 1, 0, 0, isVerticalSplit ? GridBagConstraints.EAST : GridBagConstraints.NORTH, glueFill, new Insets(0, 0, 0, 0), 0, 0));
581 JLabel splitDownlabel = new JLabel(isVerticalSplit ? AllIcons.General.SplitDown : AllIcons.General.SplitRight);
582 splitDownlabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
583 splitDownlabel.setToolTipText(isVerticalSplit ? UIBundle.message("splitter.down.tooltip.text") : UIBundle
584 .message("splitter.right.tooltip.text"));
585 new ClickListener() {
587 public boolean onClick(@NotNull MouseEvent e, int clickCount) {
588 if (myInnerComponent != null) {
589 final int income = myVerticalSplit ? myInnerComponent.getHeight() : myInnerComponent.getWidth();
591 setFirstSize(myFirstSize + income);
594 setLastSize(myLastSize + income);
599 }.installOn(splitDownlabel);
602 new GridBagConstraints(isVerticalSplit ? 1 : 0,
603 isVerticalSplit ? 0 : 5,
604 1, 1, 0, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
606 add(new JLabel(glueIcon),
607 new GridBagConstraints(2 * xMask, 2 * yMask, 1, 1, 0, 0, GridBagConstraints.CENTER, glueFill, new Insets(0, 0, 0, 0), 0, 0));
608 JLabel splitCenterlabel = new JLabel(isVerticalSplit ? AllIcons.General.SplitCenterV : AllIcons.General.SplitCenterH);
609 splitCenterlabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
610 splitCenterlabel.setToolTipText(UIBundle.message("splitter.center.tooltip.text"));
611 new ClickListener() {
613 public boolean onClick(@NotNull MouseEvent e, int clickCount) {
617 }.installOn(splitCenterlabel);
618 add(splitCenterlabel,
619 new GridBagConstraints(3 * xMask, 3 * yMask, 1, 1, 0, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
620 add(new JLabel(glueIcon),
621 new GridBagConstraints(4 * xMask, 4 * yMask, 1, 1, 0, 0, GridBagConstraints.CENTER, glueFill, new Insets(0, 0, 0, 0), 0, 0));
623 JLabel splitUpLabel = new JLabel(isVerticalSplit ? AllIcons.General.SplitUp : AllIcons.General.SplitLeft);
624 splitUpLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
625 splitUpLabel.setToolTipText(isVerticalSplit ? UIBundle.message("splitter.up.tooltip.text") : UIBundle
626 .message("splitter.left.tooltip.text"));
627 new ClickListener() {
629 public boolean onClick(@NotNull MouseEvent e, int clickCount) {
630 if (myInnerComponent != null) {
631 final int income = myVerticalSplit ? myInnerComponent.getHeight() : myInnerComponent.getWidth();
633 setFirstSize(myFirstSize + income);
636 setLastSize(myLastSize + income);
641 }.installOn(splitUpLabel);
644 new GridBagConstraints(isVerticalSplit ? 5 : 0,
645 isVerticalSplit ? 0 : 1,
646 1, 1, 0, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
647 add(new JLabel(glueIcon),
648 new GridBagConstraints(6 * xMask, 6 * yMask, 1, 1, 0, 0,
649 isVerticalSplit ? GridBagConstraints.WEST : GridBagConstraints.SOUTH, glueFill, new Insets(0, 0, 0, 0), 0, 0));
652 private void center() {
653 if (myInnerComponent != null) {
654 final int total = myFirstSize + (myVerticalSplit ? myInnerComponent.getHeight() : myInnerComponent.getWidth());
656 setFirstSize(total / 2);
659 setLastSize(total / 2);
664 protected void processMouseMotionEvent(MouseEvent e) {
665 super.processMouseMotionEvent(e);
667 if (!isShowing()) return;
669 if (MouseEvent.MOUSE_DRAGGED == e.getID() && myWasPressedOnMe) {
671 setCursor(getResizeCursor());
672 myGlassPane.setCursor(getResizeCursor(), myListener);
674 myPoint = SwingUtilities.convertPoint(this, e.getPoint(), ThreeComponentsSplitter.this);
675 final int size = getOrientation() ? ThreeComponentsSplitter.this.getHeight() : ThreeComponentsSplitter.this.getWidth();
676 if (getOrientation()) {
677 if (size > 0 || myDividerZone > 0) {
679 setFirstSize(Math.min(size - myLastSize - getMinSize(myInnerComponent) - getDividerWidth() * visibleDividersCount(), Math.max(getMinSize(myFirstComponent), myPoint.y)));
682 setLastSize(Math.min(size - myFirstSize - getMinSize(myInnerComponent) - getDividerWidth() * visibleDividersCount(), Math.max(getMinSize(myLastComponent), size - myPoint.y - getDividerWidth())));
687 if (size > 0 || myDividerZone > 0) {
689 setFirstSize(Math.min(size - myLastSize - getMinSize(myInnerComponent) - getDividerWidth() * visibleDividersCount(), Math.max(getMinSize(myFirstComponent), myPoint.x)));
692 setLastSize(Math.min(size - myFirstSize - getMinSize(myInnerComponent) - getDividerWidth() * visibleDividersCount(), Math.max(getMinSize(myLastComponent), size - myPoint.x - getDividerWidth())));
696 ThreeComponentsSplitter.this.doLayout();
697 } else if (MouseEvent.MOUSE_MOVED == e.getID()) {
698 if (myGlassPane != null) {
699 if (isInside(e.getPoint())) {
700 myGlassPane.setCursor(getResizeCursor(), myListener);
703 myGlassPane.setCursor(null, myListener);
708 if (myWasPressedOnMe) {
713 protected void processMouseEvent(MouseEvent e) {
714 super.processMouseEvent(e);
719 case MouseEvent.MOUSE_ENTERED:
720 setCursor(getResizeCursor());
722 case MouseEvent.MOUSE_EXITED:
724 setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
727 case MouseEvent.MOUSE_PRESSED:
728 if (isInside(e.getPoint())) {
729 myWasPressedOnMe = true;
730 myGlassPane.setCursor(getResizeCursor(), myListener);
733 myWasPressedOnMe = false;
736 case MouseEvent.MOUSE_RELEASED:
737 if (myWasPressedOnMe) {
740 if (isInside(e.getPoint())) {
741 myGlassPane.setCursor(getResizeCursor(), myListener);
743 myWasPressedOnMe = false;
747 case MouseEvent.MOUSE_CLICKED:
748 if (e.getClickCount() == 2) {
756 private Cursor getResizeCursor() {
757 return getOrientation() ? Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR) : Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);