org.rstudio.studio.client.workbench.views.source.SourceWindowManager.java Source code

Java tutorial

Introduction

Here is the source code for org.rstudio.studio.client.workbench.views.source.SourceWindowManager.java

Source

/*
 * SourceWindowManager.java
 *
 * Copyright (C) 2009-16 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.workbench.views.source;

import java.util.ArrayList;
import java.util.HashMap;

import org.rstudio.core.client.*;
import org.rstudio.core.client.dom.WindowCloseMonitor;
import org.rstudio.core.client.dom.WindowEx;
import org.rstudio.core.client.files.FileSystemItem;
import org.rstudio.core.client.js.JsObject;
import org.rstudio.core.client.widget.OperationWithInput;
import org.rstudio.studio.client.application.Desktop;
import org.rstudio.studio.client.application.events.CrossWindowEvent;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.application.events.RestartStatusEvent;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.filetypes.FileTypeRegistry;
import org.rstudio.studio.client.common.filetypes.events.OpenSourceFileEvent;
import org.rstudio.studio.client.common.filetypes.model.NavigationMethods;
import org.rstudio.studio.client.common.satellite.Satellite;
import org.rstudio.studio.client.common.satellite.SatelliteManager;
import org.rstudio.studio.client.common.satellite.events.AllSatellitesClosingEvent;
import org.rstudio.studio.client.common.satellite.events.SatelliteClosedEvent;
import org.rstudio.studio.client.common.satellite.events.SatelliteFocusedEvent;
import org.rstudio.studio.client.common.satellite.model.SatelliteWindowGeometry;
import org.rstudio.studio.client.events.*;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.ServerRequestCallback;
import org.rstudio.studio.client.server.Void;
import org.rstudio.studio.client.server.VoidServerRequestCallback;
import org.rstudio.studio.client.shiny.events.ShinyApplicationStatusEvent;
import org.rstudio.studio.client.workbench.MainWindowObject;
import org.rstudio.studio.client.workbench.WorkbenchContext;
import org.rstudio.studio.client.workbench.model.ClientState;
import org.rstudio.studio.client.workbench.model.Session;
import org.rstudio.studio.client.workbench.model.UnsavedChangesItem;
import org.rstudio.studio.client.workbench.model.UnsavedChangesTarget;
import org.rstudio.studio.client.workbench.model.helper.JSObjectStateValue;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefs;
import org.rstudio.studio.client.workbench.ui.PaneConfig;
import org.rstudio.studio.client.workbench.views.source.events.*;
import org.rstudio.studio.client.workbench.views.source.model.SourceDocument;
import org.rstudio.studio.client.workbench.views.source.model.SourcePosition;
import org.rstudio.studio.client.workbench.views.source.model.SourceServerOperations;
import org.rstudio.studio.client.workbench.views.source.model.SourceWindowParams;

import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Window;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;

@Singleton
public class SourceWindowManager implements PopoutDocEvent.Handler, SourceDocAddedEvent.Handler,
        SourceFileSavedHandler, CodeBrowserCreatedEvent.Handler, SatelliteFocusedEvent.Handler,
        SatelliteClosedEvent.Handler, DocTabDragStartedEvent.Handler, DocWindowChangedEvent.Handler,
        DocTabClosedEvent.Handler, AllSatellitesClosingEvent.Handler, ShinyApplicationStatusEvent.Handler,
        CollabEditStartedEvent.Handler, CollabEditEndedEvent.Handler, DocFocusedEvent.Handler,
        EditorCommandDispatchEvent.Handler, RestartStatusEvent.Handler {
    @Inject
    public SourceWindowManager(Provider<SatelliteManager> pSatelliteManager, Provider<Satellite> pSatellite,
            Provider<WorkbenchContext> pWorkbenchContext, SourceServerOperations server, EventBus events,
            FileTypeRegistry registry, GlobalDisplay display, SourceShim sourceShim, Session session,
            UIPrefs uiPrefs) {
        events_ = events;
        server_ = server;
        pSatelliteManager_ = pSatelliteManager;
        pSatellite_ = pSatellite;
        pWorkbenchContext_ = pWorkbenchContext;
        display_ = display;
        sourceShim_ = sourceShim;
        uiPrefs_ = uiPrefs;

        events_.addHandler(DocWindowChangedEvent.TYPE, this);

        if (isMainSourceWindow()) {
            // most event handlers only make sense on the main window
            events_.addHandler(EditorCommandDispatchEvent.TYPE, this);
            events_.addHandler(PopoutDocEvent.TYPE, this);
            events_.addHandler(DocTabDragStartedEvent.TYPE, this);
            events_.addHandler(ShinyApplicationStatusEvent.TYPE, this);
            events_.addHandler(AllSatellitesClosingEvent.TYPE, this);
            events_.addHandler(SourceDocAddedEvent.TYPE, this);
            events_.addHandler(SourceFileSavedEvent.TYPE, this);
            events_.addHandler(CodeBrowserCreatedEvent.TYPE, this);
            events_.addHandler(SatelliteClosedEvent.TYPE, this);
            events_.addHandler(SatelliteFocusedEvent.TYPE, this);
            events_.addHandler(DocTabClosedEvent.TYPE, this);
            events_.addHandler(CollabEditStartedEvent.TYPE, this);
            events_.addHandler(CollabEditEndedEvent.TYPE, this);
            events_.addHandler(DocFocusedEvent.TYPE, this);
            events_.addHandler(RestartStatusEvent.TYPE, this);

            JsArray<SourceDocument> docs = session.getSessionInfo().getSourceDocuments();
            sourceDocs_ = docs;

            exportFromMain();

            new JSObjectStateValue("source-window", "sourceWindowGeometry", ClientState.PROJECT_PERSISTENT,
                    session.getSessionInfo().getClientState(), false) {
                @Override
                protected void onInit(JsObject value) {
                    // save the window geometries 
                    if (value != null)
                        windowGeometry_ = value;

                    // compute the max ordinal value in the geometry set
                    JsArrayString windowIds = windowGeometry_.keys();
                    for (int i = 0; i < windowIds.length(); i++) {
                        SatelliteWindowGeometry geometry = windowGeometry_.getObject(windowIds.get(i)).cast();
                        maxOrdinal_ = Math.max(geometry.getOrdinal(), maxOrdinal_);
                    }
                }

                @Override
                protected JsObject getValue() {
                    return windowGeometry_;
                }

                @Override
                protected boolean hasChanged() {
                    return updateWindowGeometry();
                }
            };

            // keep track of whether the main window has focus (helps us infer the
            // user's perception of the 'active' doc)
            WindowEx.addFocusHandler(new FocusHandler() {
                @Override
                public void onFocus(FocusEvent arg0) {
                    mainWindowFocused_ = true;
                }
            });

            WindowEx.addBlurHandler(new BlurHandler() {

                @Override
                public void onBlur(BlurEvent arg0) {
                    mainWindowFocused_ = false;
                }
            });

            // open this session's source windows
            for (int i = 0; i < docs.length(); i++) {
                String windowId = docs.get(i).getSourceWindowId();
                if (!StringUtil.isNullOrEmpty(windowId) && !isSourceWindowOpen(windowId)) {
                    openSourceWindow(windowId, null, null, null);
                }
            }
        }

        // signal that this window has focus
        if (WindowEx.get().getDocument().hasFocus())
            MainWindowObject.lastFocusedWindow().set(getSourceWindowId());

        WindowEx.addFocusHandler(new FocusHandler() {
            @Override
            public void onFocus(FocusEvent event) {
                MainWindowObject.lastFocusedWindow().set(getSourceWindowId());
            }
        });
    }

    // Public types ------------------------------------------------------------

    public class NavigationResult {
        public NavigationResult(int type, String docId) {
            type_ = type;
            docId_ = docId;
        }

        public NavigationResult(int type) {
            this(type, null);
        }

        public int getType() {
            return type_;
        }

        public String getDocId() {
            return docId_;
        }

        private final int type_;
        private final String docId_;

        // no navigation was performed
        public final static int RESULT_NONE = 0;

        // the document was found and should be moved to the requesting window
        public final static int RESULT_RELOCATE = 1;

        // the document was found, and the navigation was completed
        public final static int RESULT_NAVIGATED = 2;
    }

    // Public methods ----------------------------------------------------------

    public static String getSourceWindowId() {
        return sourceWindowId(Window.Location.getParameter("view"));
    }

    public String getLastFocusedSourceWindowId() {
        return MainWindowObject.lastFocusedSourceWindow().get();
    }

    public String getLastFocusedWindowId() {
        return MainWindowObject.lastFocusedWindow().get();
    }

    public int getSourceWindowOrdinal() {
        return thisWindowOrdinal_;
    }

    public void setSourceWindowOrdinal(int ordinal) {
        thisWindowOrdinal_ = ordinal;
    }

    public static boolean isMainSourceWindow() {
        return !Satellite.isCurrentWindowSatellite();
    }

    public JsArray<SourceDocument> getSourceDocs() {
        if (isMainSourceWindow())
            return sourceDocs_;
        else
            return getMainWindowSourceDocs();
    }

    public boolean isSourceWindowOpen(String windowId) {
        return sourceWindows_.containsKey(windowId);
    }

    public String getWindowIdOfDocPath(String path) {
        JsArray<SourceDocument> docs = getSourceDocs();
        for (int i = 0; i < docs.length(); i++) {
            if (docs.get(i).getPath() != null && docs.get(i).getPath().equals(path)) {
                String windowId = docs.get(i).getSourceWindowId();
                if (windowId != null)
                    return windowId;
                else
                    return "";
            }
        }
        return null;
    }

    public String getWindowIdOfDocId(String id) {
        JsArray<SourceDocument> docs = getSourceDocs();
        for (int i = 0; i < docs.length(); i++) {
            if (docs.get(i).getId() == id) {
                String windowId = docs.get(i).getSourceWindowId();
                if (windowId != null)
                    return windowId;
                else
                    return "";
            }
        }
        return null;
    }

    public void saveWithPrompt(UnsavedChangesItem item, Command onCompleted) {
        String windowId = getWindowIdOfDocId(item.getId());
        WindowEx window = getSourceWindowObject(windowId);
        if (window == null || window.isClosed()) {
            onCompleted.execute();
            return;
        }
        // raise the window and ask it to save the item
        window.focus();
        saveWithPrompt(getSourceWindowObject(windowId), item, onCompleted);
    }

    public void saveAllUnsaved(Command onCompleted) {
        doForAllSourceWindows(new SourceWindowCommand() {
            @Override
            public void execute(String windowId, WindowEx window, Command continuation) {
                saveAllUnsaved(window, continuation);
            }
        }, onCompleted);
    }

    public void handleUnsavedChangesBeforeExit(ArrayList<UnsavedChangesTarget> targets, Command onCompleted) {
        // accumulate the unsaved change targets that represent satellite windows
        final JsArray<UnsavedChangesItem> items = JsArray.createArray().cast();
        for (UnsavedChangesTarget target : targets) {
            if (target instanceof UnsavedChangesItem) {
                items.push((UnsavedChangesItem) target);
            }
        }

        // let each window have a crack at saving the targets (the windows will
        // discard any targets they don't own)
        doForAllSourceWindows(new SourceWindowCommand() {
            @Override
            public void execute(String windowId, WindowEx window, Command continuation) {
                handleUnsavedChangesBeforeExit(window, items, continuation);
            }
        }, onCompleted);
    }

    public void closeAllSatelliteDocs(final String caption, final Command onCompleted) {
        doForAllSourceWindows(new SourceWindowCommand() {
            @Override
            public void execute(String windowId, WindowEx window, Command continuation) {
                window.focus();
                closeAllDocs(window, caption, continuation);
            }
        }, new Command() {
            @Override
            public void execute() {
                // return focus to the main window when finished
                if (Desktop.isDesktop() || !isMainSourceWindow())
                    pSatellite_.get().focusMainWindow();

                // complete operation
                onCompleted.execute();
            }
        });
    }

    public ArrayList<UnsavedChangesTarget> getAllSatelliteUnsavedChanges(final int type) {
        final ArrayList<UnsavedChangesTarget> targets = new ArrayList<UnsavedChangesTarget>();
        doForAllSourceWindows(new OperationWithInput<Pair<String, WindowEx>>() {
            @Override
            public void execute(Pair<String, WindowEx> input) {
                targets.addAll(JsArrayUtil.toArrayList(getUnsavedChanges(input.second, type)));
            }
        });
        return targets;
    }

    // navigates to the file in a source window
    public NavigationResult navigateToFile(FileSystemItem file, FilePosition position, int navMethod) {
        return navigateToPath(file.getPath(), new OpenSourceFileEvent(file, position, null, navMethod),
                navMethod == NavigationMethods.DEFAULT);
    }

    public NavigationResult navigateToCodeBrowser(String path, CrossWindowEvent<?> codeBrowserEvent) {
        return navigateToPath(path, codeBrowserEvent, codeBrowserEvent instanceof CodeBrowserNavigationEvent);
    }

    public boolean activateLastFocusedSource() {
        // if this is a source window, it's a no-op
        if (!StringUtil.isNullOrEmpty(getSourceWindowId()))
            return true;

        // if we don't have the capacity to activate source windows, let the
        // current window handle it
        if (!canActivateSourceWindows())
            return false;

        // if the last window focused was the main one, or there's no longer an
        // addressable window, there's nothing to do
        WindowEx lastFocusedWindow = getLastFocusedSourceWindow();
        if (lastFocusedWindow != null) {
            // we found the window that last had focus--refocus it
            pSatelliteManager_.get()
                    .activateSatelliteWindow(SourceSatellite.NAME_PREFIX + getLastFocusedSourceWindowId());
            return true;
        }

        return false;
    }

    public String getCurrentDocPath() {
        // return the document that most recently had focus, whether it was in a
        // source window or the main window
        WindowEx lastFocusedWindow = getLastFocusedSourceWindow();
        if (lastFocusedWindow == null)
            return sourceShim_.getCurrentDocPath();
        else
            return getCurrentDocPath(lastFocusedWindow);
    }

    public String getCurrentDocId() {
        WindowEx lastFocusedWindow = getLastFocusedSourceWindow();
        if (lastFocusedWindow == null)
            return sourceShim_.getCurrentDocId();
        else
            return getCurrentDocId(lastFocusedWindow);
    }

    public CollabEditStartParams getDocCollabParams(String id) {
        JsArray<SourceDocument> docs = getSourceDocs();
        for (int i = 0; i < docs.length(); i++) {
            if (docs.get(i).getId() == id) {
                return docs.get(i).getCollabParams();
            }
        }
        return null;
    }

    public void maximizeSourcePaneIfNecessary() {
        if (SourceWindowManager.isMainSourceWindow()) {
            // see if the Source and Console are paired
            PaneConfig paneConfig = uiPrefs_.paneConfig().getValue();
            if (paneConfig == null)
                paneConfig = PaneConfig.createDefault();
            if (hasSourceAndConsolePaired(paneConfig.getPanes())) {
                events_.fireEvent(new MaximizeSourceWindowEvent());
            }
        }
    }

    public void ensureVisibleSourcePaneIfNecessary() {
        if (SourceWindowManager.isMainSourceWindow()) {
            // see if the Source and Console are paired
            PaneConfig paneConfig = uiPrefs_.paneConfig().getValue();
            if (paneConfig == null)
                paneConfig = PaneConfig.createDefault();
            if (hasSourceAndConsolePaired(paneConfig.getPanes())) {
                events_.fireEvent(new EnsureVisibleSourceWindowEvent());
            }
        }
    }

    private <T extends EventHandler> void fireEventToLastFocusedWindow(CrossWindowEvent<T> event) {
        String id = getLastFocusedWindowId();
        if (StringUtil.isNullOrEmpty(id))
            events_.fireEvent(event);
        else
            fireEventToSourceWindow(id, event, false);
    }

    // Event handlers ----------------------------------------------------------

    @Override
    public void onEditorCommandDispatch(EditorCommandDispatchEvent dispatchEvent) {
        EditorCommandEvent event = dispatchEvent.getEvent();

        String type = event.getType();
        if (type.equals(EditorCommandEvent.TYPE_EDITOR_CONTEXT)) {
            GetEditorContextEvent.Data data = event.getData();
            fireEventToLastFocusedWindow(new GetEditorContextEvent(data));
        } else if (type.equals(EditorCommandEvent.TYPE_REPLACE_RANGES)) {
            ReplaceRangesEvent.Data data = event.getData();
            fireEventToLastFocusedWindow(new ReplaceRangesEvent(data));
        } else if (type.equals(EditorCommandEvent.TYPE_SET_SELECTION_RANGES)) {
            SetSelectionRangesEvent.Data data = event.getData();
            fireEventToLastFocusedWindow(new SetSelectionRangesEvent(data));
        } else
            assert false : "Unrecognized editor event type '" + type + "'";
    }

    @Override
    public void onPopoutDoc(final PopoutDocEvent evt) {
        // assign a new window ID to the source document 
        final String windowId = createSourceWindowId();
        assignSourceDocWindowId(evt.getDocId(), windowId, new Command() {
            @Override
            public void execute() {
                openSourceWindow(windowId, evt.getOriginator().getPosition(), evt.getDocId(),
                        evt.getSourcePosition());
            }
        });
    }

    @Override
    public void onSourceDocAdded(SourceDocAddedEvent e) {
        // if the window that fired the event is not already the owner of the
        // document, make it the owner
        if (e.getDoc().getSourceWindowId() != e.getWindowId()) {
            // assign on the doc itself
            e.getDoc().assignSourceWindowId(e.getWindowId());

            // assign on the server
            assignSourceDocWindowId(e.getDoc().getId(), e.getWindowId(), null);
        }

        // ensure the doc isn't already in our index
        for (int i = 0; i < sourceDocs_.length(); i++) {
            if (sourceDocs_.get(i).getId() == e.getDoc().getId())
                return;
        }

        sourceDocs_.push(e.getDoc());
    }

    @Override
    public void onSourceFileSaved(SourceFileSavedEvent event) {
        // when a user saves a new doc or does file -> save as, we need to update
        // our internal doc mappings so we can route navigations to the right
        // window for that path
        updateDocPath(event.getDocId(), event.getPath());
    }

    @Override
    public void onCodeBrowserCreated(CodeBrowserCreatedEvent event) {
        updateDocPath(event.getId(), event.getPath());
    }

    @Override
    public void onSatelliteClosed(final SatelliteClosedEvent event) {
        // if this satellite is closing for quit/shutdown/close/etc., ignore it
        // (we only care about user-initiated window closure)
        if (windowsClosing_)
            return;

        // ignore closure when not a source window
        if (!event.getName().startsWith(SourceSatellite.NAME_PREFIX))
            return;

        // we get this event when the window is unloaded; it could be that the
        // window is unloading for refresh (in which case its docs could be
        // preserved) or closing for good.
        WindowCloseMonitor.monitorSatelliteClosure(event.getName(), new Command() {
            @Override
            public void execute() {
                closeSourceWindowDocs(sourceWindowId(event.getName()));
            }
        }, null);
    }

    @Override
    public void onSatelliteFocused(SatelliteFocusedEvent event) {
        mainWindowFocused_ = false;
    }

    @Override
    public void onAllSatellitesClosing(AllSatellitesClosingEvent event) {
        windowsClosing_ = true;
    }

    @Override
    public void onDocTabDragStarted(DocTabDragStartedEvent event) {
        // we are the main source window; fire the event to all the source 
        // satellites
        fireEventToAllSourceWindows(event);
    }

    @Override
    public void onDocWindowChanged(final DocWindowChangedEvent event) {
        if (event.getNewWindowId() == getSourceWindowId()) {
            // if the doc is moving to this window, assign the ID before firing
            // events
            assignSourceDocWindowId(event.getDocId(), event.getNewWindowId(), new Command() {
                @Override
                public void execute() {
                    broadcastDocWindowChanged(event);
                }
            });
        } else {
            broadcastDocWindowChanged(event);
        }
    }

    @Override
    public void onDocTabClosed(DocTabClosedEvent event) {
        JsArray<SourceDocument> sourceDocs = getSourceDocs();
        for (int i = 0; i < sourceDocs.length(); i++) {
            if (sourceDocs.get(i).getId() == event.getDocId()) {
                JsArrayUtil.remove(sourceDocs, i);
                break;
            }
        }
    }

    @Override
    public void onShinyApplicationStatus(ShinyApplicationStatusEvent event) {
        fireEventToAllSourceWindows(event);
    }

    @Override
    public void onCollabEditStarted(CollabEditStartedEvent event) {
        JsArray<SourceDocument> sourceDocs = getSourceDocs();
        for (int i = 0; i < sourceDocs.length(); i++) {
            if (sourceDocs.get(i).getPath() == event.getStartParams().getPath()) {
                sourceDocs.get(i).setCollabParams(event.getStartParams());
                break;
            }
        }
    }

    @Override
    public void onCollabEditEnded(CollabEditEndedEvent event) {
        JsArray<SourceDocument> sourceDocs = getSourceDocs();
        for (int i = 0; i < sourceDocs.length(); i++) {
            if (sourceDocs.get(i).getPath() == event.getPath()) {
                sourceDocs.get(i).setCollabParams(null);
                break;
            }
        }
    }

    @Override
    public void onDocFocused(final DocFocusedEvent event) {
        // defer to ensure that the containing window gets focus too
        Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
            @Override
            public void execute() {
                // ignore this event if it's from main window but the main window
                // doesn't have focus
                if (!mainWindowFocused_ && event.isFromMainWindow())
                    return;
                String id = event.isFromMainWindow() ? "" : sourceWindowId(event.originWindowName());

                MainWindowObject.lastFocusedSourceWindow().set(id);
            }
        });
    }

    @Override
    public void onRestartStatus(RestartStatusEvent event) {
        fireEventToAllSourceWindows(event);
    }

    // Private methods ---------------------------------------------------------

    private void fireEventToSourceWindow(String windowId, CrossWindowEvent<?> evt, boolean focus) {
        if (StringUtil.isNullOrEmpty(windowId) && !isMainSourceWindow()) {
            pSatellite_.get().focusMainWindow();
            events_.fireEventToMainWindow(evt);
        } else {
            // focus window if requested
            if (focus) {
                pSatelliteManager_.get().activateSatelliteWindow(SourceSatellite.NAME_PREFIX + windowId);
            }
            WindowEx window = getSourceWindowObject(windowId);
            if (window != null) {
                events_.fireEventToSatellite(evt, window);
            }

        }
    }

    private void openSourceWindow(String windowId, Point position, String docId, SourcePosition sourcePosition) {
        // create default options
        Size size = new Size(800, 800);
        Integer ordinal = null;

        // if we have geometry for the window, apply it
        SatelliteWindowGeometry geometry = windowGeometry_.getObject(windowId);
        if (geometry != null) {
            size = geometry.getSize();
            ordinal = geometry.getOrdinal();
            if (position == null)
                position = geometry.getPosition();
        }

        // if we don't have window geometry for this window, tile based on the
        // geometry of the most recently used source window (otherwise the new
        // window may exactly overlap an existing source window)
        if (geometry == null) {
            if (!StringUtil.isNullOrEmpty(mostRecentSourceWindow_) && isSourceWindowOpen(mostRecentSourceWindow_)) {
                WindowEx window = getSourceWindowObject(mostRecentSourceWindow_);
                if (window != null && !window.isClosed()) {
                    size = new Size(window.getInnerWidth(), window.getInnerHeight());
                    if (position == null)
                        position = new Point(window.getScreenX() + 50, window.getScreenY() + 50);
                }
            }
        }

        // assign ordinal if not already assigned
        if (ordinal == null)
            ordinal = ++maxOrdinal_;

        pSatelliteManager_.get().openSatellite(SourceSatellite.NAME_PREFIX + windowId,
                SourceWindowParams.create(ordinal, pWorkbenchContext_.get().createWindowTitle(),
                        pWorkbenchContext_.get().getCurrentWorkingDir().getPath(), docId, sourcePosition),
                size, false, position);

        mostRecentSourceWindow_ = windowId;
        sourceWindows_.put(windowId, ordinal);
    }

    private void broadcastDocWindowChanged(DocWindowChangedEvent event) {
        if (isMainSourceWindow() && event.getOldWindowId() != getSourceWindowId()) {
            // this is the main window; pass the event on to the window that just
            // lost its doc
            fireEventToSourceWindow(event.getOldWindowId(), event, false);

            // raise the window that received the doc (doesn't happen 
            // automatically)
            focusSourceWindow(event.getNewWindowId());
        } else if (event.getNewWindowId() == getSourceWindowId()) {
            // this is a satellite; pass the event on to the main window for
            // routing
            events_.fireEventToMainWindow(event);
        }
    }

    private String createSourceWindowId() {
        return "w" + StringUtil.makeRandomId(12);
    }

    private static String sourceWindowId(String input) {
        if (input != null && input.startsWith(SourceSatellite.NAME_PREFIX)) {
            return input.substring(SourceSatellite.NAME_PREFIX.length());
        }
        return "";
    }

    private final native static JsArray<SourceDocument> getMainWindowSourceDocs() /*-{
                                                                                  return $wnd.opener.rstudioSourceDocs;
                                                                                  }-*/;

    private final native void exportFromMain() /*-{
                                               // the main window maintains an array of all open source documents 
                                               // across all satellites; rather than attempt to synchronize this list
                                               // among satellites, the main window exposes it on its window object
                                               // for the satellites to read 
                                               $wnd.rstudioSourceDocs = this.@org.rstudio.studio.client.workbench.views.source.SourceWindowManager::sourceDocs_;
                                               }-*/;

    private final native void closeAllDocs(WindowEx satellite, String caption, Command onCompleted) /*-{
                                                                                                    satellite.rstudioCloseAllDocs(caption, onCompleted);
                                                                                                    }-*/;

    private final native JsArray<UnsavedChangesItem> getUnsavedChanges(WindowEx satellite, int type) /*-{
                                                                                                     return satellite.rstudioGetUnsavedChanges(type);
                                                                                                     }-*/;

    private final native String getCurrentDocPath(WindowEx satellite) /*-{
                                                                      return satellite.rstudioGetCurrentDocPath();
                                                                      }-*/;

    private final native String getCurrentDocId(WindowEx satellite) /*-{
                                                                    return satellite.rstudioGetCurrentDocId();
                                                                    }-*/;

    private final native void handleUnsavedChangesBeforeExit(WindowEx satellite, JsArray<UnsavedChangesItem> items,
            Command onCompleted) /*-{
                                 satellite.rstudioHandleUnsavedChangesBeforeExit(items, onCompleted);
                                 }-*/;

    private final native void saveWithPrompt(WindowEx satellite, UnsavedChangesItem item,
            Command onCompleted) /*-{
                                 satellite.rstudioSaveWithPrompt(item, onCompleted);
                                 }-*/;

    private final native void saveAllUnsaved(WindowEx satellite, Command onCompleted) /*-{
                                                                                      satellite.rstudioSaveAllUnsaved(onCompleted);
                                                                                      }-*/;

    private boolean updateWindowGeometry() {
        final ArrayList<String> changedWindows = new ArrayList<String>();
        final JsObject newGeometries = JsObject.createJsObject();

        doForAllSourceWindows(new OperationWithInput<Pair<String, WindowEx>>() {
            @Override
            public void execute(Pair<String, WindowEx> input) {
                String windowId = input.first;
                WindowEx window = input.second;

                // read the window's current geometry
                SatelliteWindowGeometry newGeometry = SatelliteWindowGeometry.create(sourceWindows_.get(windowId),
                        window.getScreenX(), window.getScreenY(), window.getInnerWidth(), window.getInnerHeight());

                // compare to the old geometry (if any)
                if (windowGeometry_.hasKey(windowId)) {
                    SatelliteWindowGeometry oldGeometry = windowGeometry_.getObject(windowId);
                    if (!oldGeometry.equals(newGeometry))
                        changedWindows.add(windowId);
                } else {
                    changedWindows.add(windowId);
                }
                newGeometries.setObject(windowId, newGeometry);
            };
        });

        if (changedWindows.size() > 0)
            windowGeometry_ = newGeometries;

        return changedWindows.size() > 0;
    }

    private void fireEventToAllSourceWindows(CrossWindowEvent<?> event) {
        for (String sourceWindowId : sourceWindows_.keySet()) {
            fireEventToSourceWindow(sourceWindowId, event, false);
        }
    }

    // execute a command on all source windows (synchronously)
    private void doForAllSourceWindows(OperationWithInput<Pair<String, WindowEx>> command) {
        for (final String windowId : sourceWindows_.keySet()) {
            final WindowEx window = getSourceWindowObject(windowId);
            if (window == null || window.isClosed())
                continue;

            command.execute(new Pair<String, WindowEx>(windowId, window));
        }
    }

    // execute a command on all source windows (async continuation-style)
    private void doForAllSourceWindows(final SourceWindowCommand command, final Command completedCommand) {
        final SerializedCommandQueue queue = new SerializedCommandQueue();
        doForAllSourceWindows(new OperationWithInput<Pair<String, WindowEx>>() {
            @Override
            public void execute(final Pair<String, WindowEx> input) {
                queue.addCommand(new SerializedCommand() {
                    @Override
                    public void onExecute(Command continuation) {
                        command.execute(input.first, input.second, continuation);
                    }
                });
            }
        });

        if (completedCommand != null) {
            queue.addCommand(new SerializedCommand() {
                public void onExecute(Command continuation) {
                    completedCommand.execute();
                    continuation.execute();
                }
            });
        }
    }

    private WindowEx getSourceWindowObject(String windowId) {
        return pSatelliteManager_.get().getSatelliteWindowObject(SourceSatellite.NAME_PREFIX + windowId);
    }

    private void assignSourceDocWindowId(String docId, String windowId, final Command onComplete) {
        // assign locally
        JsArray<SourceDocument> docs = getSourceDocs();
        for (int i = 0; i < docs.length(); i++) {
            SourceDocument doc = docs.get(i);
            if (doc.getId() == docId) {
                // no point in writing a value to the server if we're not changing
                // it 
                if (doc.getSourceWindowId() == windowId)
                    return;
                doc.assignSourceWindowId(windowId);
                break;
            }
        }

        // create the new property map
        HashMap<String, String> props = new HashMap<String, String>();
        props.put(SOURCE_WINDOW_ID, windowId);

        // update the doc window ID on the server
        server_.modifyDocumentProperties(docId, props, new ServerRequestCallback<Void>() {
            @Override
            public void onResponseReceived(Void v) {
                if (onComplete != null)
                    onComplete.execute();
            }

            @Override
            public void onError(ServerError error) {
                display_.showErrorMessage("Can't Move Doc",
                        "The document could not be " + "moved to a different window: \n" + error.getMessage());
            }
        });
    }

    private void closeSourceWindowDocs(String windowId) {
        // when the user closes a source window, close all the source docs it
        // contained
        for (int i = 0; i < sourceDocs_.length(); i++) {
            final SourceDocument doc = sourceDocs_.get(i);
            if (doc.getSourceWindowId() == windowId) {
                // change the window ID of the doc back to the main window
                assignSourceDocWindowId(doc.getId(), "", new Command() {
                    @Override
                    public void execute() {
                        // close the document when finished
                        server_.closeDocument(doc.getId(), new VoidServerRequestCallback());
                    }
                });
            }
        }

        // clean up our own reference to the window
        sourceWindows_.remove(windowId);
    }

    private static boolean canActivateSourceWindows() {
        return Desktop.isDesktop() || BrowseCap.INSTANCE.isInternetExplorer();
    }

    private void focusSourceWindow(String windowId) {
        if (StringUtil.isNullOrEmpty(windowId)) {
            // activate main window
            if (Desktop.isDesktop())
                Desktop.getFrame().bringMainFrameToFront();
            else
                WindowEx.get().focus();
        } else {
            // activate satellite window
            pSatelliteManager_.get().activateSatelliteWindow(SourceSatellite.NAME_PREFIX + windowId);
        }
    }

    // this function implements the core of routing navigation requests among
    // source windows; it does the following:
    // 1. attempts to find a window open with the given path 
    //    (which can be a physical file or e.g. a code browser) 
    // 2. if such a window is found, fires the given event to the window, or
    //    closes the window's instance of the tab (server mode w/tab stealing)
    // 3. if such a window is not found, indicates as much
    private NavigationResult navigateToPath(String path, CrossWindowEvent<?> event, boolean focus) {
        // if this is the main window, check to see if we should route an event
        // there instead
        String sourceWindowId = getWindowIdOfDocPath(path);
        if (isMainSourceWindow()) {
            // if this is the main window but the doc is open in a satellite...
            if (!StringUtil.isNullOrEmpty(sourceWindowId) && isSourceWindowOpen(sourceWindowId)) {
                if (canActivateSourceWindows()) {
                    // in desktop mode (and IE) we can bring the appropriate window
                    // forward 
                    fireEventToSourceWindow(sourceWindowId, event, focus);
                    return new NavigationResult(NavigationResult.RESULT_NAVIGATED);
                } else {
                    // otherwise, move the tab over to this window by closing it in
                    // in its origin window
                    JsArray<SourceDocument> sourceDocs = getSourceDocs();
                    for (int i = 0; i < sourceDocs.length(); i++) {
                        if (sourceDocs.get(i).getPath() == path) {
                            assignSourceDocWindowId(sourceDocs.get(i).getId(), getSourceWindowId(), null);
                            fireEventToSourceWindow(sourceWindowId,
                                    new DocWindowChangedEvent(sourceDocs.get(i).getId(), sourceWindowId, null,
                                            sourceDocs.get(i).getCollabParams(), 0),
                                    true);
                            return new NavigationResult(NavigationResult.RESULT_RELOCATE,
                                    sourceDocs.get(i).getId());
                        }
                    }
                }
            }
        } else if (sourceWindowId != null && sourceWindowId != getSourceWindowId()) {
            if (canActivateSourceWindows()) {
                // in desktop mode (and IE) we can just route to the main window
                events_.fireEventToMainWindow(event);

                // if the destination is the main window, raise it
                if (sourceWindowId.isEmpty()) {
                    pSatellite_.get().focusMainWindow();
                }
                return new NavigationResult(NavigationResult.RESULT_NAVIGATED);
            } else {
                // otherwise, move the tab over to this window by closing it in
                // in its origin window
                JsArray<SourceDocument> sourceDocs = getSourceDocs();
                for (int i = 0; i < sourceDocs.length(); i++) {
                    if (sourceDocs.get(i).getPath() == path) {
                        // take ownership of the doc immediately 
                        assignSourceDocWindowId(sourceDocs.get(i).getId(), getSourceWindowId(), null);
                        events_.fireEventToMainWindow(new DocWindowChangedEvent(sourceDocs.get(i).getId(),
                                sourceWindowId, null, sourceDocs.get(i).getCollabParams(), 0));
                        return new NavigationResult(NavigationResult.RESULT_RELOCATE, sourceDocs.get(i).getId());
                    }
                }
            }
        }
        return new NavigationResult(NavigationResult.RESULT_NONE);
    }

    private void updateDocPath(String id, String path) {
        for (int i = 0; i < sourceDocs_.length(); i++) {
            if (sourceDocs_.get(i).getId() == id) {
                sourceDocs_.get(i).setPath(path);
                break;
            }
        }
    }

    private WindowEx getLastFocusedSourceWindow() {
        String lastFocusedSourceWindow = MainWindowObject.lastFocusedSourceWindow().get();

        // if the last window focused was the main one, or there's no longer an
        // addressable window, there's nothing to do
        if (StringUtil.isNullOrEmpty(lastFocusedSourceWindow) || !isSourceWindowOpen(lastFocusedSourceWindow))
            return null;

        WindowEx window = getSourceWindowObject(lastFocusedSourceWindow);
        if (window != null && !window.isClosed()) {
            return window;
        }

        return null;
    }

    private boolean hasSourceAndConsolePaired(JsArrayString panes) {
        // default config
        if (panes == null)
            return true;

        // if there aren't 4 panes this is a configuration we
        // don't recognize
        if (panes.length() != 4)
            return false;

        // check for paired config
        return hasSourceAndConsolePaired(panes.get(0), panes.get(1))
                || hasSourceAndConsolePaired(panes.get(2), panes.get(3));
    }

    private boolean hasSourceAndConsolePaired(String pane1, String pane2) {
        return (pane1.equals(PaneConfig.SOURCE) && pane2.equals(PaneConfig.CONSOLE))
                || (pane1.equals(PaneConfig.CONSOLE) && pane2.equals(PaneConfig.SOURCE));
    }

    // Private types -----------------------------------------------------------

    private interface SourceWindowCommand {
        public void execute(String windowId, WindowEx window, Command continuation);
    }

    // Members -----------------------------------------------------------------

    private final EventBus events_;
    private final Provider<SatelliteManager> pSatelliteManager_;
    private final Provider<Satellite> pSatellite_;
    private final Provider<WorkbenchContext> pWorkbenchContext_;
    private final SourceServerOperations server_;
    private final GlobalDisplay display_;
    private final SourceShim sourceShim_;
    private final UIPrefs uiPrefs_;

    private HashMap<String, Integer> sourceWindows_ = new HashMap<String, Integer>();
    private JsArray<SourceDocument> sourceDocs_ = JsArray.createArray().cast();
    private boolean windowsClosing_ = false;
    private JsObject windowGeometry_ = JsObject.createJsObject();
    private int maxOrdinal_ = 0;
    private int thisWindowOrdinal_ = 0;

    private String mostRecentSourceWindow_ = "";
    private boolean mainWindowFocused_ = true;

    public final static String SOURCE_WINDOW_ID = "source_window_id";
}