diff --git a/src/brackets.js b/src/brackets.js index 40d72ff5e7c..77c0c322490 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -68,6 +68,7 @@ define(function (require, exports, module) { CSSInlineEditor = require("editor/CSSInlineEditor"), JSUtils = require("language/JSUtils"), WorkingSetView = require("project/WorkingSetView"), + WorkingSetSort = require("project/WorkingSetSort"), DocumentCommandHandlers = require("document/DocumentCommandHandlers"), FileViewController = require("project/FileViewController"), FileSyncManager = require("project/FileSyncManager"), diff --git a/src/command/Commands.js b/src/command/Commands.js index 90fbdbafbfe..5867786d1ef 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -73,6 +73,10 @@ define(function (require, exports, module) { exports.VIEW_DECREASE_FONT_SIZE = "view.decreaseFontSize"; exports.VIEW_RESTORE_FONT_SIZE = "view.restoreFontSize"; exports.TOGGLE_JSLINT = "debug.jslint"; + exports.SORT_WORKINGSET_BY_ADDED = "view.sortWorkingSetByAdded"; + exports.SORT_WORKINGSET_BY_NAME = "view.sortWorkingSetByName"; + exports.SORT_WORKINGSET_BY_TYPE = "view.sortWorkingSetByType"; + exports.SORT_WORKINGSET_AUTO = "view.sortWorkingSetAuto"; // Navigate exports.NAVIGATE_NEXT_DOC = "navigate.nextDoc"; diff --git a/src/command/Menus.js b/src/command/Menus.js index 59c4cf3a2b9..cadfc5c5e0c 100644 --- a/src/command/Menus.js +++ b/src/command/Menus.js @@ -974,6 +974,12 @@ define(function (require, exports, module) { working_set_cmenu.addMenuItem(Commands.FILE_SAVE); working_set_cmenu.addMenuItem(Commands.FILE_RENAME); working_set_cmenu.addMenuItem(Commands.NAVIGATE_SHOW_IN_FILE_TREE); + working_set_cmenu.addMenuDivider(); + working_set_cmenu.addMenuItem(Commands.SORT_WORKINGSET_BY_ADDED); + working_set_cmenu.addMenuItem(Commands.SORT_WORKINGSET_BY_NAME); + working_set_cmenu.addMenuItem(Commands.SORT_WORKINGSET_BY_TYPE); + working_set_cmenu.addMenuDivider(); + working_set_cmenu.addMenuItem(Commands.SORT_WORKINGSET_AUTO); var editor_cmenu = registerContextMenu(ContextMenuIds.EDITOR_MENU); editor_cmenu.addMenuItem(Commands.TOGGLE_QUICK_EDIT); diff --git a/src/document/DocumentManager.js b/src/document/DocumentManager.js index 945dcbcef49..8d967c8f793 100644 --- a/src/document/DocumentManager.js +++ b/src/document/DocumentManager.js @@ -66,6 +66,8 @@ * The 2nd arg to the listener is the array of removed FileEntry objects. * - fileNameChange -- When the name of a file or folder has changed. The 2nd arg is the old name. * The 3rd arg is the new name. + * - workingSetReorder -- When the indexes of 2 files are swapped during a drag and drop order. + * - workingSetSort -- When the workingSet array is sorted. * * These are jQuery events, so to listen for them you do something like this: * $(DocumentManager).on("eventname", handler); @@ -120,11 +122,18 @@ define(function (require, exports, module) { /** * @private - * Contains the same set of items as _workinSet, but ordered by how recently they were _currentDocument (0 = most recent). + * Contains the same set of items as _workingSet, but ordered by how recently they were _currentDocument (0 = most recent). * @type {Array.} */ var _workingSetMRUOrder = []; + /** + * @private + * Contains the same set of items as _workingSet, but ordered in the way they where added to _workingSet (0 = last added). + * @type {Array.} + */ + var _workingSetAddedOrder = []; + /** * While true, the MRU order is frozen * @type {boolean} @@ -176,6 +185,16 @@ define(function (require, exports, module) { return file.fullPath === fullPath; }); } + + /** + * Returns the index of the file matching fullPath in _workingSetAddedOrder. + * Returns -1 if not found. + * @param {!string} fullPath + * @returns {number} index + */ + function findInWorkingSetAddedOrder(fullPath) { + return findInWorkingSet(fullPath, _workingSetAddedOrder); + } /** * Returns all Documents that are 'open' in the UI somewhere (for now, this means open in an @@ -218,6 +237,9 @@ define(function (require, exports, module) { _workingSetMRUOrder.push(file); } + // Add first to Added order + _workingSetAddedOrder.unshift(file); + // Dispatch event $(exports).triggerHandler("workingSetAdd", file); } @@ -233,7 +255,7 @@ define(function (require, exports, module) { var uniqueFileList = []; // Process only files not already in working set - fileList.forEach(function (file) { + fileList.forEach(function (file, index) { // If doc is already in working set, don't add it again if (findInWorkingSet(file.fullPath) === -1) { uniqueFileList.push(file); @@ -247,8 +269,12 @@ define(function (require, exports, module) { } else { _workingSetMRUOrder.push(file); } + + // Add first to Added order + _workingSetAddedOrder.splice(index, 1, file); } }); + // Dispatch event $(exports).triggerHandler("workingSetAddList", [uniqueFileList]); @@ -269,6 +295,7 @@ define(function (require, exports, module) { // Remove _workingSet.splice(index, 1); _workingSetMRUOrder.splice(findInWorkingSet(file.fullPath, _workingSetMRUOrder), 1); + _workingSetAddedOrder.splice(findInWorkingSet(file.fullPath, _workingSetAddedOrder), 1); // Dispatch event $(exports).triggerHandler("workingSetRemove", file); @@ -283,6 +310,7 @@ define(function (require, exports, module) { // Remove all _workingSet = []; _workingSetMRUOrder = []; + _workingSetAddedOrder = []; // Dispatch event $(exports).triggerHandler("workingSetRemoveList", [fileList]); @@ -314,9 +342,24 @@ define(function (require, exports, module) { temp = _workingSet[index1]; _workingSet[index1] = _workingSet[index2]; _workingSet[index2] = temp; + + // Dispatch event + $(exports).triggerHandler("workingSetReorder"); } } + /** + * Sorts _workingSet using the compare function + * @param {!function(FileEntry, FileEntry)} compareFn - the function that will be used inside JavaScript's + * sort function. The return a value should be >0 (sort a to a lower index than b), =0 (leaves a and b + * unchanged with respect to each other) or <0 (sort b to a lower index than a) and must always returns + * the same value when given a specific pair of elements a and b as its two arguments. + * Documentation: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/sort + */ + function sortWorkingSet(compareFn) { + _workingSet.sort(compareFn); + $(exports).triggerHandler("workingSetSort"); + } /** * Indicate that changes to currentDocument are temporary for now, and should not update the MRU @@ -1128,29 +1171,31 @@ define(function (require, exports, module) { } // Define public API - exports.Document = Document; - exports.getCurrentDocument = getCurrentDocument; - exports.getDocumentForPath = getDocumentForPath; - exports.getOpenDocumentForPath = getOpenDocumentForPath; - exports.getWorkingSet = getWorkingSet; - exports.findInWorkingSet = findInWorkingSet; - exports.getAllOpenDocuments = getAllOpenDocuments; - exports.setCurrentDocument = setCurrentDocument; - exports.addToWorkingSet = addToWorkingSet; - exports.addListToWorkingSet = addListToWorkingSet; - exports.removeFromWorkingSet = removeFromWorkingSet; - exports.getNextPrevFile = getNextPrevFile; - exports.swapWorkingSetIndexes = swapWorkingSetIndexes; - exports.beginDocumentNavigation = beginDocumentNavigation; - exports.finalizeDocumentNavigation = finalizeDocumentNavigation; - exports.closeFullEditor = closeFullEditor; - exports.closeAll = closeAll; - exports.notifyFileDeleted = notifyFileDeleted; - exports.notifyPathNameChanged = notifyPathNameChanged; + exports.Document = Document; + exports.getCurrentDocument = getCurrentDocument; + exports.getDocumentForPath = getDocumentForPath; + exports.getOpenDocumentForPath = getOpenDocumentForPath; + exports.getWorkingSet = getWorkingSet; + exports.findInWorkingSet = findInWorkingSet; + exports.findInWorkingSetAddedOrder = findInWorkingSetAddedOrder; + exports.getAllOpenDocuments = getAllOpenDocuments; + exports.setCurrentDocument = setCurrentDocument; + exports.addToWorkingSet = addToWorkingSet; + exports.addListToWorkingSet = addListToWorkingSet; + exports.removeFromWorkingSet = removeFromWorkingSet; + exports.getNextPrevFile = getNextPrevFile; + exports.swapWorkingSetIndexes = swapWorkingSetIndexes; + exports.sortWorkingSet = sortWorkingSet; + exports.beginDocumentNavigation = beginDocumentNavigation; + exports.finalizeDocumentNavigation = finalizeDocumentNavigation; + exports.closeFullEditor = closeFullEditor; + exports.closeAll = closeAll; + exports.notifyFileDeleted = notifyFileDeleted; + exports.notifyPathNameChanged = notifyPathNameChanged; // Setup preferences _prefs = PreferencesManager.getPreferenceStorage(PREFERENCES_CLIENT_ID); - $(exports).bind("currentDocumentChange workingSetAdd workingSetAddList workingSetRemove workingSetRemoveList fileNameChange workingSetReorder", _savePreferences); + $(exports).bind("currentDocumentChange workingSetAdd workingSetAddList workingSetRemove workingSetRemoveList fileNameChange workingSetReorder workingSetSort", _savePreferences); // Performance measurements PerfUtils.createPerfMeasurement("DOCUMENT_MANAGER_GET_DOCUMENT_FOR_PATH", "DocumentManager.getDocumentForPath()"); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index bd1814decb3..0aa2e36f4af 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -194,6 +194,10 @@ define({ "CMD_INCREASE_FONT_SIZE" : "Increase Font Size", "CMD_DECREASE_FONT_SIZE" : "Decrease Font Size", "CMD_RESTORE_FONT_SIZE" : "Restore Font Size", + "CMD_SORT_WORKINGSET_BY_ADDED" : "Sort by Added", + "CMD_SORT_WORKINGSET_BY_NAME" : "Sort by Name", + "CMD_SORT_WORKINGSET_BY_TYPE" : "Sort by Type", + "CMD_SORT_WORKINGSET_AUTO" : "Automatic Sort", // Navigate menu Commands "NAVIGATE_MENU" : "Navigate", diff --git a/src/project/WorkingSetSort.js b/src/project/WorkingSetSort.js new file mode 100644 index 00000000000..5a0eb566fb1 --- /dev/null +++ b/src/project/WorkingSetSort.js @@ -0,0 +1,377 @@ +/* + * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ +/*global define, $, window */ + +/** + * Manages the workingSet sort methods. + */ +define(function (require, exports, module) { + "use strict"; + + var Commands = require("command/Commands"), + CommandManager = require("command/CommandManager"), + DocumentManager = require("document/DocumentManager"), + PreferencesManager = require("preferences/PreferencesManager"), + AppInit = require("utils/AppInit"), + Strings = require("strings"); + + var PREFERENCES_CLIENT_ID = "com.adobe.brackets.WorkingSetSort", + defaultPrefs = { + currentSort: Commands.SORT_WORKINGSET_BY_ADDED, + automaticSort: false + }; + + /** + * @private + * @type {PreferenceStorage} + */ + var _prefs = {}; + + /** + * @private + * @type {Array.} + */ + var _sorts = []; + + /** + * @private + * @type {Sort} + */ + var _currentSort = null; + + /** + * @private + * @type {boolean} + */ + var _automaticSort = false; + + /** + * @private + * @type {boolean} + * Used to know when to do the automatic sort for MRU order. + */ + var _openedDocument = false; + + /** + * Retrieves a Sort object by id + * @param {string} commandID + * @return {Sort} + */ + function get(commandID) { + return _sorts[commandID]; + } + + /** + * @return {boolean} Enabled state of Automatic Sort. + */ + function getAutomatic() { + return _automaticSort; + } + + + /** + * @private + * Sets the value of Automatic Sort and updates the menu item. + * @param {boolean} value + */ + function _setAutomatic(value) { + _automaticSort = value; + _prefs.setValue("automaticSort", _automaticSort); + CommandManager.get(Commands.SORT_WORKINGSET_AUTO).setChecked(_automaticSort); + } + + /** + * @private + * Removes current sort DocumentManager listeners. + */ + function _removeListeners() { + if (_currentSort && _currentSort.getEvents()) { + $(DocumentManager).off(".sort"); + } + } + + /** + * @private + * Disables Automatic Sort. + */ + function _disableAutomatic() { + _setAutomatic(false); + _removeListeners(); + } + + /** + * @private + * Adds current sort DocumentManager listeners. + */ + function _addListeners() { + if (_automaticSort && _currentSort && _currentSort.getEvents()) { + $(DocumentManager) + .on(_currentSort.getEvents(), function (event) { + _currentSort.callAutomaticFn(event); + }) + .on("workingSetReorder.sort", function () { + _disableAutomatic(); + }); + } + } + + /** + * @private + * Enables Automatic Sort. + */ + function _enableAutomatic() { + _setAutomatic(true); + _addListeners(); + } + + /** + * Enables/Disables Automatic Sort depending on the value. + * @param {boolean} value + */ + function setAutomatic(value) { + if (value) { + _enableAutomatic(); + } else { + _disableAutomatic(); + } + } + + + /** + * @private + * Sets the current sort method and checks it on the context menu. + * @param {Sort} newSort + */ + function _setCurrentSort(newSort) { + var command; + if (_currentSort !== newSort) { + if (_currentSort !== null) { + command = CommandManager.get(_currentSort.getCommandID()); + if (command) { + command.setChecked(false); + } + } + command = CommandManager.get(newSort.getCommandID()); + if (command) { + command.setChecked(true); + } + + CommandManager.get(Commands.SORT_WORKINGSET_AUTO).setEnabled(!!newSort.getEvents()); + _currentSort = newSort; + _prefs.setValue("currentSort", _currentSort.getCommandID()); + } + } + + + /** + * @constructor + * @private + * + * @param {!string} commandID - valid command identifier. + * @param {!function(FileEntry, FileEntry)} compareFn - a valid sort function (see register for a longer explanation). + * @param {!string} events - space-separated DocumentManager possible events ending on ".sort". + * @param {!function($.Event)} automaticFn - the function that will be called when an automatic sort event is triggered. + */ + function Sort(commandID, compareFn, events, automaticFn) { + this._commandID = commandID; + this._compareFn = compareFn; + this._events = events; + this._automaticFn = automaticFn; + } + + /** @return {CommandID} */ + Sort.prototype.getCommandID = function () { + return this._commandID; + }; + + /** @return {CompareFn} */ + Sort.prototype.getCompareFn = function () { + return this._compareFn; + }; + + /** @return {Events} */ + Sort.prototype.getEvents = function () { + return this._events; + }; + + /** Calls automaticFn */ + Sort.prototype.callAutomaticFn = function (event) { + return this._automaticFn(event); + }; + + /** + * Performs the sort and makes it the current sort method + */ + Sort.prototype.execute = function () { + _removeListeners(); + _setCurrentSort(this); + _addListeners(); + this.sort(); + }; + + /** + * Only performs the working set sort if this is the current sort + */ + Sort.prototype.sort = function () { + if (_currentSort === this) { + DocumentManager.sortWorkingSet(this._compareFn); + } + }; + + + /** + * Registers a working set sort method. + * @param {!string} commandID - valid command identifier used for the sort method. + * Core commands in Brackets use a simple command title as an id, for example "open.file". + * Extensions should use the following format: "author.myextension.mycommandname". + * For example, "lschmitt.csswizard.format.css". + * @param {!function(FileEntry, FileEntry)} compareFn - the function that will be used inside JavaScript's + * sort function. The return a value should be >0 (sort a to a lower index than b), =0 (leaves a and b + * unchanged with respect to each other) or <0 (sort b to a lower index than a) and must always returns + * the same value when given a specific pair of elements a and b as its two arguments. + * Documentation: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/sort + * @param {?string} events - one or more space-separated event types that DocumentManger uses. + * Each event passed will trigger the automatic sort. If no events are passed, the automatic + * sort will be disabled for that sort method. + * @param {?function($.Event)} automaticFn - the function that will be called when an automatic sort + * event is triggered. If no function is passed the automatic sort will just call the sort function. + * @return {?Sort} + */ + function register(commandID, compareFn, events, automaticFn) { + if (_sorts[commandID]) { + console.log("Attempting to register an already-registered sort: " + commandID); + return; + } + if (!commandID || !compareFn) { + console.log("Attempting to register a sort with a missing id, or compare function: " + commandID); + return; + } + + // Adds ".sort" to the end of each event to make them specific for the automatic sort + if (events) { + events = events.split(" "); + events.forEach(function (event, index) { + events[index] = events[index].trim() + ".sort"; + }); + events = events.join(" "); + } + + automaticFn = automaticFn || function (event) { + _currentSort.sort(); + }; + + var sort = new Sort(commandID, compareFn, events, automaticFn); + _sorts[commandID] = sort; + return sort; + } + + + /** Command Handlers */ + function _handleSortWorkingSetByAdded() { + get(Commands.SORT_WORKINGSET_BY_ADDED).execute(); + } + + function _handleSortWorkingSetByName() { + get(Commands.SORT_WORKINGSET_BY_NAME).execute(); + } + + function _handleSortWorkingSetByType() { + get(Commands.SORT_WORKINGSET_BY_TYPE).execute(); + } + + function _handleAutomaticSort() { + setAutomatic(!getAutomatic()); + } + + + // Register Sort Methods + register( + Commands.SORT_WORKINGSET_BY_ADDED, + function (file1, file2) { + var index1 = DocumentManager.findInWorkingSetAddedOrder(file1.fullPath), + index2 = DocumentManager.findInWorkingSetAddedOrder(file2.fullPath); + return index1 - index2; + }, + "workingSetAdd workingSetAddList" + ); + register( + Commands.SORT_WORKINGSET_BY_NAME, + function (file1, file2) { + return file1.name.toLocaleLowerCase().localeCompare(file2.name.toLocaleLowerCase()); + }, + "workingSetAdd workingSetAddList" + ); + register( + Commands.SORT_WORKINGSET_BY_TYPE, + function (file1, file2) { + var ext1 = file1.name.split('.').pop(), + ext2 = file2.name.split('.').pop(), + cmp = ext1.localeCompare(ext2); + + if (cmp === 0) { + return file1.name.toLocaleLowerCase().localeCompare(file2.name.toLocaleLowerCase()); + } else { + return cmp; + } + }, + "workingSetAdd workingSetAddList" + ); + + + // Register Command Handlers + CommandManager.register(Strings.CMD_SORT_WORKINGSET_BY_ADDED, Commands.SORT_WORKINGSET_BY_ADDED, _handleSortWorkingSetByAdded); + CommandManager.register(Strings.CMD_SORT_WORKINGSET_BY_NAME, Commands.SORT_WORKINGSET_BY_NAME, _handleSortWorkingSetByName); + CommandManager.register(Strings.CMD_SORT_WORKINGSET_BY_TYPE, Commands.SORT_WORKINGSET_BY_TYPE, _handleSortWorkingSetByType); + CommandManager.register(Strings.CMD_SORT_WORKINGSET_AUTO, Commands.SORT_WORKINGSET_AUTO, _handleAutomaticSort); + + + // Initialize PreferenceStorage + _prefs = PreferencesManager.getPreferenceStorage(PREFERENCES_CLIENT_ID, defaultPrefs); + + + // Initialize items dependent on extensions/workingSet + AppInit.appReady(function () { + var curSort = get(_prefs.getValue("currentSort")), + autoSort = _prefs.getValue("automaticSort"); + + if (curSort) { + _setCurrentSort(curSort); + } + if (autoSort) { + _enableAutomatic(); + } + if (curSort && autoSort) { + curSort.sort(); + } + }); + + + // Define public API + exports.register = register; + exports.get = get; + exports.getAutomatic = getAutomatic; + exports.setAutomatic = setAutomatic; +}); diff --git a/src/project/WorkingSetView.js b/src/project/WorkingSetView.js index fb8a445c9dc..0e50c4cbd02 100644 --- a/src/project/WorkingSetView.js +++ b/src/project/WorkingSetView.js @@ -245,9 +245,6 @@ define(function (require, exports, module) { ViewUtils.addScrollerShadow($openFilesContainer[0], null, true); } } - - // Dispatch event - $(DocumentManager).triggerHandler("workingSetReorder"); } @@ -275,7 +272,6 @@ define(function (require, exports, module) { $openFilesContainer.on("mouseup.workingSet mouseleave.workingSet", function (e) { $openFilesContainer.off("mousemove.workingSet mouseup.workingSet mouseleave.workingSet"); drop(); - }); } @@ -506,6 +502,14 @@ define(function (require, exports, module) { _redraw(); } + + /** + * @private + */ + function _handleWorkingSetSort() { + _rebuildWorkingSet(); + _scrollSelectedDocIntoView(); + } /** * @private @@ -553,6 +557,10 @@ define(function (require, exports, module) { $(DocumentManager).on("workingSetRemoveList", function (event, removedFiles) { _handleRemoveList(removedFiles); }); + + $(DocumentManager).on("workingSetSort", function (event) { + _handleWorkingSetSort(); + }); $(DocumentManager).on("dirtyFlagChange", function (event, doc) { _handleDirtyFlagChanged(doc);