1 /* 2 * WorkbenchTabPanel.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 16 package org.rstudio.studio.client.workbench.ui; 17 18 import com.google.gwt.dom.client.NativeEvent; 19 import com.google.gwt.dom.client.Style.Unit; 20 import com.google.gwt.event.dom.client.ClickEvent; 21 import com.google.gwt.event.dom.client.ClickHandler; 22 import com.google.gwt.event.logical.shared.HasSelectionHandlers; 23 import com.google.gwt.event.logical.shared.SelectionHandler; 24 import com.google.gwt.event.shared.HandlerRegistration; 25 import com.google.gwt.user.client.ui.Composite; 26 import com.google.gwt.user.client.ui.HTML; 27 import com.google.gwt.user.client.ui.LayoutPanel; 28 import com.google.gwt.user.client.ui.MenuItem; 29 import com.google.gwt.user.client.ui.ProvidesResize; 30 import com.google.gwt.user.client.ui.RequiresResize; 31 import com.google.gwt.user.client.ui.Widget; 32 33 import org.rstudio.core.client.Debug; 34 import org.rstudio.core.client.ElementIds; 35 import org.rstudio.core.client.HandlerRegistrations; 36 import org.rstudio.core.client.events.*; 37 import org.rstudio.core.client.layout.LogicalWindow; 38 import org.rstudio.core.client.theme.ModuleTabLayoutPanel; 39 import org.rstudio.core.client.theme.WindowFrame; 40 import org.rstudio.core.client.theme.res.ThemeStyles; 41 import org.rstudio.core.client.widget.ToolbarPopupMenu; 42 import org.rstudio.core.client.widget.model.ProvidesBusy; 43 44 import java.util.ArrayList; 45 46 class WorkbenchTabPanel 47 extends Composite 48 implements RequiresResize, 49 ProvidesResize, 50 HasSelectionHandlers<Integer>, 51 HasEnsureVisibleHandlers, 52 HasEnsureHeightHandlers 53 { WorkbenchTabPanel(WindowFrame owner, LogicalWindow parentWindow, String tabListName)54 public WorkbenchTabPanel(WindowFrame owner, LogicalWindow parentWindow, String tabListName) 55 { 56 final int UTILITY_AREA_SIZE = 52; 57 panel_ = new LayoutPanel(); 58 59 parentWindow_ = parentWindow; 60 61 tabPanel_ = new ModuleTabLayoutPanel(owner, tabListName); 62 panel_.add(tabPanel_); 63 panel_.setWidgetTopBottom(tabPanel_, 0, Unit.PX, 0, Unit.PX); 64 panel_.setWidgetLeftRight(tabPanel_, 0, Unit.PX, 0, Unit.PX); 65 66 tabPanel_.setSize("100%", "100%"); 67 tabPanel_.addStyleDependentName("Workbench"); 68 69 utilPanel_ = new HTML(); 70 utilPanel_.setStylePrimaryName(ThemeStyles.INSTANCE.multiPodUtilityArea()); 71 utilPanel_.addStyleName(ThemeStyles.INSTANCE.rstheme_multiPodUtilityTabArea()); 72 panel_.add(utilPanel_); 73 panel_.setWidgetRightWidth(utilPanel_, 74 0, Unit.PX, 75 UTILITY_AREA_SIZE, Unit.PX); 76 panel_.setWidgetTopHeight(utilPanel_, 0, Unit.PX, 22, Unit.PX); 77 78 initWidget(panel_); 79 } 80 81 @Override onLoad()82 protected void onLoad() 83 { 84 super.onLoad(); 85 86 releaseOnUnload_.add(tabPanel_.addBeforeSelectionHandler(beforeSelectionEvent -> 87 { 88 if (clearing_) 89 return; 90 91 if (getSelectedIndex() >= 0) 92 { 93 int unselectedTab = getSelectedIndex(); 94 if (unselectedTab < tabs_.size()) 95 { 96 WorkbenchTab lastTab = tabs_.get(unselectedTab); 97 lastTab.onBeforeUnselected(); 98 } 99 } 100 101 int selectedTab = beforeSelectionEvent.getItem().intValue(); 102 if (selectedTab < tabs_.size()) 103 { 104 WorkbenchTab tab = tabs_.get(selectedTab); 105 tab.onBeforeSelected(); 106 } 107 })); 108 releaseOnUnload_.add(tabPanel_.addSelectionHandler(selectionEvent -> 109 { 110 if (clearing_) 111 return; 112 113 WorkbenchTab pane = tabs_.get(selectionEvent.getSelectedItem().intValue()); 114 pane.onSelected(); 115 })); 116 117 int selectedIndex = tabPanel_.getSelectedIndex(); 118 if (selectedIndex >= 0) 119 { 120 WorkbenchTab tab = tabs_.get(selectedIndex); 121 tab.onBeforeSelected(); 122 tab.onSelected(); 123 } 124 } 125 126 @Override onUnload()127 protected void onUnload() 128 { 129 releaseOnUnload_.removeHandler(); 130 131 super.onUnload(); 132 } 133 setTabs(ArrayList<WorkbenchTab> tabs)134 public void setTabs(ArrayList<WorkbenchTab> tabs) 135 { 136 if (areTabsIdentical(tabs)) 137 return; 138 139 tabPanel_.clear(); 140 tabs_.clear(); 141 142 for (WorkbenchTab tab : tabs) 143 add(tab); 144 } 145 areTabsIdentical(ArrayList<WorkbenchTab> tabs)146 private boolean areTabsIdentical(ArrayList<WorkbenchTab> tabs) 147 { 148 if (tabs_.size() != tabs.size()) 149 return false; 150 151 // In case tab panels were removed implicitly (such as Console) 152 if (tabPanel_.getWidgetCount() != tabs.size()) 153 return false; 154 155 for (int i = 0; i < tabs.size(); i++) 156 if (tabs_.get(i) != tabs.get(i)) 157 return false; 158 159 return true; 160 } 161 162 @SuppressWarnings("unused") add(final WorkbenchTab tab)163 private void add(final WorkbenchTab tab) 164 { 165 if (tab.isSuppressed()) 166 return; 167 168 tabs_.add(tab); 169 final Widget widget = tab.asWidget(); 170 tabPanel_.add(widget, tab.getTitle(), false, !tab.closeable() ? null : new ClickHandler() 171 { 172 @Override 173 public void onClick(ClickEvent event) 174 { 175 tab.confirmClose(() -> tab.ensureHidden()); 176 } 177 }, 178 tab instanceof ProvidesBusy ? (ProvidesBusy) tab : null); 179 180 int widgetIndex = tabPanel_.getWidgetIndex(tab); 181 if (widgetIndex >= 0) 182 { 183 // add context menu to the Tab 184 tabPanel_.setTabContextMenuHandler(widgetIndex, contextMenuEvent -> 185 { 186 if (tab.closeable()) 187 { 188 final ToolbarPopupMenu menu = new ToolbarPopupMenu(); 189 final NativeEvent nativeEvent = contextMenuEvent.getNativeEvent(); 190 191 menu.addItem(ElementIds.TAB_CLOSE, new MenuItem("Close", () -> 192 { 193 tab.confirmClose(() -> tab.ensureHidden()); 194 })); 195 196 menu.showRelativeTo(nativeEvent.getClientX(), 197 nativeEvent.getClientY(), 198 ElementIds.FEATURE_TAB_CONTEXT); 199 } 200 201 // a tab that isn't closable will no-op when right-clicked, seems 202 // preferable to bringing up the browser context menu 203 contextMenuEvent.preventDefault(); 204 contextMenuEvent.stopPropagation(); 205 }); 206 } 207 208 tab.addEnsureVisibleHandler(ensureVisibleEvent -> 209 { 210 if (!neverVisible_) 211 { 212 // First ensure that we ourselves are visible 213 int myInt = tabPanel_.getWidgetCount(); 214 LogicalWindow window = getParentWindow(); 215 fireEvent(new EnsureVisibleEvent(ensureVisibleEvent.getActivate())); 216 if (ensureVisibleEvent.getActivate()) 217 tabPanel_.selectTab(widget); 218 } 219 }); 220 221 tab.addEnsureHeightHandler(ensureHeightEvent -> fireEvent(ensureHeightEvent)); 222 } 223 selectNextTab()224 public void selectNextTab() 225 { 226 selectTabRelative(1); 227 } 228 selectPreviousTab()229 public void selectPreviousTab() 230 { 231 selectTabRelative(-1); 232 } 233 selectTabRelative(int offset)234 public void selectTabRelative(int offset) 235 { 236 int index = (getSelectedIndex() + offset) % tabs_.size(); 237 selectTab(index); 238 } 239 selectTab(int tabIndex)240 public void selectTab(int tabIndex) 241 { 242 if (tabPanel_.getSelectedIndex() == tabIndex) 243 { 244 // if it's already selected then we still want to fire the 245 // onBeforeSelected and onSelected methods (so that actions 246 // like auto-focus are always taken) 247 int selected = getSelectedIndex(); 248 if (selected != -1) 249 { 250 WorkbenchTab tab = tabs_.get(selected); 251 tab.onBeforeSelected(); 252 tab.onSelected(); 253 } 254 255 return; 256 } 257 258 // deal with migrating from n+1 to n tabs, and with -1 values 259 int safeIndex = Math.min(Math.max(0, tabIndex), tabs_.size() - 1); 260 if (safeIndex >= 0) 261 tabPanel_.selectTab(safeIndex); 262 else 263 Debug.logToConsole("Attempted to select tab in empty tab panel."); 264 } 265 selectTab(WorkbenchTab pane)266 public void selectTab(WorkbenchTab pane) 267 { 268 int index = tabs_.indexOf(pane); 269 if (index != -1) 270 selectTab(index); 271 else 272 { 273 String title = pane.getTitle(); 274 for (int i = 0; i < tabs_.size(); i++) 275 { 276 WorkbenchTab tab = tabs_.get(i); 277 if (tab.getTitle() == title) 278 { 279 selectTab(i); 280 return; 281 } 282 } 283 } 284 } 285 isEmpty()286 public boolean isEmpty() 287 { 288 return tabs_.isEmpty(); 289 } 290 getTab(int index)291 public WorkbenchTab getTab(int index) 292 { 293 return tabs_.get(index); 294 } 295 getSelectedTab()296 public WorkbenchTab getSelectedTab() 297 { 298 return tabs_.get(getSelectedIndex()); 299 } 300 getSelectedIndex()301 public int getSelectedIndex() 302 { 303 return tabPanel_.getSelectedIndex(); 304 } 305 getWidgetCount()306 public int getWidgetCount() 307 { 308 return tabPanel_.getWidgetCount(); 309 } 310 addSelectionHandler(SelectionHandler<Integer> integerSelectionHandler)311 public HandlerRegistration addSelectionHandler(SelectionHandler<Integer> integerSelectionHandler) 312 { 313 return tabPanel_.addSelectionHandler(integerSelectionHandler); 314 } 315 onResize()316 public void onResize() 317 { 318 Widget w = getWidget(); 319 if (w instanceof RequiresResize) 320 ((RequiresResize)w).onResize(); 321 } 322 addEnsureVisibleHandler(EnsureVisibleEvent.Handler handler)323 public HandlerRegistration addEnsureVisibleHandler(EnsureVisibleEvent.Handler handler) 324 { 325 return addHandler(handler, EnsureVisibleEvent.TYPE); 326 } 327 328 @Override addEnsureHeightHandler(EnsureHeightEvent.Handler handler)329 public HandlerRegistration addEnsureHeightHandler(EnsureHeightEvent.Handler handler) 330 { 331 return addHandler(handler, EnsureHeightEvent.TYPE); 332 } 333 clear()334 public void clear() 335 { 336 clearing_ = true; 337 tabPanel_.clear(); 338 tabs_.clear(); 339 clearing_ = false; 340 } 341 getParentWindow()342 public LogicalWindow getParentWindow() 343 { 344 return parentWindow_; 345 } 346 setNeverVisible(boolean value)347 public void setNeverVisible(boolean value) 348 { 349 neverVisible_ = value; 350 } 351 352 private ModuleTabLayoutPanel tabPanel_; 353 private ArrayList<WorkbenchTab> tabs_ = new ArrayList<>(); 354 private final LogicalWindow parentWindow_; 355 private final HandlerRegistrations releaseOnUnload_ = new HandlerRegistrations(); 356 private boolean clearing_ = false; 357 private boolean neverVisible_ = false; 358 private LayoutPanel panel_; 359 private HTML utilPanel_; 360 } 361