1 /*
2  * BinarySplitLayoutPanel.java
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 package org.rstudio.core.client.layout;
16 
17 import com.google.gwt.aria.client.OrientationValue;
18 import com.google.gwt.aria.client.Roles;
19 import com.google.gwt.core.client.Scheduler;
20 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
21 import com.google.gwt.dom.client.Style;
22 import com.google.gwt.event.dom.client.*;
23 import com.google.gwt.event.shared.HandlerRegistration;
24 import com.google.gwt.layout.client.Layout;
25 import com.google.gwt.user.client.Event;
26 import com.google.gwt.user.client.ui.*;
27 
28 public class BinarySplitLayoutPanel extends LayoutPanel
29       implements MouseDownHandler, MouseMoveHandler, MouseUpHandler,
30                  KeyDownHandler, BlurHandler, FocusHandler
31 {
32    /**
33     * Default number of pixels pane splitters are moved by arrow keys
34     */
35    public static final int KEYBOARD_MOVE_SIZE = 20;
36 
BinarySplitLayoutPanel(String name, Widget[] widgets, int splitterHeight)37    public BinarySplitLayoutPanel(String name, Widget[] widgets, int splitterHeight)
38    {
39       widgets_ = widgets;
40       splitterHeight_ = splitterHeight;
41 
42       setWidgets(widgets);
43 
44       splitterPos_ = 300;
45       topIsFixed_ = false;
46       splitter_ = new HTML();
47       splitter_.setStylePrimaryName("gwt-SplitLayoutPanel-VDragger");
48       splitter_.addMouseDownHandler(this);
49       splitter_.addMouseMoveHandler(this);
50       splitter_.addMouseUpHandler(this);
51       splitter_.addDomHandler(this, KeyDownEvent.getType());
52       splitter_.addDomHandler(this, BlurEvent.getType());
53       splitter_.addDomHandler(this, FocusEvent.getType());
54       splitter_.getElement().getStyle().setZIndex(200);
55       Roles.getSeparatorRole().set(splitter_.getElement());
56       Roles.getSeparatorRole().setAriaOrientationProperty(splitter_.getElement(),
57          OrientationValue.HORIZONTAL);
58       Roles.getSeparatorRole().setAriaLabelProperty(splitter_.getElement(), name + " splitter");
59       splitter_.getElement().setTabIndex(-1);
60       add(splitter_);
61       setWidgetLeftRight(splitter_, 0, Style.Unit.PX, 0, Style.Unit.PX);
62       setWidgetBottomHeight(splitter_,
63                             splitterPos_, Style.Unit.PX,
64                             splitterHeight_, Style.Unit.PX);
65    }
66 
setWidgets(Widget[] widgets)67    public void setWidgets(Widget[] widgets)
68    {
69       for (Widget w : widgets_)
70          remove(w);
71 
72       widgets_ = widgets;
73       for (Widget w : widgets)
74       {
75          add(w);
76          setWidgetLeftRight(w, 0, Style.Unit.PX, 0, Style.Unit.PX);
77          setWidgetTopHeight(w, 0, Style.Unit.PX, 100, Style.Unit.PX);
78          w.setVisible(false);
79          AnimationHelper.setParentZindex(w, -10);
80       }
81 
82       if (top_ >= 0)
83          setTopWidget(top_, true);
84       if (bottom_ >= 0)
85          setBottomWidget(bottom_, true);
86    }
87 
88    @Override
onAttach()89    protected void onAttach()
90    {
91       super.onAttach();
92       Scheduler.get().scheduleDeferred(new ScheduledCommand()
93       {
94          public void execute()
95          {
96             offsetHeight_ = getOffsetHeight();
97          }
98       });
99    }
100 
addSplitterBeforeResizeHandler( SplitterBeforeResizeHandler handler)101    public HandlerRegistration addSplitterBeforeResizeHandler(
102          SplitterBeforeResizeHandler handler)
103    {
104       return addHandler(handler, SplitterBeforeResizeEvent.TYPE);
105    }
106 
addSplitterResizedHandler( SplitterResizedHandler handler)107    public HandlerRegistration addSplitterResizedHandler(
108          SplitterResizedHandler handler)
109    {
110       return addHandler(handler, SplitterResizedEvent.TYPE);
111    }
112 
setTopWidget(Widget widget, boolean manageVisibility)113    public void setTopWidget(Widget widget, boolean manageVisibility)
114    {
115       if (widget == null)
116       {
117          setTopWidget(-1, manageVisibility);
118          return;
119       }
120 
121       for (int i = 0; i < widgets_.length; i++)
122          if (widgets_[i] == widget)
123          {
124             setTopWidget(i, manageVisibility);
125             return;
126          }
127 
128       assert false;
129    }
130 
setTopWidget(int widgetIndex, boolean manageVisibility)131    public void setTopWidget(int widgetIndex, boolean manageVisibility)
132    {
133       if (manageVisibility && top_ >= 0)
134          widgets_[top_].setVisible(false);
135 
136       top_ = widgetIndex;
137       if (bottom_ == top_)
138          setBottomWidget(-1, manageVisibility);
139 
140       if (manageVisibility && top_ >= 0)
141          widgets_[top_].setVisible(true);
142 
143       updateLayout();
144    }
145 
setBottomWidget(Widget widget, boolean manageVisibility)146    public void setBottomWidget(Widget widget, boolean manageVisibility)
147    {
148       if (widget == null)
149       {
150          setBottomWidget(-1, manageVisibility);
151          return;
152       }
153 
154       for (int i = 0; i < widgets_.length; i++)
155          if (widgets_[i] == widget)
156          {
157             setBottomWidget(i, manageVisibility);
158             return;
159          }
160 
161       assert false;
162    }
163 
setBottomWidget(int widgetIndex, boolean manageVisibility)164    public void setBottomWidget(int widgetIndex, boolean manageVisibility)
165    {
166       if (manageVisibility && bottom_ >= 0)
167          widgets_[bottom_].setVisible(false);
168 
169       bottom_ = widgetIndex;
170       if (top_ == bottom_)
171          setTopWidget(-1, manageVisibility);
172 
173       if (manageVisibility && bottom_ >= 0)
174          widgets_[bottom_].setVisible(true);
175 
176       updateLayout();
177    }
178 
isSplitterVisible()179    public boolean isSplitterVisible()
180    {
181       return splitter_.isVisible();
182    }
183 
setSplitterVisible(boolean visible)184    public void setSplitterVisible(boolean visible)
185    {
186       splitter_.setVisible(visible);
187    }
188 
setSplitterPos(int splitterPos, boolean fromTop)189    public void setSplitterPos(int splitterPos, boolean fromTop)
190    {
191       if (isVisible() && isAttached() && splitter_.isVisible())
192       {
193          splitterPos = Math.min(getOffsetHeight() - splitterHeight_,
194                                 splitterPos);
195       }
196 
197       if (splitter_.isVisible())
198          splitterPos = Math.max(splitterHeight_, splitterPos);
199 
200       if (splitterPos_ == splitterPos
201           && topIsFixed_ == fromTop
202           && offsetHeight_ == getOffsetHeight())
203       {
204          return;
205       }
206 
207       splitterPos_ = splitterPos;
208       topIsFixed_ = fromTop;
209       offsetHeight_ = getOffsetHeight();
210       if (topIsFixed_)
211       {
212          setWidgetTopHeight(splitter_,
213                             splitterPos_,
214                             Style.Unit.PX,
215                             splitterHeight_,
216                             Style.Unit.PX);
217       }
218       else
219       {
220          setWidgetBottomHeight(splitter_,
221                                splitterPos_,
222                                Style.Unit.PX,
223                                splitterHeight_,
224                                Style.Unit.PX);
225       }
226 
227       updateLayout();
228    }
229 
getSplitterBottom()230    public int getSplitterBottom()
231    {
232       assert !topIsFixed_;
233       return splitterPos_;
234    }
235 
updateLayout()236    private void updateLayout()
237    {
238       if (topIsFixed_)
239       {
240          if (top_ >= 0)
241             setWidgetTopHeight(widgets_[top_],
242                                0,
243                                Style.Unit.PX,
244                                splitterPos_,
245                                Style.Unit.PX);
246 
247          if (bottom_ >= 0)
248             setWidgetTopBottom(widgets_[bottom_],
249                                splitterPos_ + splitterHeight_,
250                                Style.Unit.PX,
251                                0,
252                                Style.Unit.PX);
253       }
254       else
255       {
256          if (top_ >= 0)
257             setWidgetTopBottom(widgets_[top_],
258                                0,
259                                Style.Unit.PX,
260                                splitterPos_ + splitterHeight_,
261                                Style.Unit.PX);
262 
263          if (bottom_ >= 0)
264             setWidgetBottomHeight(widgets_[bottom_],
265                                   0,
266                                   Style.Unit.PX,
267                                   splitterPos_,
268                                   Style.Unit.PX);
269       }
270 
271       // Not sure why, but onResize() doesn't seem to get called unless we
272       // do this manually. This matters for ShellPane scroll position updating.
273       animate(0, new Layout.AnimationCallback()
274       {
275          public void onAnimationComplete()
276          {
277             onResize();
278          }
279 
280          public void onLayout(Layout.Layer layer, double progress)
281          {
282          }
283       });
284    }
285 
286    @Override
onResize()287    public void onResize()
288    {
289       super.onResize();
290       // getOffsetHeight() > 0 is to deal with Firefox tab tear-off, which
291       // causes us to be resized to 0 (bug 1586)
292       if (offsetHeight_ > 0 && splitter_.isVisible() && getOffsetHeight() > 0)
293       {
294          double pct = ((double)splitterPos_ / offsetHeight_);
295          int newPos = (int) Math.round(getOffsetHeight() * pct);
296          setSplitterPos(newPos, topIsFixed_);
297       }
298    }
299 
onMouseDown(MouseDownEvent event)300    public void onMouseDown(MouseDownEvent event)
301    {
302       resizing_ = true;
303       Event.setCapture(splitter_.getElement());
304       event.preventDefault();
305       event.stopPropagation();
306       fireEvent(new SplitterBeforeResizeEvent());
307    }
308 
onMouseMove(MouseMoveEvent event)309    public void onMouseMove(MouseMoveEvent event)
310    {
311       if (event.getNativeButton() == 0)
312          resizing_ = false;
313 
314       if (!resizing_)
315          return;
316 
317       event.preventDefault();
318       event.stopPropagation();
319       if (topIsFixed_)
320          setSplitterPos(event.getRelativeY(getElement()), true);
321       else
322          setSplitterPos(getOffsetHeight() - event.getRelativeY(getElement()),
323                         false);
324    }
325 
onMouseUp(MouseUpEvent event)326    public void onMouseUp(MouseUpEvent event)
327    {
328       if (resizing_)
329       {
330          resizing_ = false;
331          Event.releaseCapture(splitter_.getElement());
332          fireEvent(new SplitterResizedEvent());
333       }
334    }
335 
onKeyDown(KeyDownEvent event)336    public void onKeyDown(KeyDownEvent event)
337    {
338       if (!isVisible())
339          return;
340 
341       int delta = 0;
342       switch (event.getNativeKeyCode())
343       {
344          case KeyCodes.KEY_UP:
345             delta = KEYBOARD_MOVE_SIZE;
346             break;
347 
348          case KeyCodes.KEY_DOWN:
349             delta = -KEYBOARD_MOVE_SIZE;
350             break;
351       }
352       if (delta == 0)
353          return;
354 
355       event.preventDefault();
356       event.stopPropagation();
357 
358       // use Shift key with arrows to make small adjustments
359       if (event.getNativeEvent().getShiftKey())
360          delta = delta < 0 ? -1 : 1;
361       fireEvent(new SplitterBeforeResizeEvent());
362       setSplitterPos(splitterPos_ + delta, topIsFixed_);
363       fireEvent(new SplitterResizedEvent());
364    }
365 
366    public void onBlur(BlurEvent event)
367    {
368       splitter_.removeStyleDependentName("focused");
369    }
370 
371    public void onFocus(FocusEvent event)
372    {
373       splitter_.addStyleDependentName("focused");
374    }
375 
376    public void focusSplitter()
377    {
378       if (isSplitterVisible())
379          splitter_.getElement().focus();
380    }
381 
382    public int getSplitterHeight()
383    {
384       return splitterHeight_;
385    }
386 
387    private int top_;
388    private int bottom_;
389 
390    private HTML splitter_;
391    private int splitterPos_;
392    private int splitterHeight_;
393    // If true, then bottom widget should scale and top widget should stay
394    // fixed. If false, then vice versa.
395    private boolean topIsFixed_ = true;
396    private Widget[] widgets_;
397    private boolean resizing_;
398    private int offsetHeight_;
399 }
400