/*
* SatelliteManager.java
*
* Copyright (C) 2009-12 by RStudio, Inc.
*
* Unless you have received this program directly from RStudio pursuant
* to the terms of a commercial license agreement with RStudio, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
package org.rstudio.studio.client.common.satellite;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map.Entry;
import com.google.inject.Provider;
import org.rstudio.core.client.BrowseCap;
import org.rstudio.core.client.Debug;
import org.rstudio.core.client.Point;
import org.rstudio.core.client.Size;
import org.rstudio.core.client.command.AppCommand;
import org.rstudio.core.client.dom.WindowEx;
import org.rstudio.core.client.layout.ScreenUtils;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.application.ApplicationUncaughtExceptionHandler;
import org.rstudio.studio.client.application.Desktop;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.common.GlobalDisplay.NewWindowOptions;
import org.rstudio.studio.client.common.satellite.events.WindowClosedEvent;
import org.rstudio.studio.client.common.satellite.events.WindowOpenedEvent;
import org.rstudio.studio.client.workbench.model.Session;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.Document;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.user.client.Window;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@Singleton
public class SatelliteManager implements CloseHandler<Window>
{
@Inject
public SatelliteManager(
Session session,
EventBus events,
Provider<ApplicationUncaughtExceptionHandler> pUncaughtExceptionHandler)
{
session_ = session;
events_ = events;
pUncaughtExceptionHandler_ = pUncaughtExceptionHandler;
}
// the main window should call this method during startup to set itself
// up to manage and communicate with the satellite windows
public void initialize()
{
// export the registration hook used by satellites
exportSatelliteRegistrationCallback();
// handle onClosed to automatically close all satellites
Window.addCloseHandler(this);
}
// open a satellite window (re-activate existing if possible)
public void openSatellite(String name,
JavaScriptObject params,
Size preferredSize)
{
openSatellite(name, params, preferredSize, true, null);
}
// open a satellite window (re-activate existing if possible)
private void openSatellite(String name,
JavaScriptObject params,
Size preferredSize,
boolean adjustSize,
Point position)
{
// satellites can't launch other satellites -- this is because the
// delegating/forwarding of remote server calls and events doesn't
// cascade correctly -- it wouldn't be totally out of the question
// to make this work but we'd rather not have this complexity
// if we don't need to.
if (isCurrentWindowSatellite())
{
Debug.log("Satellite windows can't launch other satellites");
assert false;
return;
}
// check for a re-activation of an existing window
for (ActiveSatellite satellite : satellites_)
{
if (satellite.getName().equals(name))
{
WindowEx window = satellite.getWindow();
if (!window.isClosed())
{
// for web mode bring the window to the front, notify
// it that it has been reactivated, then exit.
if (!Desktop.isDesktop())
{
// don't do this for chrome (since it doesn't allow
// window.focus). for chrome we'll just fall through
// and openSatelliteWindow will be called and the
// window will be reloaded)
if (!BrowseCap.isChrome())
{
window.focus();
callNotifyReactivated(window, params);
return;
}
else
{
// for Chrome, let the window know it's about to be
// closed and reopened
callNotifyPendingReactivate(window);
}
}
// desktop mode: activate and return
else
{
Desktop.getFrame().activateSatelliteWindow(
SatelliteUtils.getSatelliteWindowName(satellite.getName()));
callNotifyReactivated(window, params);
return;
}
}
}
}
// Start buffering events sent to this satellite. That way, we won't miss
// anything while the satellite is being loaded/reactivated
if (!pendingEventsBySatelliteName_.containsKey(name))
{
pendingEventsBySatelliteName_.put(name,
new ArrayList<JavaScriptObject>());
}
// record satellite params for subsequent setting (this value is read
// by the satellite within the call to registerAsSatellite)
if (params != null)
satelliteParams_.put(name, params);
// set size and position, if desired
Size windowSize = adjustSize ?
ScreenUtils.getAdjustedWindowSize(preferredSize) :
preferredSize;
NewWindowOptions options = new NewWindowOptions();
if (position != null)
options.setPosition(position);
// open the satellite - it will call us back on registerAsSatellite
// at which time we'll call setSessionInfo, setParams, etc.
RStudioGinjector.INSTANCE.getGlobalDisplay().openSatelliteWindow(
name,
windowSize.width,
windowSize.height,
options);
}
// Forcefully reopen a satellite window. This refreshes the window and
// pushes it to the front in Chrome. It should be used as a last resort;
// if responding to a UI event, use openSatellite instead, since Chrome
// permits window.open to reactivate windows in that context.
public void forceReopenSatellite(final String name,
final JavaScriptObject params)
{
Size preferredSize = null;
Point preferredPos = null;
for (ActiveSatellite satellite : satellites_)
{
if (satellite.getName().equals(name) &&
!satellite.getWindow().isClosed())
{
// save the window's geometry so we can restore it after the window
// is destroyed
final WindowEx win = satellite.getWindow();
Document doc = win.getDocument();
preferredSize = new Size(doc.getClientWidth(), doc.getClientHeight());
preferredPos = new Point(win.getLeft(), win.getTop());
callNotifyPendingReactivate(win);
// close the window
try
{
win.close();
}
catch(Throwable e)
{
}
break;
}
}
// didn't find an open window to reopen
if (preferredSize == null)
return;
// open a new window with the same geometry as the one we just destroyed,
// but with the newly supplied set of parameters
final Size windowSize = preferredSize;
final Point windowPos = preferredPos;
openSatellite(name, params, windowSize, false, windowPos);
}
public boolean satelliteWindowExists(String name)
{
return getSatelliteWindowObject(name) != null;
}
public WindowEx getSatelliteWindowObject(String name)
{
for (ActiveSatellite satellite : satellites_)
if (satellite.getName().equals(name) &&
!satellite.getWindow().isClosed())
return satellite.getWindow();
return null;
}
public void activateSatelliteWindow(String name)
{
if (Desktop.isDesktop())
{
Desktop.getFrame().activateSatelliteWindow(
SatelliteUtils.getSatelliteWindowName(name));
}
else
{
for (ActiveSatellite satellite : satellites_)
{
if (satellite.getName().equals(name) &&
!satellite.getWindow().isClosed())
{
satellite.getWindow().focus();
break;
}
}
}
}
// close all satellite windows
public void closeAllSatellites()
{
for (ActiveSatellite satellite : satellites_)
{
try
{
satellite.getWindow().close();
}
catch(Throwable e)
{
}
}
satellites_.clear();
pendingEventsBySatelliteName_.clear();
}
// close one satellite window
public void closeSatelliteWindow(String name)
{
for (ActiveSatellite satellite : satellites_)
{
if (satellite.getName().equals(name) &&
!satellite.getWindow().isClosed())
{
try
{
satellite.getWindow().close();
}
catch(Throwable e)
{
}
break;
}
}
}
// dispatch an event to all satellites
public void dispatchEvent(JavaScriptObject clientEvent)
{
// list of windows to remove (because they were closed)
ArrayList<ActiveSatellite> removeWindows = null;
// iterate over the satellites (make a copy to avoid races if
// for some reason firing an event creates or destroys a satellite)
@SuppressWarnings("unchecked")
ArrayList<ActiveSatellite> satellites =
(ArrayList<ActiveSatellite>)satellites_.clone();
for (ActiveSatellite satellite : satellites)
{
try
{
// If we're buffering events for this satellite, then don't dispatch
// them
if (pendingEventsBySatelliteName_.containsKey(satellite.getName()))
continue;
WindowEx satelliteWnd = satellite.getWindow();
if (satelliteWnd.isClosed())
{
if (removeWindows == null)
removeWindows = new ArrayList<ActiveSatellite>();
removeWindows.add(satellite);
}
else
{
callDispatchEvent(satelliteWnd, clientEvent);
}
}
catch(Throwable e)
{
}
}
for (Entry<String, ArrayList<JavaScriptObject>> entry :
pendingEventsBySatelliteName_.entrySet())
{
entry.getValue().add(clientEvent);
}
// remove windows if necessary
if (removeWindows != null)
{
for (ActiveSatellite satellite : removeWindows)
{
satellites_.remove(satellite);
}
}
}
// dispatch a command to all satellites.
public void dispatchCommand(AppCommand command)
{
for (ActiveSatellite satellite: satellites_)
{
callDispatchCommand(satellite.getWindow(), command.getId());
}
}
// close all satellites when we are closed
@Override
public void onClose(CloseEvent<Window> event)
{
closeAllSatellites();
}
// call notifyPendingReactivate on a satellite
public native static void callNotifyPendingReactivate(JavaScriptObject satellite) /*-{
satellite.notifyPendingReactivate();
}-*/;
// called by satellites to connect themselves with the main window
private void registerAsSatellite(final String name, JavaScriptObject wnd)
{
// get the satellite and add it to our list. in some cases (such as
// the Ctrl+R reload of an existing satellite window) we actually
// already have a reference to this satellite in our list so in that
// case we make sure not to add a duplicate
WindowEx satelliteWnd = wnd.<WindowEx>cast();
ActiveSatellite satellite = new ActiveSatellite(name, satelliteWnd);
if (!satellites_.contains(satellite))
satellites_.add(satellite);
// call setSessionInfo
callSetSessionInfo(satelliteWnd, session_.getSessionInfo());
// call setParams
JavaScriptObject params = satelliteParams_.get(name);
if (params != null)
callSetParams(satelliteWnd, params);
}
// called to register child windows (not necessarily full-fledged
// satellites). only used in desktop mode, since in server mode we have the
// child window object as a return value from window.open.
private void registerDesktopChildWindow (String name, JavaScriptObject window)
{
events_.fireEvent(new WindowOpenedEvent(name, (WindowEx) window.cast()));
}
private void unregisterDesktopChildWindow (String name)
{
if (SatelliteUtils.windowNameIsSatellite(name))
name = SatelliteUtils.getWindowNameFromSatelliteName(name);
events_.fireEvent(new WindowClosedEvent(name));
}
private void flushPendingEvents(String name)
{
ArrayList<JavaScriptObject> events =
pendingEventsBySatelliteName_.remove(name);
if (events == null || events.size() == 0)
return;
for (ActiveSatellite satellite :
new ArrayList<ActiveSatellite>(satellites_))
{
if (satellite.getName().equals(name)
&& !satellite.getWindow().isClosed())
{
for (JavaScriptObject evt : events)
{
try
{
callDispatchEvent(satellite.getWindow(), evt);
}
catch (Exception e)
{
pUncaughtExceptionHandler_.get().onUncaughtException(e);
}
}
}
}
}
// export the global function required for satellites to register
private native void exportSatelliteRegistrationCallback() /*-{
var manager = this;
$wnd.registerAsRStudioSatellite = $entry(
function(name, satelliteWnd) {
manager.@org.rstudio.studio.client.common.satellite.SatelliteManager::registerAsSatellite(Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;)(name, satelliteWnd);
}
);
$wnd.flushPendingEvents = $entry(
function(name) {
manager.@org.rstudio.studio.client.common.satellite.SatelliteManager::flushPendingEvents(Ljava/lang/String;)(name);
}
);
$wnd.registerDesktopChildWindow = $entry(
function(name, wnd) {
manager.@org.rstudio.studio.client.common.satellite.SatelliteManager::registerDesktopChildWindow(Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;)(name, wnd);
}
);
$wnd.unregisterDesktopChildWindow = $entry(
function(name, wnd) {
manager.@org.rstudio.studio.client.common.satellite.SatelliteManager::unregisterDesktopChildWindow(Ljava/lang/String;)(name);
}
);
}-*/;
// call setSessionInfo on a satellite
private native void callSetSessionInfo(JavaScriptObject satellite,
JavaScriptObject sessionInfo) /*-{
satellite.setRStudioSatelliteSessionInfo(sessionInfo);
}-*/;
// call setParams on a satellite
private native void callSetParams(JavaScriptObject satellite,
JavaScriptObject params) /*-{
satellite.setRStudioSatelliteParams(params);
}-*/;
// call notifyReactivated on a satellite
private native void callNotifyReactivated(JavaScriptObject satellite,
JavaScriptObject params) /*-{
satellite.notifyRStudioSatelliteReactivated(params);
}-*/;
// dispatch event to a satellite
private native void callDispatchEvent(JavaScriptObject satellite,
JavaScriptObject clientEvent) /*-{
satellite.dispatchEventToRStudioSatellite(clientEvent);
}-*/;
// dispatch command to a satellite
private native void callDispatchCommand(JavaScriptObject satellite,
String commandId) /*-{
satellite.dispatchCommandToRStudioSatellite(commandId);
}-*/;
// check whether the current window is a satellite (note this method
// is also implemented in the Satellite class -- we don't want this class
// to depend on Satellite so we duplicate the definition)
private native boolean isCurrentWindowSatellite() /*-{
return !!$wnd.isRStudioSatellite;
}-*/;
// alert callback (used for testing html preview sandbox)
//private void showAlert(String message)
//{
// RStudioGinjector.INSTANCE.getGlobalDisplay().showErrorMessage("Alert",
// message);
//}
//private native void exportSatelliteAlertCallback() /*-{
// var manager = this;
// $wnd.rstudioSatelliteAlert = $entry(
// function(message) {
// manager.@org.rstudio.studio.client.common.satellite.SatelliteManager::showAlert(Ljava/lang/String;)(message);
// }
// );
//}-*/;
private final Session session_;
private final EventBus events_;
private final Provider<ApplicationUncaughtExceptionHandler> pUncaughtExceptionHandler_;
private final ArrayList<ActiveSatellite> satellites_ =
new ArrayList<ActiveSatellite>();
private final HashMap<String,JavaScriptObject> satelliteParams_ =
new HashMap<String,JavaScriptObject>();
private final HashMap<String, ArrayList<JavaScriptObject>>
pendingEventsBySatelliteName_ = new HashMap<String, ArrayList<JavaScriptObject>>();
private class ActiveSatellite
{
public ActiveSatellite(String name, WindowEx window)
{
name_ = name;
window_ = window;
}
public String getName()
{
return name_;
}
public WindowEx getWindow()
{
return window_;
}
@Override
public boolean equals(Object other)
{
if (other == null)
return false;
ActiveSatellite otherSatellite = (ActiveSatellite)other;
return getName().equals(otherSatellite.getName()) &&
getWindow().equals(otherSatellite.getWindow());
}
private final String name_;
private final WindowEx window_;
}
}