/*global apf*/ define(function(require, exports, module) { main.consumes = [ "Plugin", "settings", "menus", "preferences", "commands", "tabManager", "ui", "save", "panels", "tree", "Menu", "fs", "dialog.question", "clipboard" ]; main.provides = ["tabbehavior"]; return main; function main(options, imports, register) { var Plugin = imports.Plugin; var settings = imports.settings; var tabs = imports.tabManager; var menus = imports.menus; var Menu = imports.Menu; var commands = imports.commands; var clipboard = imports.clipboard; var tree = imports.tree; var save = imports.save; var panels = imports.panels; var ui = imports.ui; var fs = imports.fs; var prefs = imports.preferences; var question = imports["dialog.question"].show; /***** Initialization *****/ var plugin = new Plugin("Ajax.org", main.consumes); // var emit = plugin.getEmitter(); var mnuContext, mnuEditors, mnuTabs; var menuItems = [], menuClosedItems = []; var paneList = []; var accessedPane = 0; var cycleKeyPressed, changedTabs, unchangedTabs, dirtyNextTab, dirtyNextPane; var ACTIVEPAGE = function() { return tabs.focussedTab; }; var ACTIVEPATH = function() { var tab = mnuContext.$tab || tabs.focussedTab; return tab && (tab.path || tab.relatedPath || tab.editor.getPathAsync); }; var MORETABS = function() { return tabs.getTabs().length > 1; }; var MORETABSINPANE = function() { return tabs.focussedTab && tabs.focussedTab.pane.getTabs().length > 1; }; var MOREPANES = function() { return tabs.getPanes().length > 1; }; var movekey = "Command-Option-Shift"; var definition = [ ["clonetab", "", "", ACTIVEPAGE, "create a new tab with a view on the same file"], ["closetab", "Option-W", "Alt-W", ACTIVEPAGE, "close the tab that is currently active"], ["closealltabs", "Option-Shift-W", "Alt-Shift-W", ACTIVEPAGE, "close all opened tabs"], ["closeallbutme", "Option-Ctrl-W", "Ctrl-Alt-W", MORETABS, "close all opened tabs, except the tab that is currently active"], ["gototabright", "Command-]", "Ctrl-]", MORETABSINPANE, "navigate to the next tab, right to the tab that is currently active"], ["gototableft", "Command-[", "Ctrl-[", MORETABSINPANE, "navigate to the next tab, left to the tab that is currently active"], ["movetabright", movekey + "-Right", "Ctrl-Meta-Right", MORETABS, "move the tab that is currently active to the right. Will create a split tab to the right if it's the right most tab."], ["movetableft", movekey + "-Left", "Ctrl-Meta-Left", MORETABS, "move the tab that is currently active to the left. Will create a split tab to the left if it's the left most tab."], ["movetabup", movekey + "-Up", "Ctrl-Meta-Up", MORETABS, "move the tab that is currently active to the up. Will create a split tab to the top if it's the top most tab."], ["movetabdown", movekey + "-Down", "Ctrl-Meta-Down", MORETABS, "move the tab that is currently active to the down. Will create a split tab to the bottom if it's the bottom most tab."], ["tab1", "Command-1", "Ctrl-1", null, "navigate to the first tab"], ["tab2", "Command-2", "Ctrl-2", null, "navigate to the second tab"], ["tab3", "Command-3", "Ctrl-3", null, "navigate to the third tab"], ["tab4", "Command-4", "Ctrl-4", null, "navigate to the fourth tab"], ["tab5", "Command-5", "Ctrl-5", null, "navigate to the fifth tab"], ["tab6", "Command-6", "Ctrl-6", null, "navigate to the sixth tab"], ["tab7", "Command-7", "Ctrl-7", null, "navigate to the seventh tab"], ["tab8", "Command-8", "Ctrl-8", null, "navigate to the eighth tab"], ["tab9", "Command-9", "Ctrl-9", null, "navigate to the ninth tab"], ["tab0", "Command-0", "Ctrl-0", null, "navigate to the tenth tab"], ["revealtab", "Command-Shift-L", "Ctrl-Shift-L", ACTIVEPATH, "reveal current tab in the file tree"], ["nexttab", "Option-Tab", "Ctrl-Tab|Alt-`", MORETABSINPANE, "navigate to the next tab in the stack of accessed tabs"], ["previoustab", "Option-Shift-Tab", "Ctrl-Shift-Tab|Alt-Shift-`", MORETABSINPANE, "navigate to the previous tab in the stack of accessed tabs"], ["nextpane", "Option-ESC", "Ctrl-`", MOREPANES, "navigate to the next tab in the stack of panes"], ["previouspane", "Option-Shift-ESC", "Ctrl-Shift-`", MOREPANES, "navigate to the previous tab in the stack of panes"], ["gotopaneright", "Ctrl-Meta-Right", "Ctrl-Meta-Right", null, "navigate to the pane on the right"], ["gotopaneleft", "Ctrl-Meta-Left", "Ctrl-Meta-Left", null, "navigate to the pane on the left"], ["gotopaneup", "Ctrl-Meta-Up", "Ctrl-Meta-Up", null, "navigate to the pane on the top"], ["gotopanedown", "Ctrl-Meta-Down", "Ctrl-Meta-Down", null, "navigate to the pane on the bottom"], ["reopenLastTab", "Option-Shift-T", "Alt-Shift-T", function() { return menuClosedItems.length; }, "reopen last closed tab"], ["closealltotheright", "", "", function() { var tab = mnuContext.$tab || mnuContext.$pane && mnuContext.$pane.getTab(); if (tab) { var pages = tab.pane.getTabs(); return pages.pop() != tab; } }, "close all tabs to the right of the focussed tab"], ["closealltotheleft", "", "", function() { var tab = mnuContext.$tab || mnuContext.$pane && mnuContext.$pane.getTab(); if (tab) { var pages = tab.pane.getTabs(); return pages.length > 1 && pages[0] != tab; } }, "close all tabs to the left of the focussed tab"], ["closepane", "Command-Ctrl-W", "Ctrl-W", function() { return mnuContext.$tab || mnuContext.$pane || tabs.getTabs().length; }, "close this pane"], ["nosplit", "", "", null, "no split"], ["hsplit", "", "", null, "split the current pane in two columns and move the active tab to it"], ["vsplit", "", "", null, "split the current pane in two rows and move the active tab to it"], ["twovsplit", "", "", null, "create a two pane row layout"], ["twohsplit", "", "", null, "create a two pane column layout"], ["foursplit", "", "", null, "create a four pane layout"], ["threeleft", "", "", null, "create a three pane layout with the stack on the left side"], ["threeright", "", "", null, "create a three pane layout with the stack on the right side"] ]; var loaded = false; function load() { if (loaded) return false; loaded = true; // Settings settings.on("read", function(e) { settings.setDefaults("user/general", [["revealfile", false]]); var list = settings.getJson("state/panecycle"); if (list) { list.remove(null); paneList = list; } }, plugin); settings.on("write", function(e) { var list; if (paneList.changed) { list = []; paneList.forEach(function(tab, i) { if (tab && tab.name) list.push(tab.name); }); settings.setJson("state/panecycle", list); paneList.changed = false; } }, plugin); // Preferences prefs.add({ "General": { "Tree & Navigate": { "Reveal Active File in Project Tree": { type: "checkbox", position: 4000, path: "user/general/@revealfile" } } } }, plugin); // Commands definition.forEach(function(item) { commands.addCommand({ name: item[0], bindKey: { mac: item[1], win: item[2] }, group: "Tabs", hint: item[4], isAvailable: item[3], exec: function(editor, arg) { if (arg && !arg[0] && arg.source == "click") arg = [mnuContext.$tab, mnuContext.$pane]; plugin[item[0]].apply(plugin, arg); } }, plugin); }); commands.addCommand({ name: "refocusTab", bindKey: { mac: "Esc", win: "Esc", position: -1000 }, group: "Tabs", isAvailable: function() { var el = apf.activeElement; if (el && (el.tagName == "page" || el.tagName == "menu")) return false; return !!tabs.focussedTab; }, exec: function(e) { if (tabs.focussedTab) tabs.focusTab(tabs.focussedTab); }, passEvent: true }, plugin); commands.addCommand({ name: "copyFilePath", group: "", isAvailable: function() { var el = apf.popup.getCurrentElement(); if (el && el.visible) { if (el.$tab) return !!(el.$tab.path || el.$tab.relatedPath || el.$tab.editor.getPathAsync); } return true; }, exec: function(editor, args) { var text = ""; var el = apf.popup.getCurrentElement(); var fromContextMenu = args && args.source == "click"; var tab; if (!fromContextMenu || !el) { tab = tabs.focussedTab; text = tab.path || tab.relatedPath; } else if (el.name == "mnuCtxTree") { text = tree.selectedNodes.map(function(n) { return n.path; }).join("\n"); } else if (el.$tab) { tab = el.$tab; text = tab.path || tab.relatedPath; } if (text) { clipboard.clipboardData.setData("text/plain", text); } else if (tab && tab.editor.getPathAsync) { tab.editor.getPathAsync(function(err, text) { if (!err && text) clipboard.clipboardData.setData("text/plain", text); }); } } }, plugin); // General Menus menus.addItemByPath("File/~", new ui.divider(), 100000, plugin); menus.addItemByPath("File/Close File", new ui.item({ command: "closetab" }), 110000, plugin); menus.addItemByPath("File/Close All Files", new ui.item({ command: "closealltabs" }), 120000, plugin); mnuTabs = new ui.menu(); menus.addItemByPath("Window/Tabs", mnuTabs, 10100, plugin); menus.addItemByPath("Window/Tabs/Close Pane", new ui.item({ command: "closepane" }), 100, plugin); menus.addItemByPath("Window/Tabs/Close All Tabs In All Panes", new ui.item({ command: "closealltabs" }), 200, plugin); menus.addItemByPath("Window/Tabs/Close All But Current Tab", new ui.item({ command: "closeallbutme" }), 300, plugin); menus.addItemByPath("Window/Tabs/~", new ui.divider(), 1000000, plugin); menus.addItemByPath("Window/Tabs/Split Pane in Two Rows", new ui.item({ command: "vsplit" }), 1000100, plugin); menus.addItemByPath("Window/Tabs/Split Pane in Two Columns", new ui.item({ command: "hsplit" }), 1000200, plugin); menus.addItemByPath("Window/Tabs/~", new ui.divider(), 1000300, plugin); menus.addItemByPath("Window/Tabs/~", new apf.label({ class: "splits", caption: [ ["span", { class: "nosplit" }], ["span", { class: "twovsplit" }], ["span", { class: "twohsplit" }], ["span", { class: "foursplit" }], ["span", { class: "threeleft" }], ["span", { class: "threeright" }], ], onclick: function(e) { var span = e.htmlEvent.target; if (!span || span.tagName != "SPAN") return; plugin[span.className](); mnuTabs.hide(); } }), 1000400, plugin); menus.addItemByPath("Window/~", new ui.divider(), 9000, plugin); menus.addItemByPath("Window/Navigation/", null, 9100, plugin); menus.addItemByPath("Window/Navigation/Tab to the Right", new ui.item({ command: "gototabright" }), 100, plugin); menus.addItemByPath("Window/Navigation/Tab to the Left", new ui.item({ command: "gototableft" }), 200, plugin); menus.addItemByPath("Window/Navigation/Next Tab in History", new ui.item({ command: "nexttab" }), 300, plugin); menus.addItemByPath("Window/Navigation/Previous Tab in History", new ui.item({ command: "previoustab" }), 400, plugin); menus.addItemByPath("Window/Navigation/~", new ui.divider(), 500, plugin); menus.addItemByPath("Window/Navigation/Move Tab to Right", new ui.item({ command: "movetabright" }), 600, plugin); menus.addItemByPath("Window/Navigation/Move Tab to Left", new ui.item({ command: "movetableft" }), 700, plugin); menus.addItemByPath("Window/Navigation/Move Tab to Up", new ui.item({ command: "movetabup" }), 800, plugin); menus.addItemByPath("Window/Navigation/Move Tab to Down", new ui.item({ command: "movetabdown" }), 900, plugin); menus.addItemByPath("Window/Navigation/~", new ui.divider(), 1000, plugin); menus.addItemByPath("Window/Navigation/Go to Pane to Right", new ui.item({ command: "gotopaneright" }), 1100, plugin); menus.addItemByPath("Window/Navigation/Go to Pane to Left", new ui.item({ command: "gotopaneleft" }), 1200, plugin); menus.addItemByPath("Window/Navigation/Go to Pane to Up", new ui.item({ command: "gotopaneup" }), 1300, plugin); menus.addItemByPath("Window/Navigation/Go to Pane to Down", new ui.item({ command: "gotopanedown" }), 1400, plugin); menus.addItemByPath("Window/Navigation/~", new ui.divider(), 1500, plugin); menus.addItemByPath("Window/Navigation/Next Pane in History", new ui.item({ command: "nextpane" }), 1600, plugin); menus.addItemByPath("Window/Navigation/Previous Pane in History", new ui.item({ command: "previouspane" }), 1700, plugin); // Tab Helper Menu menus.addItemByPath("Window/~", new ui.divider(), 10000, plugin); mnuTabs.addEventListener("prop.visible", function(e) { if (e.value) { if (mnuTabs.opener && mnuTabs.opener.parentNode.localName == "tab") { mnuContext.$pane = mnuTabs.opener.parentNode.cloud9pane; mnuContext.$tab = mnuContext.$pane.getTab(); } updateTabMenu(); } else { removeContextInfo(e); } if (mnuTabs.opener && mnuTabs.opener["class"] == "tabmenubtn") apf.setStyleClass(mnuTabs.$ext, "tabsContextMenu"); else apf.setStyleClass(mnuTabs.$ext, "", ["tabsContextMenu"]); }, true); // Tree Context Menu menus.addItemByPath("context/tree/Copy file path", new ui.item({ command: "copyFilePath" }), 800, plugin); menus.addItemByPath("context/tree/~", new ui.divider({}), 850, menus); // Tab Context Menu mnuContext = new Menu({ id: "mnuContext" }, plugin).aml; menus.addItemByPath("context/tabs/", mnuContext, 0, plugin); function removeContextInfo(e) { if (!e.value) { // use setTimeout because apf closes menu before menuitem onclick event setTimeout(function() { mnuContext.$tab = null; mnuContext.$pane = null; }); } } mnuContext.on("prop.visible", removeContextInfo, false); menus.addItemByPath("Reveal in File Tree", new ui.item({ command: "revealtab" }), 100, mnuContext, plugin); menus.addItemByPath("~", new ui.divider(), 200, mnuContext, plugin); menus.addItemByPath("Copy file path", new ui.item({ command: "copyFilePath" }), 230, mnuContext, plugin); menus.addItemByPath("~", new ui.divider(), 260, mnuContext, plugin); menus.addItemByPath("Close Tab", new ui.item({ command: "closetab" }), 300, mnuContext, plugin); menus.addItemByPath("Close All Tabs", new ui.item({ command: "closepane" }), 450, mnuContext, plugin); menus.addItemByPath("Close Other Tabs", new ui.item({ command: "closeallbutme" }), 500, mnuContext, plugin); menus.addItemByPath("Close Tabs to the Left", new ui.item({ command: "closealltotheleft" }), 600, mnuContext, plugin); menus.addItemByPath("Close Tabs to the Right", new ui.item({ command: "closealltotheright" }), 700, mnuContext, plugin); menus.addItemByPath("~", new ui.divider(), 750, mnuContext, plugin); menus.addItemByPath("Split Pane in Two Rows", new ui.item({ command: "vsplit" }), 800, mnuContext, plugin); menus.addItemByPath("Split Pane in Two Columns", new ui.item({ command: "hsplit" }), 900, mnuContext, plugin); menus.addItemByPath("~", new ui.divider(), 1000, mnuContext, plugin); menus.addItemByPath("Duplicate View", new ui.item({ command: "clonetab" }), 1010, mnuContext, plugin); menus.addItemByPath("View/~", new ui.divider(), 800, plugin); menus.addItemByPath("View/Layout/", null, 900, plugin); menus.addItemByPath("View/Layout/Single", new ui.item({ command: "nosplit" }), 100, mnuContext, plugin); menus.addItemByPath("View/Layout/Vertical Split", new ui.item({ command: "twovsplit" }), 100, mnuContext, plugin); menus.addItemByPath("View/Layout/Horizontal Split", new ui.item({ command: "twohsplit" }), 200, mnuContext, plugin); menus.addItemByPath("View/Layout/Cross Split", new ui.item({ command: "foursplit" }), 300, mnuContext, plugin); menus.addItemByPath("View/Layout/Split 1:2", new ui.item({ command: "threeright" }), 400, mnuContext, plugin); menus.addItemByPath("View/Layout/Split 2:1", new ui.item({ command: "threeleft" }), 500, mnuContext, plugin); mnuEditors = tabs.getElement("mnuEditors"); var div, label; div = menus.addItemToMenu(mnuEditors, new ui.divider(), 1000000, plugin); label = menus.addItemToMenu(mnuEditors, new ui.item({ caption: "Recently Closed Tabs", disabled: true }), 1000001, plugin); menuClosedItems.hide = function() { div.hide(); label.hide(); }; menuClosedItems.show = function() { div.show(); label.show(); }; menuClosedItems.hide(); // Other Hooks tabs.on("paneCreate", function(e) { var pane = e.pane.aml; pane.on("contextmenu", function(e) { if (e.currentTarget) { mnuContext.$tab = e.currentTarget.tagName == "page" ? e.currentTarget.cloud9tab : null; mnuContext.$pane = (mnuContext.$tab || 0).pane || e.currentTarget.cloud9pane; } if (ui.isChildOf(pane.$buttons, e.htmlEvent.target, true)) { mnuContext.display(e.x, e.y); return false; } }); var meta = e.pane.meta; if (!meta.accessList) meta.accessList = []; if (!meta.accessList.toJson) meta.accessList.toJson = accessListToJson; }, plugin); //@todo store the stack for availability after reload tabs.on("tabBeforeClose", function(e) { var tab = e.tab; var event = e.htmlEvent || {}; // Shift = close all if (event.shiftKey) { closealltabs(); return false; } // Alt/ Option = close all but this else if (event.altKey) { closeallbutme(tab); return false; } }, plugin); tabs.on("tabAfterClose", function(e) { // Hack to force focus on the right pane var accessList = e.tab.pane.meta.accessList; if (tabs.focussedTab == e.tab && accessList[1]) e.tab.pane.aml.nextTabInLine = accessList[1].aml; }, plugin); tabs.on("tabBeforeReparent", function(e) { // Move to new access list var lastList = e.lastPane.meta.accessList; var accessList = e.tab.pane.meta.accessList; lastList.splice(lastList.indexOf(e.tab), 1); if (e.tab == tabs.focussedTab) accessList.unshift(e.tab); else accessList.push(e.tab); // Hack to force focus on the right pane if (tabs.focussedTab == e.tab && lastList[0]) e.lastPane.aml.nextTabInLine = lastList[0].aml; }, plugin); tabs.on("tabAfterClose", function(e) { var tab = e.tab; if (tab.document.meta.preview) return; addTabToClosedMenu(tab); tab.pane.meta.accessList.remove(tab); paneList.remove(tab); }, plugin); tabs.on("tabCreate", function(e) { var tab = e.tab; if (tab.title) { // @todo candidate for optimization using a hash var path = tab.path || tab.editorType; for (var i = menuClosedItems.length - 1; i >= 0; i--) { if (menuClosedItems[i].path == path) { menuClosedItems.splice(i, 1)[0].destroy(true, true); if (!menuClosedItems.length) menuClosedItems.hide(); } } } if (tab.document.meta.preview) return; var accessList = tab.pane.meta.accessList; var idx; if (accessList.indexOf(tab) == -1) { idx = accessList.indexOf(tab.name); if (idx == -1) { //Load accesslist from index if (tab == tabs.focussedTab) accessList.unshift(tab); else accessList.push(tab); //splice(1, 0, tab); } else accessList[idx] = tab; } if (paneList.indexOf(tab) == -1) { idx = paneList.indexOf(tab.name); if (idx == -1) { //Load paneList from index if (tab.isActive()) addToPaneList(tab); } else paneList[idx] = tab; } }, plugin); tabs.on("focusSync", function(e) { var tab = e.tab; if (!tab.loaded) return; if (!cycleKeyPressed) { var accessList = tab.pane.meta.accessList; accessList.remove(tab); accessList.unshift(tab); accessList.changed = true; addToPaneList(tab, true); paneList.changed = true; settings.save(); } // @todo panel switch if (tree.area && tree.area.activePanel == "tree" && settings.getBool('user/general/@revealfile')) { revealtab(tab, true); } }, plugin); tabs.on("tabAfterActivate", function(e) { var tab = e.tab; if (tab == tabs.focussedTab || !tab.loaded) return; if (!cycleKeyPressed) { var accessList = tab.pane.meta.accessList; accessList.remove(tab); accessList.splice(1, 0, tab); accessList.changed = true; addToPaneList(tab, 2); paneList.changed = true; settings.save(); } }, plugin); apf.addEventListener("keydown", function(eInfo) { if (eInfo.keyCode == 17 || eInfo.keyCode == 18) { cycleKeyPressed = true; } }); apf.addEventListener("keyup", function(eInfo) { if (eInfo.keyCode == 17 || eInfo.keyCode == 18) { cycleKeyPressed = false; if (dirtyNextTab) { var tab = tabs.focussedTab; var accessList = tab.pane.meta.accessList; if (accessList[0] != tab) { accessList.remove(tab); accessList.unshift(tab); accessList.changed = true; settings.save(); } dirtyNextTab = false; } if (dirtyNextPane) { accessedPane = 0; var tab = tabs.focussedTab; if (paneList[accessedPane] != tab && tab) { paneList.remove(tab); paneList.unshift(tab); paneList.changed = true; settings.save(); } dirtyNextPane = false; } } }); // tabs.addEventListener("aftersavedialogcancel", function(e) { // if (!changedTabs) // return; // var i, l, tab; // for (i = 0, l = changedTabs.length; i < l; i++) { // tab = changedTabs[i]; // tab.removeEventListener("aftersavedialogclosed", arguments.callee); // } // }); createLayoutMenus(); } /***** Methods *****/ function addToPaneList(tab, first) { var pane = tab.pane, found; paneList.every(function(tab) { if (tab && tab.pane && tab.pane == pane) { found = tab; return false; } return true; }); if (found) paneList.remove(found); if (first == 2) paneList.splice(1, 0, tab); else if (first) paneList.unshift(tab); else paneList.push(tab); } function accessListToJson() { var list = []; this.forEach(function(tab, i) { if (tab && tab.name) list.push(tab.name); }); return list; } function clonetab(tab) { if (!tab) tab = mnuContext.$tab || tabs.focussedTab; var pane; tabs.getTabs().every(function(tab) { if (tab.document.meta.cloned) { pane = tab.pane; return false; } return true; }); if (!pane || pane == tab.pane) pane = tab.pane.hsplit(true); tabs.clone(tab, pane, function(err, tab) { }); } function closetab(tab) { if (!tab) tab = mnuContext.$tab || tabs.focussedTab; var pages = tabs.getTabs(); var isLast = pages[pages.length - 1] == tab; tab.close(); tabs.resizePanes(isLast); return false; } function closealltabs(callback) { callback = typeof callback == "function" ? callback : null; changedTabs = []; unchangedTabs = []; var pages = tabs.getTabs(); for (var i = 0, l = pages.length; i < l; i++) { closepage(pages[i], callback); } checkTabRender(callback); } function closeallbutme(me, pages, callback) { if (!me) { me = mnuContext.$tab || tabs.focussedTab; } changedTabs = []; unchangedTabs = []; if (!pages || !(pages instanceof Array)) { var container = me && me.aml && me.aml.parentNode || tabs.container; pages = tabs.getTabs(container); } var tab; for (var i = 0, l = pages.length; i < l; i++) { tab = pages[i]; if (tab !== me) closepage(tab, callback); } tabs.resizePanes(); checkTabRender(callback); } function closepage(tab, callback) { var doc = tab.document; if (doc.changed && (!doc.meta.newfile || doc.value)) changedTabs.push(tab); else unchangedTabs.push(tab); } function checkTabRender(callback) { save.saveAllInteractive(changedTabs, function(result) { if (result != save.CANCEL) { changedTabs.forEach(function(tab) { tab.unload(); }); closeUnchangedTabs(done); } else done(); }); function done() { if (callback) callback(); // todo dialog calls this twice when selecting no with changed tab setTimeout(function() { changedTabs = []; unchangedTabs = []; }); } } function closeUnchangedTabs(callback) { var tab; for (var i = 0, l = unchangedTabs.length; i < l; i++) { tab = unchangedTabs[i]; tab.close(true); } if (callback) callback(); } function closealltotheright(tab) { if (!tab) tab = mnuContext.$tab || tabs.focussedTab; var pages = tab.pane.getTabs(); var currIdx = pages.indexOf(tab); closeallbutme(tab, pages.slice(currIdx)); } function closealltotheleft(tab) { if (!tab) tab = mnuContext.$tab || tabs.focussedTab; var pages = tab.pane.getTabs(); var currIdx = pages.indexOf(tab); closeallbutme(tab, pages.slice(0, currIdx)); } function nexttab(dir, keepOrder) { if (tabs.getTabs().length === 1) return; var tab = tabs.focussedTab; var accessList = tab.pane.meta.accessList; var index = accessList.indexOf(tab); index += dir || 1; if (index >= accessList.length) index = 0; else if (index < 0) index = accessList.length - 1; var next = accessList[index]; if (typeof next != "object" || !next.pane.visible) return nexttab(dir, keepOrder); if (keepOrder && cycleKeyPressed == false) { cycleKeyPressed = true; tabs.focusTab(next, null, true); cycleKeyPressed = false; } else { tabs.focusTab(next, null, true); } dirtyNextTab = !keepOrder; } function previoustab(dir, keepOrder) { nexttab(dir || -1, keepOrder) } function nextpane() { return $nextPane(1); } function previouspane() { return $nextPane(-1); } function $nextPane(dir) { if (tabs.getPanes().length === 1) return; var l = paneList.length; for (var i = 1; i <= l; i++) { var index = (accessedPane + dir * i) % l; var next = paneList[index]; if (typeof next != "object" || !next.pane.visible) continue; if (next.pane.activeTab == tabs.focussedTab) { console.error("error in panelist"); continue; } accessedPane = index; tabs.focusTab(next.pane.activeTab, null, true); dirtyNextPane = true; return next.pane; } } function gotopaneleft() { return $goToPane("left"); } function gotopaneright() { return $goToPane("right"); } function gotopanedown() { return $goToPane("down"); } function gotopaneup() { return $goToPane("up"); } function $goToPane(direction) { var newPane = findPaneToGoTo(direction); if (!newPane) return; var activeTab = newPane.activeTab; tabs.focusTab(activeTab); } function getPaneDimensions(pane) { var element = pane.container; var size = getElementSize(element); var dimensions = { x: getElementOffset(element, "Left"), y: getElementOffset(element, "Top"), width: size.width, height: size.height }; return dimensions; } function getElementOffset(element, type) { var offset = 0; do { if (!isNaN(element['offset' + type])) { offset += element['offset' + type]; } } while (element = element.offsetParent); return offset; } function getElementSize(element) { var computedStyle = window.getComputedStyle(element); return { width: parseInt(computedStyle.width, 10), height: parseInt(computedStyle.height, 10), }; } /** For each direction * Exclude all panes not in the direction of this one * Exclude all panes that don't intersect on the other axis * Choose the closest pane * In case of tie choose the pane to the furthest left or top. **/ function findBoxToGoTo(boxes, currentBox, direction) { var possibleBoxes = []; switch (direction) { case "left": possibleBoxes = boxes .filter(function (box) { return box.x < currentBox.x; }) .filter(areBoxesInLineVertically.bind(null, currentBox)); if (!possibleBoxes.length) return null; var chosenBox = possibleBoxes.reduce(function (prev, cur) { if (!prev || cur.x > prev.x) return cur; if (cur.x == prev.x && cur.y < prev.y) return cur; return prev; }); return chosenBox; break; case "right": possibleBoxes = boxes .filter(function (box) { return box.x > currentBox.x; }) .filter(areBoxesInLineVertically.bind(null, currentBox)); if (!possibleBoxes.length) return null; var chosenBox = possibleBoxes.reduce(function (prev, cur) { if (!prev || cur.x < prev.x) return cur; if (cur.x == prev.x && cur.y < prev.y) return cur; return prev; }); return chosenBox; break; case "up": possibleBoxes = boxes .filter(function (box) { return box.y < currentBox.y; }) .filter(areBoxesInLineHorizontally.bind(null, currentBox)); if (!possibleBoxes.length) return null; var chosenBox = possibleBoxes.reduce(function (prev, cur) { if (!prev || cur.y > prev.y) return cur; if (cur.y == prev.y && cur.x < prev.x) return cur; return prev; }); return chosenBox; break; case "down": possibleBoxes = boxes .filter(function (box) { return box.y > currentBox.y; }) .filter(areBoxesInLineHorizontally.bind(null, currentBox)); if (!possibleBoxes.length) return null; var chosenBox = possibleBoxes.reduce(function (prev, cur) { if (!prev || cur.y < prev.y) return cur; if (cur.y == prev.y && cur.x < prev.x) return cur; return prev; }); return chosenBox; break; } } function areBoxesInLineVertically(box1, box2) { return !(box1.y + box1.height < box2.y || box2.y + box2.height < box1.y); } function areBoxesInLineHorizontally(box1, box2) { return !(box1.x + box1.width < box2.x || box2.x + box2.width < box1.x); } function findPaneToGoTo(direction) { var panes = tabs.getPanes(); if (!tabs.focussed || !tabs.focussedTab) return; var currentPane = tabs.focussedTab.pane; if (!currentPane) return; var boxes = panes.map(function (pane) { return getPaneDimensions(pane); }); var currentBox = getPaneDimensions(currentPane); var newBox = findBoxToGoTo(boxes, currentBox, direction); if (!newBox) return; var newPane = null; panes.forEach(function (pane) { var paneDimensions = getPaneDimensions(pane); if (paneDimensions.x == newBox.x && paneDimensions.y == newBox.y) { newPane = pane; } }); return newPane; } function gototabright(opts) { return cycleTab("right", opts); } function gototableft(opts) { return cycleTab("left", opts); } function cycleTab(dir, opts) { var curr = tabs.focussedTab; var pages = curr && curr.pane.getTabs(); if (!pages || pages.length == 1) return; if (opts && opts.editorType) { pages = pages.filter(function(p) { return p.editorType == opts.editorType; }); } var currIdx = pages.indexOf(curr); var start = currIdx; var tab; do { var idx = currIdx; switch (dir) { case "right": idx++; break; case "left": idx--; break; case "first": idx = 0; break; case "last": idx = pages.length - 1; break; default: idx--; } if (idx < 0) idx = pages.length - 1; if (idx > pages.length - 1) idx = 0; // No pages found that can be focussed if (start == idx) return; tab = pages[idx]; } while (!tab.pane.visible); if (tab.pane.visible) tabs.focusTab(tab, null, true); return false; } function movetabright() { hmoveTab("right"); } function movetableft() { hmoveTab("left"); } function movetabup() { vmoveTab("up"); } function movetabdown() { vmoveTab("down"); } function hmoveTab(dir) { var bRight = dir == "right"; var tab = tabs.focussedTab; if (!tab) return; // Tabs within the current pane var pages = tab.pane.getTabs(); // Get new index var idx = pages.indexOf(tab) + (bRight ? 2 : -1); // Before current pane if (idx < 0 || idx > pages.length) { tab.pane.moveTabToSplit(tab, dir); } // In current pane else { tab.attachTo(tab.pane, pages[idx], null, true); } tabs.focusTab(tab); return false; } function vmoveTab(dir) { var tab = tabs.focussedTab; if (!tab) return; tab.pane.moveTabToSplit(tab, dir); tabs.focusTab(tab); return false; } function tab1() { return showTab(1); } function tab2() { return showTab(2); } function tab3() { return showTab(3); } function tab4() { return showTab(4); } function tab5() { return showTab(5); } function tab6() { return showTab(6); } function tab7() { return showTab(7); } function tab8() { return showTab(8); } function tab9() { return showTab(9); } function tab0() { return showTab(10); } function showTab(idx) { // our indexes are 0 based an the number coming in is 1 based var pages = []; tabs.getPanes().forEach(function(pane) { pages = pages.concat(pane.getTabs()); }); pages = pages.filter(function(tab) { return tab.title; }); var tab = pages[idx - 1]; if (!tab) return false; tabs.focusTab(tab, null, true); return false; } /** * Scrolls to the selected pane's file path in the "Project Files" tree * * Works by Finding the node related to the active pane in the tree, and * unfolds its parent folders until the node can be reached by an xpath * selector and focused, to finally scroll to the selected node. */ function revealtab(tab, noFocus) { if (!tab || tab.command) tab = tabs.focussedTab; if (!tab) return false; // Tell other extensions to exit their fullscreen mode (for ex. Zen) // so this operation is visible // ide.dispatchEvent("exitfullscreen"); revealInTree(tab, noFocus); } function revealInTree(tab, noFocus) { panels.activate("tree"); var path = tab.path || tab.relatedPath; if (path) done(null, path); else if (tab.editor.getPathAsync) tab.editor.getPathAsync(done); if (!noFocus) tree.focus(); function done(err, path) { if (err || !path) return console.error(err); tree.expand(path, function(err) { if (!err) tree.select(path); tree.scrollToSelection(); }); } } function canTabBeRemoved(pane, min) { if (!pane || pane.getTabs().length > (min || 0)) return false; var containers = tabs.containers; for (var i = 0; i < containers.length; i++) { if (ui.isChildOf(containers[i], pane.aml)) { return containers[i] .getElementsByTagNameNS(apf.ns.aml, "tab").length > 1; } } return false; } function closepane(tab, pane) { if (!tab) tab = tabs.focussedTab; if (!pane) pane = tab.pane; if (!pane) return; var pages = pane.getTabs(); if (!pages.length) { if (canTabBeRemoved(pane)) pane.unload(); return; } changedTabs = []; unchangedTabs = []; // Ignore closing tabs menuClosedItems.ignore = true; // Keep information to restore pane set var state = []; var type = pane.aml.parentNode.localName; var nodes = pane.aml.parentNode.childNodes.filter(function(p) { return p.localName != "splitter"; }); state.title = pages.length + " Tabs"; state.type = type == "vsplitbox" ? "vsplit" : "hsplit"; state.far = nodes.indexOf(pane.aml) == 1; state.sibling = nodes[state.far ? 0 : 1]; state.getState = function() { return state; }; state.restore = $restoreTabGroup; state.paneName = pane.name; state.document = { meta: {}}; // Close pages pages.forEach(function(tab) { state.push(tab.getState()); closepage(tab); }); tabs.resizePanes(); checkTabRender(function() { if (canTabBeRemoved(pane)) pane.unload(); // Stop ignoring closing tabs menuClosedItems.ignore = false; // @todo there should probably be some check here addTabToClosedMenu(state); }); } function $restoreTabGroup(state) { // pane was not being used. Why? // var pane = state.sibling; // if (pane && pane.cloud9pane) // pane = pane.cloud9pane.aml; var pane = tabs.findPane(state.paneName) || {}; var oldpane = state.pane; var newpane = oldpane.getTabs().length === 0 ? oldpane : oldpane[state.type](state.far, null, pane.aml); state.forEach(function(s) { s.pane = newpane; tabs.open(s, function() {}); }); } function hsplit(tab, pane) { if (!tab) tab = tabs.focussedTab; if (tab) pane = tab.pane; var newpane = pane.hsplit(true); if (pane.getTabs().length > 1) tab.attachTo(newpane); } function vsplit(tab, pane) { if (!tab) tab = tabs.focussedTab; if (tab) pane = tab.pane; var newpane = pane.vsplit(true); if (pane.getTabs().length > 1) tab.attachTo(newpane); } function nosplit() { var panes = tabs.getPanes(tabs.container); var first = panes[0]; for (var pane, i = 1, li = panes.length; i < li; i++) { var pages = (pane = panes[i]).getTabs(); for (var j = 0, lj = pages.length; j < lj; j++) { pages[j].attachTo(first, null, true); } pane.unload(); } } function twovsplit(hsplit) { var panes = tabs.getPanes(tabs.container); // We're already in a two vsplit if (panes.length == 2 && panes[0].aml.parentNode.localName == (hsplit ? "hsplitbox" : "vsplitbox")) return panes; // Split the only pane there is if (panes.length == 1) { var newtab = panes[0][hsplit ? "hsplit" : "vsplit"](true); return [panes[0], newtab]; } var c = tabs.containers[0].firstChild.childNodes.filter(function(f) { return f.localName != "splitter"; }); // var left = c[0].getElementsByTagNameNS(apf.ns.aml, "tab"); var right = c[1].getElementsByTagNameNS(apf.ns.aml, "tab"); for (var i = 1, l = panes.length; i < l; i++) { panes[i].unload(); } var newtab = panes[0][hsplit ? "hsplit" : "vsplit"](true); right.forEach(function(tab) { if (tab.cloud9tab) tab.cloud9tab.attachTo(newtab, null, true); }); return [panes[0], newtab]; } function twohsplit() { return twovsplit(true); } function foursplit() { var panes = twohsplit(); panes[0].vsplit(true); panes[1].vsplit(true); } function threeleft() { var panes = twohsplit(); panes[0].vsplit(true); } function threeright() { var panes = twohsplit(); panes[1].vsplit(true); } function checkReopenedTab(e) { var tab = e.tab; if (!tab.path) return; fs.stat(tab.path, function(err, stat) { if (err) return; // @todo this won't work well on windows, because // there is a 20s period in which the mtime is // the same. The solution would be to have a // way to compare the saved document to the // loaded document that created the state if (tab.document.meta.timestamp < stat.mtime) { var doc = tab.document; question("File Changed", tab.path + " has been changed on disk.", "Would you like to reload this file?", function() { tabs.reload(tab, function() {}); }, function() { // Set to changed doc.undoManager.bookmark(-2); }, { merge: false, all: false } ); } }); } // Record the last 10 closed tabs or pane sets function addTabToClosedMenu(tab) { if (menuClosedItems.ignore) return; if (tab.document.meta.preview || tab.document.meta.cloned) return; // Record state var state = tab.getState(); var restore = tab.restore; var path = tab.path || tab.editorType; if (!restore) { for (var i = menuClosedItems.length - 1; i >= 0; i--) { if (menuClosedItems[i].path == path) { menuClosedItems.splice(i, 1)[0].destroy(true, true); } } } // Create menu item var item = new ui.item({ caption: tab.title, path: path, style: "padding-left:35px", onclick: function(e) { // Update State state.active = true; state.pane = this.parentNode.pane; tabs.on("open", checkReopenedTab); // Open pane restore ? restore(state) : tabs.open(state, function() {}); tabs.off("open", checkReopenedTab); // Remove pane from menu menuClosedItems.remove(item); item.destroy(true, true); // Clear label and divider if there are no items if (menuClosedItems.length === 0) menuClosedItems.hide(); } }); // TODO: passing path to item doesn't work since apf adds it only when menu is shown item.path = path; // Add item to menu menuClosedItems.push(item); var index = menuClosedItems.index = (menuClosedItems.index || 0) + 1; menus.addItemToMenu(mnuEditors, item, 2000000 - index, false); // Show label and divider menuClosedItems.show(); // Remove excess menu item if (menuClosedItems.length > 10) menuClosedItems.shift().destroy(true, true); tab = null; } function updateTabMenu(force) { // Approximating order var pages = []; tabs.getPanes().forEach(function(pane) { pages = pages.concat(pane.getTabs()); }); var length = Math.min(10, pages.length); var start = 1000; // Destroy all items menuItems.forEach(function(item) { item.destroy(true, true); }); menuItems = []; if (!pages.length) return; var mnu, tab; // Create new divider menus.addItemToMenu(mnuTabs, mnu = new ui.divider(), start, false); menuItems.push(mnu); // Create new items for (var i = 0; i < length; i++) { tab = pages[i]; if (!tab.title) continue; menus.addItemToMenu(mnuTabs, mnu = new ui.item({ caption: tab.title.replace(/[/]/g, "\u2044"), relPage: tab, command: "tab" + (i == 9 ? 0 : i + 1) }), start + i + 1, false); menuItems.push(mnu); } if (pages.length > length) { menus.addItemToMenu(mnuTabs, mnu = new ui.item({ caption: "More...", onclick: function() { commands.exec("toggleOpenfiles", null, { forceOpen: true }); } }), start + length + 1, false); menuItems.push(mnu); } tab = pages = null; } function reopenLastTab() { var item = menuClosedItems[menuClosedItems.length - 1]; if (item) item.getAttribute("onclick").call(item); } function createLayoutMenus() { var LAYOUT_MENU_PATH = "Window/Saved Layouts/"; var SAVED_LAYOUTS_PATH = "/.c9/saved-layouts/"; commands.addCommand({ name: "savePaneLayout", group: "Window", bindKey: {}, exec: function (editor, args) { var state = tabs.getState(null, true); var stateName = prompt("Name your layout", getAutoSaveName()); if (!stateName) return; var sanitizedStateName = stateName.trim().replace(/[\\\/:\r\n~]|\.\./g, "-") + ".tabstate"; fs.writeFile(SAVED_LAYOUTS_PATH + sanitizedStateName, JSON.stringify(state, null, "\t"), function(err) { if (err) { return alert(err); } }); } }, plugin); commands.addCommand({ name: "savePaneLayoutAndCloseTabs", group: "Window", bindKey: {}, exec: function (editor, args) { commands.exec("savePaneLayout"); tabs.setState(null, function() {}); } }, plugin); // menus.insert menus.addItemByPath(LAYOUT_MENU_PATH, new ui.menu({ "onprop.visible": function(e) { if (e.value) { rebuildLayoutMenu(); fs.readdir(SAVED_LAYOUTS_PATH, function(err, files) { rebuildLayoutMenu(err, files); }); } }, "onitemclick": function(e) { var stat = e.relatedNode && e.relatedNode.value; if (stat && stat.name) { fs.readFile(SAVED_LAYOUTS_PATH + stat.name, function(err, contents) { if (err) return alert(err); try { contents = JSON.parse(contents); } catch (e) { return alert(e); } tabs.setState(null, function() {}); tabs.setState(contents, function(err) { if (err) return alert(err); }); }); } } }), 10050, plugin); function getAutoSaveName() { return (new Date()).toLocaleString() + " [" + tabs.getTabs().length + " tabs]"; } function rebuildLayoutMenu(err, stats) { menus.remove(LAYOUT_MENU_PATH); var c = 0; menus.addItemByPath(LAYOUT_MENU_PATH + "Save...", new ui.item({ command: "savePaneLayout" }), c += 100, plugin); menus.addItemByPath(LAYOUT_MENU_PATH + "Save And Close All...", new ui.item({ command: "savePaneLayoutAndCloseTabs" }), c += 100, plugin); menus.addItemByPath(LAYOUT_MENU_PATH + "~", new ui.divider({ }), c += 100, plugin); menus.addItemByPath(LAYOUT_MENU_PATH + "Show Saved Layouts in File Tree", new ui.item({ onclick: function() { revealInTree({ path: SAVED_LAYOUTS_PATH }); } }), c += 100, plugin); menus.addItemByPath(LAYOUT_MENU_PATH + "~", new ui.divider({ }), c += 100, plugin); if (err) { if (err.code == "ENOENT") return; return menus.addItemByPath(LAYOUT_MENU_PATH + "Error loading saved layouts", new ui.item({ disabled: true, }), c += 100, plugin); } else if (!stats) { return menus.addItemByPath(LAYOUT_MENU_PATH + "loading...", new ui.item({ disabled: true, }), c += 100, plugin); } for (var i = 0; i < stats.length; i++) { var stat = stats[i]; var caption = stat.name.replace(/.tabstate$/, ""); menus.addItemByPath(LAYOUT_MENU_PATH + caption, new ui.item({ value: stat, }), c += 100, plugin); } } } /***** Lifecycle *****/ plugin.on("load", function() { load(); }); plugin.on("enable", function() { }); plugin.on("disable", function() { }); plugin.on("unload", function() { menuItems.forEach(function(item) { item.destroy(true, true); }); menuItems = []; menuClosedItems.forEach(function(item) { item.destroy(true, true); }); menuClosedItems.length = 0; // = []; mnuContext = null; mnuEditors = null; mnuTabs = null; paneList = null; accessedPane = null; cycleKeyPressed = null; changedTabs = null; unchangedTabs = null; dirtyNextTab = null; dirtyNextPane = null; loaded = false; }); /***** Register and define API *****/ /** * Draws the file tree * @event afterfilesave Fires after a file is saved * @param {Object} e * node {XMLNode} description * oldpath {String} description **/ plugin.freezePublicAPI({ /** * */ get contextMenu() { return mnuContext; }, /** * */ clonetab: clonetab, /** * */ closetab: closetab, /** * */ closealltabs: closealltabs, /** * */ closeallbutme: closeallbutme, /** * */ gototabright: gototabright, /** * */ gototableft: gototableft, /** * */ movetabright: movetabright, /** * */ movetableft: movetableft, /** * */ movetabup: movetabup, /** * */ movetabdown: movetabdown, /** * */ tab1: tab1, /** * */ tab2: tab2, /** * */ tab3: tab3, /** * */ tab4: tab4, /** * */ tab5: tab5, /** * */ tab6: tab6, /** * */ tab7: tab7, /** * */ tab8: tab8, /** * */ tab9: tab9, /** * */ tab0: tab0, /** * */ revealtab: revealtab, /** * */ reopenLastTab: reopenLastTab, /** * */ nexttab: nexttab, /** * */ previoustab: previoustab, /** * */ closealltotheright: closealltotheright, /** * */ closealltotheleft: closealltotheleft, /** * */ closepane: closepane, /** * */ hsplit: hsplit, /** * */ vsplit: vsplit, /** * */ nosplit: nosplit, /** * */ twovsplit: twovsplit, /** * */ twohsplit: twohsplit, /** * */ foursplit: foursplit, /** * */ threeleft: threeleft, /** * */ threeright: threeright, /** * */ nextpane: nextpane, /** * */ previouspane: previouspane, /** * */ gotopaneleft: gotopaneleft, /** * */ gotopaneright: gotopaneright, /** * */ gotopanedown: gotopanedown, /** * */ gotopaneup: gotopaneup, /** * @ignore */ cycleTab: cycleTab }); register(null, { tabbehavior: plugin }); } });