adobe/brackets

View on GitHub
src/view/MainViewManager.js

Summary

Maintainability
F
4 days
Test Coverage
/*
 * Copyright (c) 2014 - present Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/**
 * MainViewManager manages the arrangement of all open panes as well as provides the controller
 * logic behind all views in the MainView (e.g. ensuring that a file doesn't appear in 2 lists)
 *
 * Each pane contains one or more views wich are created by a view factory and inserted into a pane list.
 * There may be several panes managed by the MainViewManager with each pane containing a list of views.
 * The panes are always visible and the layout is determined by the MainViewManager and the user.
 *
 * Currently we support only 2 panes.
 *
 * All of the WorkingSet APIs take a paneId Argument.  This can be an actual pane Id, ALL_PANES (in most cases)
 * or ACTIVE_PANE. ALL_PANES may not be supported for some APIs.  See the API for details.
 *
 * This module dispatches several events:
 *
 *    - activePaneChange - When the active pane changes.  There will always be an active pane.
 *          (e, newPaneId:string, oldPaneId:string)
 *    - currentFileChange -- When the user has switched to another pane, file, document. When the user closes a view
 *      and there are no other views to show the current file will be null.
 *          (e, newFile:File, newPaneId:string, oldFile:File, oldPaneId:string)
 *    - paneLayoutChange -- When Orientation changes.
 *          (e, orientation:string)
 *    - paneCreate -- When a pane is created
 *          (e, paneId:string)
 *    - paneDestroy -- When a pane is destroyed
 *          (e, paneId:string)
 *
 *
 *    To listen for working set changes, you must listen to *all* of these events:
 *    - workingSetAdd -- When a file is added to the working set
 *          (e, fileAdded:File, index:number, paneId:string)
 *    - workingSetAddList -- When multiple files are added to the working set
 *          (e, fileAdded:Array.<File>, paneId:string)
 *    - workingSetMove - When a File has moved to a different working set
 *          (e, File:FILE, sourcePaneId:string, destinationPaneId:string)
 *    - workingSetRemove -- When a file is removed from the working set
 *          (e, fileRemoved:File, suppressRedraw:boolean, paneId:string)
 *    - workingSetRemoveList -- When multiple files are removed from the working set
 *          (e, filesRemoved:Array.<File>, paneId:string)
 *    - workingSetSort -- When a pane's view array is reordered without additions or removals.
 *          (e, paneId:string)
 *    - workingSetUpdate -- When changes happen due to system events such as a file being deleted.
 *                              listeners should discard all working set info and rebuilt it from the pane
 *                              by calling getWorkingSet()
 *          (e, paneId:string)
 *    - _workingSetDisableAutoSort -- When the working set is reordered by manually dragging a file.
 *          (e, paneId:string) For Internal Use Only.
 *
 * To listen for events, do something like this: (see EventDispatcher for details on this pattern)
 *    `MainViewManager.on("eventname", handler);`
 */
