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