@Nullable
[idea/community.git] / platform / util / src / com / intellij / openapi / ui / Splitter.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package com.intellij.openapi.ui;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.util.IconLoader;
20 import com.intellij.openapi.wm.FocusWatcher;
21 import com.intellij.ui.UIBundle;
22 import org.jetbrains.annotations.NonNls;
23 import org.jetbrains.annotations.Nullable;
24
25 import javax.swing.*;
26 import java.awt.*;
27 import java.awt.event.MouseAdapter;
28 import java.awt.event.MouseEvent;
29
30 /**
31  * @author Vladimir Kondratyev
32  */
33 public class Splitter extends JPanel {
34   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.ui.Splitter");
35   @NonNls public static final String PROP_PROPORTION = "proportion";
36
37   private int myDividerWidth;
38   /**
39    * /------/
40    * |  1   |
41    * This is vertical split |------|
42    * |  2   |
43    * /------/
44    * <p/>
45    * /-------/
46    * |   |   |
47    * This is horizontal split | 1 | 2 |
48    * |   |   |
49    * /-------/
50    */
51   private boolean myVerticalSplit;
52   private boolean myHonorMinimumSize = false;
53   private final float myMinProp;
54   private final float myMaxProp;
55
56
57   protected float myProportion;
58
59   private final Divider myDivider;
60   private JComponent mySecondComponent;
61   private JComponent myFirstComponent;
62   private final FocusWatcher myFocusWatcher;
63   private boolean myShowDividerIcon;
64   private boolean myShowDividerControls;
65   private static final Rectangle myNullBounds = new Rectangle();
66
67
68   /**
69    * Creates horizontal split with proportion equals to .5f
70    */
71   public Splitter() {
72     this(false);
73   }
74
75   /**
76    * Creates split with specified orientation and proportion equals to .5f
77    */
78   public Splitter(boolean vertical) {
79     this(vertical, .5f);
80   }
81
82   /**
83    * Creates split with specified orientation and proportion.
84    */
85   public Splitter(boolean vertical, float proportion) {
86     this(vertical, proportion, 0.0f, 1.0f);
87   }
88
89   public Splitter(boolean vertical, float proportion, float minProp, float maxProp) {
90     myMinProp = minProp;
91     myMaxProp = maxProp;
92     LOG.assertTrue(minProp >= 0.0f);
93     LOG.assertTrue(maxProp <= 1.0f);
94     LOG.assertTrue(minProp <= maxProp);
95     myVerticalSplit = vertical;
96     myShowDividerControls = false;
97     myShowDividerIcon = true;
98     myHonorMinimumSize = true;
99     myDivider = createDivider();
100     setProportion(proportion);
101     myDividerWidth = 7;
102     super.add(myDivider);
103     myFocusWatcher = new FocusWatcher();
104     myFocusWatcher.install(this);
105     setOpaque(false);
106   }
107
108   public void setShowDividerControls(boolean showDividerControls) {
109     myShowDividerControls = showDividerControls;
110     setOrientation(myVerticalSplit);
111   }
112
113   public void setShowDividerIcon(boolean showDividerIcon) {
114     myShowDividerIcon = showDividerIcon;
115     setOrientation(myVerticalSplit);
116   }
117
118   public boolean isShowDividerIcon() {
119     return myShowDividerIcon;
120   }
121
122   public boolean isShowDividerControls() {
123     return myShowDividerControls;
124   }
125
126   public boolean isHonorMinimumSize() {
127     return myHonorMinimumSize;
128   }
129
130   public void setHonorComponentsMinimumSize(boolean honorMinimumSize) {
131     myHonorMinimumSize = honorMinimumSize;
132   }
133
134   /**
135    * This is temporary solution for UIDesigner. <b>DO NOT</b> use it from code.
136    *
137    * @see #setFirstComponent(JComponent)
138    * @see #setSecondComponent(JComponent)
139    * @deprecated
140    */
141   public Component add(Component comp) {
142     final int childCount = getComponentCount();
143     LOG.assertTrue(childCount >= 1);
144     if (childCount > 3) {
145       throw new IllegalStateException("" + childCount);
146     }
147     LOG.assertTrue(childCount <= 3);
148     if (childCount == 1) {
149       setFirstComponent((JComponent)comp);
150     }
151     else {
152       setSecondComponent((JComponent)comp);
153     }
154     return comp;
155   }
156
157   public void dispose() {
158     myFocusWatcher.deinstall(this);
159   }
160
161   protected Divider createDivider() {
162     return new Divider();
163   }
164
165   public boolean isVisible() {
166     return super.isVisible() &&
167            (myFirstComponent != null && myFirstComponent.isVisible() || mySecondComponent != null && mySecondComponent.isVisible());
168   }
169
170   public Dimension getMinimumSize() {
171     final int dividerWidth = getDividerWidth();
172     if (myFirstComponent != null && myFirstComponent.isVisible() && mySecondComponent != null && mySecondComponent.isVisible()) {
173       final Dimension firstMinSize = myFirstComponent.getMinimumSize();
174       final Dimension secondMinSize = mySecondComponent.getMinimumSize();
175       return getOrientation()
176              ? new Dimension(Math.max(firstMinSize.width, secondMinSize.width), firstMinSize.height + dividerWidth + secondMinSize.height)
177              : new Dimension(firstMinSize.width + dividerWidth + secondMinSize.width, Math.max(firstMinSize.height, secondMinSize.height));
178     }
179
180     if (myFirstComponent != null && myFirstComponent.isVisible()) { // only first component is visible
181       return myFirstComponent.getMinimumSize();
182     }
183
184     if (mySecondComponent != null && mySecondComponent.isVisible()) { // only second component is visible
185       return mySecondComponent.getMinimumSize();
186     }
187
188     return super.getMinimumSize();
189   }
190
191   @Override
192   public Dimension getPreferredSize() {
193     final int dividerWidth = getDividerWidth();
194     if (myFirstComponent != null && myFirstComponent.isVisible() && mySecondComponent != null && mySecondComponent.isVisible()) {
195       final Dimension firstPrefSize = myFirstComponent.getPreferredSize();
196       final Dimension secondPrefSize = mySecondComponent.getPreferredSize();
197       return getOrientation()
198              ? new Dimension(Math.max(firstPrefSize.width, secondPrefSize.width),
199                              firstPrefSize.height + dividerWidth + secondPrefSize.height)
200              : new Dimension(firstPrefSize.width + dividerWidth + secondPrefSize.width,
201                              Math.max(firstPrefSize.height, secondPrefSize.height));
202     }
203
204     if (myFirstComponent != null && myFirstComponent.isVisible()) { // only first component is visible
205       return myFirstComponent.getPreferredSize();
206     }
207
208     if (mySecondComponent != null && mySecondComponent.isVisible()) { // only second component is visible
209       return mySecondComponent.getPreferredSize();
210     }
211
212     return super.getPreferredSize();
213   }
214
215   public void doLayout() {
216     final double width = getWidth();
217     final double height = getHeight();
218
219     final double componentSize = getOrientation() ? height : width;
220     if (componentSize <= 0) return;
221
222     if (!isNull(myFirstComponent) && myFirstComponent.isVisible() && !isNull(mySecondComponent) && mySecondComponent.isVisible()) {
223       // both first and second components are visible
224       Rectangle firstRect = new Rectangle();
225       Rectangle dividerRect = new Rectangle();
226       Rectangle secondRect = new Rectangle();
227
228       double dividerWidth = getDividerWidth();
229       double firstComponentSize;
230       double secondComponentSize;
231
232       if (componentSize <= dividerWidth) {
233         firstComponentSize = 0;
234         secondComponentSize = 0;
235         dividerWidth = componentSize;
236       }
237       else {
238         firstComponentSize = myProportion * (float)(componentSize - dividerWidth);
239         secondComponentSize = getOrientation() ? height - firstComponentSize - dividerWidth : width - firstComponentSize - dividerWidth;
240
241         if (isHonorMinimumSize()) {
242
243           final double firstMinSize = getOrientation() ? myFirstComponent.getMinimumSize().getHeight() : myFirstComponent.getMinimumSize().getWidth();
244           final double secondMinSize = getOrientation() ? mySecondComponent.getMinimumSize().getHeight() : mySecondComponent.getMinimumSize().getWidth();
245
246           if (firstComponentSize + secondComponentSize < firstMinSize + secondMinSize) {
247             double proportion = firstMinSize / (firstMinSize + secondMinSize);
248             firstComponentSize = (int)(proportion * (float)(componentSize - dividerWidth));
249             secondComponentSize = getOrientation() ? height - firstComponentSize - dividerWidth : width - firstComponentSize - dividerWidth;
250           }
251           else {
252             if (firstComponentSize < firstMinSize) {
253               secondComponentSize -= firstMinSize - firstComponentSize;
254               firstComponentSize = firstMinSize;
255             }
256             else if (secondComponentSize < secondMinSize) {
257               firstComponentSize -= secondMinSize - secondComponentSize;
258               secondComponentSize = secondMinSize;
259             }
260           }
261         }
262       }
263
264       myProportion = (float)(firstComponentSize / (firstComponentSize + secondComponentSize));
265
266       firstComponentSize = Math.floor(firstComponentSize);
267       secondComponentSize = Math.floor(secondComponentSize);
268       
269       if (getOrientation()) {
270         // fix flooring
271         secondComponentSize += (int) (height - firstComponentSize - secondComponentSize - dividerWidth);
272         
273         firstRect.setBounds(0, 0, (int)width, (int)firstComponentSize);
274         dividerRect.setBounds(0, (int)firstComponentSize, (int)width, (int)dividerWidth);
275         secondRect.setBounds(0, (int)(firstComponentSize + dividerWidth), (int)width, (int)secondComponentSize);
276       }
277       else {
278         // fix flooring
279         secondComponentSize += (int) (width - firstComponentSize - secondComponentSize - dividerWidth);
280         
281         firstRect.setBounds(0, 0, (int)firstComponentSize, (int)height);
282         dividerRect.setBounds((int)firstComponentSize, 0, (int)dividerWidth, (int)height);
283         secondRect.setBounds((int)(firstComponentSize + dividerWidth), 0, (int)secondComponentSize, (int)height);
284       }
285       myDivider.setVisible(true);
286       myFirstComponent.setBounds(firstRect);
287       myDivider.setBounds(dividerRect);
288       mySecondComponent.setBounds(secondRect);
289       myFirstComponent.revalidate();
290       mySecondComponent.revalidate();
291     }
292     else if (!isNull(myFirstComponent) && myFirstComponent.isVisible()) { // only first component is visible
293       hideNull(mySecondComponent);
294       myDivider.setVisible(false);
295       myFirstComponent.setBounds(0, 0, (int)width, (int)height);
296       myFirstComponent.revalidate();
297     }
298     else if (!isNull(mySecondComponent) && mySecondComponent.isVisible()) { // only second component is visible
299       hideNull(myFirstComponent);
300       myDivider.setVisible(false);
301       mySecondComponent.setBounds(0, 0, (int)width, (int)height);
302       mySecondComponent.revalidate();
303     }
304     else { // both components are null or invisible
305       myDivider.setVisible(false);
306       if (myFirstComponent != null) {
307         myFirstComponent.setBounds(0, 0, 0, 0);
308         myFirstComponent.revalidate();
309       }
310       else {
311         hideNull(myFirstComponent);
312       }
313       if (mySecondComponent != null) {
314         mySecondComponent.setBounds(0, 0, 0, 0);
315         mySecondComponent.revalidate();
316       }
317       else {
318         hideNull(mySecondComponent);
319       }
320     }
321     myDivider.revalidate();
322   }
323
324   static boolean isNull(Component component) {
325     return NullableComponent.Check.isNull(component);
326   }
327
328   static void hideNull(Component component) {
329     if (component instanceof NullableComponent) {
330       if (!component.getBounds().equals(myNullBounds)) {
331         component.setBounds(myNullBounds);
332         component.validate();
333       }
334     }
335   }
336
337   public int getDividerWidth() {
338     return myDividerWidth;
339   }
340
341   public void setDividerWidth(int width) {
342     if (width <= 0) {
343       throw new IllegalArgumentException("Wrong divider width: " + width);
344     }
345     if (myDividerWidth != width) {
346       myDividerWidth = width;
347       revalidate();
348       repaint();
349     }
350   }
351
352   public float getProportion() {
353     return myProportion;
354   }
355
356   public void setProportion(float proportion) {
357     if (myProportion == proportion) {
358       return;
359     }
360     if (proportion < .0f || proportion > 1.0f) {
361       throw new IllegalArgumentException("Wrong proportion: " + proportion);
362     }
363     if (proportion < myMinProp) proportion = myMinProp;
364     if (proportion > myMaxProp) proportion = myMaxProp;
365     float oldProportion = myProportion;
366     myProportion = proportion;
367     firePropertyChange(PROP_PROPORTION, new Float(oldProportion), new Float(myProportion));
368     revalidate();
369     repaint();
370   }
371
372   /**
373    * Swaps components.
374    */
375   public void swapComponents() {
376     JComponent tmp = myFirstComponent;
377     myFirstComponent = mySecondComponent;
378     mySecondComponent = tmp;
379     revalidate();
380     repaint();
381   }
382
383   /**
384    * @return <code>true</code> if splitter has vertical orientation, <code>false</code> otherwise
385    */
386   public boolean getOrientation() {
387     return myVerticalSplit;
388   }
389
390   /**
391    * @param verticalSplit <code>true</code> means that splitter will have vertical split
392    */
393   public void setOrientation(boolean verticalSplit) {
394     myVerticalSplit = verticalSplit;
395     myDivider.setOrientation(verticalSplit);
396     revalidate();
397     repaint();
398   }
399
400   public JComponent getFirstComponent() {
401     return myFirstComponent;
402   }
403
404   /**
405    * Sets component which is located as the "first" splitted area. The method doesn't validate and
406    * repaint the splitter. If there is already
407    *
408    * @param component
409    */
410   public void setFirstComponent(@Nullable JComponent component) {
411     if (myFirstComponent != component) {
412       if (myFirstComponent != null) {
413         remove(myFirstComponent);
414       }
415       myFirstComponent = component;
416       if (myFirstComponent != null) {
417         super.add(myFirstComponent);
418       }
419       revalidate();
420       repaint();
421     }
422   }
423
424   public JComponent getSecondComponent() {
425     return mySecondComponent;
426   }
427
428   public JComponent getOtherComponent(final Component comp) {
429     if (comp.equals(getFirstComponent())) return getSecondComponent();
430     if (comp.equals(getSecondComponent())) return getFirstComponent();
431     LOG.error("invalid component");
432     return getFirstComponent();
433   }
434
435   /**
436    * Sets component which is located as the "second" splitted area. The method doesn't validate and
437    * repaint the splitter.
438    *
439    * @param component
440    */
441   public void setSecondComponent(@Nullable JComponent component) {
442     if (mySecondComponent != component) {
443       if (mySecondComponent != null) {
444         remove(mySecondComponent);
445       }
446       mySecondComponent = component;
447       if (mySecondComponent != null) {
448         super.add(mySecondComponent);
449       }
450       revalidate();
451       repaint();
452     }
453   }
454
455   public JPanel getDivider() {
456     return myDivider;
457   }
458
459   public class Divider extends JPanel {
460     private boolean myResizeEnabled;
461     protected Point myPoint;
462
463     public Divider() {
464       super(new GridBagLayout());
465       myResizeEnabled = true;
466       setFocusable(false);
467       enableEvents(MouseEvent.MOUSE_EVENT_MASK | MouseEvent.MOUSE_MOTION_EVENT_MASK);
468       setOpaque(false);
469       setOrientation(myVerticalSplit);
470     }
471
472     private void setOrientation(boolean isVerticalSplit) {
473       removeAll();
474
475       setCursor(getOrientation() ?
476                 Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR) :
477                 Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
478
479       if (!myShowDividerControls && !myShowDividerIcon) {
480         return;
481       }
482
483       Icon glueIcon = IconLoader.getIcon(isVerticalSplit ? "/general/splitGlueV.png" : "/general/splitGlueH.png");
484       int leftInsetIcon = 1;
485       int leftInsetArrow = 0;
486       int glueFill = isVerticalSplit ? GridBagConstraints.VERTICAL : GridBagConstraints.HORIZONTAL;
487       add(new JLabel(glueIcon), new GridBagConstraints(0, 0, 1, 1, 0, 0,
488                                                        GridBagConstraints.CENTER, GridBagConstraints.EAST,
489                                                        new Insets(0, leftInsetIcon, 0, 0), 0, 0));
490
491       if (myShowDividerControls && false) {
492         int xMask = isVerticalSplit ? 1 : 0;
493         int yMask = isVerticalSplit ? 0 : 1;
494
495         JLabel splitDownlabel = new JLabel(IconLoader.getIcon(isVerticalSplit ? "/general/splitDown.png" : "/general/splitRight.png"));
496         splitDownlabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
497         splitDownlabel.setToolTipText(isVerticalSplit ? UIBundle.message("splitter.down.tooltip.text") : UIBundle
498           .message("splitter.right.tooltip.text"));
499         splitDownlabel.addMouseListener(new MouseAdapter() {
500           public void mouseClicked(MouseEvent e) {
501             setProportion(1.0f - getMinProportion(mySecondComponent));
502           }
503         });
504         add(splitDownlabel, new GridBagConstraints(isVerticalSplit ? 1 : 0, isVerticalSplit ? 0 : 5, 1, 1, 0, 0, GridBagConstraints.CENTER,
505                                                    GridBagConstraints.NONE, new Insets(0, leftInsetArrow, 0, 0), 0, 0));
506         //
507         add(new JLabel(glueIcon),
508             new GridBagConstraints(2 * xMask, 2 * yMask, 1, 1, 0, 0, GridBagConstraints.CENTER, glueFill, new Insets(0, leftInsetIcon, 0, 0), 0, 0));
509         JLabel splitCenterlabel =
510           new JLabel(IconLoader.getIcon(isVerticalSplit ? "/general/splitCenterV.png" : "/general/splitCenterH.png"));
511         splitCenterlabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
512         splitCenterlabel.setToolTipText(UIBundle.message("splitter.center.tooltip.text"));
513         splitCenterlabel.addMouseListener(new MouseAdapter() {
514           public void mouseClicked(MouseEvent e) {
515             setProportion(.5f);
516           }
517         });
518         add(splitCenterlabel, new GridBagConstraints(3 * xMask, 3 * yMask, 1, 1, 0, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE,
519                                                      new Insets(0, leftInsetArrow, 0, 0), 0, 0));
520         add(new JLabel(glueIcon),
521             new GridBagConstraints(4 * xMask, 4 * yMask, 1, 1, 0, 0, GridBagConstraints.CENTER, glueFill, new Insets(0, leftInsetIcon, 0, 0), 0, 0));
522         //
523         JLabel splitUpLabel = new JLabel(IconLoader.getIcon(isVerticalSplit ? "/general/splitUp.png" : "/general/splitLeft.png"));
524         splitUpLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
525         splitUpLabel.setToolTipText(isVerticalSplit ? UIBundle.message("splitter.up.tooltip.text") : UIBundle
526           .message("splitter.left.tooltip.text"));
527         splitUpLabel.addMouseListener(new MouseAdapter() {
528           public void mouseClicked(MouseEvent e) {
529             setProportion(getMinProportion(myFirstComponent));
530           }
531         });
532         add(splitUpLabel, new GridBagConstraints(isVerticalSplit ? 5 : 0, isVerticalSplit ? 0 : 1, 1, 1, 0, 0, GridBagConstraints.CENTER,
533                                                  GridBagConstraints.NONE, new Insets(0, leftInsetArrow, 0, 0), 0, 0));
534         add(new JLabel(glueIcon), new GridBagConstraints(6 * xMask, 6 * yMask, 1, 1, 0, 0,
535                                                          isVerticalSplit ? GridBagConstraints.WEST : GridBagConstraints.SOUTH, glueFill,
536                                                          new Insets(0, leftInsetIcon, 0, 0), 0, 0));
537       }
538       
539       revalidate();
540       repaint();
541     }
542
543     protected void processMouseMotionEvent(MouseEvent e) {
544       super.processMouseMotionEvent(e);
545       if (!myResizeEnabled) return;
546       if (MouseEvent.MOUSE_DRAGGED == e.getID()) {
547         myPoint = SwingUtilities.convertPoint(this, e.getPoint(), Splitter.this);
548         float proportion;
549         if (getOrientation()) {
550           if (getHeight() > 0) {
551             proportion = Math.min(1.0f, Math.max(.0f, Math.min(Math.max(getMinProportion(myFirstComponent), (float)myPoint.y / (float)Splitter.this.getHeight()), 1 - getMinProportion(mySecondComponent))));
552             setProportion(proportion);
553           }
554         }
555         else {
556           if (getWidth() > 0) {
557             proportion = Math.min(1.0f, Math.max(.0f, Math.min(Math.max(getMinProportion(myFirstComponent), (float)myPoint.x / (float)Splitter.this.getWidth()), 1 - getMinProportion(mySecondComponent))));
558             setProportion(proportion);
559           }
560         }
561       }
562     }
563
564     private float getMinProportion(JComponent component) {
565       if (isHonorMinimumSize()) {
566         if (component != null && myFirstComponent != null && myFirstComponent.isVisible() && mySecondComponent != null &&
567             mySecondComponent.isVisible()) {
568           if (getOrientation()) {
569             return (float)component.getMinimumSize().height / (float)(Splitter.this.getHeight() - getDividerWidth());
570           }
571           else {
572             return (float)component.getMinimumSize().width / (float)(Splitter.this.getWidth() - getDividerWidth());
573           }
574         }
575       }
576       return 0.0f;
577     }
578
579     protected void processMouseEvent(MouseEvent e) {
580       super.processMouseEvent(e);
581       if (!myResizeEnabled) return;
582       switch (e.getID()) {
583         case MouseEvent.MOUSE_CLICKED: {
584           if (e.getClickCount() == 2) {
585             Splitter.this.setProportion(.5f);
586           }
587           break;
588         }
589       }
590     }
591
592     public void setResizeEnabled(boolean resizeEnabled) {
593       myResizeEnabled = resizeEnabled;
594     }
595   }
596 }