define(function(require, exports, module) { main.consumes = [ "PreferencePanel", "commands", "settings", "ui", "util", "Form", "c9", "dialog.alert", "tabManager", "save", "dialog.confirm", "layout", "menus" ]; main.provides = ["preferences.keybindings"]; return main; function main(options, imports, register) { var PreferencePanel = imports.PreferencePanel; var commands = imports.commands; var settings = imports.settings; var layout = imports.layout; var ui = imports.ui; var c9 = imports.c9; var util = imports.util; var save = imports.save; var menus = imports.menus; var tabManager = imports.tabManager; var alert = imports["dialog.alert"].show; var confirm = imports["dialog.confirm"].show; var search = require("../c9.ide.navigate/search"); var keys = require("ace/lib/keys"); var Tree = require("ace_tree/tree"); var TreeData = require("./editordp"); var TreeEditor = require("ace_tree/edit"); /***** Initialization *****/ var plugin = new PreferencePanel("Ajax.org", main.consumes, { caption: "Keybindings", className: "keybindings flatform", form: true, noscroll: true, index: 200 }); // var emit = plugin.getEmitter(); var model, datagrid, changed, container, filterbox; var appliedCustomSets, intro, reloading; var loaded = false; function load() { if (loaded) return false; loaded = true; settings.on("user/key-bindings", function(){ var platform = settings.get("user/key-bindings/@platform"); if (platform == "auto") platform = apf.isMac ? "mac" : "win"; if (commands.platform != platform) { commands.changePlatform(platform); reloadModel(); } }, plugin); settings.on("read", function(e) { updateCommandsFromSettings(); }, plugin); commands.on("update", function(){ if (!reloading) { changed = true; updateCommandsFromSettings(); } }, plugin); save.on("beforeSave", function(e) { if (e.document.meta.keybindings) { // Doing save as, it is now a normal document if (e.path != "~/.c9/keybindings.settings") { delete e.document.meta.keybindings; return; } var value = e.document.value .replace(/\/\/.*/g, "") .replace(/("(?:\\.|[^"])")|(?:,\s*)+([\]\}])|(\w+)\s*:|([\]\}]\s*[\[\{])/g, function(_, str, extraComma, noQuote, missingComma) { if (missingComma) return missingComma[0] + "," + missingComma.slice(1); return str || extraComma || '"' + noQuote + '":'; }) .trim() || "[]"; var json; try { json = JSON.parse(value); if (!Array.isArray(json)) throw new Error(""); } catch (e) { alert("Syntax Error", "Found a Syntax Error in Keybindings", "Please correct it and try saving again."); return false; } settings.setJson("user/key-bindings", json); updateCommandsFromSettings(); e.document.undoManager.bookmark(); return false; } }, plugin); menus.addItemByPath("Help/Key Bindings Editor", new ui.item({ onclick: function(){ commands.exec("openpreferences", null, { panel: plugin }); } }), 250, plugin); } var drawn; function draw(e) { if (drawn) return; drawn = true; model = new TreeData(); model.emptyMessage = "No keybindings to display"; model.columns = [{ caption: "Name", value: "name", width: "150", type: "tree" }, { caption: "Keystroke", value: "keys", width: "20%", editor: "textbox" }, { caption: "Description", value: "info", width: "80%" }]; layout.on("eachTheme", function(e){ var height = parseInt(ui.getStyleRule(".bar-preferences .blackdg .tree-row", "height"), 10) || 24; model.rowHeightInner = height; model.rowHeight = height; if (e.changed) datagrid.resize(true); }); reloadModel(); plugin.form.add([ { type: "custom", title: "Introduction", position: 1, node: intro = new ui.bar({ height: 149, "class" : "intro", style: "padding:12px;position:relative;" }) }, { type: "button", title: "Reset to Default Keybindings", caption: "Reset to Defaults", width: 140, onclick: function(){ confirm("Reset Settings", "Are you sure you want to reset your keybindings?", "By resetting your keybindings to their " + "defaults you will lose all custom keybindings.", function(){ settings.setJson("user/key-bindings", []); settings.set("user/ace/@keyboardmode", "default"); settings.set("user/key-bindings/@platform", "auto"); commands.reset(false, true); }, function(){}); }, position: 90 }, { title: "Keyboard Mode", type: "dropdown", path: "user/ace/@keyboardmode", items: [ { caption: "Default", value: "default" }, { caption: "Vim", value: "vim" }, { caption: "Emacs", value: "emacs" }, { caption: "Sublime", value: "sublime" } ], position: 100 }, { type: "dropdown", title: "Operating System", path: "user/key-bindings/@platform", items: [ {caption: "Auto", value: "auto"}, {caption: "Apple OSX", value: "mac"}, {caption: "Windows / Linux", value: "win"}, ], position: 110 }, { type: "custom", title: "Keybindings Editor", position: 120, node: container = new ui.bar({ anchors: "269 0 0 0", "class" : "keybindings", style: "padding:44px 10px 10px 10px" }) } ], commands); intro.$int.innerHTML = '
Change these settings to configure ' + 'how Cloud9 responds to your keyboard commands.
' + 'You can also manually edit your keymap file.
' + 'Hint: Double click on the keystroke cell in the table below to change the keybinding.
'; intro.$int.querySelector("a").onclick = function(){ editUserKeys(); }; var div = container.$ext.appendChild(document.createElement("div")); div.style.width = div.style.height = "100%"; datagrid = new Tree(div); datagrid.setTheme({ cssClass: "blackdg" }); datagrid.setDataProvider(model); datagrid.edit = new TreeEditor(datagrid); layout.on("resize", function(){ datagrid.resize() }, plugin); filterbox = new apf.codebox({ realtime: true, skin: "codebox", "class": "dark", clearbutton: true, focusselect: true, height: 27, left: 10, top: 10, width: 250, singleline: true, "initial-message": "Search Keybindings" }); container.appendChild(filterbox); function setTheme(e) { filterbox.setAttribute("class", e.theme.indexOf("dark") > -1 ? "dark" : ""); } layout.on("themeChange", setTheme); setTheme({ theme: settings.get("user/general/@skin") }); filterbox.ace.commands.addCommands([ { bindKey: "Enter", exec: function(){ } }, { bindKey: "Esc", exec: function(ace){ ace.setValue(""); } } ]); filterbox.ace.on("input", function(e) { applyFilter(); }); container.on("contextmenu", function(){ return false; }); var lastKey, displayKey; datagrid.on("createEditor", function(e) { displayKey = lastKey = ""; e.ace.keyBinding.addKeyboardHandler(function(data, hashId, keyString, keyCode, e) { var ace = data.editor; var disable = { command: "null" }; var mod = keys.KEY_MODS[hashId]; var isTextInput = keyString.length == 1 && (!mod || mod == "shift-"); if (!e || isTextInput || keyCode <= 0) return; var key = mod.split("-").filter(Boolean); // do not allow binding to enter and escape without modifiers if (!key.length) { if (e.keyCode == 8) { ace.setValue(""); lastKey = displayKey = ""; return disable; } if (e.keyCode == 27) { ace.treeEditor.endRename(true); return disable; } if (e.keyCode == 13) { key = ace.getValue().split("-"); keyString = key.pop(); setTimeout(function() { ace.treeEditor.endRename(false); }); return disable; } } if (keyCode > 0) key.push(keys[keys[keyString]] || keyString); displayKey = lastKey = key.map(function(x) { return x.uCaseFirst(); }).join("-"); if (commands.platform == "mac") displayKey = apf.hotkeys.toMacNotation(displayKey); ace.setValue(displayKey); return disable; }); }); datagrid.on("rename", function(e) { var node = e.node; var name = node.name; node.keys = displayKey; node.actualKeys = lastKey; // Make sure key is not already used var used = commands.findKey(lastKey); if (used.length > 1 || (used[0] && used[0].name != name)) { alert("Notice", "There are other commands bound to this key combination", "[" + used.map(function(x) { return x.name }).join(", ") + "]" ); } // Add key commands.bindKey(lastKey, commands.commands[name]); var n = { command: node.name, keys: (node.actualKeys || node.keys).split("|") }; var cmds = settings.getJson("user/key-bindings") || []; if (!cmds.some(function(node, i) { if (node.command == n.command) { cmds[i] = n; return true; } })) cmds.push(n); reloadModel(); settings.setJson("user/key-bindings", cmds); }); // when tab is restored datagrids size might be wrong // todo: remove this when apf bug is fixed datagrid.once("mousemove", function() { datagrid.resize(true); }); } /***** Methods *****/ // @todo move this to commands function updateCommandsFromSettings() { c9.once("ready", function() { if (appliedCustomSets) reset(true); var cmds = settings.getJson("user/key-bindings"); if (cmds) { cmds.forEach(function(cmd) { if (!cmd || !cmd.command) return; var keys = cmd.keys; if (Array.isArray(keys)) keys = keys.join("|"); if (typeof keys == "string") commands.bindKey(keys, commands.commands[cmd.command]); }); appliedCustomSets = true; } reloadModel(); reloading = true; commands.flushUpdateQueue(); reloading = false; }); } function reset(noReload) { reloading = true; commands.reset(true); if (!noReload) { settings.setJson("user/key-bindings", []); reloadModel(); } reloading = false; } function reloadModel() { if (!model) return; var groups = {}; var root = []; var platform = commands.platform; Object.keys(commands.commands).forEach(function(name) { var item = commands.commands[name]; if (!item.name) return; var groupName = item.group || "General"; if (groupName == "ignore") return; var group = groups[groupName]; if (!group) root.push(group = groups[groupName] = { items: [], isOpen: true, className: "group", noSelect: true, name: groupName }); var keys = commands.commandManager[item.name] || ""; //item.bindKey && item.bindKey[platform] || ""; group.items.push({ name: item.name, enabled: "true", info: item.hint || "", keys: platform == "mac" ? apf.hotkeys.toMacNotation(keys) : keys }); }); model.cachedRoot = { items: root }; applyFilter(); } function applyFilter() { model.keyword = filterbox && filterbox.getValue(); if (!model.keyword) { model.reKeyword = null; model.setRoot(model.cachedRoot); } else { model.reKeyword = new RegExp("(" + util.escapeRegExp(model.keyword) + ")", 'i'); var root = search.treeSearch(model.cachedRoot.items, model.keyword, true); model.setRoot(root); } } function editUserKeys(tab) { // preferences.hide(); var keys = settings.getJson("user/key-bindings") || []; var value = "// Edit this keymap file and save to apply.\n[\n"; value += keys.map(function(key) { return " " + JSON.stringify(key) .replace(/":/g, "\": ") .replace(/,/g, ", ") .replace(/\{/g, "{ ") .replace(/\}/g, " }"); }).join(",\n"); if (!keys.length) value += ' // { "command": "nexttab", "keys": ["Ctrl-Tab"] }'; value += "\n]"; if (tab) { tab.document.value = value; } else { tabManager.open({ path: "~/.c9/keybindings.settings", value: value, active: true, editorType: "ace", document: { ace: { customSyntax: "javascript" }, meta: { keybindings: true, nofs: true } } }, function(err, tab) { }); } } /***** Lifecycle *****/ plugin.on("load", function() { load(); }); plugin.on("draw", function(e) { draw(e); }); plugin.on("activate", function(e) { datagrid && datagrid.resize(); }); plugin.on("resize", function(e) { datagrid && datagrid.resize(); }); plugin.on("unload", function() { loaded = false; drawn = false; model = null; datagrid = null; changed = null; container = null; filterbox = null; appliedCustomSets = null; intro = null; reloading = null; }); /***** Register and define API *****/ /** * **/ plugin.freezePublicAPI({ /** * */ editUserKeys: editUserKeys }); register(null, { "preferences.keybindings" : plugin }); } });