define(function(require, exports, module) { main.consumes = [ "Plugin", "ui", "fs.cache", "tree", "settings", "util", "commands", "navigate", "find", "preferences", "c9", "watcher" ]; main.provides = ["tree.favorites"]; return main; function main(options, imports, register) { var Plugin = imports.Plugin; var ui = imports.ui; var c9 = imports.c9; var tree = imports.tree; var util = imports.util; var find = imports.find; var settings = imports.settings; var commands = imports.commands; var navigate = imports.navigate; var watcher = imports.watcher; var prefs = imports.preferences; var fsCache = imports["fs.cache"]; var basename = require("path").basename; var dirname = require("path").dirname; /***** Initialization *****/ var plugin = new Plugin("Ajax.org", main.consumes); var emit = plugin.getEmitter(); var lut = {}; var model = fsCache.model; var enabled = false; var startEmpty = options.startEmpty; var realRoot = options.realRoot; var alwaysScope = options.alwaysScope; var home = options.home; var reFavs = ""; var changed, altRoot, favRoot, stored, hasScoping; var loaded = false; function load() { if (loaded) return false; loaded = true; commands.addCommand({ name: "addfavorite", exec: function() { tree.selectedNodes.forEach(function(node) { addFavorite(node.path); }); } }, plugin); commands.addCommand({ name: "removefavorite", isAvailable: function(){ return tree && tree.selectedNode && isFavoriteNode(tree.selectedNode); }, exec: function() { tree.selectedNodes.forEach(function(node) { removeFavorite(node.path); }); } }, plugin); // Set real root model.realRoot = model.root; // Set empty message if (startEmpty) { model.emptyMessage = "Drag one or more folders here to populate" + " this panel with files. Use the settings button in the" + " top right to enable browsing the root file system."; } else if (realRoot) { model.emptyMessage = "There are no favorites defined. Please" + " add some favorites to the tree using the settings" + " button in the top right."; } else { model.emptyMessage = "There are no favorites defined. Use" + " the settings button in the top right to add your" + " workspace directory."; } altRoot = { children: [favRoot = { label: "favorites", path: "!favorites", isOpen: true, className: "heading", isRoot: true, isFolder: true, status: "loaded", map: {}, children: [], noSelect: true }] }; if (startEmpty) toggleRootFS(false); tree.once("draw", function(){ tree.tree.on("dblclick", function(e) { var selected = e.getNode(); var favNode = selected && isFavoritePath(selected.path); if (favNode && favNode != selected) { tree.select(favNode); tree.scrollToSelection(); e.preventDefault(); } }, plugin); tree.tree.keyBinding.addKeyboardHandler(function(data, hashId, keyString, keyCode, e) { if (hashId !== 0) return; var nodes = tree.selectedNodes; if (nodes.length != 1) return; var node = nodes[0]; var favNode = isFavoritePath(node.path); if (!favNode) { if (keyString == "left" && !node.isOpen) { node = node.parent; favNode = node && isFavoritePath(node.path); if (favNode) { tree.select(favNode); tree.scrollToSelection(); return {command: "null"}; } } } else { if (keyString == "left") { if (favNode == node) { if (node.isOpen) tree.collapse(node); else tree.expandAndSelect(node.path); } else tree.expandAndSelect(node.parent); return {command: "null"}; } else if (keyString == "right" || keyString == "return") { if (favNode != node) { tree.select(favNode); tree.scrollToSelection(); return {command: "null"}; } } } }); tree.tree.on("folderDragEnter", function(dragInfo) { if (dragInfo.$allowSortMode) { if (dragInfo.hoverNode == favRoot) { dragInfo.mode = "sort"; dragInfo.hoverNode = favRoot.children[0]; dragInfo.insertPos = -1; } else if (isFavoriteNode(dragInfo.hoverNode)) { dragInfo.mode = "sort"; dragInfo.insertPos = 1; } else { var fsRoot = fsCache.model.realRoot; var node = dragInfo.hoverNode; do { if (node == fsRoot) { dragInfo.hoverNode = fsRoot; dragInfo.insertPos = 1; break; } else if (isFavoritePath(node.path)) { dragInfo.hoverNode = isFavoritePath(node.path); dragInfo.insertPos = 1; break; } } while (node = node.parent); } } else if (dragInfo.hoverNode == favRoot) { dragInfo.mode = "sort"; dragInfo.insertPos = 1; } else if (dragInfo.mode == "sort") { dragInfo.mode = "move"; dragInfo.insertPos = 0; } }); tree.tree.on("dragStarted", function(e) { var dragInfo = e.dragInfo; var favs = dragInfo.selectedNodes.filter(function(node) { return isFavoriteNode(node); }); if (favs.length) { if (favs.length < dragInfo.selectedNodes.length) { dragInfo.selectedNodes = dragInfo.selectedNodes.filter(function(node) { return isFavoriteNode(node); }); } else { dragInfo.$allowSortMode = true; dragInfo.selectedNodes = []; dragInfo.mode = "sort"; } } }); tree.tree.on("drop", function(dragInfo) { var target = dragInfo.hoverNode; if (target == favRoot) { dragInfo.mode = "sort"; target = favRoot.children[0]; } if (dragInfo.mode == "sort") { dragInfo.selectedNodes = null; if (!dragInfo.node || !target) return; if (dragInfo.hoverNode == fsCache.model.realRoot) return removeFavorite(dragInfo.node.path); var favNode = addFavorite(dragInfo.node.path); var index = favRoot.children.indexOf(target); if (index == -1) return; if (dragInfo.insertPos == 1) index++; var oldIndex = favRoot.children.indexOf(favNode); if (oldIndex != -1) favRoot.children.splice(oldIndex, 1); if (oldIndex < index) index--; favRoot.children.splice(index, 0, favNode); fsCache.refresh(favRoot); emit("favoriteReorder"); } }, true); tree.on("isRootContext", function(node){ return isFavoritePath(node.path) || node.path.charAt(0) == "~" && isFavoritePath(node.path.replace(/^~/, c9.home)); }); }); tree.on("menuUpdate", function(e){ if (e.node && isFavoriteNode(e.node)){ e.menu.childNodes.some(function(item) { if (item.caption == "Delete") { item.setAttribute("disabled", true); return true; } }); } }); tree.on("beforeRename", function(e) { return !e.node.isFavorite; }, plugin); tree.on("delete", function(e) { var ok = []; e.selection.forEach(function(node) { if (node.isFavorite) removeFavorite(node.path); else ok.push(node); }); if (ok.length == e.selection.length) return; if (ok.length) tree.remove(ok); return false; }, plugin); fsCache.on("findNode", function(e) { var path = e.path; if (path == "~") { return lut["~"]; } else if (path.charAt(0) == "~") { var findNode = function(parts, node){ var iter = 0; var newp = parts[iter]; while (iter < parts.length) { if (node) { node = node.map[parts[++iter]]; if (!node) break; } else { node = lut[newp]; newp += "/" + parts[++iter]; } } return node; } var parts = path.split("/"); var node = findNode(parts); return node; } else if (path.charAt(0) == "!") { // if (e.path == "!favorite") return favRoot; // if (e.path == "!fsroot") return model.realRoot; return false; } else if (enabled && lut[path]) { //e.type == "expand" || e.type == "collapse" || e.type == "refresh" if (e.type) return lut[path]; } else if (path == "/" && e.type == "expand") { toggleRootFS(true); } }, plugin); fsCache.on("readdir", function(e) { var node = isFavoritePath(e.parent.path); if (node) model.setAttribute(node, "status", "loaded"); }); function keepSane(path) { var len = path.length + 1; for (var favPath in lut) { if (favPath == path || favPath.substr(0, len) == path + "/") removeFavorite(favPath); } } fsCache.on("remove", function(e) { // Lets ignore hidden files. It's most likely still there and // the user just switching hidden files off in the file tree // Possible solution is to do the filtering of hidden paths // only in the renderer of the tree if (!fsCache.showHidden && e.path.indexOf("/.") > -1) return; keepSane(e.path); }); tree.on("rename", function(e) { keepSane(e.oldpath); }); model.on("startUpdate", function(node) { var favNode = isFavoritePath(node.path); if (favNode) { favNode.wasOpen = favNode.isOpen; model.close(favNode, null, true); } }); model.on("endUpdate", function(node, wasOpen) { var favNode = isFavoritePath(node.path); if (favNode && favNode.wasOpen) { model.open(favNode, null, true); favNode.wasOpen = false; } }); model.on("expand", function(node) { var favNode = isFavoritePath(node.path); if (favNode) { if (isFavoriteNode(node)) { node = fsCache.findNode(node.path); model.open(node); } } }); model.on("collapse", function(node) { var favNode = isFavoritePath(node.path); if (favNode) { if (isFavoriteNode(node)) { node = fsCache.findNode(node.path); model.close(node); } } }); // Navigate // limit filelist to favorites find.on("fileList", function(options) { if (!hasScoping) return; if (options.path == "/" && !options.startPaths) { var paths = Object.keys(lut); if (!paths.length) return false; options.startPaths = paths.filter(function(p) { return !lut[p].excludeFilelist && !paths.some(function(n) { return p != n && p.substr(0, n.length) == n; }); }); } }, plugin); // Make sure the file list is updated when a favorite is added var updateNavigate = function(e) { if (!hasScoping) return; navigate.markDirty(null, e.init ? -100 : 0, true); }; plugin.on("favoriteRemove", updateNavigate); plugin.on("favoriteAdd", updateNavigate); // Scope the paths in navigate navigate.on("draw", function(){ var replaceStrong = navigate.tree.provider.replaceStrong; navigate.tree.provider.replaceStrong = function(path) { if (hasScoping && reFavs && path.charAt(0) == "/") path = path.replace(reFavs, function(m, n) { return "(" + n.split("/").pop() + ")/"; }); return replaceStrong.call(navigate.tree.provider, path); }; }, plugin); // Context Menu tree.getElement("mnuCtxTree", function(mnuCtxTree) { var remove, add; ui.insertByIndex(mnuCtxTree, add = new ui.item({ match: "folder|file", command: "addfavorite", caption: "Add to Favorites", }), 1000, plugin); ui.insertByIndex(mnuCtxTree, remove = new ui.item({ command: "removefavorite", caption: "Remove from Favorites", }), 1001, plugin); ui.insertByIndex(mnuCtxTree, new ui.divider(), 1100, plugin); mnuCtxTree.on("prop.visible", function(e) { if (e.value) { var node = tree.selectedNode; if (node && isFavoritePath(node.path)) { add.hide(); remove.show(); } else { add.show(); remove.hide(); } } }); var mnuSettings = tree.getElement("mnuFilesSettings"); ui.insertByIndex(mnuSettings, new ui.item({ type: "check", caption: realRoot ? "Show Root File System" : "Show Workspace Directory", checked: "state/projecttree/@showfs", onclick: function(e) { toggleRootFS(); } }), 250, plugin); var cbHome = new ui.item({ type: "check", caption: "Show Home in Favorites", write: true, onclick: function(e) { if (this.checked) addFavorite(c9.toInternalPath(home)); else removeFavorite(c9.toInternalPath(home)); } }); ui.insertByIndex(mnuSettings, cbHome, 260, plugin); mnuSettings.on("prop.visible", function(e) { if (e.value) cbHome.setAttribute("checked", lut["~"] ? true : false); }); }); // Settings var init = false; settings.on("read", function(){ settings.setDefaults("state/projecttree", [ ["showfs", startEmpty ? "false" : "true"] ]); settings.setDefaults("user/projecttree", [ ["scope", "false"] ]); if (settings.getBool("state/projecttree/@showfs")) toggleRootFS(true); else toggleRootFS(false); hasScoping = alwaysScope || settings.getBool("user/projecttree/@scope"); if (!init) { (settings.getJson("state/projecttree/favorites") || []).forEach(function(n) { addFavorite(n, null, true); }); // Load file list navigate.markDirty(null, 0); // @todo instead remove all favorites and set them again init = true; } }, plugin); settings.on("user/projecttree", function(){ var wasScoping = hasScoping; hasScoping = alwaysScope || settings.getBool("user/projecttree/@scope"); if (wasScoping != hasScoping) navigate.markDirty(null, 0, true); }); settings.on("write", function(){ if (!changed) return; var saved = favRoot.children.map(function(n) { var node = util.extend({}, n); delete node.isSelected; delete node.children; delete node.map; delete node.parent; delete node.$depth; return node; }); settings.setJson("state/projecttree/favorites", saved); changed = false; }); // Prefs if (!alwaysScope) { prefs.add({ "General" : { "Tree & Navigate" : { "Scope Navigate To Favorites": { type: "checkbox", position: 1000, path: "user/projecttree/@scope" } } } }, plugin); } function update(e) { if (e && isFavoriteNode(e)) { changed = true; settings.save(); } } model.on("expand", update); model.on("collapse", update); } /***** Methods *****/ function wrap(){ enabled = true; var getClassName = model.getClassName; model.getClassName = function(node) { var favNode = isFavoritePath(node.path); if (favNode && favNode != node) return "favorite"; else return getClassName.call(model, node); }; var getChildren = model.getChildren; model.getChildren = function(node) { if (isFavoriteNode(node)) { var realNode = fsCache.findNode(node.path); var nodes = realNode && getChildren.call(model, realNode) || []; nodes.forEach(function(child) { child.$depth = node.$depth + 1; }); return nodes; } else if (isFavoritePath(node.path)) return []; else return getChildren.call(model, node); }; var hasChildren = model.hasChildren; model.hasChildren = function(node) { if (isFavoriteNode(node)) { var realNode = fsCache.findNode(node.path); return realNode ? hasChildren.call(model, realNode) : true; } else if (isFavoritePath(node.path)) return false; else return hasChildren.call(model, node); }; var setRoot = model.setRoot; model.setRoot = function(node) { if (node != altRoot) { if (!node.empty) { altRoot.children.push(node); node.isRoot = true; node.isFSRoot = true; node.label = "file system"; node.isFolder = true; node.path = "!fsroot"; node.status = "loaded"; node.className = "heading"; node.noSelect = true; altRoot.map = node.map; } else { if (!altRoot.map) altRoot.map = { "": model.realRoot }; if (favRoot.children.length == 2) favRoot.children.splice(1, 1); } } // model.realRoot = node; setRoot.call(model, altRoot); }; var getRowIndent = model.getRowIndent; model.getRowIndent = function(node) { return node.$depth ? node.$depth - 1 : 0; }; var getIconHTML = model.getIconHTML; model.getIconHTML = function(node) { if (node.isFavorite) { var realNode = fsCache.findNode(node.path); node = { label: node.path, status: realNode ? realNode.status : node.status, isFolder: node.isFolder }; } return getIconHTML.call(model, node); }; var getCaptionHTML = model.getCaptionHTML; model.getCaptionHTML = function(node) { if (node.isFavorite) { var path = node.labelPath || node.path; return basename(path) + " - " + dirname(path) + ""; } else return getCaptionHTML.call(model, node); }; if (model.root) model.setRoot(model.root); toggleRootFS(); stored = { setRoot: setRoot, getRowIndent: getRowIndent, getChildren: getChildren, hasChildren: hasChildren, getCaptionHTML: getCaptionHTML, getClassName: getClassName }; } function unwrap(){ enabled = false; for (var prop in stored) { model[prop] = stored[prop]; } stored = null; lut = {}; altRoot.children.splice(1, 1); var node = model.realRoot; delete node.isRoot; delete node.isFSRoot; delete node.label; delete node.isFolder; delete node.path; delete node.className; delete node.$depth; model.setRoot(node); toggleRootFS(); } function isFavoritePath(path) { return lut[path]; } function isFavoriteNode(node) { return lut[node.path] == node; } function getFavoritePaths() { return favRoot && favRoot.children.map(function(n){ return n.path }) || []; } function addFavorite(path, name, init) { var node, favNode; if (lut[path.path || path]) return lut[path.path || path]; if (typeof path == "string") { node = fsCache.findNode(path); favNode = { path: path, labelPath: name, isFavorite: true, isRootContext: true, isFolder: node ? node.isFolder : true, offset: node ? node.$depth : path.split("/").length - 1 }; if (favNode.isFolder) { favNode.status = "pending"; favNode.map = node ? node.map : {}; favNode.isOpen = node && node.isOpen; } } else { favNode = path; path = favNode.path; favNode.status = "pending"; favNode.map = {}; path = favNode.path; } lut[path] = favNode; favRoot.children.push(favNode); updateRegExp(); if (path == "~") { favRoot.map["~"] = favNode; } changed = true; settings.save(); if (!enabled) wrap(); fsCache.refresh(node); fsCache.refresh(favRoot); // tree.expand(favNode); watcher.watch(path == "~" ? path : dirname(path)); emit("favoriteAdd", { path: path, init: init }); return favNode; } function removeFavorite(path) { var node = lut[path]; if (!node) return; favRoot.children.splice(favRoot.children.indexOf(node), 1); delete lut[path]; updateRegExp(); changed = true; settings.save(); if (path == "~") { delete favRoot.map["~"]; delete model.root.map["~"]; } if (!favRoot.children.length) unwrap(); var realNode = fsCache.findNode(path); realNode && fsCache.refresh(realNode); fsCache.refresh(favRoot); emit("favoriteRemove", { path: path, node: node }); } function updateRegExp(){ var paths = Object.keys(lut).map(function(p){ return util.escapeRegExp(p.replace(/^~/, c9.home)); }); reFavs = paths.length ? new RegExp("^(" + paths.join("|") + ")\/") : ""; } function toggleRootFS(force) { var showFS = typeof force == "boolean" ? force : settings.getBool("state/projecttree/@showfs"); if (!showFS && !enabled) model.hideAllNodes(); else if (!showFS && enabled) { altRoot.children.splice(1, 1); model.showAllNodes(); } else if (showFS && !enabled) { model.showAllNodes(); // @todo setRoot ?? } else if (showFS && enabled) { if (altRoot.children.length == 1) altRoot.children.push(model.realRoot); model.showAllNodes(); } if (force) settings.set("state/projecttree/@showfs", force); } /***** Lifecycle *****/ plugin.on("load", function() { load(); }); plugin.on("enable", function() { }); plugin.on("disable", function() { }); plugin.on("unload", function() { unwrap(); loaded = false; }); /***** Register and define API *****/ /** * **/ plugin.freezePublicAPI({ get favorites(){ return getFavoritePaths(); }, _events: [ /** * @event favoriteAdd */ "favoriteAdd", /** * @event favoriteRemove */ "favoriteRemove" ], /** * */ isFavoritePath: isFavoritePath, /** * */ isFavoriteNode: isFavoriteNode, /** * */ addFavorite: addFavorite, /** * */ removeFavorite: removeFavorite, /** * */ getFavoritePaths: getFavoritePaths, }); register(null, { "tree.favorites": plugin }); } });