1 /* 2 * PythonPreferencesPaneBase.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.studio.client.workbench.prefs.views; 16 17 import org.rstudio.core.client.CommandWithArg; 18 import org.rstudio.core.client.Debug; 19 import org.rstudio.core.client.ElementIds; 20 import org.rstudio.core.client.StringUtil; 21 import org.rstudio.core.client.files.FileSystemItem; 22 import org.rstudio.core.client.prefs.PreferencesDialogPaneBase; 23 import org.rstudio.core.client.prefs.RestartRequirement; 24 import org.rstudio.core.client.resources.ImageResource2x; 25 import org.rstudio.core.client.widget.HelpButton; 26 import org.rstudio.core.client.widget.InfoBar; 27 import org.rstudio.core.client.widget.ModalDialogBase; 28 import org.rstudio.core.client.widget.OperationWithInput; 29 import org.rstudio.core.client.widget.TextBoxWithButton; 30 import org.rstudio.studio.client.RStudioGinjector; 31 import org.rstudio.studio.client.server.ServerError; 32 import org.rstudio.studio.client.server.ServerRequestCallback; 33 import org.rstudio.studio.client.workbench.model.Session; 34 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs; 35 import org.rstudio.studio.client.workbench.prefs.views.python.PythonInterpreterListEntryUi; 36 import org.rstudio.studio.client.workbench.prefs.views.python.PythonInterpreterSelectionDialog; 37 38 import com.google.gwt.core.client.GWT; 39 import com.google.gwt.dom.client.Element; 40 import com.google.gwt.event.dom.client.BlurEvent; 41 import com.google.gwt.event.dom.client.ClickEvent; 42 import com.google.gwt.event.dom.client.ClickHandler; 43 import com.google.gwt.event.dom.client.FocusEvent; 44 import com.google.gwt.event.dom.client.KeyCodes; 45 import com.google.gwt.event.dom.client.KeyDownEvent; 46 import com.google.gwt.event.logical.shared.ValueChangeEvent; 47 import com.google.gwt.resources.client.ClientBundle; 48 import com.google.gwt.resources.client.CssResource; 49 import com.google.gwt.resources.client.ImageResource; 50 import com.google.gwt.user.client.ui.CheckBox; 51 import com.google.gwt.user.client.ui.FlowPanel; 52 import com.google.gwt.user.client.ui.SimplePanel; 53 import com.google.inject.Inject; 54 55 public abstract class PythonPreferencesPaneBase<T> extends PreferencesDialogPaneBase<T> 56 { PythonPreferencesPaneBase(String width, String placeholderText, boolean isProjectOptions)57 public PythonPreferencesPaneBase(String width, 58 String placeholderText, 59 boolean isProjectOptions) 60 { 61 RStudioGinjector.INSTANCE.injectMembers(this); 62 63 add(headerLabel("Python")); 64 65 mismatchWarningBar_ = new InfoBar(InfoBar.WARNING); 66 mismatchWarningBar_.setText( 67 "The active Python interpreter has been changed by an R startup script."); 68 mismatchWarningBar_.setVisible(false); 69 add(mismatchWarningBar_); 70 71 tbPythonInterpreter_ = new TextBoxWithButton( 72 "Python interpreter:", 73 null, 74 placeholderText, 75 "Select...", 76 new HelpButton("using_python", "Using Python in RStudio"), 77 ElementIds.TextBoxButtonId.PYTHON_PATH, 78 true, 79 true, 80 new ClickHandler() 81 { 82 @Override 83 public void onClick(ClickEvent event) 84 { 85 getProgressIndicator().onProgress("Finding interpreters..."); 86 87 server_.pythonFindInterpreters(new ServerRequestCallback<PythonInterpreters>() 88 { 89 @Override 90 public void onResponseReceived(final PythonInterpreters response) 91 { 92 getProgressIndicator().onCompleted(); 93 94 PythonInterpreterSelectionDialog dialog = 95 new PythonInterpreterSelectionDialog( 96 response.getPythonInterpreters(), 97 new OperationWithInput<PythonInterpreter>() 98 { 99 @Override 100 public void execute(PythonInterpreter input) 101 { 102 String path = input == null ? "" : input.getPath(); 103 tbPythonInterpreter_.setText(path); 104 } 105 }); 106 107 dialog.showModal(true); 108 } 109 110 @Override 111 public void onError(ServerError error) 112 { 113 String message = 114 "Error finding Python interpreters: " + 115 error.getUserMessage(); 116 getProgressIndicator().onError(message); 117 118 Debug.logError(error); 119 } 120 }); 121 } 122 }); 123 124 tbPythonInterpreter_.useNativePlaceholder(); 125 126 tbPythonInterpreter_.addValueChangeHandler((ValueChangeEvent<String> event) -> 127 { 128 updateDescription(); 129 }); 130 131 tbPythonInterpreter_.getTextBox().addDomHandler((KeyDownEvent event) -> 132 { 133 if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) 134 { 135 event.stopPropagation(); 136 event.preventDefault(); 137 tbPythonInterpreter_.blur(); 138 } 139 else if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) 140 { 141 event.stopPropagation(); 142 event.preventDefault(); 143 if (lastValue_ != null) 144 tbPythonInterpreter_.setText(lastValue_); 145 tbPythonInterpreter_.blur(); 146 } 147 }, KeyDownEvent.getType()); 148 149 tbPythonInterpreter_.addDomHandler((BlurEvent event) -> 150 { 151 updateDescription(); 152 }, BlurEvent.getType()); 153 154 // save the contents of the text box on focus 155 // (we'll restore the value if the user blurs via the Escape key) 156 tbPythonInterpreter_.getTextBox().addFocusHandler((FocusEvent event) -> 157 { 158 lastValue_ = tbPythonInterpreter_.getText(); 159 }); 160 161 Element tbEl = tbPythonInterpreter_.getTextBox().getElement(); 162 tbEl.addClassName(ModalDialogBase.ALLOW_ENTER_KEY_CLASS); 163 tbEl.addClassName(ModalDialogBase.ALLOW_ESCAPE_KEY_CLASS); 164 165 tbPythonInterpreter_.setWidth(width); 166 tbPythonInterpreter_.setReadOnly(false); 167 add(spaced(tbPythonInterpreter_)); 168 169 add(interpreterDescription_); 170 171 if (!isProjectOptions) 172 { 173 cbAutoUseProjectInterpreter_ = 174 new CheckBox("Automatically activate project-local Python environments"); 175 176 initialAutoUseProjectInterpreter_ = prefs_.pythonProjectEnvironmentAutomaticActivate().getGlobalValue(); 177 cbAutoUseProjectInterpreter_.setValue(initialAutoUseProjectInterpreter_); 178 179 cbAutoUseProjectInterpreter_.getElement().setTitle( 180 "When enabled, RStudio will automatically find and activate a " + 181 "Python environment located within the project root directory (if any)."); 182 183 add(lessSpaced(cbAutoUseProjectInterpreter_)); 184 } 185 } 186 187 @Inject initialize(PythonDialogResources res, PythonServerOperations server, Session session, UserPrefs prefs)188 private void initialize(PythonDialogResources res, 189 PythonServerOperations server, 190 Session session, 191 UserPrefs prefs) 192 { 193 res_ = res; 194 server_ = server; 195 session_ = session; 196 prefs_ = prefs; 197 } 198 clearDescription()199 protected void clearDescription() 200 { 201 interpreterDescription_.setWidget(new FlowPanel()); 202 } 203 updateDescription()204 protected void updateDescription() 205 { 206 // avoid recursive calls 207 if (updatingDescription_) 208 return; 209 210 try 211 { 212 updatingDescription_ = true; 213 updateDescriptionImpl(); 214 } 215 finally 216 { 217 updatingDescription_ = false; 218 } 219 } 220 updateDescriptionImpl()221 private void updateDescriptionImpl() 222 { 223 String path = tbPythonInterpreter_.getText().trim(); 224 225 // reset to default when empty 226 if (StringUtil.isNullOrEmpty(path)) 227 { 228 tbPythonInterpreter_.setText(""); 229 clearDescription(); 230 return; 231 } 232 233 server_.pythonInterpreterInfo( 234 path, 235 new ServerRequestCallback<PythonInterpreter>() 236 { 237 @Override 238 public void onResponseReceived(PythonInterpreter info) 239 { 240 updateDescriptionImpl(info); 241 } 242 243 @Override 244 public void onError(ServerError error) 245 { 246 Debug.logError(error); 247 } 248 }); 249 } 250 updateDescriptionImpl(PythonInterpreter info)251 private void updateDescriptionImpl(PythonInterpreter info) 252 { 253 interpreter_ = info; 254 255 if (!info.isValid()) 256 { 257 String reason = info.getInvalidReason(); 258 if (StringUtil.isNullOrEmpty(reason)) 259 reason = "The selected Python interpreter does not appear to be valid."; 260 261 InfoBar bar = new InfoBar(InfoBar.WARNING); 262 bar.setText(reason); 263 interpreterDescription_.setWidget(bar); 264 } 265 else 266 { 267 PythonInterpreterListEntryUi ui = new PythonInterpreterListEntryUi(info); 268 ui.addStyleName(RES.styles().interpreterDescription()); 269 270 String type = info.getType(); 271 272 if (type == null) 273 { 274 type = "[Unknown]"; 275 } 276 else if (type == "virtualenv") 277 { 278 type = "Virtual Environment"; 279 } 280 else if (type == "conda") 281 { 282 type = "Conda Environment"; 283 } 284 else if (type == "system") 285 { 286 type = "System Interpreter"; 287 } 288 289 ui.getPath().setText("[" + type + "]"); 290 interpreterDescription_.setWidget(ui); 291 } 292 } 293 checkForMismatch(PythonInterpreter activeInterpreter)294 protected void checkForMismatch(PythonInterpreter activeInterpreter) 295 { 296 // nothing to do if there isn't an active interpreter 297 if (StringUtil.isNullOrEmpty(activeInterpreter.getPath())) 298 { 299 setMismatchBarVisible(false); 300 return; 301 } 302 303 // nothing to do if the user hasn't changed the configured Python 304 String requestedPath = tbPythonInterpreter_.getText(); 305 boolean isSet = !StringUtil.isNullOrEmpty(requestedPath); 306 307 if (!isSet) 308 { 309 setMismatchBarVisible(false); 310 return; 311 } 312 313 // toggle visibility 314 boolean mismatch = 315 !StringUtil.equals(requestedPath, activeInterpreter.getPath()); 316 317 setMismatchBarVisible(mismatch); 318 } 319 setMismatchBarVisible(boolean visible)320 private void setMismatchBarVisible(boolean visible) 321 { 322 mismatchWarningBar_.setVisible(visible); 323 324 if (visible) 325 { 326 mismatchWarningBar_.addStyleName(RES.styles().mismatchBar()); 327 } 328 else 329 { 330 mismatchWarningBar_.addStyleName(RES.styles().mismatchBar()); 331 } 332 } 333 334 @Override getIcon()335 public ImageResource getIcon() 336 { 337 return new ImageResource2x(res_.iconPython2x()); 338 } 339 340 @Override getName()341 public String getName() 342 { 343 return "Python"; 344 } 345 initialize(String pythonPath)346 protected void initialize(String pythonPath) 347 { 348 initialPythonPath_ = pythonPath; 349 350 if (!StringUtil.isNullOrEmpty(pythonPath)) 351 { 352 tbPythonInterpreter_.setText(pythonPath); 353 updateDescription(); 354 } 355 356 server_.pythonActiveInterpreter(new ServerRequestCallback<PythonInterpreter>() 357 { 358 @Override 359 public void onResponseReceived(PythonInterpreter response) 360 { 361 checkForMismatch(response); 362 } 363 364 @Override 365 public void onError(ServerError error) 366 { 367 Debug.logError(error); 368 } 369 }); 370 } 371 onApply(boolean isProjectPrefs, CommandWithArg<PythonInterpreter> update)372 protected RestartRequirement onApply(boolean isProjectPrefs, 373 CommandWithArg<PythonInterpreter> update) 374 { 375 RestartRequirement requirement = new RestartRequirement(); 376 377 // read current Python path 378 String newValue = tbPythonInterpreter_.getText().trim(); 379 380 // for project preferences, use project-relative path to interpreter 381 if (isProjectPrefs) 382 { 383 FileSystemItem projDir = session_.getSessionInfo().getActiveProjectDir(); 384 if (projDir.exists() && newValue.startsWith(projDir.getPath())) 385 newValue = newValue.substring(projDir.getLength() + 1); 386 } 387 else 388 { 389 boolean newAutoActivateValue = cbAutoUseProjectInterpreter_.getValue(); 390 if (newAutoActivateValue != initialAutoUseProjectInterpreter_) 391 { 392 prefs_.pythonProjectEnvironmentAutomaticActivate().setGlobalValue(newAutoActivateValue); 393 requirement.setSessionRestartRequired(true); 394 } 395 } 396 397 // check if the interpreter appears to have been set by the user 398 boolean isValidInterpreterSet = 399 interpreter_ != null && 400 interpreter_.isValid() && 401 !StringUtil.isNullOrEmpty(newValue); 402 403 // if an interpreter was set, update to the new value 404 if (isValidInterpreterSet) 405 { 406 update.execute(interpreter_); 407 } 408 else 409 { 410 update.execute(PythonInterpreter.create()); 411 } 412 413 // restart the IDE if the python path has been changed 414 // (we'd prefer to just restart the session but that's not enough 415 // to refresh requisite project preferences, or so it seems) 416 if (!StringUtil.equals(initialPythonPath_, newValue)) 417 { 418 if (isProjectPrefs) 419 { 420 requirement.setRestartRequired(); 421 } 422 else 423 { 424 requirement.setSessionRestartRequired(true); 425 } 426 } 427 428 return requirement; 429 } 430 431 432 public interface Styles extends CssResource 433 { overrideLabel()434 String overrideLabel(); interpreterDescription()435 String interpreterDescription(); mismatchBar()436 String mismatchBar(); 437 } 438 439 public interface Resources extends ClientBundle 440 { 441 @Source("PythonPreferencesPane.css") styles()442 Styles styles(); 443 } 444 445 protected final InfoBar mismatchWarningBar_; 446 protected final TextBoxWithButton tbPythonInterpreter_; 447 protected final SimplePanel interpreterDescription_ = new SimplePanel(); 448 449 protected CheckBox cbAutoUseProjectInterpreter_; 450 protected boolean initialAutoUseProjectInterpreter_; 451 452 protected String initialPythonPath_; 453 protected PythonInterpreter interpreter_; 454 455 protected boolean updatingDescription_; 456 457 protected PythonDialogResources res_; 458 protected PythonServerOperations server_; 459 protected Session session_; 460 protected UserPrefs prefs_; 461 462 private String lastValue_ = null; 463 464 465 protected static Resources RES = GWT.create(Resources.class); 466 static 467 { 468 RES.styles().ensureInjected(); 469 } 470 471 472 } 473