summaryrefslogtreecommitdiffstats
path: root/devtools/client/projecteditor/lib/projecteditor.js
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /devtools/client/projecteditor/lib/projecteditor.js
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/projecteditor/lib/projecteditor.js')
-rw-r--r--devtools/client/projecteditor/lib/projecteditor.js816
1 files changed, 816 insertions, 0 deletions
diff --git a/devtools/client/projecteditor/lib/projecteditor.js b/devtools/client/projecteditor/lib/projecteditor.js
new file mode 100644
index 000000000..a3ef06249
--- /dev/null
+++ b/devtools/client/projecteditor/lib/projecteditor.js
@@ -0,0 +1,816 @@
+/* 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/. */
+
+"use strict";
+
+const { Cc, Ci, Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { Project } = require("devtools/client/projecteditor/lib/project");
+const { ProjectTreeView } = require("devtools/client/projecteditor/lib/tree");
+const { ShellDeck } = require("devtools/client/projecteditor/lib/shells");
+const { Resource } = require("devtools/client/projecteditor/lib/stores/resource");
+const { registeredPlugins } = require("devtools/client/projecteditor/lib/plugins/core");
+const { EventTarget } = require("sdk/event/target");
+const { on, forget } = require("devtools/client/projecteditor/lib/helpers/event");
+const { emit } = require("sdk/event/core");
+const { merge } = require("sdk/util/object");
+const promise = require("promise");
+const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers");
+const { DOMHelpers } = require("resource://devtools/client/shared/DOMHelpers.jsm");
+const Services = require("Services");
+const { Task } = require("devtools/shared/task");
+const ITCHPAD_URL = "chrome://devtools/content/projecteditor/chrome/content/projecteditor.xul";
+const { confirm } = require("devtools/client/projecteditor/lib/helpers/prompts");
+const { getLocalizedString } = require("devtools/client/projecteditor/lib/helpers/l10n");
+
+// Enabled Plugins
+require("devtools/client/projecteditor/lib/plugins/dirty/dirty");
+require("devtools/client/projecteditor/lib/plugins/delete/delete");
+require("devtools/client/projecteditor/lib/plugins/new/new");
+require("devtools/client/projecteditor/lib/plugins/rename/rename");
+require("devtools/client/projecteditor/lib/plugins/save/save");
+require("devtools/client/projecteditor/lib/plugins/image-view/plugin");
+require("devtools/client/projecteditor/lib/plugins/app-manager/plugin");
+require("devtools/client/projecteditor/lib/plugins/status-bar/plugin");
+
+// Uncomment to enable logging.
+// require("devtools/client/projecteditor/lib/plugins/logging/logging");
+
+/**
+ * This is the main class tying together an instance of the ProjectEditor.
+ * The frontend is contained inside of this.iframe, which loads projecteditor.xul.
+ *
+ * Usage:
+ * let projecteditor = new ProjectEditor(frame);
+ * projecteditor.loaded.then((projecteditor) => {
+ * // Ready to use.
+ * });
+ *
+ * Responsible for maintaining:
+ * - The list of Plugins for this instance.
+ * - The ShellDeck, which includes all Shells for opened Resources
+ * -- Shells take in a Resource, and construct the appropriate Editor
+ * - The Project, which includes all Stores for this instance
+ * -- Stores manage all Resources starting from a root directory
+ * --- Resources are a representation of a file on disk
+ * - The ProjectTreeView that builds the UI for interacting with the
+ * project.
+ *
+ * This object emits the following events:
+ * - "onEditorDestroyed": When editor is destroyed
+ * - "onEditorSave": When editor is saved
+ * - "onEditorLoad": When editor is loaded
+ * - "onEditorActivated": When editor is activated
+ * - "onEditorChange": When editor is changed
+ * - "onEditorCursorActivity": When there is cursor activity in a text editor
+ * - "onCommand": When a command happens
+ * - "onEditorDestroyed": When editor is destroyed
+ * - "onContextMenuOpen": When the context menu is opened on the project tree
+ *
+ * The events can be bound like so:
+ * projecteditor.on("onEditorCreated", (editor) => { });
+ */
+var ProjectEditor = Class({
+ extends: EventTarget,
+
+ /**
+ * Initialize ProjectEditor, and load into an iframe if specified.
+ *
+ * @param Iframe iframe
+ * The iframe to inject the DOM into. If this is not
+ * specified, then this.load(frame) will need to be called
+ * before accessing ProjectEditor.
+ * @param Object options
+ * - menubar: a <menubar> element to inject menus into
+ * - menuindex: Integer child index to insert menus
+ */
+ initialize: function (iframe, options = {}) {
+ this._onTreeSelected = this._onTreeSelected.bind(this);
+ this._onTreeResourceRemoved = this._onTreeResourceRemoved.bind(this);
+ this._onEditorCreated = this._onEditorCreated.bind(this);
+ this._onEditorActivated = this._onEditorActivated.bind(this);
+ this._onEditorDeactivated = this._onEditorDeactivated.bind(this);
+ this._updateMenuItems = this._updateMenuItems.bind(this);
+ this._updateContextMenuItems = this._updateContextMenuItems.bind(this);
+ this.destroy = this.destroy.bind(this);
+ this.menubar = options.menubar || null;
+ this.menuindex = options.menuindex || null;
+ this._menuEnabled = true;
+ this._destroyed = false;
+ this._loaded = false;
+ this._pluginCommands = new Map();
+ if (iframe) {
+ this.load(iframe);
+ }
+ },
+
+ /**
+ * Load the instance inside of a specified iframe.
+ * This can be called more than once, and it will return the promise
+ * from the first call.
+ *
+ * @param Iframe iframe
+ * The iframe to inject the projecteditor DOM into
+ * @returns Promise
+ * A promise that is resolved once the iframe has been
+ * loaded.
+ */
+ load: function (iframe) {
+ if (this.loaded) {
+ return this.loaded;
+ }
+
+ let deferred = promise.defer();
+ this.loaded = deferred.promise;
+ this.iframe = iframe;
+
+ let domReady = () => {
+ if (this._destroyed) {
+ deferred.reject("Error: ProjectEditor has been destroyed before loading");
+ return;
+ }
+ this._onLoad();
+ this._loaded = true;
+ deferred.resolve(this);
+ };
+
+ let domHelper = new DOMHelpers(this.iframe.contentWindow);
+ domHelper.onceDOMReady(domReady);
+
+ this.iframe.setAttribute("src", ITCHPAD_URL);
+
+ return this.loaded;
+ },
+
+ /**
+ * Build the projecteditor DOM inside of this.iframe.
+ */
+ _onLoad: function () {
+ this.document = this.iframe.contentDocument;
+ this.window = this.iframe.contentWindow;
+
+ this._initCommands();
+ this._buildMenubar();
+ this._buildSidebar();
+
+ this.window.addEventListener("unload", this.destroy, false);
+
+ // Editor management
+ this.shells = new ShellDeck(this, this.document);
+ this.shells.on("editor-created", this._onEditorCreated);
+ this.shells.on("editor-activated", this._onEditorActivated);
+ this.shells.on("editor-deactivated", this._onEditorDeactivated);
+
+ let shellContainer = this.document.querySelector("#shells-deck-container");
+ shellContainer.appendChild(this.shells.elt);
+
+ // We are not allowing preset projects for now - rebuild a fresh one
+ // each time.
+ this.setProject(new Project({
+ id: "",
+ name: "",
+ directories: [],
+ openFiles: []
+ }));
+
+ this._initPlugins();
+ },
+
+ _buildMenubar: function () {
+
+ this.contextMenuPopup = this.document.getElementById("context-menu-popup");
+ this.contextMenuPopup.addEventListener("popupshowing", this._updateContextMenuItems);
+
+ this.textEditorContextMenuPopup = this.document.getElementById("texteditor-context-popup");
+ this.textEditorContextMenuPopup.addEventListener("popupshowing", this._updateMenuItems);
+
+ this.editMenu = this.document.getElementById("edit-menu");
+ this.fileMenu = this.document.getElementById("file-menu");
+
+ this.editMenuPopup = this.document.getElementById("edit-menu-popup");
+ this.fileMenuPopup = this.document.getElementById("file-menu-popup");
+ this.editMenu.addEventListener("popupshowing", this._updateMenuItems);
+ this.fileMenu.addEventListener("popupshowing", this._updateMenuItems);
+
+ if (this.menubar) {
+ let body = this.menubar.ownerDocument.body ||
+ this.menubar.ownerDocument.querySelector("window");
+ body.appendChild(this.projectEditorCommandset);
+ body.appendChild(this.projectEditorKeyset);
+ body.appendChild(this.editorCommandset);
+ body.appendChild(this.editorKeyset);
+ body.appendChild(this.contextMenuPopup);
+ body.appendChild(this.textEditorContextMenuPopup);
+
+ let index = this.menuindex || 0;
+ this.menubar.insertBefore(this.editMenu, this.menubar.children[index]);
+ this.menubar.insertBefore(this.fileMenu, this.menubar.children[index]);
+ } else {
+ this.document.getElementById("projecteditor-menubar").style.display = "block";
+ }
+
+ // Insert a controller to allow enabling and disabling of menu items.
+ this._commandWindow = this.editorCommandset.ownerDocument.defaultView;
+ this._commandController = getCommandController(this);
+ this._commandWindow.controllers.insertControllerAt(0, this._commandController);
+ },
+
+ /**
+ * Create the project tree sidebar that lists files.
+ */
+ _buildSidebar: function () {
+ this.projectTree = new ProjectTreeView(this.document, {
+ resourceVisible: this.resourceVisible.bind(this),
+ resourceFormatter: this.resourceFormatter.bind(this),
+ contextMenuPopup: this.contextMenuPopup
+ });
+ on(this, this.projectTree, "selection", this._onTreeSelected);
+ on(this, this.projectTree, "resource-removed", this._onTreeResourceRemoved);
+
+ let sourcesBox = this.document.querySelector("#sources > vbox");
+ sourcesBox.appendChild(this.projectTree.elt);
+ },
+
+ /**
+ * Set up listeners for commands to dispatch to all of the plugins
+ */
+ _initCommands: function () {
+
+ this.projectEditorCommandset = this.document.getElementById("projecteditor-commandset");
+ this.projectEditorKeyset = this.document.getElementById("projecteditor-keyset");
+
+ this.editorCommandset = this.document.getElementById("editMenuCommands");
+ this.editorKeyset = this.document.getElementById("editMenuKeys");
+
+ this.projectEditorCommandset.addEventListener("command", (evt) => {
+ evt.stopPropagation();
+ evt.preventDefault();
+ this.pluginDispatch("onCommand", evt.target.id, evt.target);
+ });
+ },
+
+ /**
+ * Initialize each plugin in registeredPlugins
+ */
+ _initPlugins: function () {
+ this._plugins = [];
+
+ for (let plugin of registeredPlugins) {
+ try {
+ this._plugins.push(plugin(this));
+ } catch (ex) {
+ console.exception(ex);
+ }
+ }
+
+ this.pluginDispatch("lateInit");
+ },
+
+ /**
+ * Enable / disable necessary menu items using globalOverlay.js.
+ */
+ _updateMenuItems: function () {
+ let window = this.editMenu.ownerDocument.defaultView;
+ let commands = ["cmd_undo", "cmd_redo", "cmd_delete", "cmd_cut", "cmd_copy", "cmd_paste"];
+ commands.forEach(window.goUpdateCommand);
+
+ for (let c of this._pluginCommands.keys()) {
+ window.goUpdateCommand(c);
+ }
+ },
+
+ /**
+ * Enable / disable necessary context menu items by passing an event
+ * onto plugins.
+ */
+ _updateContextMenuItems: function () {
+ let resource = this.projectTree.getSelectedResource();
+ this.pluginDispatch("onContextMenuOpen", resource);
+ },
+
+ /**
+ * Destroy all objects on the iframe unload event.
+ */
+ destroy: function () {
+ this._destroyed = true;
+
+
+ // If been destroyed before the iframe finished loading, then
+ // the properties below will not exist.
+ if (!this._loaded) {
+ this.iframe.setAttribute("src", "about:blank");
+ return;
+ }
+
+ // Reset the src for the iframe so if it reused for a new ProjectEditor
+ // instance, the load will fire properly.
+ this.window.removeEventListener("unload", this.destroy, false);
+ this.iframe.setAttribute("src", "about:blank");
+
+ this._plugins.forEach(plugin => { plugin.destroy(); });
+
+ forget(this, this.projectTree);
+ this.projectTree.destroy();
+ this.projectTree = null;
+
+ this.shells.destroy();
+
+ this.projectEditorCommandset.remove();
+ this.projectEditorKeyset.remove();
+ this.editorCommandset.remove();
+ this.editorKeyset.remove();
+ this.contextMenuPopup.remove();
+ this.textEditorContextMenuPopup.remove();
+ this.editMenu.remove();
+ this.fileMenu.remove();
+
+ this._commandWindow.controllers.removeController(this._commandController);
+ this._commandController = null;
+
+ forget(this, this.project);
+ this.project.destroy();
+ this.project = null;
+ },
+
+ /**
+ * Set the current project viewed by the projecteditor.
+ *
+ * @param Project project
+ * The project to set.
+ */
+ setProject: function (project) {
+ if (this.project) {
+ forget(this, this.project);
+ }
+ this.project = project;
+ this.projectTree.setProject(project);
+
+ // Whenever a store gets removed, clean up any editors that
+ // exist for resources within it.
+ on(this, project, "store-removed", (store) => {
+ store.allResources().forEach((resource) => {
+ this.shells.removeResource(resource);
+ });
+ });
+ },
+
+ /**
+ * Set the current project viewed by the projecteditor to a single path,
+ * used by the app manager.
+ *
+ * @param string path
+ * The file path to set
+ * @param Object opts
+ * Custom options used by the project.
+ * - name: display name for project
+ * - iconUrl: path to icon for project
+ * - validationStatus: one of 'unknown|error|warning|valid'
+ * - projectOverviewURL: path to load for iframe when project
+ * is selected in the tree.
+ * @param Promise
+ * Promise that is resolved once the project is ready to be used.
+ */
+ setProjectToAppPath: function (path, opts = {}) {
+ this.project.appManagerOpts = opts;
+
+ let existingPaths = this.project.allPaths();
+ if (existingPaths.length !== 1 || existingPaths[0] !== path) {
+ // Only fully reset if this is a new path.
+ this.project.removeAllStores();
+ this.project.addPath(path);
+ } else {
+ // Otherwise, just ask for the root to be redrawn
+ let rootResource = this.project.localStores.get(path).root;
+ emit(rootResource, "label-change", rootResource);
+ }
+
+ return this.project.refresh();
+ },
+
+ /**
+ * Open a resource in a particular shell.
+ *
+ * @param Resource resource
+ * The file to be opened.
+ */
+ openResource: function (resource) {
+ let shell = this.shells.open(resource);
+ this.projectTree.selectResource(resource);
+ shell.editor.focus();
+ },
+
+ /**
+ * When a node is selected in the tree, open its associated editor.
+ *
+ * @param Resource resource
+ * The file that has been selected
+ */
+ _onTreeSelected: function (resource) {
+ // Don't attempt to open a directory that is not the root element.
+ if (resource.isDir && resource.parent) {
+ return;
+ }
+ this.pluginDispatch("onTreeSelected", resource);
+ this.openResource(resource);
+ },
+
+ /**
+ * When a node is removed, destroy it and its associated editor.
+ *
+ * @param Resource resource
+ * The resource being removed
+ */
+ _onTreeResourceRemoved: function (resource) {
+ this.shells.removeResource(resource);
+ },
+
+ /**
+ * Create an xul element with options
+ *
+ * @param string type
+ * The tag name of the element to create.
+ * @param Object options
+ * "command": DOMNode or string ID of a command element.
+ * "parent": DOMNode or selector of parent to append child to.
+ * anything other keys are set as an attribute as the element.
+ * @returns DOMElement
+ * The element that has been created.
+ */
+ createElement: function (type, options) {
+ let elt = this.document.createElement(type);
+
+ let parent;
+
+ for (let opt in options) {
+ if (opt === "command") {
+ let command = typeof (options.command) === "string" ? options.command : options.command.id;
+ elt.setAttribute("command", command);
+ } else if (opt === "parent") {
+ continue;
+ } else {
+ elt.setAttribute(opt, options[opt]);
+ }
+ }
+
+ if (options.parent) {
+ let parent = options.parent;
+ if (typeof (parent) === "string") {
+ parent = this.document.querySelector(parent);
+ }
+ parent.appendChild(elt);
+ }
+
+ return elt;
+ },
+
+ /**
+ * Create a "menuitem" xul element with options
+ *
+ * @param Object options
+ * See createElement for available options.
+ * @returns DOMElement
+ * The menuitem that has been created.
+ */
+ createMenuItem: function (options) {
+ return this.createElement("menuitem", options);
+ },
+
+ /**
+ * Add a command to the projecteditor document.
+ * This method is meant to be used with plugins.
+ *
+ * @param Object definition
+ * key: a key/keycode string. Example: "f".
+ * id: Unique ID. Example: "find".
+ * modifiers: Key modifiers. Example: "accel".
+ * @returns DOMElement
+ * The command element that has been created.
+ */
+ addCommand: function (plugin, definition) {
+ this._pluginCommands.set(definition.id, plugin);
+ let document = this.projectEditorKeyset.ownerDocument;
+ let command = document.createElement("command");
+ command.setAttribute("id", definition.id);
+ if (definition.key) {
+ let key = document.createElement("key");
+ key.id = "key_" + definition.id;
+
+ let keyName = definition.key;
+ if (keyName.startsWith("VK_")) {
+ key.setAttribute("keycode", keyName);
+ } else {
+ key.setAttribute("key", keyName);
+ }
+ key.setAttribute("modifiers", definition.modifiers);
+ key.setAttribute("command", definition.id);
+ this.projectEditorKeyset.appendChild(key);
+ }
+ command.setAttribute("oncommand", "void(0);"); // needed. See bug 371900
+ this.projectEditorCommandset.appendChild(command);
+ return command;
+ },
+
+ /**
+ * Get the instance of a plugin registered with a certain type.
+ *
+ * @param Type pluginType
+ * The type, such as SavePlugin
+ * @returns Plugin
+ * The plugin instance matching the specified type.
+ */
+ getPlugin: function (pluginType) {
+ for (let plugin of this.plugins) {
+ if (plugin.constructor === pluginType) {
+ return plugin;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Get all plugin instances active for the current project
+ *
+ * @returns [Plugin]
+ */
+ get plugins() {
+ if (!this._plugins) {
+ console.log("plugins requested before _plugins was set");
+ return [];
+ }
+ // Could filter further based on the type of project selected,
+ // but no need right now.
+ return this._plugins;
+ },
+
+ /**
+ * Dispatch an onEditorCreated event, and listen for other events specific
+ * to this editor instance.
+ *
+ * @param Editor editor
+ * The new editor instance.
+ */
+ _onEditorCreated: function (editor) {
+ this.pluginDispatch("onEditorCreated", editor);
+ this._editorListenAndDispatch(editor, "change", "onEditorChange");
+ this._editorListenAndDispatch(editor, "cursorActivity", "onEditorCursorActivity");
+ this._editorListenAndDispatch(editor, "load", "onEditorLoad");
+ this._editorListenAndDispatch(editor, "saveRequested", "onEditorSaveRequested");
+ this._editorListenAndDispatch(editor, "save", "onEditorSave");
+
+ editor.on("focus", () => {
+ this.projectTree.selectResource(this.resourceFor(editor));
+ });
+ },
+
+ /**
+ * Dispatch an onEditorActivated event and finish setting up once the
+ * editor is ready to use.
+ *
+ * @param Editor editor
+ * The editor instance, which is now appended in the document.
+ * @param Resource resource
+ * The resource used by the editor
+ */
+ _onEditorActivated: function (editor, resource) {
+ editor.setToolbarVisibility();
+ this.pluginDispatch("onEditorActivated", editor, resource);
+ },
+
+ /**
+ * Dispatch an onEditorDactivated event once an editor loses focus
+ *
+ * @param Editor editor
+ * The editor instance, which is no longer active.
+ * @param Resource resource
+ * The resource used by the editor
+ */
+ _onEditorDeactivated: function (editor, resource) {
+ this.pluginDispatch("onEditorDeactivated", editor, resource);
+ },
+
+ /**
+ * Call a method on all plugins that implement the method.
+ * Also emits the same handler name on `this`.
+ *
+ * @param string handler
+ * Which function name to call on plugins.
+ * @param ...args args
+ * All remaining parameters are passed into the handler.
+ */
+ pluginDispatch: function (handler, ...args) {
+ emit(this, handler, ...args);
+ this.plugins.forEach(plugin => {
+ try {
+ if (handler in plugin) plugin[handler](...args);
+ } catch (ex) {
+ console.error(ex);
+ }
+ });
+ },
+
+ /**
+ * Listen to an event on the editor object and dispatch it
+ * to all plugins that implement the associated method
+ *
+ * @param Editor editor
+ * Which editor to listen to
+ * @param string event
+ * Which editor event to listen for
+ * @param string handler
+ * Which plugin method to call
+ */
+ _editorListenAndDispatch: function (editor, event, handler) {
+ editor.on(event, (...args) => {
+ this.pluginDispatch(handler, editor, this.resourceFor(editor), ...args);
+ });
+ },
+
+ /**
+ * Find a shell for a resource.
+ *
+ * @param Resource resource
+ * The file to be opened.
+ * @returns Shell
+ */
+ shellFor: function (resource) {
+ return this.shells.shellFor(resource);
+ },
+
+ /**
+ * Returns the Editor for a given resource.
+ *
+ * @param Resource resource
+ * The file to check.
+ * @returns Editor
+ * Instance of the editor for this file.
+ */
+ editorFor: function (resource) {
+ let shell = this.shellFor(resource);
+ return shell ? shell.editor : shell;
+ },
+
+ /**
+ * Returns a resource for the given editor
+ *
+ * @param Editor editor
+ * The editor to check
+ * @returns Resource
+ * The resource associated with this editor
+ */
+ resourceFor: function (editor) {
+ if (editor && editor.shell && editor.shell.resource) {
+ return editor.shell.resource;
+ }
+ return null;
+ },
+
+ /**
+ * Decide whether a given resource should be hidden in the tree.
+ *
+ * @param Resource resource
+ * The resource in the tree
+ * @returns Boolean
+ * True if the node should be visible, false if hidden.
+ */
+ resourceVisible: function (resource) {
+ return true;
+ },
+
+ /**
+ * Format the given node for display in the resource tree view.
+ *
+ * @param Resource resource
+ * The file to be opened.
+ * @param DOMNode elt
+ * The element in the tree to render into.
+ */
+ resourceFormatter: function (resource, elt) {
+ let editor = this.editorFor(resource);
+ let renderedByPlugin = false;
+
+ // Allow plugins to override default templating of resource in tree.
+ this.plugins.forEach(plugin => {
+ if (!plugin.onAnnotate) {
+ return;
+ }
+ if (plugin.onAnnotate(resource, editor, elt)) {
+ renderedByPlugin = true;
+ }
+ });
+
+ // If no plugin wants to handle it, just use a string from the resource.
+ if (!renderedByPlugin) {
+ elt.textContent = resource.displayName;
+ }
+ },
+
+ get sourcesVisible() {
+ return this.sourceToggle.classList.contains("pane-collapsed");
+ },
+
+ get currentShell() {
+ return this.shells.currentShell;
+ },
+
+ get currentEditor() {
+ return this.shells.currentEditor;
+ },
+
+ /**
+ * Whether or not menu items should be able to be enabled.
+ * Note that even if this is true, certain menu items will not be
+ * enabled until the correct state is achieved (for instance, the
+ * 'copy' menu item is only enabled when there is a selection).
+ * But if this is false, then nothing will be enabled.
+ */
+ set menuEnabled(val) {
+ this._menuEnabled = val;
+ if (this._loaded) {
+ this._updateMenuItems();
+ }
+ },
+
+ get menuEnabled() {
+ return this._menuEnabled;
+ },
+
+ /**
+ * Are there any unsaved resources in the Project?
+ */
+ get hasUnsavedResources() {
+ return this.project.allResources().some(resource=> {
+ let editor = this.editorFor(resource);
+ return editor && !editor.isClean();
+ });
+ },
+
+ /**
+ * Check with the user about navigating away with unsaved changes.
+ *
+ * @returns Boolean
+ * True if there are no unsaved changes
+ * Otherwise, ask the user to confirm and return the outcome.
+ */
+ confirmUnsaved: function () {
+ if (this.hasUnsavedResources) {
+ return confirm(
+ getLocalizedString("projecteditor.confirmUnsavedTitle"),
+ getLocalizedString("projecteditor.confirmUnsavedLabel2")
+ );
+ }
+
+ return true;
+ },
+
+ /**
+ * Save all the changes in source files.
+ *
+ * @returns Boolean
+ * True if there were resources to save.
+ */
+ saveAllFiles: Task.async(function* () {
+ if (this.hasUnsavedResources) {
+ for (let resource of this.project.allResources()) {
+ let editor = this.editorFor(resource);
+ if (editor && !editor.isClean()) {
+ yield editor.save(resource);
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ })
+
+});
+
+
+/**
+ * Returns a controller object that can be used for
+ * editor-specific commands such as find, jump to line,
+ * copy/paste, etc.
+ */
+function getCommandController(host) {
+ return {
+ supportsCommand: function (cmd) {
+ return host._pluginCommands.get(cmd);
+ },
+
+ isCommandEnabled: function (cmd) {
+ if (!host.menuEnabled) {
+ return false;
+ }
+ let plugin = host._pluginCommands.get(cmd);
+ if (plugin && plugin.isCommandEnabled) {
+ return plugin.isCommandEnabled(cmd);
+ }
+ return true;
+ },
+ doCommand: function (cmd) {
+ }
+ };
+}
+
+exports.ProjectEditor = ProjectEditor;