define(function(require, module, exports) { main.consumes = [ "Plugin", "tabManager", "menus", "settings", "layout", "ui", "commands", "anims" ]; main.provides = ["console"]; return main; function main(options, imports, register) { var Plugin = imports.Plugin; var ui = imports.ui; var tabs = imports.tabManager; var settings = imports.settings; var menus = imports.menus; var anims = imports.anims; var commands = imports.commands; var layout = imports.layout; var cssString = require("text!./style.css"); var markup = require("text!./console.xml"); /***** Initialization *****/ var plugin = new Plugin("Ajax.org", main.consumes); var emit = plugin.getEmitter(); var onFinishTimer, consoleRow, animating; var container, changed, maximized, maxHeight, lastZIndex, height; var hidden = true; var minHeight = 60; var collapsedHeight = 0; var loaded = false; function load(){ if (loaded) return false; loaded = true; // Commands commands.addCommand({ name: "toggleconsole", group: "Panels", bindKey: {mac: "Ctrl-Esc", win: "F6"}, exec: function(editor, args) { var el; if (hidden || args.show) { show(); el = container; } else { hide(); el = tabs.container; } var pane = tabs.findPane(container.$activePaneName); var tab = pane && pane.activeTab || tabs.getTabs(el)[0]; tabs.focusTab(tab); } }, plugin); // Menus menus.addItemByPath("View/Console", new apf.item({ type: "check", command: "toggleconsole", checked: "state/console/@expanded" }), 700, plugin); // Settings settings.on("read", function(e) { // Defaults settings.setDefaults("state/console", [ ["expanded", "true"], ["maximized", "false"], ["height", "153"] ]); // Height height = Math.max(minHeight, Math.min(layout.maxConsoleHeight, settings.getNumber("state/console/@height") || 0)); setTimeout(function() { // Expanded if (settings.getBool("state/console/@expanded")) show(true); // Maximized if (settings.getBool("state/console/@maximized")) maximizeConsoleHeight(); }, 0); changed = false; }, plugin); settings.on("write", function(e) { // if (!changed) // return; if (!container) return; if (drawn) { var state = getState(true); settings.setJson("state/console", state); } }, plugin); tabs.on("focus", function(e) { if (drawn && ui.isChildOf(container, e.tab.aml)) show(); }); if (apf.isGecko) { ui.setStyleRule(".console .btnsesssioncontainer .inside", "padding-right", "30px"); } // @todo fix Focus Handling in pane } var drawn = false; function draw(){ if (drawn) return; drawn = true; ui.insertCss(cssString, options.staticPrefix, plugin); consoleRow = layout.findParent(plugin); container = consoleRow.appendChild(new ui.bar({ zindex: 99, height: height, // minheight : 60, "class" : "console codeditorHolder" })); plugin.addElement(container); tabs.containers.push(container); if (settings.getBool("user/tabs/@asterisk")) ui.setStyleClass(container, "asterisk"); ui.insertMarkup(container, markup, plugin); // Track splitter and update state var splitter = consoleRow.$handle; splitter && splitter.on("dragdrop", function(e) { height = Math.max(minHeight, container.height); if (height) settings.set("state/console/@height", height); emit("resize"); }); plugin.getElement("btnClose").onclick = function(){ hide(); this.blur(); }; plugin.getElement("btnConsoleMax").onclick = function(){ if (this.value) maximizeConsoleHeight(); else restoreConsoleHeight(); }; // Create new tabs var state = settings.getJson("state/console"); if (!state) { state = options.defaultState || { type: "pane", nodes: [ { type: "tab", editorType: "terminal", active: "true" }, { type: "tab", editorType: "immediate", document: { title: "Immediate" } } ] }; } if (options.testing != 2) { setState(state, true, function(){}); emit.sticky("ready"); } tabs.on("paneCreate", function(e) { if (hidden && container && ui.isChildOf(container, e.pane.aml)) { e.pane._visible = false; } }); emit("draw"); } /***** Methods *****/ function getState(filter) { var state = tabs.getState(container.lastChild, filter); var tab = tabs.focus && tabs.findTab(state.focus); if (!tab || !ui.isChildOf(container, tab.pane.aml)) delete state.focus; return state; } function setState(state, init, callback) { state.container = container; return tabs.setState(state, init, callback); } function getTabs(){ return tabs.getTabs(container); } function getPanes(){ return tabs.getPanes(container); } function clear(){ var tabNodes = tabs.getPanes(container); for (var i = tabNodes.length - 1; i >= 0; i--) { var pane = tabNodes[i], nodes = pane.getTabs(); if (!ui.isChildOf(container, pane.aml)) continue; for (var j = nodes.length - 1; j >= 0; j--) { var tab = nodes[j]; tab.unload(); } pane.unload(); } } function maximizeConsoleHeight(){ if (maximized) return; maximized = true; apf.document.documentElement.appendChild(container); var top = layout.getElement("root").getTop() + 1; container.setAttribute('anchors', top + ' 0 0 0'); lastZIndex = container.$ext.style.zIndex; container.removeAttribute('height'); container.$ext.style.maxHeight = "10000px"; container.$ext.style.zIndex = 100000; settings.set("state/console/@maximized", true); plugin.getElement("btnConsoleMax").setValue(true); setTimeout(function(){ getPanes().forEach(function(pane) { var tab = pane.activeTab; if (tab) tab.editor.resize(); }); }); emit("resize"); } function restoreConsoleHeight(){ if (!maximized) return; maximized = false; layout.findParent(plugin).appendChild(container); container.removeAttribute('anchors'); maxHeight = window.innerHeight - 70; // container.$ext.style.maxHeight = maxHeight + "px"; container.setAttribute('height', maxHeight && height > maxHeight ? maxHeight : height); container.$ext.style.zIndex = lastZIndex; settings.set("state/console/@maximized", false); plugin.getElement("btnConsoleMax").setValue(false); getPanes().forEach(function(pane) { var tab = pane.activeTab; if (tab) tab.editor.resize(); }); emit("resize"); } function hide(immediate) { show(immediate, true); } function show(immediate, shouldHide) { if (!shouldHide) draw(); if (hidden == shouldHide || animating) return; hidden = shouldHide; animating = true; maxHeight = window.innerHeight - 70; getPanes().forEach(function(pane) { pane._visible = !shouldHide; }); if (!shouldHide && !tabs.focussedTab) { getPanes().some(function(pane) { if (pane.getTab()) { tabs.focusTab(pane.getTab()); return true; } }); } var finish = function() { if (onFinishTimer) clearTimeout(onFinishTimer); onFinishTimer = setTimeout(function(){ if (shouldHide) { container.hide(); // If the focussed tab is in the console, make the first // tab we can find inside the tabs the focussed tab. if (tabs.focussedTab && getPanes().indexOf(tabs.focussedTab.pane) > -1) { tabs.getPanes(tabs.container).every(function(pane) { var tab = pane.getTab(); if (!tab) { tabs.focusTab(); // blur return true; } else { tabs.focusTab(tab); return false; } }); } } else { container.$ext.style.minHeight = minHeight + "px"; container.minheight = minHeight; maxHeight = window.innerHeight - 70; // container.$ext.style.maxHeight = maxHeight + "px"; } container.height = height + 1; container.setAttribute("height", height); container.$ext.style[apf.CSSPREFIX + "TransitionDuration"] = ""; animating = false; settings.set("state/console/@expanded", !shouldHide); apf.layout.forceResize(); emit("resize"); }, 100); }; var toHeight; var animOn = settings.getBool("user/general/@animateui"); if (!shouldHide) { toHeight = Math.max(minHeight, Math.min(maxHeight, height)); container.$ext.style.minHeight = 0; container.setHeight(collapsedHeight); //container.$ext.style.height = collapsedHeight + "px"; container.show(); anims.animateSplitBoxNode(container, { height: toHeight + "px", immediate: immediate || !animOn, duration: 0.2, timingFunction: "cubic-bezier(.30, .08, 0, 1)" }, finish); } else { toHeight = collapsedHeight; if (container.parentNode != consoleRow) restoreConsoleHeight(); container.$ext.style.minHeight = 0; container.$ext.style.maxHeight = "10000px"; anims.animateSplitBoxNode(container, { height: toHeight + "px", immediate: immediate || !animOn, duration: 0.2, timingFunction: "ease-in-out" }, finish); } } function openFile(path, active, callback) { if (typeof active == "function") callback = active, active = false; open({path: path, active: active}, callback); } function openEditor(type, active, callback) { if (typeof active == "function") callback = active, active = false; open({editorType: type, active: active}, callback); } function open(options, callback) { if (!options.pane) { draw(); options.pane = container.getElementsByTagNameNS(apf.ns.aml, "tab")[0].cloud9pane; } return tabs.open(options, callback); } /***** Lifecycle *****/ plugin.on("load", function(){ load(); }); plugin.on("enable", function(){ }); plugin.on("disable", function(){ }); plugin.on("unload", function(){ tabs.containers.remove(container); clear(); loaded = false; }); /***** Register and define API *****/ /** * A collapsable panel in the bottom of Cloud9's UI that contains panes, * tabs and editors. * * To open new tabs in the console you can use: * * console.open({ * path : "/file.txt", * active : true * }, function(){}); * * This is equivalent to using the {@link tabManager} to open new tabs: * * tabManager.open({ * path : "/file.txt", * active : true, * pane : console.getPanes()[0] * }, function(){}); * * @singleton */ plugin.freezePublicAPI({ /** * The AMLElement for the main panes area. * @property {AMLElement} container * @readonly */ get container(){ return container; }, _events: [ /** * Fires after the console is drawn. * @event draw */ "draw", /** * Fires when the console is resized * @event resize **/ "resize", /** * Fires when the console is ready for adding new tabs */ "ready" ], /** * Returns an array containing all the tabs in the console. */ getTabs: getTabs, /** * Returns an array containing all the panes in the console. */ getPanes: getPanes, /** * Retrieves the state of all panes, tabs and documents * as a single serializable object. * @returns {Object} */ getState: getState, /** * Loads the state of all panes panes, tabs anddocuments from a * simple object. * @param {Object} state The state describing the pane layout. * @param {Boolean} [init] When set to true, optimizes the state * loading for initialization of Cloud9. * @param {Function} callback Called when the state loading is completed. */ setState: setState, /** * Removes all panes, except one, and destroys all tabs, documents * and editors. * * @param {Boolean} [soft=false] When set to true clear * will not unload tabs. This can be useful when loading a new * state with exactly the same tabs. WARNING: this can lead to * leaking tabs/documents, etc. Use with caution! getTabs() will * continue to return all the tabs. Even though they are no longer * attached to a pane. */ clear: clear, /** * Expands the console. */ show: show, /** * Collapses the console. */ hide: hide, /** * Opens a new tab in the console. If the tab with a specified * path already exists, that tab is activated. If state is given * for a document, then that state is set prior to * loading the tab. If a path is specified the file contents is * loaded into the document. If no editorType is specified, the * editor is determined based on the extension of the filename. * * See also {@link tabManager#method-open} * * @param options * @param {String} [options.path] The path of the file to open * @param {Pane} [options.pane] The pane to attach the new tab to * @param {String} [options.editorType] The type of the editor for this tab * @param {Boolean} [options.active=false] Whether this tab is set as active * @param {Boolean} [options.demandExisting=false] Whether to try opening an * existing tab even for tabs without a path. * @param {Object} [options.document] Object describing the * state of the document (see {@link Document#method-getState}) * @param {String} [options.value] The contents of the file * @param {String} [options.title] The title of the tab * @param {String} [options.tooltip] The tooltip at the button of the tab * @param {Function} callback * @param {Error} callback.err An error that might * occur during the load of the file contents. * @param {Tab} callback.tab The created tab. * @param {Function} callback.done Call this function * when done retrieving the value. This is only relevant if * -1 is passed to `value`. You are responsible for settings the * document value yourself, like so: `tab.document.value = "value";` * @returns {Tab} The created tab. * @fires open * @fires beforeOpen */ open: open, /** * Opens a new pane tab in the console with the default editor and loads the file * contents into the document. This is a convenience method. For * the full method description see {@link tabManager#method-open}. * * @param {String} path The path of the file to open. * @param {Boolean} [active] When set to true the new tab will become active in it's pane. * @param {Function} callback Called when the file contents is loaded. * @param {Error} callback.err An error that might * occur during the load of the file contents. * @param {Tab} callback.tab The created tab. * @returns {Tab} The created tab. */ openFile: openFile, /** * Opens a new tab in the console with a specific editor * @param {String} editorType The type of the editor for this tab. * @param {Boolean} [active] When set to true the new tab will become active in it's pane. * @param {Function} callback Called when the editor is loaded. * @param {Error} callback.err An error that might * occur during the load of the editor. * @param {Tab} callback.tab The created tab. * @returns {Tab} The created tab. */ openEditor: openEditor }); register(null, { console: plugin }); } });