1 /*
2  * TerminalPopupMenu.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.views.terminal;
17 
18 import org.rstudio.core.client.ElementIds;
19 import org.rstudio.core.client.StringUtil;
20 import org.rstudio.core.client.command.AppCommand;
21 import org.rstudio.core.client.widget.ToolbarButton;
22 import org.rstudio.core.client.widget.ToolbarMenuButton;
23 import org.rstudio.core.client.widget.ToolbarPopupMenu;
24 import org.rstudio.studio.client.RStudioGinjector;
25 import org.rstudio.studio.client.application.events.EventBus;
26 import org.rstudio.studio.client.common.icons.StandardIcons;
27 import org.rstudio.studio.client.workbench.commands.Commands;
28 import org.rstudio.studio.client.workbench.model.WorkbenchServerOperations;
29 import org.rstudio.studio.client.workbench.views.terminal.events.SwitchToTerminalEvent;
30 import org.rstudio.studio.client.server.ErrorLoggingServerRequestCallback;
31 
32 import com.google.gwt.core.client.Scheduler;
33 import com.google.gwt.user.client.ui.MenuItem;
34 import com.google.inject.Inject;
35 
36 /**
37  * Drop-down menu used in terminal pane. Has commands, and a list of
38  * terminal sessions.
39  */
40 public class TerminalPopupMenu extends ToolbarPopupMenu
41 {
TerminalPopupMenu(TerminalList terminals)42    public TerminalPopupMenu(TerminalList terminals)
43    {
44       RStudioGinjector.INSTANCE.injectMembers(this);
45       terminals_ = terminals;
46    }
47 
48    @Inject
initialize(Commands commands, EventBus events, WorkbenchServerOperations server)49    private void initialize(Commands commands,
50                            EventBus events,
51                            WorkbenchServerOperations server)
52    {
53       commands_ = commands;
54       eventBus_ = events;
55       server_ = server;
56    }
57 
58    @Override
getDynamicPopupMenu(final DynamicPopupMenuCallback callback)59    public void getDynamicPopupMenu(final DynamicPopupMenuCallback callback)
60    {
61       // clean out existing entries
62       clearItems();
63       addItem(commands_.newTerminal().createMenuItem(false));
64       addSeparator();
65 
66       if (terminals_.terminalCount() > 0)
67       {
68          for (final String handle : terminals_)
69          {
70             Scheduler.ScheduledCommand cmd = () ->
71                   eventBus_.fireEvent(new SwitchToTerminalEvent(handle, null));
72 
73             String caption = trimCaption(terminals_.getCaption(handle));
74             if (caption == null)
75             {
76                continue;
77             }
78 
79             // visual indicator that terminal has processes running
80             caption = addBusyIndicator(caption, terminals_.getHasSubprocs(handle));
81 
82             String menuHtml = AppCommand.formatMenuLabel(
83                   null,              /*icon*/
84                   caption,           /*label*/
85                   false,             /*html*/
86                   null,              /*shortcut*/
87                   null,              /*rightImage*/
88                   null);             /*rightImageDesc*/
89             addItem(new MenuItem(menuHtml, true, cmd));
90          }
91          addSeparator();
92          addItem(commands_.setTerminalToCurrentDirectory().createMenuItem(false));
93          addItem(commands_.renameTerminal().createMenuItem(false));
94          addItem(commands_.sendTerminalToEditor().createMenuItem(false));
95          addSeparator();
96          addItem(commands_.previousTerminal().createMenuItem(false));
97          addItem(commands_.nextTerminal().createMenuItem(false));
98          addSeparator();
99          addItem(commands_.interruptTerminal().createMenuItem(false));
100          addItem(commands_.clearTerminalScrollbackBuffer().createMenuItem(false));
101          addItem(commands_.closeTerminal().createMenuItem(false));
102          addSeparator();
103          addItem(commands_.closeAllTerminals().createMenuItem(false));
104          addSeparator();
105       }
106 
107       addItem(commands_.showTerminalOptions().createMenuItem(false));
108       callback.onPopupMenu(this);
109    }
110 
getToolbarButton()111    public ToolbarButton getToolbarButton()
112    {
113       if (toolbarButton_ == null)
114       {
115          String buttonText = "Terminal";
116 
117          toolbarButton_ = new ToolbarMenuButton(
118                 buttonText,
119                 ToolbarButton.NoTitle,
120                 StandardIcons.INSTANCE.empty_command(),
121                 this,
122                 false);
123 
124          ElementIds.assignElementId(toolbarButton_, ElementIds.TERMINAL_DROPDOWN_MENUBUTTON);
125 
126          setNoActiveTerminal();
127       }
128       return toolbarButton_;
129    }
130 
131    /**
132     * @param caption caption of the active terminal
133     * @param handle handle of the active terminal
134     */
setActiveTerminal(String caption, String handle)135    public void setActiveTerminal(String caption, String handle)
136    {
137       activeTerminalHandle_ = handle;
138       String trimmed = trimCaption(caption);
139       if (handle != null)
140       {
141          trimmed = addBusyIndicator(trimmed, terminals_.getHasSubprocs(handle));
142       }
143       toolbarButton_.setText(trimmed);
144 
145       updateTerminalCommands();
146 
147       // inform server of the selection
148       server_.processNotifyVisible(
149             activeTerminalHandle_,
150             new ErrorLoggingServerRequestCallback<>());
151    }
152 
153    /**
154     * Update terminal commands based on current selection
155     */
updateTerminalCommands()156    public void updateTerminalCommands()
157    {
158       boolean haveActiveTerminal = activeTerminalHandle_ != null;
159       commands_.setTerminalToCurrentDirectory().setEnabled(haveActiveTerminal);
160       commands_.closeTerminal().setEnabled(haveActiveTerminal);
161       commands_.renameTerminal().setEnabled(haveActiveTerminal);
162       commands_.clearTerminalScrollbackBuffer().setEnabled(haveActiveTerminal);
163       commands_.interruptTerminal().setEnabled(haveActiveTerminal);
164       commands_.previousTerminal().setEnabled(getPreviousTerminalHandle() != null);
165       commands_.nextTerminal().setEnabled(getNextTerminalHandle() != null);
166       commands_.sendTerminalToEditor().setEnabled(haveActiveTerminal);
167    }
168 
setActiveTerminalByCaption(String caption, boolean createdByApi)169    public void setActiveTerminalByCaption(String caption, boolean createdByApi)
170    {
171       String handle = terminals_.handleForCaption(caption);
172       if (StringUtil.isNullOrEmpty(handle))
173          return;
174       eventBus_.fireEvent(new SwitchToTerminalEvent(handle, null, createdByApi));
175    }
176 
177    /**
178     * Refresh caption of active terminal based on busy status.
179     */
refreshActiveTerminal()180    public void refreshActiveTerminal()
181    {
182       if (toolbarButton_ == null || activeTerminalHandle_ == null)
183          return;
184 
185       String caption = terminals_.getCaption(activeTerminalHandle_);
186       if (caption == null)
187          return;
188 
189       toolbarButton_.setText(addBusyIndicator(trimCaption(caption),
190             terminals_.getHasSubprocs(activeTerminalHandle_)));
191    }
192 
193    /**
194     * set state to indicate no active terminals
195     */
setNoActiveTerminal()196    public void setNoActiveTerminal()
197    {
198       setActiveTerminal("Terminal", null);
199    }
200 
201    /**
202     * @return Handle of active terminal, or null if no active terminal.
203     */
getActiveTerminalHandle()204    public String getActiveTerminalHandle()
205    {
206       return activeTerminalHandle_;
207    }
208 
209    /**
210     * Switch to previous terminal tab.
211     */
previousTerminal()212    public void previousTerminal()
213    {
214       String prevHandle = getPreviousTerminalHandle();
215       if (prevHandle != null)
216       {
217          eventBus_.fireEvent(new SwitchToTerminalEvent(prevHandle, null));
218       }
219    }
220 
221    /**
222     * Switch to next terminal tab.
223     */
nextTerminal()224    public void nextTerminal()
225    {
226       String nextHandle = getNextTerminalHandle();
227       if (nextHandle != null)
228       {
229          eventBus_.fireEvent(new SwitchToTerminalEvent(nextHandle, null));
230       }
231    }
232 
233    /**
234     * Add indicator of busy status to a caption.
235     * @param caption
236     * @return Caption with busy indicator added.
237     */
addBusyIndicator(String caption, boolean busy)238    private String addBusyIndicator(String caption, boolean busy)
239    {
240       if (busy)
241          return caption + " (busy)";
242       else
243          return caption;
244    }
245 
trimCaption(String caption)246    private String trimCaption(String caption)
247    {
248       // TODO (gary) look at doing this via css text-overflow
249       // when I do the theming work
250 
251       // Enforce a sane visual limit on terminal captions
252       if (caption.length() > 32)
253       {
254          caption = caption.substring(0, 31) + "...";
255       }
256       return caption;
257    }
258 
259    /**
260     * @return handle of previous terminal or null if there is no previous
261     * terminal
262     */
getPreviousTerminalHandle()263    private String getPreviousTerminalHandle()
264    {
265       if (terminals_.terminalCount() > 0 && activeTerminalHandle_ != null)
266       {
267          String prevHandle = null;
268          for (final String handle : terminals_)
269          {
270             if (StringUtil.equals(activeTerminalHandle_, handle))
271             {
272                return prevHandle;
273             }
274             else
275             {
276                prevHandle = handle;
277             }
278          }
279       }
280       return null;
281    }
282 
283    /**
284     * @return handle of next terminal or null if no next terminal
285     */
getNextTerminalHandle()286    private String getNextTerminalHandle()
287    {
288       if (terminals_.terminalCount() > 0 && activeTerminalHandle_ != null)
289       {
290          boolean foundCurrent = false;
291          for (final String handle : terminals_)
292          {
293             if (foundCurrent)
294             {
295                return handle;
296             }
297             if (StringUtil.equals(activeTerminalHandle_, handle))
298             {
299                foundCurrent = true;
300             }
301          }
302       }
303       return null;
304    }
305 
306    private ToolbarMenuButton toolbarButton_;
307    private String activeTerminalHandle_;
308    private final TerminalList terminals_;
309 
310    // Injected ----
311    private Commands commands_;
312    private EventBus eventBus_;
313    private WorkbenchServerOperations server_;
314 }
315