1 /* 2 * ShinyApplicationPresenter.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.shiny; 16 17 import org.rstudio.core.client.BrowseCap; 18 import org.rstudio.core.client.StringUtil; 19 import org.rstudio.core.client.command.CommandBinder; 20 import org.rstudio.core.client.command.Handler; 21 import org.rstudio.studio.client.application.events.EventBus; 22 import org.rstudio.studio.client.common.GlobalDisplay; 23 import org.rstudio.studio.client.common.satellite.Satellite; 24 import org.rstudio.studio.client.shiny.model.ShinyApplicationParams; 25 import org.rstudio.studio.client.shiny.events.ShinyApplicationStatusEvent; 26 import org.rstudio.studio.client.workbench.commands.Commands; 27 import org.rstudio.studio.client.workbench.model.Session; 28 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs; 29 30 import com.google.gwt.core.client.JavaScriptObject; 31 import com.google.gwt.event.dom.client.LoadHandler; 32 import com.google.gwt.user.client.ui.IsWidget; 33 import com.google.gwt.user.client.ui.Widget; 34 import com.google.inject.Inject; 35 36 public class ShinyApplicationPresenter implements 37 IsWidget, 38 ShinyApplicationStatusEvent.Handler, 39 ShinyDisconnectNotifier.ShinyDisconnectSource 40 { 41 public interface Binder 42 extends CommandBinder<Commands, ShinyApplicationPresenter> 43 {} 44 45 public interface Display extends IsWidget 46 { getDocumentTitle()47 String getDocumentTitle(); getUrl()48 String getUrl(); getAbsoluteUrl()49 String getAbsoluteUrl(); showApp(ShinyApplicationParams params, LoadHandler handler)50 void showApp(ShinyApplicationParams params, LoadHandler handler); reloadApp()51 void reloadApp(); 52 } 53 54 @Inject ShinyApplicationPresenter(Display view, GlobalDisplay globalDisplay, Binder binder, final Commands commands, EventBus eventBus, Satellite satellite, Session session, UserPrefs prefs)55 public ShinyApplicationPresenter(Display view, 56 GlobalDisplay globalDisplay, 57 Binder binder, 58 final Commands commands, 59 EventBus eventBus, 60 Satellite satellite, 61 Session session, 62 UserPrefs prefs) 63 { 64 view_ = view; 65 satellite_ = satellite; 66 events_ = eventBus; 67 globalDisplay_ = globalDisplay; 68 disconnect_ = new ShinyDisconnectNotifier(this); 69 session_ = session; 70 prefs_ = prefs; 71 72 loadHandler_ = (evt) -> 73 { 74 if (BrowseCap.isFirefox()) 75 { 76 disconnect_.unsuppress(); 77 } 78 }; 79 80 binder.bind(commands, this); 81 82 initializeEvents(); 83 } 84 85 @Override asWidget()86 public Widget asWidget() 87 { 88 return view_.asWidget(); 89 } 90 91 @Override onShinyApplicationStatus(ShinyApplicationStatusEvent event)92 public void onShinyApplicationStatus(ShinyApplicationStatusEvent event) 93 { 94 if (event.getParams().getState() == ShinyApplicationParams.STATE_RELOADING) 95 { 96 reload(); 97 } 98 } 99 100 @Override getShinyUrl()101 public String getShinyUrl() 102 { 103 return view_.getAbsoluteUrl(); 104 } 105 106 @Override onShinyDisconnect()107 public void onShinyDisconnect() 108 { 109 appStopped_ = true; 110 notifyShinyAppDisconnected(params_); 111 closeShinyApp(); 112 } 113 114 @Handler onReloadShinyApp()115 public void onReloadShinyApp() 116 { 117 reload(); 118 } 119 120 @Handler onViewerPopout()121 public void onViewerPopout() 122 { 123 globalDisplay_.openWindow(params_.getUrl()); 124 } 125 loadApp(ShinyApplicationParams params)126 public void loadApp(ShinyApplicationParams params) 127 { 128 params_ = params; 129 view_.showApp(params, loadHandler_); 130 } 131 initializeEvents()132 private native void initializeEvents() /*-{ 133 var thiz = this; 134 135 // we observed that sometimes (with RStudio Server) the 'unload' event was 136 // not fired on window closing, and yet 'beforeunload' was not fired with 137 // RStudio Desktop. to be safe, attach to both events and just properly handle 138 // the close request there 139 $wnd.addEventListener( 140 "unload", 141 $entry(function() { 142 thiz.@org.rstudio.studio.client.shiny.ShinyApplicationPresenter::onClose()(); 143 }), 144 true); 145 146 $wnd.addEventListener( 147 "beforeunload", 148 $entry(function() { 149 thiz.@org.rstudio.studio.client.shiny.ShinyApplicationPresenter::onClose()(); 150 }), 151 true); 152 }-*/; 153 onClose()154 private void onClose() 155 { 156 // don't stop the app if the window is closing just to be opened again. 157 // (we close and reopen as a workaround to forcefully activate the window 158 // on browsers that don't permit manual event reactivation) 159 if (satellite_.isReactivatePending()) 160 return; 161 162 if (closed_) 163 return; 164 165 closed_ = true; 166 167 ShinyApplicationParams params = ShinyApplicationParams.create( 168 params_.getPath(), 169 ShinyApplicationSatellite.getIdFromName( 170 satellite_.getSatelliteName()), 171 params_.getUrl(), 172 appStopped_ ? 173 ShinyApplicationParams.STATE_STOPPED : 174 ShinyApplicationParams.STATE_STOPPING); 175 notifyShinyAppClosed(params); 176 } 177 reload()178 private void reload() 179 { 180 if (BrowseCap.isFirefox() && !StringUtil.isNullOrEmpty(getShinyUrl())) 181 { 182 // Firefox allows Shiny's disconnection notification (a "disconnected" 183 // postmessage) through during the unload that occurs during refresh. 184 // To keep this transient disconnection from being treated as an app 185 // stop, we temporarily suppress it here. 186 disconnect_.suppress(); 187 } 188 view_.reloadApp(); 189 } 190 closeShinyApp()191 private final native void closeShinyApp() /*-{ 192 $wnd.close(); 193 }-*/; 194 notifyShinyAppClosed(JavaScriptObject params)195 private final native void notifyShinyAppClosed(JavaScriptObject params) /*-{ 196 $wnd.opener.notifyShinyAppClosed(params); 197 }-*/; 198 notifyShinyAppDisconnected(JavaScriptObject params)199 private final native void notifyShinyAppDisconnected(JavaScriptObject params) /*-{ 200 if ($wnd.opener) 201 $wnd.opener.notifyShinyAppDisconnected(params); 202 }-*/; 203 204 private final Display view_; 205 private final Satellite satellite_; 206 private final EventBus events_; 207 private final GlobalDisplay globalDisplay_; 208 private final ShinyDisconnectNotifier disconnect_; 209 private final Session session_; 210 private final UserPrefs prefs_; 211 private final LoadHandler loadHandler_; 212 213 private ShinyApplicationParams params_; 214 private boolean closed_ = false; 215 private boolean appStopped_ = false; 216 private boolean popoutToBrowser_ = false; 217 } 218