Mypal/devtools/client/projecteditor/lib/shells.js
2021-02-04 16:48:36 +02:00

243 lines
6.4 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { Cu } = require("chrome");
const { Class } = require("sdk/core/heritage");
const { EventTarget } = require("sdk/event/target");
const { emit } = require("sdk/event/core");
const { EditorTypeForResource } = require("devtools/client/projecteditor/lib/editors");
const NetworkHelper = require("devtools/shared/webconsole/network-helper");
const promise = require("promise");
/**
* The Shell is the object that manages the editor for a single resource.
* It is in charge of selecting the proper Editor (text/image/plugin-defined)
* and instantiating / appending the editor.
* This object is not exported, it is just used internally by the ShellDeck.
*
* This object has a promise `editorAppended`, that will resolve once the editor
* is ready to be used.
*/
var Shell = Class({
extends: EventTarget,
/**
* @param ProjectEditor host
* @param Resource resource
*/
initialize: function (host, resource) {
this.host = host;
this.doc = host.document;
this.resource = resource;
this.elt = this.doc.createElement("vbox");
this.elt.classList.add("view-project-detail");
this.elt.shell = this;
let constructor = this._editorTypeForResource();
this.editor = constructor(this.host);
this.editor.shell = this;
this.editorAppended = this.editor.appended;
this.editor.on("load", () => {
this.editorDeferred.resolve();
});
this.elt.appendChild(this.editor.elt);
},
/**
* Start loading the resource. The 'load' event happens as
* a result of this function, so any listeners to 'editorAppended'
* need to be added before calling this.
*/
load: function () {
this.editorDeferred = promise.defer();
this.editorLoaded = this.editorDeferred.promise;
this.editor.load(this.resource);
},
/**
* Destroy the shell and its associated editor
*/
destroy: function () {
this.editor.destroy();
this.resource.destroy();
},
/**
* Make sure the correct editor is selected for the resource.
* @returns Type:Editor
*/
_editorTypeForResource: function () {
let resource = this.resource;
let constructor = EditorTypeForResource(resource);
if (this.host.plugins) {
this.host.plugins.forEach(plugin => {
if (plugin.editorForResource) {
let pluginEditor = plugin.editorForResource(resource);
if (pluginEditor) {
constructor = pluginEditor;
}
}
});
}
return constructor;
}
});
/**
* The ShellDeck is in charge of managing the list of active Shells for
* the current ProjectEditor instance (aka host).
*
* This object emits the following events:
* - "editor-created": When an editor is initially created
* - "editor-activated": When an editor is ready to use
* - "editor-deactivated": When an editor is ready to use
*/
var ShellDeck = Class({
extends: EventTarget,
/**
* @param ProjectEditor host
* @param Document document
*/
initialize: function (host, document) {
this.doc = document;
this.host = host;
this.deck = this.doc.createElement("deck");
this.deck.setAttribute("flex", "1");
this.elt = this.deck;
this.shells = new Map();
this._activeShell = null;
},
/**
* Open a resource in a Shell. Will create the Shell
* if it doesn't exist yet.
*
* @param Resource resource
* The file to be opened
* @returns Shell
*/
open: function (defaultResource) {
let shell = this.shellFor(defaultResource);
if (!shell) {
shell = this._createShell(defaultResource);
this.shells.set(defaultResource, shell);
}
this.selectShell(shell);
return shell;
},
/**
* Create a new Shell for a resource. Called by `open`.
*
* @returns Shell
*/
_createShell: function (defaultResource) {
let shell = Shell(this.host, defaultResource);
shell.editorAppended.then(() => {
this.shells.set(shell.resource, shell);
emit(this, "editor-created", shell.editor);
if (this.currentShell === shell) {
this.selectShell(shell);
}
});
shell.load();
this.deck.appendChild(shell.elt);
return shell;
},
/**
* Remove the shell for a given resource.
*
* @param Resource resource
*/
removeResource: function (resource) {
let shell = this.shellFor(resource);
if (shell) {
this.shells.delete(resource);
shell.destroy();
}
},
destroy: function () {
for (let [resource, shell] of this.shells.entries()) {
this.shells.delete(resource);
shell.destroy();
}
},
/**
* Select a given shell and open its editor.
* Will fire editor-deactivated on the old selected Shell (if any),
* and editor-activated on the new one once it is ready
*
* @param Shell shell
*/
selectShell: function (shell) {
// Don't fire another activate if this is already the active shell
if (this._activeShell != shell) {
if (this._activeShell) {
emit(this, "editor-deactivated", this._activeShell.editor, this._activeShell.resource);
}
this.deck.selectedPanel = shell.elt;
this._activeShell = shell;
// Only reload the shell if the editor doesn't have local changes.
if (shell.editor.isClean()) {
shell.load();
}
shell.editorLoaded.then(() => {
// Handle case where another shell has been requested before this
// one is finished loading.
if (this._activeShell === shell) {
emit(this, "editor-activated", shell.editor, shell.resource);
}
});
}
},
/**
* Find a Shell for a Resource.
*
* @param Resource resource
* @returns Shell
*/
shellFor: function (resource) {
return this.shells.get(resource);
},
/**
* The currently active Shell. Note: the editor may not yet be available
* on the current shell. Best to wait for the 'editor-activated' event
* instead.
*
* @returns Shell
*/
get currentShell() {
return this._activeShell;
},
/**
* The currently active Editor, or null if it is not ready.
*
* @returns Editor
*/
get currentEditor() {
let shell = this.currentShell;
return shell ? shell.editor : null;
},
});
exports.ShellDeck = ShellDeck;