define(function (require, exports, module) {
    "use strict";

    var _                   = require("thirdparty/lodash"),
        EventDispatcher     = require("utils/EventDispatcher"),
        Strings             = require("strings"),
        AppInit             = require("utils/AppInit"),
        CommandManager      = require("command/CommandManager"),
        MainViewFactory     = require("view/MainViewFactory"),
        ViewStateManager    = require("view/ViewStateManager"),
        Commands            = require("command/Commands"),
        EditorManager       = require("editor/EditorManager"),
        FileSystemError     = require("filesystem/FileSystemError"),
        DocumentManager     = require("document/DocumentManager"),
        PreferencesManager  = require("preferences/PreferencesManager"),
        ProjectManager      = require("project/ProjectManager"),
        WorkspaceManager    = require("view/WorkspaceManager"),
        AsyncUtils          = require("utils/Async"),
        ViewUtils           = require("utils/ViewUtils"),
        Resizer             = require("utils/Resizer"),
        Pane                = require("view/Pane").Pane,
        KeyBindingManager   = brackets.getModule("command/KeyBindingManager");

    /**
     * Preference setting name for the MainView Saved State
     * @const
     * @private
     */
    var PREFS_NAME          = "mainView.state";

    /**
     * Legacy Preference setting name used to migrate old preferences
     * @const
     * @private
     */
    var OLD_PREFS_NAME      = "project.files";

    /**
     * Special paneId shortcut that can be used to specify that
     * all panes should be targeted by the API.
     * Not all APIs support this constnant.
     * Check the API documentation before use.
     * @const
     */
    var ALL_PANES           = "ALL_PANES";

    /**
     * Special paneId shortcut that can be used to specify that
     * the API should target the focused pane only.
     * All APIs support this shortcut.
     * @const
     */
    var ACTIVE_PANE        = "ACTIVE_PANE";

    /**
     * Internal pane id
     * @const
     * @private
     */
    var FIRST_PANE          = "first-pane";

    /**
     * Internal pane id
     * @const
     * @private
     */
    var SECOND_PANE         = "second-pane";

    /*
     * NOTE: The following commands and constants will change
     *        when implementing the UX UI Treatment @larz0
     */

    /**
     * Vertical layout state name
     * @const
     * @private
     */
    var VERTICAL            = "VERTICAL";

    /**
     * Horizontal layout state name
     * @const
     * @private
     */
    var HORIZONTAL          = "HORIZONTAL";

    /**
     * The minimum width or height that a pane can be
     * @const
     * @private
     */
    var MIN_PANE_SIZE      = 75;

    /**
     * current orientation (null, VERTICAL or HORIZONTAL)
     * @type {string=}
     * @private
     */
    var _orientation = null;

    /**
     * current pane id. May not be null
     * @type {!string}
     * @private
     */
    var _activePaneId = null;

    /**
     * DOM element hosting the Main View.
     * @type {jQuery}
     * @private
     */
    var _$el;

    /**
     * Maps paneId to Pane objects
     * @type {Object.<string, Pane>}
     * @private
     */
    var _panes = {};


    /**
     * map of pane scroll states
     * @type {Object.map<string, *>}
     * @private
     */
    var _paneScrollStates = {};


    /**
     * flag indicating if traversing is currently taking place
     * When True, changes the current pane's MRU list will not be updated.
     * Useful for next/previous keyboard navigation (until Ctrl is released)
     * or for incremental-search style document preview like Quick Open will eventually have.
     * @type {!boolean}
     * @private
     */
    var _traversingFileList = false;

    /**
     * The global MRU list (for traversing)
     * @type {Array.<file:File, paneId:string>}
     */
    var _mruList = [];

    /**
     * localized pane titles
     * @type {Object.<FIRST_PANE|SECOND_PANE, <VERTICAL.string, HORIZONTAL.string>}}
     *  Localized string for first and second panes in the current orientation.
     * @see {@link #getPaneTitle} for more information
     * @private
     */
    var _paneTitles  = {};

    /*
     * Initialize _paneTitles
     */
    _paneTitles[FIRST_PANE] = {};
    _paneTitles[SECOND_PANE] = {};

    _paneTitles[FIRST_PANE][VERTICAL]     = Strings.LEFT;
    _paneTitles[FIRST_PANE][HORIZONTAL]   = Strings.TOP;
    _paneTitles[SECOND_PANE][VERTICAL]    = Strings.RIGHT;
    _paneTitles[SECOND_PANE][HORIZONTAL]  = Strings.BOTTOM;

    /**
     * Makes a MRU List Entry
     * @param {!File} File - the file
     * @param {!string} paneId - the paneId
     * @return {{file:File, paneId:string}}
     * @private
     */
    function _makeMRUListEntry(file, paneId) {
        return {file: file, paneId: paneId};
    }

    /**
     * Locates the first  MRU entry of a file for the requested pane
     * @param {!string} paneId - the paneId
     * @param {!File} File - the file
     * @return {{file:File, paneId:string}}
     * @private
     */
    function _findFileInMRUList(paneId, file) {
        return _.findIndex(_mruList, function (record) {
            return (record.file.fullPath === file.fullPath && record.paneId === paneId);
        });
    }

    /**
     * Checks whether a file is listed exclusively in the provided pane
     * @param {!File} File - the file
     * @return {{file:File, paneId:string}}
     */
    function isExclusiveToPane(file, paneId) {
        paneId = paneId === ACTIVE_PANE && _activePaneId ? _activePaneId : paneId;
        var index = _.findIndex(_mruList, function (record) {
            return (record.file.fullPath === file.fullPath && record.paneId !== paneId);
        });
        return index === -1;
    }


    /**
     * Retrieves the currently active Pane Id
     * @return {!string} Active Pane's ID.
     */
    function getActivePaneId() {
        return _activePaneId;
    }

    /**
     * Resolve paneId to actual pane.
     * @param {?string} paneId - id of the desired pane. May be symbolic or null (to indicate current pane)
     * @return {string} id of the pane in which to open the document
     */
    function _resolvePaneId(paneId) {
        if (!paneId || paneId === ACTIVE_PANE) {
            return getActivePaneId();
        }
        return paneId;
    }

    /**
     * Retrieves the Pane object for the given paneId
     * @param {!string} paneId - id of the pane to retrieve
     * @return {?Pane} the Pane object or null if a pane object doesn't exist for the pane
     * @private
     */
    function _getPane(paneId) {
        paneId = _resolvePaneId(paneId);

        if (_panes[paneId]) {
            return _panes[paneId];
        }

        return null;
    }

    /**
     * Focuses the current pane. If the current pane has a current view, then the pane will focus the view.
     */
    function focusActivePane() {
        _getPane(ACTIVE_PANE).focus();
    }

    /**
     * Determines if the pane id is a special pane id
     * @param {!string} paneId - the id to test
     * @return {boolean} true if the pane id is a special identifier, false if not
     */
    function _isSpecialPaneId(paneId) {
        return paneId === ACTIVE_PANE || paneId === ALL_PANES;
    }

    /**
     * Makes the file the most recent for the pane and the global mru lists
     * @param {!string} paneId - id of the pane to mae th file most recent or ACTIVE_PANE
     * @param {!File} file - File object to make most recent
     * @private
     */
    function _makeFileMostRecent(paneId, file) {
        var index,
            entry,
            pane = _getPane(paneId);

        if (!_traversingFileList) {
            pane.makeViewMostRecent(file);

            index = _findFileInMRUList(pane.id, file);

            entry = _makeMRUListEntry(file, pane.id);

            if (index !== -1) {
                _mruList.splice(index, 1);
            }

            if (_findFileInMRUList(pane.id, file) !== -1) {
                console.log(file.fullPath + " duplicated in mru list");
            }

            // add it to the front of the list
            _mruList.unshift(entry);
        }
    }

    /**
     * Makes the Pane's current file the most recent
     * @param {!string} paneId - id of the pane to make the file most recent, or ACTIVE_PANE
     * @param {!File} file - File object to make most recent
     * @private
     */
    function _makePaneMostRecent(paneId) {
        var pane = _getPane(paneId);

        if (pane.getCurrentlyViewedFile()) {
            _makeFileMostRecent(paneId, pane.getCurrentlyViewedFile());
        }
    }

    /**
     * Switch active pane to the specified pane id (or ACTIVE_PANE/ALL_PANES, in which case this
     * call does nothing).
     * @param {!string} paneId - the id of the pane to activate
     */
    function setActivePaneId(newPaneId) {
        if (!_isSpecialPaneId(newPaneId) && newPaneId !== _activePaneId) {
            var oldPaneId = _activePaneId,
                oldPane = _getPane(ACTIVE_PANE),
                newPane = _getPane(newPaneId);

            if (!newPane) {
                throw new Error("invalid pane id: " + newPaneId);
            }

            _activePaneId = newPaneId;

            exports.trigger("activePaneChange", newPaneId, oldPaneId);
            exports.trigger("currentFileChange", _getPane(ACTIVE_PANE).getCurrentlyViewedFile(),
                                                            newPaneId,
                                                            oldPane.getCurrentlyViewedFile(),
                                                            oldPaneId);

            _makePaneMostRecent(_activePaneId);
            focusActivePane();
        }
    }

    /**
     * Retrieves the Pane ID for the specified container
     * @param {!jQuery} $el - the element of the pane to fetch
     * @return {?string} the id of the pane that matches the container or undefined if a pane doesn't exist for that container
     */
    function _getPaneFromElement($el) {
        return _.find(_panes, function (pane) {
            if (pane.$el[0] === $el[0]) {
                return pane;
            }
        });
    }

    /**
     * Retrieves the currently viewed file of the specified paneId
     * @param {?string} paneId - the id of the pane in which to retrieve the currently viewed file
     * @return {?File} File object of the currently viewed file, or null if there isn't one or there's no such pane
     */
    function getCurrentlyViewedFile(paneId) {
        var pane = _getPane(paneId);
        return pane ? pane.getCurrentlyViewedFile() : null;
    }

    /**
     * Retrieves the currently viewed path of the pane specified by paneId
     * @param {?string} paneId - the id of the pane in which to retrieve the currently viewed path
     * @return {?string} the path of the currently viewed file or null if there isn't one
     */
    function getCurrentlyViewedPath(paneId) {
        var file = getCurrentlyViewedFile(paneId);
        return file ? file.fullPath : null;
    }

    /**
     * EditorManager.activeEditorChange handler
     *   This event is triggered when an visible editor gains focus
     *   Therefore we need to Activate the pane that the active editor belongs to
     * @private
     * @param {!jQuery.Event} e - jQuery Event object
     * @param {Editor=} current - editor being made the current editor
     */
    function _activeEditorChange(e, current) {
        if (current) {
            var $container = current.$el.parent().parent(),
                pane = _getPaneFromElement($container);

            if (pane) {
                // Editor is a full editor
                if (pane.id !== _activePaneId) {
                    // we just need to set the active pane in this case
                    //  it will dispatch the currentFileChange message as well
                    //  as dispatching other events when the active pane changes
                    setActivePaneId(pane.id);
                }
            } else {
                // Editor is an inline editor, find the parent pane
                var parents = $container.parents(".view-pane");
                if (parents.length === 1) {
                    $container = $(parents[0]);
                    pane = _getPaneFromElement($container);
                    if (pane) {
                        if (pane.id !== _activePaneId) {
                            // activate the pane which will put focus in the pane's doc
                            setActivePaneId(pane.id);
                            // reset the focus to the inline editor
                            current.focus();
                        }
                    }
                }
            }
        }
    }


    /**
     * Iterates over the pane or ALL_PANES and calls the callback function for each.
     * @param {!string} paneId - id of the pane in which to adjust the scroll state, ALL_PANES or ACTIVE_PANE
     * @param {!function(!pane:Pane):boolean} callback - function to callback on to perform work.
     * The callback will receive a Pane and should return false to stop iterating.
     * @private
     */
    function _forEachPaneOrPanes(paneId, callback) {
        if (paneId === ALL_PANES) {
            _.forEach(_panes, callback);
        } else {
            callback(_getPane(paneId));
        }
    }

    /**
     * Caches the specified pane's current scroll state
     * If there was already cached state for the specified pane, it is discarded and overwritten
     * @param {!string} paneId - id of the pane in which to cache the scroll state,
     *                            ALL_PANES or ACTIVE_PANE
     */
    function cacheScrollState(paneId) {
        _forEachPaneOrPanes(paneId, function (pane) {
            _paneScrollStates[pane.id] = pane.getScrollState();
        });
    }


    /**
     * Restores the scroll state from cache and applies the heightDelta
     * The view implementation is responsible for applying or ignoring the heightDelta.
     * This is used primarily when a modal bar opens to keep the editor from scrolling the current
     * page out of view in order to maintain the appearance.
     * The state is removed from the cache after calling this function.
     * @param {!string} paneId - id of the pane in which to adjust the scroll state,
     *                              ALL_PANES or ACTIVE_PANE
     * @param {!number} heightDelta - delta H to apply to the scroll state
     */
    function restoreAdjustedScrollState(paneId, heightDelta) {
        _forEachPaneOrPanes(paneId, function (pane) {
            pane.restoreAndAdjustScrollState(_paneScrollStates[pane.id], heightDelta);
            delete _paneScrollStates[pane.id];
        });
    }


    /**
     * Retrieves the WorkingSet for the given paneId not including temporary views
     * @param {!string} paneId - id of the pane in which to get the view list, ALL_PANES or ACTIVE_PANE
     * @return {Array.<File>}
     */
    function getWorkingSet(paneId) {
        var result = [];

        _forEachPaneOrPanes(paneId, function (pane) {
            var viewList = pane.getViewList();
            result = _.union(result, viewList);
        });

        return result;
    }


    /**
     * Retrieves the list of all open files including temporary views
     * @return {array.<File>} the list of all open files in all open panes
     */
    function getAllOpenFiles() {
        var result = getWorkingSet(ALL_PANES);
        _.forEach(_panes, function (pane) {
            var file = pane.getCurrentlyViewedFile();
            if (file) {
                result = _.union(result, [file]);
            }
        });
        return result;
    }

    /**
     * Retrieves the list of all open pane ids
     * @return {array.<string>} the list of all open panes
     */
    function getPaneIdList() {
        return Object.keys(_panes);
    }

    /**
     * Retrieves the size of the selected pane's view list
     * @param {!string} paneId - id of the pane in which to get the workingset size.
     *      Can use `ALL_PANES` or `ACTIVE_PANE`
     * @return {!number} the number of items in the specified pane
     */
    function getWorkingSetSize(paneId) {
        var result = 0;
        _forEachPaneOrPanes(paneId, function (pane) {
            result += pane.getViewListSize();
        });
        return result;
    }

    /**
     * Retrieves the title to display in the workingset view
     * @param {!string} paneId - id of the pane in which to get the title
     * @return {?string} title
     */
    function getPaneTitle(paneId) {
        return _paneTitles[paneId][_orientation];
    }

    /**
     * Retrieves the number of panes
     * @return {number}
     */
    function getPaneCount() {
        return Object.keys(_panes).length;
    }

    /**
     * Helper to abastract the common working set search functions
     * @param {!string} paneId - id of the pane to search or ALL_PANES to search all panes
     * @param {!string} fullPath - path of the file to locate
     * @param {!string} method - name of the method to use for searching
     *       "findInViewList", "findInViewListAddedOrder" or "FindInViewListMRUOrder"
     *
     * @private
     */
    function _doFindInWorkingSet(paneId, fullPath, method) {
        var result = -1;
        _forEachPaneOrPanes(paneId, function (pane) {
            var index = pane[method].call(pane, fullPath);
            if (index >= 0) {
                result = index;
                return false;
            }
        });
        return result;
    }

    /**
     * Finds all instances of the specified file in all working sets.
     * If there is a temporary view of the file, it is not part of the result set
     * @param {!string} fullPath - path of the file to find views of
     * @return {Array.<{pane:string, index:number}>} an array of paneId/index records
     */
    function findInAllWorkingSets(fullPath) {
        var index,
            result = [];

        _.forEach(_panes, function (pane) {
            index = pane.findInViewList(fullPath);
            if (index >= 0) {
                result.push({paneId: pane.id, index: index});
            }
        });

        return result;
    }

    /**
     * Gets the index of the file matching fullPath in the workingset
     * @param {!string} paneId - id of the pane in which to search or ALL_PANES or ACTIVE_PANE
     * @param {!string} fullPath - full path of the file to search for
     * @return {number} index, -1 if not found.
     */
    function findInWorkingSet(paneId, fullPath) {
        return _doFindInWorkingSet(paneId, fullPath, "findInViewList");
    }

    /**
     * Gets the index of the file matching fullPath in the added order workingset
     * @param {!string} paneId - id of the pane in which to search or ALL_PANES or ACTIVE_PANE
     * @param {!string} fullPath - full path of the file to search for
     * @return {number} index, -1 if not found.
     */
    function findInWorkingSetByAddedOrder(paneId, fullPath) {
        return _doFindInWorkingSet(paneId, fullPath, "findInViewListAddedOrder");
    }

    /**
     * Gets the index of the file matching fullPath in the MRU order workingset
     * @param {!string} paneId - id of the pane in which to search or ALL_PANES or ACTIVE_PANE
     * @param {!string} fullPath - full path of the file to search for
     * @return {number} index, -1 if not found.
     */
    function findInWorkingSetByMRUOrder(paneId, fullPath) {
        return _doFindInWorkingSet(paneId, fullPath, "findInViewListMRUOrder");
    }

    /**
     * @private
     * Retrieves pane id where the specified file has been opened. Used to ensure that a file
     *  is open in only one pane so this will change once support for multiple views is added
     * The result includes panes with a temporary view of the file not just working set instances
     * @param {!string} fullPath - full path of the file to search for
     * @return {?string} pane id where the file has been opened or null if it wasn't found
     */
    function _getPaneIdForPath(fullPath) {
        // Search all working sets and pull off the first one
        var info = findInAllWorkingSets(fullPath).shift();

        // Look for a view that has not been added to a working set
        if (!info) {
            _.forEach(_panes, function (pane) {
                if (pane.getCurrentlyViewedPath() === fullPath) {
                    info = {paneId: pane.id};
                    return false;
                }
            });
        }

        if (!info) {
            return null;
        }

        return info.paneId;
    }

    /**
     * Adds the given file to the end of the workingset, if it is not already there.
     *  This API does not create a view of the file, it just adds it to the working set
     * Views of files in the working set are persisted and are not destroyed until the user
     *  closes the file using FILE_CLOSE; Views are created using FILE_OPEN and, when opened, are
     *  made the current view. If a File is already opened then the file is just made current
     *  and its view is shown.
     * @param {!string} paneId - The id of the pane in which to add the file object to or ACTIVE_PANE
     * @param {!File} file - The File object to add to the workingset
     * @param {number=} index - Position to add to list (defaults to last); -1 is ignored
     * @param {boolean=} forceRedraw - If true, a workingset change notification is always sent
     *    (useful if suppressRedraw was used with removeView() earlier)
     */
    function addToWorkingSet(paneId, file, index, force) {
        // look for the file to have already been added to another pane
        var pane = _getPane(paneId);
        if (!pane) {
            throw new Error("invalid pane id: " + paneId);
        }

        var result = pane.reorderItem(file, index, force),
            entry = _makeMRUListEntry(file, pane.id);


        // handles the case of save as so that the file remains in the
        //  the same location in the working set as the file that was renamed
        if (result === pane.ITEM_FOUND_NEEDS_SORT) {
            console.warn("pane.reorderItem returned pane.ITEM_FOUND_NEEDS_SORT which shouldn't happen " + file);
            exports.trigger("workingSetSort", pane.id);
        } else if (result === pane.ITEM_NOT_FOUND) {
            index = pane.addToViewList(file, index);

            if (_findFileInMRUList(pane.id, file) === -1) {
                // Add to or update the position in MRU
                if (pane.getCurrentlyViewedFile() === file) {
                    _mruList.unshift(entry);
                } else {
                    _mruList.push(entry);
                }
            }

            exports.trigger("workingSetAdd", file, index, pane.id);
        }
    }

    /**
     * Adds the given file list to the end of the workingset.
     * @param {!string} paneId - The id of the pane in which to add the file object to or ACTIVE_PANE
     * @param {!Array.<File>} fileList - Array of files to add to the pane
     */
    function addListToWorkingSet(paneId, fileList) {
        var uniqueFileList,
            pane = _getPane(paneId);

        uniqueFileList = pane.addListToViewList(fileList);

        uniqueFileList.forEach(function (file) {
            if (_findFileInMRUList(pane.id, file) !== -1) {
                console.log(file.fullPath + " duplicated in mru list");
            }
            _mruList.push(_makeMRUListEntry(file, pane.id));
        });

        exports.trigger("workingSetAddList", uniqueFileList, pane.id);

        //  find all of the files that could be added but were not
        var unsolvedList = fileList.filter(function (item) {
            // if the file open in another pane, then add it to the list of unsolvedList
            return (pane.findInViewList(item.fullPath) === -1 && _getPaneIdForPath(item.fullPath));
        });

        // Use the pane id of the first one in the list for pane id and recurse
        //  if we add more panes, then this will recurse until all items in the list are satisified
        if (unsolvedList.length) {
            addListToWorkingSet(_getPaneIdForPath(unsolvedList[0].fullPath), unsolvedList);
        }
    }

    /**
     * Removes a file from the global MRU list. Future versions of this
     *  implementation may support the ALL_PANES constant but FOCUS_PANE is not allowed
     * @param {!string} paneId - Must be a valid paneId (not a shortcut e.g. ALL_PANES)
     @ @param {File} file The file object to remove.
     * @private
     */
    function _removeFileFromMRU(paneId, file) {
        var index,
            compare = function (record) {
                return (record.file === file && record.paneId === paneId);
            };

        // find and remove all instances
        do {
            index = _.findIndex(_mruList, compare);
            if (index !== -1) {
                _mruList.splice(index, 1);
            }
        } while (index !== -1);
    }

    /**
     * Removes a file the specified pane
     * @param {!string} paneId - Must be a valid paneId (not a shortcut e.g. ALL_PANES)
     * @param {!File} file - the File to remove
     * @param {boolean=} suppressRedraw - true to tell listeners not to redraw
     *          Use the suppressRedraw flag when calling this function along with many changes to prevent flicker
     * @private
     */
    function _removeView(paneId, file, suppressRedraw) {
        var pane = _getPane(paneId);

        if (pane.removeView(file)) {
            _removeFileFromMRU(pane.id, file);
            exports.trigger("workingSetRemove", file, suppressRedraw, pane.id);
        }
    }

    /**
     * moves a view from one pane to another
     * @param {!string} sourcePaneId - id of the source pane
     * @param {!string} destinationPaneId - id of the destination pane
     * @param {!File} file - the File to move
     * @param {Number} destinationIndex - the working set index of the file in the destination pane
     * @return {jQuery.Promise} a promise that resolves when the move has completed.
     * @private
     */
    function _moveView(sourcePaneId, destinationPaneId, file, destinationIndex) {
        var result = new $.Deferred(),
            sourcePane = _getPane(sourcePaneId),
            destinationPane = _getPane(destinationPaneId);

        sourcePane.moveView(file, destinationPane, destinationIndex)
            .done(function () {
                // remove existing entry from mrulist for the same document if present 
                _removeFileFromMRU(destinationPane.id, file);
                // update the mru list
                _mruList.every(function (record) {
                    if (record.file === file && record.paneId === sourcePane.id) {
                        record.paneId = destinationPane.id;
                        return false;
                    }
                    return true;
                });
                exports.trigger("workingSetMove", file, sourcePane.id, destinationPane.id);
                result.resolve();
            });

        return result.promise();
    }

    /**
     * Switch between panes
     */
    function switchPaneFocus() {
        var $firstPane = $('#first-pane'), $secondPane = $('#second-pane');
        if($firstPane.hasClass('active-pane')) {
            $secondPane.click();
        }
        else {
            $firstPane.click();
        }
    }

    /**
     * DocumentManager.pathDeleted Event handler to remove a file
     * from the MRU list
     * @param {!jQuery.event} e -
     * @param {!string} fullPath - path of the file to remove
     * @private
     */
    function _removeDeletedFileFromMRU(e, fullPath) {
        var index,
            compare = function (record) {
                return (record.file.fullPath === fullPath);
            };

        // find and remove all instances
        do {
            index = _.findIndex(_mruList, compare);
            if (index !== -1) {
                _mruList.splice(index, 1);
            }
        } while (index !== -1);
    }

    /**
     * sorts the pane's view list
     * @param {!string} paneId - id of the pane to sort, ALL_PANES or ACTIVE_PANE
     * @param {sortFunctionCallback} compareFn - callback to determine sort order (called on each item)
     * @see {@link Pane#sortViewList} for more information
     * @see {@link https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/sort|Sort Array - MDN}
     * @private
     */
    function _sortWorkingSet(paneId, compareFn) {
        _forEachPaneOrPanes(paneId, function (pane) {
            pane.sortViewList(compareFn);
            exports.trigger("workingSetSort", pane.id);
        });
    }

    /**
     * moves a working set item from one index to another shifting the items
     * after in the working set up and reinserting it at the desired location
     * @param {!string} paneId - id of the pane to sort
     * @param {!number} fromIndex - the index of the item to move
     * @param {!number} toIndex - the index to move to
     * @private
     */
    function _moveWorkingSetItem(paneId, fromIndex, toIndex) {
        var pane = _getPane(paneId);

        pane.moveWorkingSetItem(fromIndex, toIndex);
        exports.trigger("workingSetSort", pane.id);
        exports.trigger("_workingSetDisableAutoSort", pane.id);
    }

    /**
     * Mutually exchanges the files at the indexes passed by parameters.
     * @param {!string} paneId - id of the pane to swap indices or ACTIVE_PANE
     * @param {!number} index1 - the index on the left
     * @param {!number} index2 - the index on the rigth
     * @private
     */
    function _swapWorkingSetListIndexes(paneId, index1, index2) {
        var pane = _getPane(paneId);

        pane.swapViewListIndexes(index1, index2);
        exports.trigger("workingSetSort", pane.id);
        exports.trigger("_workingSetDisableAutoSort", pane.id);
    }

    /**
     * Get the next or previous file in the MRU list.
     * @param {!number} direction - Must be 1 or -1 to traverse forward or backward
     * @return {?{file:File, paneId:string}} The File object of the next item in the traversal order or null if there aren't any files to traverse.
     *                                       May return current file if there are no other files to traverse.
     */
    function traverseToNextViewByMRU(direction) {
        var file = getCurrentlyViewedFile(),
            paneId = getActivePaneId(),
            index = _.findIndex(_mruList, function (record) {
                return (record.file === file && record.paneId === paneId);
            });

        return ViewUtils.traverseViewArray(_mruList, index, direction);
    }

    /**
     * Get the next or previous file in list order.
     * @param {!number} direction - Must be 1 or -1 to traverse forward or backward
     * @return {?{file:File, paneId:string}} The File object of the next item in the traversal order or null if there aren't any files to traverse.
     *                                       May return current file if there are no other files to traverse.
     */
    function traverseToNextViewInListOrder(direction) {
        var file = getCurrentlyViewedFile(),
            curPaneId = getActivePaneId(),
            allFiles = [],
            index;

        getPaneIdList().forEach(function (paneId) {
            var paneFiles = getWorkingSet(paneId).map(function (file) {
                return { file: file, pane: paneId };
            });
            allFiles = allFiles.concat(paneFiles);
        });

        index = _.findIndex(allFiles, function (record) {
            return (record.file === file && record.pane === curPaneId);
        });

        return ViewUtils.traverseViewArray(allFiles, index, direction);
    }

    /**
     * Indicates that traversal has begun.
     * Can be called any number of times.
     */
    function beginTraversal() {
        _traversingFileList = true;
    }

    /**
     * Un-freezes the MRU list after one or more beginTraversal() calls.
     * Whatever file is current is bumped to the front of the MRU list.
     */
    function endTraversal() {
        var pane = _getPane(ACTIVE_PANE);

        if (_traversingFileList) {
            _traversingFileList = false;

            _makeFileMostRecent(pane.id, pane.getCurrentlyViewedFile());
        }
    }

    /**
     * Synchronizes the pane's sizer element, updates the pane's resizer maxsize value
     *   and tells the pane to update its layout
     * @param {boolean} forceRefresh - true to force a resize and refresh of the entire view
     * @private
     */
    function _synchronizePaneSize(pane, forceRefresh) {
        var available;

        if (_orientation === VERTICAL) {
            available = _$el.innerWidth();
        } else {
            available = _$el.innerHeight();
        }

        // Update the pane's sizer element if it has one and update the max size
        Resizer.resyncSizer(pane.$el);
        pane.$el.data("maxsize", available - MIN_PANE_SIZE);
        pane.updateLayout(forceRefresh);
    }


    /**
     * Event handler for "workspaceUpdateLayout" to update the layout
     * @param {jQuery.Event} event - jQuery event object
     * @param {number} viewAreaHeight - unused
     * @param {boolean} forceRefresh - true to force a resize and refresh of the entire view
     * @private
     */
    function _updateLayout(event, viewAreaHeight, forceRefresh) {
        var available;

        if (_orientation === VERTICAL) {
            available = _$el.innerWidth();
        } else {
            available = _$el.innerHeight();
        }

        _.forEach(_panes, function (pane) {
            // For VERTICAL orientation, we set the second pane to be width: auto
            //  so that it resizes to fill the available space in the containing div
            // unfortunately, that doesn't work in the HORIZONTAL orientation so we
            //  must update the height and convert it into a percentage
            if (pane.id === SECOND_PANE && _orientation === HORIZONTAL) {
                var percentage = ((_panes[FIRST_PANE].$el.height() + 1) / available);
                pane.$el.css("height", 100 - (percentage * 100) + "%");
            }

            _synchronizePaneSize(pane, forceRefresh);
        });
    }

    /**
     * Sets up the initial layout so panes are evenly distributed
     * This also sets css properties that aid in the layout when _updateLayout is called
     * @param {boolean} forceRefresh - true to force a resize and refresh of the entire view
     * @private
     */
    function _initialLayout(forceRefresh) {
        var panes = Object.keys(_panes),
            size = 100 / panes.length;

        _.forEach(_panes, function (pane) {
            if (pane.id === FIRST_PANE) {
                if (_orientation === VERTICAL) {
                    pane.$el.css({height: "100%",
                                  width: size + "%",
                                  float: "left"
                                 });
                } else {
                    pane.$el.css({ height: size + "%",
                                   width: "100%"
                                 });
                }
            } else {
                if (_orientation === VERTICAL) {
                    pane.$el.css({  height: "100%",
                                    width: "auto",
                                    float: "none"
                                 });
                } else {
                    pane.$el.css({ width: "100%",
                                   height: "50%"
                                 });
                }
            }

            _synchronizePaneSize(pane, forceRefresh);
        });
    }

    /**
     * Updates the header text for all panes
     */
    function _updatePaneHeaders() {
        _forEachPaneOrPanes(ALL_PANES, function (pane) {
            pane.updateHeaderText();
        });

    }

    /**
     * Creates a pane for paneId if one doesn't already exist
     * @param {!string} paneId - id of the pane to create
     * @private
     * @return {?Pane} - the pane object of the new pane, or undefined if no pane created
     */
    function _createPaneIfNecessary(paneId) {
        var newPane;

        if (!_panes.hasOwnProperty(paneId)) {
            newPane = new Pane(paneId, _$el);
            _panes[paneId] = newPane;

            exports.trigger("paneCreate", newPane.id);

            newPane.$el.on("click.mainview dragover.mainview", function () {
                setActivePaneId(newPane.id);
            });

            newPane.on("viewListChange.mainview", function () {
                _updatePaneHeaders();
                exports.trigger("workingSetUpdate", newPane.id);
            });
            newPane.on("currentViewChange.mainview", function (e, newView, oldView) {
                _updatePaneHeaders();
                if (_activePaneId === newPane.id) {
                    exports.trigger("currentFileChange",
                                               newView && newView.getFile(),
                                               newPane.id, oldView && oldView.getFile(),
                                               newPane.id);
                }
            });
            newPane.on("viewDestroy.mainView", function (e, view) {
                _removeFileFromMRU(newPane.id, view.getFile());
            });
        }

        return newPane;
    }

    /**
     * Makes the first pane resizable
     * @private
     */
    function _makeFirstPaneResizable() {
        var firstPane = _panes[FIRST_PANE];
        Resizer.makeResizable(firstPane.$el,
                              _orientation === HORIZONTAL ? Resizer.DIRECTION_VERTICAL : Resizer.DIRECTION_HORIZONTAL,
                              _orientation === HORIZONTAL ? Resizer.POSITION_BOTTOM : Resizer.POSITION_RIGHT,
                              MIN_PANE_SIZE, false, false, false, true, true);

        firstPane.$el.on("panelResizeUpdate", function () {
            _updateLayout();
        });
    }


    /**
     * Creates a split for the specified orientation
     * @private
     * @param {!string} orientation (VERTICAL|HORIZONTAL)
     */
    function _doSplit(orientation) {
        var firstPane, newPane;

        if (orientation === _orientation) {
            return;
        }

        firstPane = _panes[FIRST_PANE];
        Resizer.removeSizable(firstPane.$el);

        if (_orientation) {
            _$el.removeClass("split-" + _orientation.toLowerCase());
        }
        _$el.addClass("split-" + orientation.toLowerCase());

        _orientation = orientation;
        newPane = _createPaneIfNecessary(SECOND_PANE);
        _makeFirstPaneResizable();

        // reset the layout to 50/50 split
        // if we changed orientation then
        //  the percentages are reset as well
        _initialLayout();

        exports.trigger("paneLayoutChange", _orientation);

        // if new pane was created, and original pane is not empty, make new pane the active pane
        if (newPane && getCurrentlyViewedFile(firstPane.id)) {
            setActivePaneId(newPane.id);
        }
    }

    /**
     * Edits a document in the specified pane.
     * This function is only used by:
     *  - Unit Tests (which construct Mock Document objects),
     *  - by File > New  because there is yet to be an established File object
     *  - by Find In Files which needs to open documents synchronously in some cases
     * Do not use this API it is for internal use only
     * @param {!string} paneId - id of the pane in which to open the document
     * @param {!Document} doc - document to edit
     * @param {{noPaneActivate:boolean=}=} optionsIn - options
     * @private
     */
    function _edit(paneId, doc, optionsIn) {
        var options = optionsIn || {};

        var pane = _getPane(paneId);

        // If file is untitled or otherwise not within project tree, add it to
        // working set right now (don't wait for it to become dirty)
        if (doc.isUntitled() || !ProjectManager.isWithinProject(doc.file.fullPath)) {
            addToWorkingSet(paneId, doc.file);
        }

        // open document will show the editor if there is one already
        EditorManager.openDocument(doc, pane, options);
        _makeFileMostRecent(paneId, doc.file);

        if (!options.noPaneActivate) {
            setActivePaneId(paneId);
        }
    }

    /**
     * Opens a file in the specified pane this can be used to open a file with a custom viewer
     * or a document for editing.  If it's a document for editing, edit is called on the document
     * @param {!string} paneId - id of the pane in which to open the document
     * @param {!File} file - file to open
     * @param {{noPaneActivate:boolean=}=} optionsIn - options
     * @return {jQuery.Promise}  promise that resolves to a File object or
     *                           rejects with a File error or string
     */
    function _open(paneId, file, optionsIn) {
        var result = new $.Deferred(),
            options = optionsIn || {};

        function doPostOpenActivation() {
            if (!options.noPaneActivate) {
                setActivePaneId(paneId);
            }
        }

        if (!file || !_getPane(paneId)) {
            return result.reject("bad argument").promise();
        }


        // See if there is already a view for the file
        var pane = _getPane(paneId);

        // See if there is a factory to create a view for this file
        //  we want to do this first because, we don't want our internal
        //  editor to edit files for which there are suitable viewfactories
        var factory = MainViewFactory.findSuitableFactoryForPath(file.fullPath);

        if (factory) {
            file.exists(function (fileError, fileExists) {
                if (fileExists) {
                    // let the factory open the file and create a view for it
                    factory.openFile(file, pane)
                        .done(function () {
                            // if we opened a file that isn't in the project
                            //  then add the file to the working set
                            if (!ProjectManager.isWithinProject(file.fullPath)) {
                                addToWorkingSet(paneId, file);
                            }
                            doPostOpenActivation();
                            result.resolve(file);
                        })
                        .fail(function (fileError) {
                            result.reject(fileError);
                        });
                } else {
                    result.reject(fileError || FileSystemError.NOT_FOUND);
                }
            });
        } else {
            DocumentManager.getDocumentForPath(file.fullPath, file)
                .done(function (doc) {
                    if (doc) {
                        _edit(paneId, doc, $.extend({}, options, {
                            noPaneActivate: true
                        }));
                        doPostOpenActivation();
                        result.resolve(doc.file);
                    } else {
                        result.resolve(null);
                    }
                })
                .fail(function (fileError) {
                    result.reject(fileError);
                });
        }

       result.done(function () {
           _makeFileMostRecent(paneId, file);
       });

        return result;
    }

    /**
     * Merges second pane into first pane and opens the current file
     * @private
     */
    function _mergePanes() {
        if (_panes.hasOwnProperty(SECOND_PANE)) {

            var firstPane = _panes[FIRST_PANE],
                secondPane = _panes[SECOND_PANE],
                fileList = secondPane.getViewList(),
                lastViewed = getCurrentlyViewedFile();

            Resizer.removeSizable(firstPane.$el);
            firstPane.mergeFrom(secondPane);

            exports.trigger("workingSetRemoveList", fileList, secondPane.id);

            setActivePaneId(firstPane.id);

            secondPane.$el.off(".mainview");
            secondPane.off(".mainview");

            secondPane.destroy();
            delete _panes[SECOND_PANE];
            exports.trigger("paneDestroy", secondPane.id);
            exports.trigger("workingSetAddList", fileList, firstPane.id);

            _mruList.forEach(function (record) {
                if (record.paneId === secondPane.id) {
                    record.paneId = firstPane.id;
                }
            });

            _$el.removeClass("split-" + _orientation.toLowerCase());
            _orientation = null;
            // this will set the remaining pane to 100%
            _initialLayout();

            exports.trigger("paneLayoutChange", _orientation);

            // if the current view before the merger was in the pane
            //  that went away then reopen it so that it's now the current view again
            if (lastViewed && getCurrentlyViewedFile() !== lastViewed) {
                exports._open(firstPane.id, lastViewed);
            }
        }
    }

    /**
     * Closes a file in the specified pane or panes
     * @param {!string} paneId - id of the pane in which to open the document
     * @param {!File} file - file to close
     * @param {Object={noOpenNextFile:boolean}} optionsIn - options
     * This function does not fail if the file is not open
     */
    function _close(paneId, file, optionsIn) {
        var options = optionsIn || {};
        _forEachPaneOrPanes(paneId, function (pane) {
            if (pane.removeView(file, options.noOpenNextFile) && (paneId === ACTIVE_PANE || pane.id === paneId)) {
                _removeFileFromMRU(pane.id, file);
                exports.trigger("workingSetRemove", file, false, pane.id);
                return false;
            }
        });
    }

    /**
     * Closes a list of file in the specified pane or panes
     * @param {!string} paneId - id of the pane in which to open the document
     * @param {!Array.<File>} fileList - files to close
     * This function does not fail if the file is not open
     */
    function _closeList(paneId, fileList) {
        _forEachPaneOrPanes(paneId, function (pane) {
            var closedList = pane.removeViews(fileList);
            closedList.forEach(function (file) {
                _removeFileFromMRU(pane.id, file);
            });

            exports.trigger("workingSetRemoveList", closedList, pane.id);
        });
    }

    /**
     * Closes all files in the specified pane or panes
     * @param {!string} paneId - id of the pane in which to open the document
     * This function does not fail if the file is not open
     */
    function _closeAll(paneId) {
        _forEachPaneOrPanes(paneId, function (pane) {
            var closedList = pane.getViewList();
            closedList.forEach(function (file) {
                _removeFileFromMRU(pane.id, file);
            });

            pane._reset();
            exports.trigger("workingSetRemoveList", closedList, pane.id);
        });
    }


    /**
     * Finds which pane a document belongs to
     * @param {!Document} document - the document to locate
     * @return {?Pane} the pane where the document lives or NULL if it isn't in a pane
     * @private
     */
    function _findPaneForDocument(document) {
        // First check for an editor view of the document
        var pane = _getPaneFromElement($(document._masterEditor.$el.parent().parent()));

        if (!pane) {
            // No view of the document, it may be in a working set and not yet opened
            var info = findInAllWorkingSets(document.file.fullPath).shift();
            if (info) {
                pane = _panes[info.paneId];
            }
        }

        return pane;
    }

    /**
     * Destroys an editor object if a document is no longer referenced
     * @param {!Document} doc - document to destroy
     */
    function _destroyEditorIfNotNeeded(document) {
        if (!(document instanceof DocumentManager.Document)) {
            throw new Error("_destroyEditorIfUnneeded() should be passed a Document");
        }
        if (document._masterEditor) {
            // findPaneForDocument tries to locate the pane in which the document
            //  is either opened or will be opened (in the event that the document is
            //  in a working set but has yet to be opened) and then asks the pane
            //  to destroy the view if it doesn't need it anymore
            var pane = _findPaneForDocument(document);

            if (pane) {
                // let the pane deceide if it wants to destroy the view if it's no needed
                pane.destroyViewIfNotNeeded(document._masterEditor);
            } else {
                // in this case, the document isn't referenced at all so just destroy it
                document._masterEditor.destroy();
            }
        }
    }


    /**
     * Loads the workingset state
     * @private
     */
    function _loadViewState(e) {
        // file root is appended for each project
        var panes,
            promises = [],
            context = { location : { scope: "user",
                                     layer: "project" } },
            state = PreferencesManager.getViewState(PREFS_NAME, context);

        function convertViewState() {
            var context = { location : { scope: "user",
                                         layer: "project" } },
                files = PreferencesManager.getViewState(OLD_PREFS_NAME, context);

            if (!files) {
                // nothing to convert
                return;
            }

            var result = {
                orientation: null,
                activePaneId: FIRST_PANE,
                panes: {
                    "first-pane": []
                }
            };

            // Add all files to the workingset without verifying that
            // they still exist on disk (for faster project switching)
            files.forEach(function (value) {
                result.panes[FIRST_PANE].push(value);
            });

            return result;
        }

        if (!state) {
            // not converted yet
            state = convertViewState();
        }

        // reset
        _mergePanes();
        _mruList = [];
        ViewStateManager.reset();

        if (state) {

            panes = Object.keys(state.panes);
            _orientation = (panes.length > 1) ? state.orientation : null;

            _.forEach(state.panes, function (paneState, paneId) {
                _createPaneIfNecessary(paneId);
                promises.push(_panes[paneId].loadState(paneState));
            });

            AsyncUtils.waitForAll(promises).then(function (opensList) {

                // this will set the default layout of 50/50 or 100
                //  based on the number of panes
                _initialLayout();

                // More than 1 pane, then make it resizable
                //  and layout the panes from serialized state
                if (panes.length > 1) {
                    _makeFirstPaneResizable();

                    // If the split state was serialized correctly
                    //  then setup the splits according to was serialized
                    // Avoid a zero and negative split percentages
                    if ($.isNumeric(state.splitPercentage) && state.splitPercentage > 0) {
                        var prop;
                        if (_orientation === VERTICAL) {
                            prop = "width";
                        } else {
                            prop = "height";
                        }

                        _panes[FIRST_PANE].$el.css(prop, state.splitPercentage * 100 + "%");
                        _updateLayout();
                    }
                }

                if (_orientation) {
                    _$el.addClass("split-" + _orientation.toLowerCase());
                    exports.trigger("paneLayoutChange", _orientation);
                }

                _.forEach(_panes, function (pane) {
                    var fileList = pane.getViewList();

                    fileList.forEach(function (file) {
                        if (_findFileInMRUList(pane.id, file) !== -1) {
                            console.log(file.fullPath + " duplicated in mru list");
                        }
                        _mruList.push(_makeMRUListEntry(file, pane.id));
                    });
                    exports.trigger("workingSetAddList", fileList, pane.id);
                });

                promises = [];

                opensList.forEach(function (openData) {
                    if (openData) {
                        promises.push(CommandManager.execute(Commands.FILE_OPEN, openData));
                    }
                });

                // finally set the active pane
                AsyncUtils.waitForAll(promises).then(function () {
                    setActivePaneId(state.activePaneId);
                });
            });
        }
    }

    /**
     * Saves the workingset state
     * @private
     */
    function _saveViewState() {
        function _computeSplitPercentage() {
            var available,
                used;

            if (getPaneCount() === 1) {
                // just short-circuit here and
                //  return 100% to avoid any rounding issues
                return 1;
            } else {
                if (_orientation === VERTICAL) {
                    available = _$el.innerWidth();
                    used = _panes[FIRST_PANE].$el.width();
                } else {
                    available = _$el.innerHeight();
                    used = _panes[FIRST_PANE].$el.height();
                }

                return used / available;
            }
        }

        var projectRoot     = ProjectManager.getProjectRoot(),
            context         = { location : { scope: "user",
                                         layer: "project",
                                         layerID: projectRoot.fullPath } },

            state = {
                orientation: _orientation,
                activePaneId: getActivePaneId(),
                splitPercentage: _computeSplitPercentage(),
                panes: {
                }
            };


        if (!projectRoot) {
            return;
        }

        _.forEach(_panes, function (pane) {
            state.panes[pane.id] = pane.saveState();
        });

        PreferencesManager.setViewState(PREFS_NAME, state, context);
    }

    /**
     * Initializes the MainViewManager's view state
     * @param {jQuery} $container - the container where the main view will live
     * @private
     */
    function _initialize($container) {
        if (_activePaneId) {
            throw new Error("MainViewManager has already been initialized");
        }

        _$el = $container;
        _createPaneIfNecessary(FIRST_PANE);
        _activePaneId = FIRST_PANE;
        // One-time init so the pane has the "active" appearance
        _panes[FIRST_PANE]._handleActivePaneChange(undefined, _activePaneId);
        _initialLayout();

        // This ensures that unit tests that use this function
        //  get an event handler for workspace events and we don't listen
        //  to the event before we've been initialized
        WorkspaceManager.on("workspaceUpdateLayout", _updateLayout);

        // Listen to key Alt-W to toggle between panes
        CommandManager.register(Strings.CMD_SWITCH_PANE_FOCUS, Commands.CMD_SWITCH_PANE_FOCUS, switchPaneFocus);
        KeyBindingManager.addBinding(Commands.CMD_SWITCH_PANE_FOCUS, {key: 'Alt-W'});
    }

    /**
     * Changes the layout scheme
     * @param {!number} rows (may be 1 or 2)
     * @param {!number} columns (may be 1 or 2)
     * @summay Rows or Columns may be 1 or 2 but both cannot be 2. 1x2, 2x1 or 1x1 are the legal values
     */
    function setLayoutScheme(rows, columns) {
        if ((rows < 1) || (rows > 2) || (columns < 1) || (columns > 2) || (columns === 2 && rows === 2)) {
            console.error("setLayoutScheme unsupported layout " + rows + ", " + columns);
            return false;
        }

        if (rows === columns) {
            _mergePanes();
        } else if (rows > columns) {
            _doSplit(HORIZONTAL);
        } else {
            _doSplit(VERTICAL);
        }
        return true;
    }

    /**
     * Retrieves the current layout scheme
     * @return {!{rows: number, columns: number>}}
     */
    function getLayoutScheme() {
        var result = {
            rows: 1,
            columns: 1
        };

        if (_orientation === HORIZONTAL) {
            result.rows = 2;
        } else if (_orientation === VERTICAL) {
            result.columns = 2;
        }

        return result;
    }
    
    
    /**
     * Setup a ready event to initialize ourself
     */
    AppInit.htmlReady(function () {
        _initialize($("#editor-holder"));
    });

    // Event handlers - not safe to call on() directly, due to circular dependencies
    EventDispatcher.on_duringInit(ProjectManager, "projectOpen",                       _loadViewState);
    EventDispatcher.on_duringInit(ProjectManager, "beforeProjectClose beforeAppClose", _saveViewState);
    EventDispatcher.on_duringInit(EditorManager, "activeEditorChange",                 _activeEditorChange);
    EventDispatcher.on_duringInit(DocumentManager, "pathDeleted",                      _removeDeletedFileFromMRU);


    EventDispatcher.makeEventDispatcher(exports);

    // Unit Test Helpers
    exports._initialize                   = _initialize;
    exports._getPane                      = _getPane;

    // Private Helpers
    exports._removeView                   = _removeView;
    exports._moveView                     = _moveView;

    // Private API
    exports._sortWorkingSet               = _sortWorkingSet;
    exports._moveWorkingSetItem           = _moveWorkingSetItem;
    exports._swapWorkingSetListIndexes    = _swapWorkingSetListIndexes;
    exports._destroyEditorIfNotNeeded     = _destroyEditorIfNotNeeded;
    exports._edit                         = _edit;
    exports._open                         = _open;
    exports._close                        = _close;
    exports._closeAll                     = _closeAll;
    exports._closeList                    = _closeList;
    exports._getPaneIdForPath             = _getPaneIdForPath;

    // WorkingSet Management
    exports.addToWorkingSet               = addToWorkingSet;
    exports.addListToWorkingSet           = addListToWorkingSet;
    exports.getWorkingSetSize             = getWorkingSetSize;
    exports.getWorkingSet                 = getWorkingSet;

    // Pane state
    exports.cacheScrollState              = cacheScrollState;
    exports.restoreAdjustedScrollState    = restoreAdjustedScrollState;

    // Searching
    exports.findInWorkingSet              = findInWorkingSet;
    exports.findInWorkingSetByAddedOrder  = findInWorkingSetByAddedOrder;
    exports.findInWorkingSetByMRUOrder    = findInWorkingSetByMRUOrder;
    exports.findInAllWorkingSets          = findInAllWorkingSets;
    exports.findInGlobalMRUList           = _findFileInMRUList;

    // Traversal
    exports.beginTraversal                = beginTraversal;
    exports.endTraversal                  = endTraversal;
    exports.traverseToNextViewByMRU       = traverseToNextViewByMRU;
    exports.traverseToNextViewInListOrder = traverseToNextViewInListOrder;

    // PaneView Attributes
    exports.getActivePaneId               = getActivePaneId;
    exports.setActivePaneId               = setActivePaneId;
    exports.getPaneIdList                 = getPaneIdList;
    exports.getPaneTitle                  = getPaneTitle;
    exports.getPaneCount                  = getPaneCount;
    exports.isExclusiveToPane             = isExclusiveToPane;

    exports.getAllOpenFiles               = getAllOpenFiles;
    exports.focusActivePane               = focusActivePane;
    exports.switchPaneFocus               = switchPaneFocus;

    // Layout
    exports.setLayoutScheme               = setLayoutScheme;
    exports.getLayoutScheme               = getLayoutScheme;

    // Convenience
    exports.getCurrentlyViewedFile        = getCurrentlyViewedFile;
    exports.getCurrentlyViewedPath        = getCurrentlyViewedPath;

    // Constants
    exports.ALL_PANES                     = ALL_PANES;
    exports.ACTIVE_PANE                   = ACTIVE_PANE;
    exports.FIRST_PANE                    = FIRST_PANE;
    exports.SECOND_PANE                   = SECOND_PANE;
});