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