/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/view/Pane.js
1 585 строк
59 KB
GABINJUNG
pased -> passed (#13921)
10 янв 2018, 23:43
10 янв 2018, 23:43
254bb77
Код
Авторство
О чём код?
/* * 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. * */ /** * Pane objects host views of files, editors, etc... Clients cannot access * Pane objects directly. Instead the implementation is protected by the * MainViewManager -- however View Factories are given a Pane object which * they can use to add views. References to Pane objects should not be kept * as they may be destroyed and removed from the DOM. * * To get a custom view, there are two components: * * 1) A View Factory * 2) A View Object * * View objects are anonymous object that have a particular interface. * * Views can be added to a pane but do not have to exist in the Pane object's view list. * Such views are "temporary views". Temporary views are not serialized with the Pane state * or reconstituted when the pane is serialized from disk. They are destroyed at the earliest * opportunity. * * Temporary views are added by calling `Pane.showView()` and passing it the view object. The view * will be destroyed when the next view is shown, the pane is mereged with another pane or the "Close All" * command is exectuted on the Pane. Temporary Editor Views do not contain any modifications and are * added to the workingset (and are no longer tempoary views) once the document has been modified. They * will remain in the working set until closed from that point on. * * Views that have a longer life span are added by calling addView to associate the view with a * filename in the _views object. These views are not destroyed until they are removed from the pane * by calling one of the following: removeView, removeViews, or _reset * * Pane Object Events: * * - viewListChange - Whenever there is a file change to a file in the working set. These 2 events: `DocumentManager.pathRemove` * and `DocumentManager.fileNameChange` will cause a `viewListChange` event so the WorkingSetView can update. * * - currentViewChange - Whenever the current view changes. * (e, newView:View, oldView:View) * * - viewDestroy - Whenever a view has been destroyed * (e, view:View) * * View Interface: * * The view is an anonymous object which has the following method signatures. see ImageViewer for an example or the sample * provided with Brackets `src/extensions/samples/BracketsConfigCentral` * * { * $el:jQuery * getFile: function ():!File * updateLayout: function(forceRefresh:boolean) * destroy: function() * getScrollPos: function():*= * adjustScrollPos: function(state:Object=, heightDelta:number)= * notifyContainerChange: function()= * notifyVisibilityChange: function(boolean)= * focus:function()= * } * * When views are created they can be added to the pane by calling `pane.addView()`. * Views can be created and parented by attaching directly to `pane.$el` * * this._codeMirror = new CodeMirror(pane.$el, ...) * * Factories can create a view that's initially hidden by calling `pane.addView(view)` and passing `false` for the show parameter. * Hidden views can be later shown by calling `pane.showView(view)` * * `$el:jQuery!` * * property that stores the jQuery wrapped DOM element of the view. All views must have one so pane objects can manipulate the DOM * element when necessary (e.g. `showView`, `_reparent`, etc...) * * `getFile():File!` * * Called throughout the life of a View when the current file is queried by the system. * * `updateLayout(forceRefresh:boolean)` * * Called to notify the view that it should be resized to fit its parent container. This may be called several times * or only once. Views can ignore the `forceRefresh` flag. It is used for editor views to force a relayout of the editor * which probably isn't necessary for most views. Views should implement their html to be dynamic and not rely on this * function to be called whenever possible. * * `destroy()` * * Views must implement a destroy method to remove their DOM element at the very least. There is no default * implementation and views are hidden before this method is called. The Pane object doesn't make assumptions * about when it is safe to remove a node. In some instances other cleanup must take place before a the DOM * node is destroyed so the implementation details are left to the view. * * Views can implement a simple destroy by calling * * this.$el.remove() * * These members are optional and need not be implemented by Views * * getScrollPos() * adjustScrollPos() * * The system at various times will want to save and restore a view's scroll position. The data returned by `getScrollPos()` * is specific to the view and will be passed back to `adjustScrollPos()` when the scroll position needs to be restored. * * When Modal Bars are invoked, the system calls `getScrollPos()` so that the current scroll psotion of all visible Views can be cached. * That cached scroll position is later passed to `adjustScrollPos()` along with a height delta. The height delta is used to * scroll the view so that it doesn't appear to have "jumped" when invoking the Modal Bar. * * Height delta will be a positive when the Modal Bar is being shown and negative number when the Modal Bar is being hidden. * * `getViewState()` is another optional member that is used to cache a view's state when hiding or destroying a view or closing the project. * The data returned by this member is stored in `ViewStateManager` and is saved with the project. * * Views or View Factories are responsible for restoring the view state when the view of that file is created by recalling the cached state * * var view = createIconView(file, pane); * view.restoreViewState(ViewStateManager.getViewState(file.fullPath)); * * Notifications * The following optional methods receive notifications from the Pane object when certain events take place which affect the view: * * `notifyContainerChange()` * * Optional Notification callback called when the container changes. The view can perform any synchronization or state update * it needs to do when its parent container changes. * * `notifyVisiblityChange()` * * Optional Notification callback called when the view's vsibility changes. The view can perform any synchronization or * state update it needs to do when its visiblity state changes. */ define(function (require, exports, module) { "use strict"; var _ = require("thirdparty/lodash"), Mustache = require("thirdparty/mustache/mustache"), EventDispatcher = require("utils/EventDispatcher"), FileSystem = require("filesystem/FileSystem"), InMemoryFile = require("document/InMemoryFile"), ViewStateManager = require("view/ViewStateManager"), MainViewManager = require("view/MainViewManager"), PreferencesManager = require("preferences/PreferencesManager"), DocumentManager = require("document/DocumentManager"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), ViewUtils = require("utils/ViewUtils"), ProjectManager = require("project/ProjectManager"), paneTemplate = require("text!htmlContent/pane.html"); /** * Internal pane id * @const * @private */ var FIRST_PANE = "first-pane"; /** * Internal pane id * @const * @private */ var SECOND_PANE = "second-pane"; // Define showPaneHeaderButtons, which controls when to show close and flip-view buttons // on the header. PreferencesManager.definePreference("pane.showPaneHeaderButtons", "string", "hover", { description: Strings.DESCRIPTION_SHOW_PANE_HEADER_BUTTONS, values: ["hover", "always", "never"] }); // Define mergePanesWhenLastFileClosed, which controls if a split view pane should be // closed when the last file is closed, skipping the "Open a file while this pane has focus" // step completely. PreferencesManager.definePreference("pane.mergePanesWhenLastFileClosed", "boolean", false, { description: Strings.DESCRIPTION_MERGE_PANES_WHEN_LAST_FILE_CLOSED }); /** * Make an index request object * @param {boolean} requestIndex - true to request an index, false if not * @param {number} index - the index to request * @return {indexRequested:boolean, index:number} an object that can be passed to * {@link Pane#addToViewList} to insert the item at a specific index * @see Pane#addToViewList */ function _makeIndexRequestObject(requestIndex, index) { return {indexRequested: requestIndex, index: index}; } /** * Ensures that the given pane is focused after other focus related events occur * @params {string} paneId - paneId of the pane to focus * @private */ function _ensurePaneIsFocused(paneId) { var pane = MainViewManager._getPane(paneId); // Defer the focusing until other focus events have occurred. setTimeout(function () { // Focus has most likely changed: give it back to the given pane. pane.focus(); this._lastFocusedElement = pane.$el[0]; MainViewManager.setActivePaneId(paneId); }, 1); } /** * @typedef {!$el: jQuery, getFile:function():!File, updateLayout:function(forceRefresh:boolean), destroy:function(), getScrollPos:function():?, adjustScrollPos:function(state:Object=, heightDelta:number)=, getViewState:function():?*=, restoreViewState:function(viewState:!*)=, notifyContainerChange:function()=, notifyVisibilityChange:function(boolean)=} View */ /** * Pane Objects are constructed by the MainViewManager object when a Pane view is needed * @see {@link MainViewManager} for more information * * @constructor * @param {!string} id - The id to use to identify this pane * @param {!JQuery} $container - The parent $container to place the pane view */ function Pane(id, $container) { this._initialize(); // Setup the container and the element we're inserting var self = this, showPaneHeaderButtonsPref = PreferencesManager.get("pane.showPaneHeaderButtons"), $el = $container.append(Mustache.render(paneTemplate, {id: id})).find("#" + id), $header = $el.find(".pane-header"), $headerText = $header.find(".pane-header-text"), $headerFlipViewBtn = $header.find(".pane-header-flipview-btn"), $headerCloseBtn = $header.find(".pane-header-close-btn"), $content = $el.find(".pane-content"); $el.on("focusin.pane", function (e) { self._lastFocusedElement = e.target; }); // Flips the current file to the other pane when clicked $headerFlipViewBtn.on("click.pane", function (e) { var currentFile = self.getCurrentlyViewedFile(); var otherPaneId = self.id === FIRST_PANE ? SECOND_PANE : FIRST_PANE; var otherPane = MainViewManager._getPane(otherPaneId); var sameDocInOtherView = otherPane.getViewForPath(currentFile.fullPath); // If the same doc view is present in the destination, show the file instead of flipping it if (sameDocInOtherView) { CommandManager.execute(Commands.FILE_OPEN, {fullPath: currentFile.fullPath, paneId: otherPaneId}).always(function () { _ensurePaneIsFocused(otherPaneId); }); return; } // Currently active pane is not necessarily self.id as just clicking the button does not // give focus to the pane. This way it is possible to flip multiple panes to the active one // without losing focus. var activePaneIdBeforeFlip = MainViewManager.getActivePaneId(); MainViewManager._moveView(self.id, otherPaneId, currentFile).always(function () { CommandManager.execute(Commands.FILE_OPEN, {fullPath: currentFile.fullPath, paneId: otherPaneId}).always(function () { // Trigger view list changes for both panes self.trigger("viewListChange"); otherPane.trigger("viewListChange"); _ensurePaneIsFocused(activePaneIdBeforeFlip); }); }); }); // Closes the current view on the pane when clicked. If pane has no files, merge // panes. $headerCloseBtn.on("click.pane", function () { //set clicked pane as active to ensure that this._currentView is updated before closing MainViewManager.setActivePaneId(self.id); var file = self.getCurrentlyViewedFile(); if (file) { CommandManager.execute(Commands.FILE_CLOSE, {File: file, paneId: self.id}); if (!self.getCurrentlyViewedFile() && PreferencesManager.get("pane.mergePanesWhenLastFileClosed")) { MainViewManager.setLayoutScheme(1, 1); } } else { MainViewManager.setLayoutScheme(1, 1); } }); this._lastFocusedElement = $el[0]; // Make these properties read only Object.defineProperty(this, "id", { get: function () { return id; }, set: function () { console.error("cannot change the id of a working pane"); } }); Object.defineProperty(this, "$el", { get: function () { return $el; }, set: function () { console.error("cannot change the DOM node of a working pane"); } }); Object.defineProperty(this, "$header", { get: function () { return $header; }, set: function () { console.error("cannot change the DOM node of a working pane"); } }); Object.defineProperty(this, "$headerText", { get: function () { return $headerText; }, set: function () { console.error("cannot change the DOM node of a working pane"); } }); Object.defineProperty(this, "$headerFlipViewBtn", { get: function () { return $headerFlipViewBtn; }, set: function () { console.error("cannot change the DOM node of a working pane"); } }); Object.defineProperty(this, "$headerCloseBtn", { get: function () { return $headerCloseBtn; }, set: function () { console.error("cannot change the DOM node of a working pane"); } }); Object.defineProperty(this, "$content", { get: function () { return $content; }, set: function () { console.error("cannot change the DOM node of a working pane"); } }); Object.defineProperty(this, "$container", { get: function () { return $container; }, set: function () { console.error("cannot change the DOM node of a working pane"); } }); this.updateHeaderText(); switch (showPaneHeaderButtonsPref) { case "always": this.$header.addClass("always-show-header-buttons"); break; case "never": this.$headerFlipViewBtn.css("display", "none"); this.$headerCloseBtn.css("display", "none"); break; } // Listen to document events so we can update ourself DocumentManager.on(this._makeEventName("fileNameChange"), _.bind(this._handleFileNameChange, this)); DocumentManager.on(this._makeEventName("pathDeleted"), _.bind(this._handleFileDeleted, this)); MainViewManager.on(this._makeEventName("activePaneChange"), _.bind(this._handleActivePaneChange, this)); MainViewManager.on(this._makeEventName("workingSetAdd"), _.bind(this.updateHeaderText, this)); MainViewManager.on(this._makeEventName("workingSetRemove"), _.bind(this.updateHeaderText, this)); MainViewManager.on(this._makeEventName("workingSetAddList"), _.bind(this.updateHeaderText, this)); MainViewManager.on(this._makeEventName("workingSetRemoveList"), _.bind(this.updateHeaderText, this)); MainViewManager.on(this._makeEventName("paneLayoutChange"), _.bind(this.updateFlipViewIcon, this)); } EventDispatcher.makeEventDispatcher(Pane.prototype); /** * id of the pane * @readonly * @type {!string} */ Pane.prototype.id = null; /** * container where the pane lives * @readonly * @type {JQuery} */ Pane.prototype.$container = null; /** * the wrapped DOM node of this pane * @readonly * @type {JQuery} */ Pane.prototype.$el = null; /** * the wrapped DOM node container that contains name of current view and the switch view button, or informational string if there is no view * @readonly * @type {JQuery} */ Pane.prototype.$header = null; /** * the wrapped DOM node that contains name of current view, or informational string if there is no view * @readonly * @type {JQuery} */ Pane.prototype.$headerText = null; /** * the wrapped DOM node that is used to flip the view to another pane * @readonly * @type {JQuery} */ Pane.prototype.$headerFlipViewBtn = null; /** * close button of the pane * @readonly * @type {JQuery} */ Pane.prototype.$headerCloseBtn = null; /** * the wrapped DOM node that contains views * @readonly * @type {JQuery} */ Pane.prototype.$content = null; /** * The list of files views * @type {Array.<File>} */ Pane.prototype._viewList = []; /** * The list of files views in MRU order * @type {Array.<File>} */ Pane.prototype._viewListMRUOrder = []; /** * The list of files views in Added order * @type {Array.<File>} */ Pane.prototype._viewListAddedOrder = []; /** * Dictionary mapping fullpath to view * @type {Object.<!string, !View>} * @private */ Pane.prototype._views = {}; /** * The current view * @type {?View} * @private */ Pane.prototype._currentView = null; /** * The last thing that received a focus event * @type {?DomElement} * @private */ Pane.prototype._lastFocusedElement = null; /** * Initializes the Pane to its default state * @private */ Pane.prototype._initialize = function () { this._viewList = []; this._viewListMRUOrder = []; this._viewListAddedOrder = []; this._views = {}; this._currentView = null; this.showInterstitial(true); }; /** * Creates a pane event namespaced to this pane * (pass an empty string to generate just the namespace key to pass to jQuery to turn off all events handled by this pane) * @private * @param {!string} name - the name of the event to namespace * @return {string} an event namespaced to this pane */ Pane.prototype._makeEventName = function (name) { return name + ".pane-" + this.id; }; /** * Reparents a view to this pane * @private * @param {!View} view - the view to reparent */ Pane.prototype._reparent = function (view) { view.$el.appendTo(this.$content); this._views[view.getFile().fullPath] = view; if (view.notifyContainerChange) { view.notifyContainerChange(); } }; /** * Hides the current view if there is one, shows the * interstitial screen and notifies that the view changed */ Pane.prototype._hideCurrentView = function () { if (this._currentView) { var currentView = this._currentView; this._setViewVisibility(this._currentView, false); this.showInterstitial(true); this._currentView = null; this._notifyCurrentViewChange(null, currentView); } }; /** * moves a view from one pane to another * @param {!File} file - the File to move * @param {Pane} destinationPane - the destination pane * @param {Number} destinationIndex - the working set index of the file in the destination pane * @return {jQuery.Promise} a promise object which resolves after the view has been moved and its * replacement document has been opened * @private */ Pane.prototype.moveView = function (file, destinationPane, destinationIndex) { var self = this, openNextPromise = new $.Deferred(), result = new $.Deferred(); // if we're moving the currently viewed file we // need to open another file so wait for that operation // to finish before we move the view if ((this.getCurrentlyViewedPath() === file.fullPath)) { var nextFile = this.traverseViewListByMRU(1, file.fullPath); if (nextFile) { this._execOpenFile(nextFile.fullPath) .fail(function () { // the FILE_OPEN failed self._hideCurrentView(); }) .always(function () { openNextPromise.resolve(); }); } else { this._hideCurrentView(); openNextPromise.resolve(); } } else { openNextPromise.resolve(); } // Once the next file has opened, we can // move the item in the working set and // open it in the destination pane openNextPromise.done(function () { var viewListIndex = self.findInViewList(file.fullPath); var shouldAddView = viewListIndex !== -1; var view = self._views[file.fullPath]; // If the file isn't in working set, destroy the view and delete it from // source pane's view map and return as solved if (!shouldAddView) { if (view) { self._doDestroyView(view); } return result.resolve(); } // Remove file from all 3 view lists self._viewList.splice(viewListIndex, 1); self._viewListMRUOrder.splice(self.findInViewListMRUOrder(file.fullPath), 1); self._viewListAddedOrder.splice(self.findInViewListAddedOrder(file.fullPath), 1); // insert the view into the working set destinationPane._addToViewList(file, _makeIndexRequestObject(true, destinationIndex)); // if we had a view, it had previously been opened // otherwise, the file was in the working set unopened if (view) { // delete it from the source pane's view map and add it to the destination pane's view map delete self._views[file.fullPath]; destinationPane.addView(view, !destinationPane.getCurrentlyViewedFile()); // we're done result.resolve(); } else if (!destinationPane.getCurrentlyViewedFile()) { // The view has not have been created and the pane was // not showing anything so open the file moved in to the pane destinationPane._execOpenFile(file.fullPath).always(function () { // wait until the file has been opened before // we resolve the promise so the working set // view can sync appropriately result.resolve(); }); } else { // nothing to do, we're done result.resolve(); } }); return result.promise(); }; /** * Merges the another Pane object's contents into this Pane * @param {!Pane} Other - Pane from which to copy */ Pane.prototype.mergeFrom = function (other) { // save this because we're setting it to null and we // may need to destroy it if it's a temporary view var otherCurrentView = other._currentView; // Hide the current view while we // merge the 2 panes together other._hideCurrentView(); // Copy the File lists this._viewList = _.union(this._viewList, other._viewList); this._viewListMRUOrder = _.union(this._viewListMRUOrder, other._viewListMRUOrder); this._viewListAddedOrder = _.union(this._viewListAddedOrder, other._viewListAddedOrder); var self = this, viewsToDestroy = []; // Copy the views _.forEach(other._views, function (view) { var file = view.getFile(), fullPath = file && file.fullPath; if (fullPath && other.findInViewList(fullPath) !== -1) { // switch the container to this Pane self._reparent(view); } else { // We don't copy temporary views so destroy them viewsToDestroy.push(view); } }); // 1-off views if (otherCurrentView && !other._isViewNeeded(otherCurrentView) && viewsToDestroy.indexOf(otherCurrentView) === -1) { viewsToDestroy.push(otherCurrentView); } // Destroy temporary views _.forEach(viewsToDestroy, function (view) { self.trigger("viewDestroy", view); view.destroy(); }); // this _reset all internal data structures // and will set the current view to null other._initialize(); }; /** * Removes the DOM node for the Pane, removes all * event handlers and _resets all internal data structures */ Pane.prototype.destroy = function () { if (this._currentView || Object.keys(this._views).length > 0 || this._viewList.length > 0) { console.warn("destroying a pane that isn't empty"); } this._reset(); DocumentManager.off(this._makeEventName("")); MainViewManager.off(this._makeEventName("")); this.$el.off(".pane"); this.$el.remove(); }; /** * Returns a copy of the view file list * @return {!Array.<File>} */ Pane.prototype.getViewList = function () { return _.clone(this._viewList); }; /** * Returns the number of entries in the view file list * @return {number} */ Pane.prototype.getViewListSize = function () { return this._viewList.length; }; /** * Returns the index of the item in the view file list * @param {!string} fullPath the full path of the item to look for * @return {number} index of the item or -1 if not found */ Pane.prototype.findInViewList = function (fullPath) { return _.findIndex(this._viewList, function (file) { return file.fullPath === fullPath; }); }; /** * Returns the order in which the item was added * @param {!string} fullPath the full path of the item to look for * @return {number} order of the item or -1 if not found */ Pane.prototype.findInViewListAddedOrder = function (fullPath) { return _.findIndex(this._viewListAddedOrder, function (file) { return file.fullPath === fullPath; }); }; /** * Returns the order in which the item was last used * @param {!string} fullPath the full path of the item to look for * @return {number} order of the item or -1 if not found. * 0 indicates most recently used, followed by 1 and so on... */ Pane.prototype.findInViewListMRUOrder = function (fullPath) { return _.findIndex(this._viewListMRUOrder, function (file) { return file.fullPath === fullPath; }); }; /** * Return value from reorderItem when the Item was not found * @see {@link Pane#reorderItem} * @const */ Pane.prototype.ITEM_NOT_FOUND = -1; /** * Return value from reorderItem when the Item was found at its natural index * and the workingset does not need to be resorted * @see {@link Pane#reorderItem} * @const */ Pane.prototype.ITEM_FOUND_NO_SORT = 0; /** * Return value from reorderItem when the Item was found and reindexed * and the workingset needs to be resorted * @see {@link Pane#reorderItem} * @const */ Pane.prototype.ITEM_FOUND_NEEDS_SORT = 1; /** * reorders the specified file in the view list to the desired position * * @param {File} file - the file object of the item to reorder * @param {number=} index - the new position of the item * @param {boolean=} force - true to force the item into that position, false otherwise. (Requires an index be requested) * @return {number} this function returns one of the following manifest constants: * ITEM_NOT_FOUND : The request file object was not found * ITEM_FOUND_NO_SORT : The request file object was found but it was already at the requested index * ITEM_FOUND_NEEDS_SORT : The request file object was found and moved to a new index and the list should be resorted */ Pane.prototype.reorderItem = function (file, index, force) { var indexRequested = (index !== undefined && index !== null && index >= 0), curIndex = this.findInViewList(file.fullPath); if (curIndex !== -1) { // File is in view list, but not at the specifically requested index - only need to reorder if (force || (indexRequested && curIndex !== index)) { var entry = this._viewList.splice(curIndex, 1)[0]; this._viewList.splice(index, 0, entry); return this.ITEM_FOUND_NEEDS_SORT; } return this.ITEM_FOUND_NO_SORT; } return this.ITEM_NOT_FOUND; }; /** * Determines if a file can be added to our file list * @private * @param {!File} file - file object to test * @return {boolean} true if it can be added, false if not */ Pane.prototype._canAddFile = function (file) { return ((this._views.hasOwnProperty(file.fullPath) && this.findInViewList(file.fullPath) === -1) || (MainViewManager._getPaneIdForPath(file.fullPath) !== this.id)); }; /** * Adds the given file to the end of the workingset, if it is not already in the list * @private * @param {!File} file * @param {Object=} inPlace record with inPlace add data (index, indexRequested). Used internally */ Pane.prototype._addToViewList = function (file, inPlace) { if (inPlace && inPlace.indexRequested) { // If specified, insert into the workingset at this 0-based index this._viewList.splice(inPlace.index, 0, file); } else { // If no index is specified, just add the file to the end of the workingset. this._viewList.push(file); } // Add to MRU order: either first or last, depending on whether it's already the current doc or not var currentPath = this.getCurrentlyViewedPath(); if (currentPath && currentPath === file.fullPath) { this._viewListMRUOrder.unshift(file); } else { this._viewListMRUOrder.push(file); } // Add first to Added order this._viewListAddedOrder.unshift(file); }; /** * Adds the given file to the end of the workingset, if it is not already in the list * Does not change which document is currently open in the editor. Completes synchronously. * @param {!File} file - file to add * @param {number=} index - position where to add the item * @return {number} index of where the item was added */ Pane.prototype.addToViewList = function (file, index) { var indexRequested = (index !== undefined && index !== null && index >= 0 && index < this._viewList.length); this._addToViewList(file, _makeIndexRequestObject(indexRequested, index)); if (!indexRequested) { index = this._viewList.length - 1; } return index; }; /** * Adds the given file list to the end of the workingset. * @param {!Array.<File>} fileList * @return {!Array.<File>} list of files added to the list */ Pane.prototype.addListToViewList = function (fileList) { var self = this, uniqueFileList = []; // Process only files not already in view list fileList.forEach(function (file) { if (self._canAddFile(file)) { self._addToViewList(file); uniqueFileList.push(file); } }); return uniqueFileList; }; /** * Dispatches a currentViewChange event * @param {?View} newView - the view become the current view * @param {?View} oldView - the view being replaced */ Pane.prototype._notifyCurrentViewChange = function (newView, oldView) { this.updateHeaderText(); this.trigger("currentViewChange", newView, oldView); }; /** * Destroys a view and removes it from the view map. If it is the current view then the view * is first hidden and the interstitial page is displayed * @private * @param {!View} view - view to destroy */ Pane.prototype._doDestroyView = function (view) { if (this._currentView === view) { // if we're removing the current // view then we need to hide the view this._hideCurrentView(); } delete this._views[view.getFile().fullPath]; this.trigger("viewDestroy", view); view.destroy(); }; /** * Removes the specifed file from all internal lists, destroys the view of the file (if there is one) * and shows the interstitial page if the current view is destroyed * @private * @param {!File} file - file to remove * @param {boolean} preventViewChange - false to hide the current view if removing the current view, true * to prevent the current view from changing. * * When passing true for preventViewChange, it is assumed that the caller will perform an OPEN_FILE op * to show the next file in line to view. Since the file was removed from the workingset in _doRemove * its view is now considered to be a temporary view and the call to showView for the OPEN_FILE op * will destroy the view. the caller needs to handle the reject case in the event of failure * * @return {boolean} true if removed, false if the file was not found either in a list or view */ Pane.prototype._doRemove = function (file, preventViewChange) { // If it's in the view list then we need to remove it var index = this.findInViewList(file.fullPath); if (index > -1) { // Remove it from all 3 view lists this._viewList.splice(index, 1); this._viewListMRUOrder.splice(this.findInViewListMRUOrder(file.fullPath), 1); this._viewListAddedOrder.splice(this.findInViewListAddedOrder(file.fullPath), 1); } // Destroy the view var view = this._views[file.fullPath]; if (view) { if (!preventViewChange) { this._doDestroyView(view); } } return ((index > -1) || Boolean(view)); }; /** * Moves the specified file to the front of the MRU list * @param {!File} file */ Pane.prototype.makeViewMostRecent = function (file) { var index = this.findInViewListMRUOrder(file.fullPath); if (index !== -1) { this._viewListMRUOrder.splice(index, 1); this._viewListMRUOrder.unshift(file); } }; /** * Sorts items in the pane's view list * @param {function(paneId:!string, left:!string, right:!string):number} compareFn - the function used to compare items in the viewList */ /** * invokes Array.sort method on the internal view list. * @param {sortFunctionCallback} compareFn - the function to call to determine if the */ Pane.prototype.sortViewList = function (compareFn) { this._viewList.sort(_.partial(compareFn, this.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 {!number} fromIndex - the index of the item to move * @param {!number} toIndex - the index to move to * @private */ Pane.prototype.moveWorkingSetItem = function (fromIndex, toIndex) { this._viewList.splice(toIndex, 0, this._viewList.splice(fromIndex, 1)[0]); }; /** * Swaps two items in the file view list (used while dragging items in the working set view) * @param {number} index1 - the index of the first item to swap * @param {number} index2 - the index of the second item to swap * @return {boolean}} true */ Pane.prototype.swapViewListIndexes = function (index1, index2) { var temp = this._viewList[index1]; this._viewList[index1] = this._viewList[index2]; this._viewList[index2] = temp; return true; }; /** * Traverses the list and returns the File object of the next item in the MRU order * @param {!number} direction - Must be 1 or -1 to traverse forward or backward * @param {string=} current - the fullPath of the item where traversal is to start. * If this parameter is omitted then the path of the current view is used. * If the current view is a temporary view then the first item in the MRU list is returned * @return {?File} The File object of the next item in the travesal order or null if there isn't one. */ Pane.prototype.traverseViewListByMRU = function (direction, current) { if (!current && this._currentView) { var file = this._currentView.getFile(); current = file && file.fullPath; } var index = current ? this.findInViewListMRUOrder(current) : -1; return ViewUtils.traverseViewArray(this._viewListMRUOrder, index, direction); }; /** * Updates flipview icon in pane header * @private */ Pane.prototype.updateFlipViewIcon = function () { var paneID = this.id, directionIndex = 0, ICON_CLASSES = ["flipview-icon-none", "flipview-icon-top", "flipview-icon-right", "flipview-icon-bottom", "flipview-icon-left"], DIRECTION_STRINGS = ["", Strings.TOP, Strings.RIGHT, Strings.BOTTOM, Strings.LEFT], layoutScheme = MainViewManager.getLayoutScheme(), hasFile = this.getCurrentlyViewedFile(); if (layoutScheme.columns > 1 && hasFile) { directionIndex = paneID === FIRST_PANE ? 2 : 4; } else if (layoutScheme.rows > 1 && hasFile) { directionIndex = paneID === FIRST_PANE ? 3 : 1; } this.$headerFlipViewBtn.removeClass(ICON_CLASSES.join(" ")) .addClass(ICON_CLASSES[directionIndex]); this.$headerFlipViewBtn.attr("title", StringUtils.format(Strings.FLIPVIEW_BTN_TOOLTIP, DIRECTION_STRINGS[directionIndex].toLowerCase())); }; /** * Updates text in pane header * @private */ Pane.prototype.updateHeaderText = function () { var file = this.getCurrentlyViewedFile(), files, displayName; if (file) { files = MainViewManager.getAllOpenFiles().filter(function (item) { return (item.name === file.name); }); if (files.length < 2) { this.$headerText.text(file.name); } else { displayName = ProjectManager.makeProjectRelativeIfPossible(file.fullPath); this.$headerText.text(displayName); } } else { this.$headerText.html(Strings.EMPTY_VIEW_HEADER); } this.updateFlipViewIcon(); }; /** * Event handler when a file changes name * @private * @param {!JQuery.Event} e - jQuery event object * @param {!string} oldname - path of the file that was renamed * @param {!string} newname - the new path to the file */ Pane.prototype._handleFileNameChange = function (e, oldname, newname) { // Check to see if we need to dispatch a viewListChange event // The list contains references to file objects and, for a rename event, // the File object's name has changed by the time we've gotten the event. // So, we need to look for the file by its new name to determine if // if we need to dispatch the event which may look funny var dispatchEvent = (this.findInViewList(newname) >= 0); // rename the view if (this._views.hasOwnProperty(oldname)) { var view = this._views[oldname]; this._views[newname] = view; delete this._views[oldname]; } this.updateHeaderText(); // dispatch the change event if (dispatchEvent) { this.trigger("viewListChange"); } }; /** * Event handler when a file is deleted * @private * @param {!JQuery.Event} e - jQuery event object * @param {!string} fullPath - path of the file that was deleted */ Pane.prototype._handleFileDeleted = function (e, fullPath) { if (this.removeView({fullPath: fullPath})) { this.trigger("viewListChange"); } }; /** * Shows the pane's interstitial page * @param {boolean} show - show or hide the interstitial page */ Pane.prototype.showInterstitial = function (show) { if (this.$content) { this.$content.find(".not-editor").css("display", (show) ? "" : "none"); } }; /** * retrieves the view object for the given path * @param {!string} path - the fullPath of the view to retrieve * @return {boolean} show - show or hide the interstitial page */ Pane.prototype.getViewForPath = function (path) { return this._views[path]; }; /** * Adds a view to the pane * @param {!View} view - the View object to add * @param {boolean} show - true to show the view right away, false otherwise */ Pane.prototype.addView = function (view, show) { var file = view.getFile(), path = file && file.fullPath; if (!path) { console.error("cannot add a view that does not have a fullPath"); return; } if (view.$el.parent() !== this.$content) { this._reparent(view); } else { this._views[path] = view; } // Ensure that we don't endup marking the custom views if (view.markPaneId) { view.markPaneId(this.id); } if (show) { this.showView(view); } }; /** * Shows or hides a view * @param {!View} view - the to show or hide * @param {boolean} visible - true to show the view, false to hide it * @private */ Pane.prototype._setViewVisibility = function (view, visible) { view.$el.css("display", (visible ? "" : "none")); if (view.notifyVisibilityChange) { view.notifyVisibilityChange(visible); } }; /** * Swaps the current view with the requested view. * If the interstitial page is shown, it is hidden. * If the currentView is a temporary view, it is destroyed. * @param {!View} view - the to show */ Pane.prototype.showView = function (view) { if (this._currentView && this._currentView === view) { this._setViewVisibility(this._currentView, true); this.updateLayout(true); return; } var file = view.getFile(), newPath = file && file.fullPath, oldView = this._currentView; if (this._currentView) { if (this._currentView.getFile()) { ViewStateManager.updateViewState(this._currentView); } this._setViewVisibility(this._currentView, false); } else { this.showInterstitial(false); } this._currentView = view; this._setViewVisibility(this._currentView, true); this.updateLayout(); this._notifyCurrentViewChange(view, oldView); if (oldView) { this.destroyViewIfNotNeeded(oldView); } if (!this._views.hasOwnProperty(newPath)) { console.error(newPath + " found in pane working set but pane.addView() has not been called for the view created for it"); } }; /** * Update header and content height */ Pane.prototype._updateHeaderHeight = function () { var paneContentHeight = this.$el.height(); // Adjust pane content height for header if (MainViewManager.getPaneCount() > 1) { this.$header.show(); paneContentHeight -= this.$header.outerHeight(); } else { this.$header.hide(); } this.$content.height(paneContentHeight); }; /** * Sets pane content height. Updates the layout causing the current view to redraw itself * @param {boolean} forceRefresh - true to force a resize and refresh of the current view, * false if just to resize forceRefresh is only used by Editor views to force a relayout * of all editor DOM elements. Custom View implementations should just ignore this flag. */ Pane.prototype.updateLayout = function (forceRefresh) { this._updateHeaderHeight(); if (this._currentView) { this._currentView.updateLayout(forceRefresh); } }; /** * Determines if the view can be disposed of * @private * @param {!View} view - the View object to test * @return {boolean}} true if the view can be disposed, false if not */ Pane.prototype._isViewNeeded = function (view) { var path = view.getFile().fullPath, currentPath = this.getCurrentlyViewedPath(); return ((this._currentView && currentPath === path) || (this.findInViewList(path) !== -1)); }; /** * Retrieves the File object of the current view * @return {?File} the File object of the current view or null if there isn't one */ Pane.prototype.getCurrentlyViewedFile = function () { return this._currentView ? this._currentView.getFile() : null; }; /** * Retrieves the path of the current view * @return {?string} the path of the current view or null if there isn't one */ Pane.prototype.getCurrentlyViewedPath = function () { var file = this.getCurrentlyViewedFile(); return file ? file.fullPath : null; }; /** * destroys the view if it isn't needed * @param {View} view - the view to destroy */ Pane.prototype.destroyViewIfNotNeeded = function (view) { if (!this._isViewNeeded(view)) { var file = view.getFile(), path = file && file.fullPath; delete this._views[path]; this.trigger("viewDestroy", view); view.destroy(); } }; /** * _resets the pane to an empty state * @private */ Pane.prototype._reset = function () { var self = this, views = [], view = this._currentView; _.forEach(this._views, function (_view) { views.push(_view); }); // If the current view is a temporary view, // add it to the destroy list to dispose of if (this._currentView && views.indexOf(this._currentView) === -1) { views.push(this._currentView); } // This will reinitialize the object back to // the default state this._initialize(); if (view) { this._notifyCurrentViewChange(null, view); } // Now destroy the views views.forEach(function (_view) { self.trigger("viewDestroy", _view); _view.destroy(); }); }; /** * Executes a FILE_OPEN command to open a file * @param {!string} fullPath - path of the file to open * @return {jQuery.promise} promise that will resolve when the file is opened */ Pane.prototype._execOpenFile = function (fullPath) { return CommandManager.execute(Commands.CMD_ADD_TO_WORKINGSET_AND_OPEN, { fullPath: fullPath, paneId: this.id, options: {noPaneActivate: true}}); }; /** * Removes the view and opens the next view * @param {File} file - the file to close * @param {boolean} suppressOpenNextFile - suppresses opening the next file in MRU order * @param {boolean} preventViewChange - if suppressOpenNextFile is truthy, this flag can be used to * prevent the current view from being destroyed. * Ignored if suppressOpenNextFile is falsy * @return {boolean} true if the file was removed from the working set * This function will remove a temporary view of a file but will return false in that case */ Pane.prototype.removeView = function (file, suppressOpenNextFile, preventViewChange) { var nextFile = !suppressOpenNextFile && this.traverseViewListByMRU(1, file.fullPath); if (nextFile && nextFile.fullPath !== file.fullPath && this.getCurrentlyViewedPath() === file.fullPath) { var self = this, fullPath = nextFile.fullPath, needOpenNextFile = this.findInViewList(fullPath) !== -1; if (this._doRemove(file, needOpenNextFile)) { if (needOpenNextFile) { // this will destroy the current view this._execOpenFile(fullPath) .fail(function () { // the FILE_OPEN op failed so destroy the current view self._doDestroyView(self._currentView); }); } return true; } else { // Nothing was removed so don't try to remove it again return false; } } else { return this._doRemove(file, preventViewChange); } }; /** * Removes the specifed file from all internal lists, destroys the view of the file (if there is one) * and shows the interstitial page if the current view is destroyed. * @param {!Array.<File>} list - Array of files to remove * @return {!Array.<File>} Array of File objects removed from the working set. * This function will remove temporary views but the file objects for those views will not be found * in the result set. Only the file objects removed from the working set are returned. */ Pane.prototype.removeViews = function (list) { var self = this, needsDestroyCurrentView = false, result; // Check to see if we need to destroy the current view later needsDestroyCurrentView = _.findIndex(list, function (file) { return file.fullPath === self.getCurrentlyViewedPath(); }) !== -1; // destroy the views in the list result = list.filter(function (file) { return (self.removeView(file, true, true)); }); // we may have been passed a list of files that did not include the current view if (needsDestroyCurrentView) { // _doRemove will have whittled the MRU list down to just the remaining views var nextFile = this.traverseViewListByMRU(1, this.getCurrentlyViewedPath()), fullPath = nextFile && nextFile.fullPath, needOpenNextFile = fullPath && (this.findInViewList(fullPath) !== -1); if (needOpenNextFile) { // A successful open will destroy the current view this._execOpenFile(fullPath) .fail(function () { // the FILE_OPEN op failed so destroy the current view self._doDestroyView(self._currentView); }); } else { // Nothing left to show so destroy the current view this._doDestroyView(this._currentView); } } // return the result return result; }; /** * Gives focus to the last thing that had focus, the current view or the pane in that order */ Pane.prototype.focus = function () { var current = window.document.activeElement, self = this; // Helper to focus the current view if it can function tryFocusingCurrentView() { if (self._currentView) { if (self._currentView.focus) { // Views can implement a focus // method for focusing a complex // DOM like codemirror self._currentView.focus(); } else { // Otherwise, no focus method // just try and give the DOM // element focus self._currentView.$el.focus(); } } else { // no view so just focus the pane self.$el.focus(); } } // short-circuit for performance if (this._lastFocusedElement === current) { return; } // If the focus was in a <textarea> (assumed to be CodeMirror) and currentView is // anything other than an Editor, blur the textarea explicitly, in case the new // _currentView's $el isn't focusable. E.g.: // 1. Open a js file in the left pane and an image in the right pane and // 2. Focus the js file using the working-set // 3. Focus the image view using the working-set. // ==> Focus is still in the text area. Any keyboard input will modify the document if (current.tagName.toLowerCase() === "textarea" && (!this._currentView || !this._currentView._codeMirror)) { current.blur(); } var $lfe = $(this._lastFocusedElement); if ($lfe.length && !$lfe.is(".view-pane") && $lfe.is(":visible")) { // if we had a last focused element // and it wasn't a pane element // and it's still visible, focus it $lfe.focus(); } else { // otherwise, just try to give focus // to the currently active view tryFocusingCurrentView(); } }; /** * MainViewManager.activePaneChange handler * @param {jQuery.event} e - event data * @param {!string} activePaneId - the new active pane id */ Pane.prototype._handleActivePaneChange = function (e, activePaneId) { this.$el.toggleClass("active-pane", Boolean(activePaneId === this.id)); }; /** * serializes the pane state from JSON * @param {!Object} state - the state to load * @return {jQuery.Promise} A promise which resolves to * {fullPath:string, paneId:string} * which can be passed as command data to FILE_OPEN */ Pane.prototype.loadState = function (state) { var filesToAdd = [], viewStates = {}, activeFile, data, self = this; var getInitialViewFilePath = function () { return (self._viewList.length > 0) ? self._viewList[0].fullPath : null; }; _.forEach(state, function (entry) { filesToAdd.push(FileSystem.getFileForPath(entry.file)); if (entry.active) { activeFile = entry.file; } if (entry.viewState) { viewStates[entry.file] = entry.viewState; } }); this.addListToViewList(filesToAdd); ViewStateManager.addViewStates(viewStates); activeFile = activeFile || getInitialViewFilePath(); if (activeFile) { data = {paneId: self.id, fullPath: activeFile}; } return new $.Deferred().resolve(data); }; /** * Returns the JSON-ified state of the object so it can be serialize * @return {!Object} state - the state to save */ Pane.prototype.saveState = function () { var result = [], currentlyViewedPath = this.getCurrentlyViewedPath(); // Save the current view state first if (this._currentView && this._currentView.getFile()) { // We save the view state of the current view before // hiding the view and showing to a different file // But the current view's view state may not be // up to date in the view state cache so update it // before we save so we don't JSON-ify stale data. ViewStateManager.updateViewState(this._currentView); } // walk the list of views and save this._viewList.forEach(function (file) { // Do not persist untitled document paths if (!(file instanceof InMemoryFile)) { result.push({ file: file.fullPath, active: (file.fullPath === currentlyViewedPath), viewState: ViewStateManager.getViewState(file) }); } }); return result; }; /** * gets the current view's scroll state data * @return {Object=} scroll state - the current scroll state */ Pane.prototype.getScrollState = function () { if (this._currentView && this._currentView.getScrollPos) { return {scrollPos: this._currentView.getScrollPos()}; } }; /** * tells the current view to restore its scroll state from cached data and apply a height delta * @param {Object=} state - the current scroll state * @param {number=} heightDelta - the amount to add or subtract from the state */ Pane.prototype.restoreAndAdjustScrollState = function (state, heightDelta) { if (this._currentView && state && state.scrollPos && this._currentView.adjustScrollPos) { this._currentView.adjustScrollPos(state.scrollPos, heightDelta); } }; exports.Pane = Pane; });