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