From 14375eb8776409648dab0cb539a7fbbd957bbeda Mon Sep 17 00:00:00 2001 From: njadbe <njaramil@adobe.com> Date: Wed, 14 Dec 2011 15:33:07 -0800 Subject: [PATCH 01/14] Initial implementation of File > Save and dirty bit management --- src/Commands.js | 3 ++- src/brackets.js | 60 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/Commands.js b/src/Commands.js index f7927341a80..829552fd979 100644 --- a/src/Commands.js +++ b/src/Commands.js @@ -4,5 +4,6 @@ // List of constants for global command IDs. var Commands = { - FILE_OPEN: "file.open" + FILE_OPEN: "file.open", + FILE_SAVE: "file.save" }; diff --git a/src/brackets.js b/src/brackets.js index 4423ca4d2bb..814877100c6 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -28,10 +28,13 @@ $(document).ready(function() { ProjectManager.openProject(); }); - // Implements the "Open File" menu + // Implements the File menu items $("#menu-file-open").click(function() { CommandManager.execute(Commands.FILE_OPEN); }); + $("#menu-file-save").click(function() { + CommandManager.execute(Commands.FILE_SAVE); + }) // Implements the 'Run Tests' menu to bring up the Jasmine unit test window var testWindow = null; @@ -49,8 +52,42 @@ $(document).ready(function() { testWindow.location.reload(); // if it was opened before, we need to reload because it will be cached } }); + + // Application state + // TODO: factor this stuff out into a real app controller + var _currentFilePath = null; + var _currentTitlePath = null; + var _isDirty = false; + var _savedUndoPosition = 0; + + editor.setOption("onChange", function() { + updateDirty(); + }); // Utility functions + function updateDirty() { + // TODO: This doesn't currently work properly with undo because of brackets-app issue #9. + // So files get dirty, but they never get un-dirty. + + // If we've undone past the undo position at the last save, and there is no redo stack, + // then we can never get back to non-dirty state. + var historySize = editor.historySize(); + var historyInfo = editor.historyInfo(); + console.log(JSON.stringify(historyInfo)); + if (historySize.undo < _savedUndoPosition && historySize.redo == 0) { + _savedUndoPosition = -1; + } + var newIsDirty = (editor.historySize().undo != _savedUndoPosition); + if (_isDirty != newIsDirty) { + _isDirty = newIsDirty; + updateTitle(); + } + } + + function updateTitle() { + $("#main-toolbar .title").text(_currentTitlePath + (_isDirty ? " \u2022" : "")); + } + function doOpen(fullPath) { if (fullPath) { // TODO: use higher-level file API instead of raw API @@ -59,6 +96,8 @@ $(document).ready(function() { // TODO--this will change with the real file API implementation } else { + _currentFilePath = _currentTitlePath = fullPath; + // TODO: have a real controller object for the editor editor.setValue(content); @@ -66,10 +105,12 @@ $(document).ready(function() { if (fullPath.indexOf(projectRootPath) == 0) { fullPath = fullPath.slice(projectRootPath.length); if (fullPath.charAt(0) == '/') { - fullPath = fullPath.slice(1); + _currentTitlePath = fullPath.slice(1); } } - $("#main-toolbar .title").text(fullPath); + editor.clearHistory(); + _savedUndoPosition = editor.historySize().undo; + updateDirty(); } }); } @@ -91,4 +132,17 @@ $(document).ready(function() { } }); + CommandManager.register(Commands.FILE_SAVE, function() { + if (_currentFilePath && _isDirty) { + brackets.fs.writeFile(_currentFilePath, editor.getValue(), "utf8", function(err) { + if (err) { + // TODO--this will change with the real file API implementation + } + else { + _savedUndoPosition = editor.historySize().undo; + updateDirty(); + } + }); + } + }); }); From f048b02f1232898588cc5192ccc00bc925b26704 Mon Sep 17 00:00:00 2001 From: njadbe <njaramil@adobe.com> Date: Wed, 14 Dec 2011 16:17:55 -0800 Subject: [PATCH 02/14] Removed call to temp debug function --- src/brackets.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/brackets.js b/src/brackets.js index 814877100c6..a705e8b1c79 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -72,8 +72,6 @@ $(document).ready(function() { // If we've undone past the undo position at the last save, and there is no redo stack, // then we can never get back to non-dirty state. var historySize = editor.historySize(); - var historyInfo = editor.historyInfo(); - console.log(JSON.stringify(historyInfo)); if (historySize.undo < _savedUndoPosition && historySize.redo == 0) { _savedUndoPosition = -1; } From c4493d7b42973438863554e68a63dd5dcb7510ed Mon Sep 17 00:00:00 2001 From: njadbe <njaramil@adobe.com> Date: Thu, 15 Dec 2011 09:57:37 -0800 Subject: [PATCH 03/14] Added comments around our dirty bit management --- src/brackets.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/brackets.js b/src/brackets.js index a705e8b1c79..cc8e12a16e2 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -66,11 +66,8 @@ $(document).ready(function() { // Utility functions function updateDirty() { - // TODO: This doesn't currently work properly with undo because of brackets-app issue #9. - // So files get dirty, but they never get un-dirty. - // If we've undone past the undo position at the last save, and there is no redo stack, - // then we can never get back to non-dirty state. + // then we can never get back to a non-dirty state. var historySize = editor.historySize(); if (historySize.undo < _savedUndoPosition && historySize.redo == 0) { _savedUndoPosition = -1; @@ -106,7 +103,11 @@ $(document).ready(function() { _currentTitlePath = fullPath.slice(1); } } + + // Make sure we can't undo back to the previous content. editor.clearHistory(); + + // This should be 0, but just to be safe... _savedUndoPosition = editor.historySize().undo; updateDirty(); } @@ -137,6 +138,8 @@ $(document).ready(function() { // TODO--this will change with the real file API implementation } else { + // Remember which position in the undo stack we're at as of the last save. + // When we're exactly at that position again, we know we're not dirty. _savedUndoPosition = editor.historySize().undo; updateDirty(); } From 8941cd9cc6583a0151f3b9f0e61ab946fb3babd0 Mon Sep 17 00:00:00 2001 From: njadbe <njaramil@adobe.com> Date: Thu, 15 Dec 2011 15:56:29 -0800 Subject: [PATCH 04/14] Beginning implementation of prompt when closing dirty file --- src/Commands.js | 3 ++- src/brackets.js | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Commands.js b/src/Commands.js index 829552fd979..b1d7c127eba 100644 --- a/src/Commands.js +++ b/src/Commands.js @@ -5,5 +5,6 @@ // List of constants for global command IDs. var Commands = { FILE_OPEN: "file.open", - FILE_SAVE: "file.save" + FILE_SAVE: "file.save", + FILE_CLOSE: "file.close" }; diff --git a/src/brackets.js b/src/brackets.js index cc8e12a16e2..36cfaa889a5 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -117,6 +117,15 @@ $(document).ready(function() { // Register global commands CommandManager.register(Commands.FILE_OPEN, function(fullPath) { + // TODO: In the future, when we implement multiple open files, we won't close the previous file when opening + // a new one. However, for now, since we only support a single open document, I'm pretending as if we're + // closing the existing file first. This is so that I can put the code that checks for an unsaved file and + // prompts the user to save it in the close command, where it belongs. When we implement multiple open files, + // we can remove this here. + if (_currentFilePath) { + CommandManager.execute(Commands.FILE_CLOSE); + } + if (!fullPath) { // Prompt the user with a dialog NativeFileSystem.showOpenDialog(false, false, "Open File", ProjectManager.getProjectRoot().fullPath, From 0d169b91554859284949b91cf0a0b59897be64ee Mon Sep 17 00:00:00 2001 From: njadbe <njaramil@adobe.com> Date: Thu, 15 Dec 2011 18:48:38 -0800 Subject: [PATCH 05/14] Implementing file close--prompting to save not yet implemented --- src/brackets.js | 23 +++++++++++++++++++++-- src/index.html | 1 + 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/brackets.js b/src/brackets.js index ac991a431b9..4954d506980 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -59,9 +59,12 @@ $(document).ready(function() { $("#menu-file-open").click(function() { CommandManager.execute(Commands.FILE_OPEN); }); + $("#menu-file-close").click(function() { + CommandManager.execute(Commands.FILE_CLOSE); + }); $("#menu-file-save").click(function() { CommandManager.execute(Commands.FILE_SAVE); - }) + }); // Implements the 'Run Tests' menu to bring up the Jasmine unit test window var testWindow = null; @@ -107,7 +110,7 @@ $(document).ready(function() { } function updateTitle() { - $("#main-toolbar .title").text(_currentTitlePath + (_isDirty ? " \u2022" : "")); + $("#main-toolbar .title").text(_currentTitlePath ? (_currentTitlePath + (_isDirty ? " \u2022" : "")) : "Untitled"); } function doOpen(fullPath) { @@ -200,4 +203,20 @@ $(document).ready(function() { }); } }); + + CommandManager.register(Commands.FILE_CLOSE, function() { + if (_currentFilePath && _isDirty) { + // *** TODO: prompt to save + } + + // TODO: When we implement multiple files being open, this will probably change to just + // dispose of the editor for the current file (and will later change again if we choose to + // limit the number of open editors). + editor.setValue(""); + editor.clearHistory(); + _currentFilePath = _currentTitlePath = null; + _savedUndoPosition = 0; + _isDirty = false; + updateTitle(); + }); }); diff --git a/src/index.html b/src/index.html index 442fcd6858a..d2456289d3a 100644 --- a/src/index.html +++ b/src/index.html @@ -41,6 +41,7 @@ <ul class="dropdown-menu"> <li><a href="#" id="menu-file-new">New</a></li> <li><a href="#" id="menu-file-open">Open</a></li> + <li><a href="#" id="menu-file-close">Close</a></li> <li><a href="#" id="menu-file-save">Save</a></li> <li><a href="#" id="menu-file-quit">Quit</a></li> </ul> From 33e8cfe0c083313604d6c8e47f631227cf99101d Mon Sep 17 00:00:00 2001 From: njadbe <njaramil@adobe.com> Date: Thu, 15 Dec 2011 22:23:42 -0800 Subject: [PATCH 06/14] Added dialog when closing dirty file. Refactored dialog code to make it more generic, driven by HTML. Added basic keymapping functions. --- src/CommandManager.js | 2 + src/KeyBindingManager.js | 112 +++++++++++++++++++++++++++++++++++++++ src/ProjectManager.js | 15 +++--- src/brackets.js | 95 ++++++++++++++++++++++++++------- src/index.html | 22 ++++++-- src/strings.js | 7 ++- src/styles/brackets.less | 4 ++ 7 files changed, 227 insertions(+), 30 deletions(-) create mode 100644 src/KeyBindingManager.js diff --git a/src/CommandManager.js b/src/CommandManager.js index fa4e20f51e4..5849cc0cefc 100644 --- a/src/CommandManager.js +++ b/src/CommandManager.js @@ -26,6 +26,8 @@ CommandManager.register = function(id, command) { * @param {string} id The ID of the command to run. */ CommandManager.execute = function(id) { + // TODO: are we guaranteed that all commands are synchronous? If not, we should + // take a callback, or (perhaps better) return a Deferred. var command = CommandManager._commands[id]; if (command) { command.apply(null, Array.prototype.slice.call(arguments, 1)); diff --git a/src/KeyBindingManager.js b/src/KeyBindingManager.js new file mode 100644 index 00000000000..a5333b4ac86 --- /dev/null +++ b/src/KeyBindingManager.js @@ -0,0 +1,112 @@ +/* + * Copyright 2011 Adobe Systems Incorporated. All Rights Reserved. + */ + +var KeyBindingManager = { + /** + * The map of all registered keymaps. + */ + _keymaps: {}, + + /** + * The map of currently active keymaps. As new keymaps come into effect, they are pushed onto the end of + * the array. Later keymaps take precedence over earlier ones. + */ + _activeKeymaps: {}, + + /** + * The maximum possible keymap level. + */ + _maxLevel: -1, + + /** + * Registers the specified keymap based on its id. + * + * TODO: do Closure annotations support user-defined types? + * @param {KeyMap} keymap The keymap to register. + */ + registerKeymap: function(keymap) { + KeyBindingManager._keymaps[keymap.id] = keymap; + if (keymap.level > KeyBindingManager._maxLevel) { + KeyBindingManager._maxLevel = keymap.level; + } + }, + + /** + * Activate the specified keymap. This installs it at the appropriate level, replacing any other keymap + * at that level. + * + * TODO: does the Closure annotation syntax support user-created types? + * @param {string} keymap The ID of the registered keymap to install. + */ + activateKeymap: function(id) { + var keymap = KeyBindingManager._keymaps[id]; + if (!keymap) { + throw new Error("Keymap " + id + " not registered"); + } + + KeyBindingManager._activeKeymaps[keymap.level] = keymap; + }, + + /** + * Deactivate the keymap with the given ID. If it's not already active, does nothing. + * + * @param {string} id The ID of the keymap to deactivate. + */ + deactivateKeymap: function(id) { + for (var i = 0; i < KeyBindingManager._maxLevel; i++) { + if (KeyBindingManager._activeKeymaps[i] && KeyBindingManager._activeKeymaps[i].id === id) { + delete KeyBindingManager._activeKeymaps[i]; + } + } + }, + + /** + * Process the keybinding for the current key. + * + * @param {string} A key-description string. + * @return {boolean} true if the key was processed, false otherwise + */ + handleKey: function(key) { + for (var i = KeyBindingManager._maxLevel; i >= 0; i--) { + var keymap = KeyBindingManager._activeKeymaps[i]; + if (keymap && keymap.map[key]) { + CommandManager.execute(keymap.map[key]); + return true; + } + } + return false; + } +}; + +/** class Keymap + * + * A keymap specifies how keys are mapped to commands. The KeyBindingManager allows for a hierarchy of + * keymaps, specified by a numeric level. It does not dictate the semantics of these levels, but in + * practice, level 0 is a global keymap, level 1 is a file-type-specific keymap, and deeper levels can + * be used for more specific contexts. When a new keymap is activated, it replaces any other keymap at + * its same level. + * + * Keys are described by strings of the form "[modifier-modifier-...-]key", where modifier is one of + * Ctrl, Alt, or Shift. If multiple modifiers are specified, they must be specified in that order + * (i.e. "Ctrl-Alt-Shift-P" is legal, "Alt-Ctrl-Shift-P" is not). + * (TODO: the above restriction is to simplify mapping--is it too onerous?) + * -- Ctrl maps to Cmd on Mac. (This means that you can't specifically bind to the Ctrl key on Mac.) + * -- Alt maps to the Option key on Mac. + * -- Letters must be uppercase, but do not require Shift by default. To indicate that Shift must be held + * down, you must specifically include Shift. + * + * @constructor + * @param {string} id A unique ID for this keymap. + * @param {number} level The level of this keymap. Must be an integer >= 0. + * @param {map} map An object mapping key-description strings to command IDs. + */ +var KeyMap = function(id, level, map) { + if (id === undefined || level === undefined || map === undefined) { + throw new Error("All parameters to the KeyMap constructor must be specified"); + } + + this.id = id; + this.level = level; + this.map = map; +}; diff --git a/src/ProjectManager.js b/src/ProjectManager.js index f717a66797e..ac0dfe6d951 100644 --- a/src/ProjectManager.js +++ b/src/ProjectManager.js @@ -34,8 +34,9 @@ ProjectManager.openProject = function() { ProjectManager.loadProject( files[0] ); }, function(error) { - brackets.showErrorDialog( - brackets.strings.ERROR_LOADING_PROJECT + brackets.showDialog( + brackets.DIALOG_ID_ERROR + , brackets.strings.ERROR_LOADING_PROJECT , brackets.strings.format(brackets.strings.OPEN_DIALOG_ERROR, error.code) ); } @@ -89,8 +90,9 @@ ProjectManager.loadProject = function(rootPath) { ProjectManager._renderTree(ProjectManager._treeDataProvider); }, function(error) { - brackets.showErrorDialog( - brackets.strings.ERROR_LOADING_PROJECT + brackets.showDialog( + brackets.DIALOG_ID_ERROR + , brackets.strings.ERROR_LOADING_PROJECT , brackets.strings.format(brackets.strings.REQUEST_NATIVE_FILE_SYSTEM_ERROR, rootPath, error.code) ); } @@ -126,8 +128,9 @@ ProjectManager._treeDataProvider = function(treeNode, jsTreeCallback) { jsTreeCallback(subtreeJSON); }, function(error) { - brackets.showErrorDialog( - brackets.strings.ERROR_LOADING_PROJECT + brackets.showDialog( + brackets.DIALOG_ID_ERROR + , brackets.strings.ERROR_LOADING_PROJECT , brackets.strings.format(brackets.strings.READ_DIRECTORY_ENTRIES_ERROR, dirEntry.fullPath, error.code) ); } diff --git a/src/brackets.js b/src/brackets.js index 4954d506980..84829412991 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -9,21 +9,36 @@ brackets = window.brackets || {}; brackets.inBrowser = !brackets.hasOwnProperty("fs"); +brackets.DIALOG_BTN_CANCEL = "cancel"; +brackets.DIALOG_BTN_OK = "ok"; +brackets.DIALOG_BTN_DONTSAVE = "dontsave"; + +brackets.DIALOG_ID_ERROR = "error-dialog"; +brackets.DIALOG_ID_SAVE_CLOSE = "save-close-dialog"; + /** - * General purpose modal error dialog. + * General purpose modal dialog. Assumes that the HTML for the dialog contains elements with "title" + * and "message" classes, as well as a number of elements with "dialog-button" class, each of which has + * a "data-button-id". * + * @param {string} id The ID of the dialog node in the HTML. * @param {string} title The title of the error dialog. Can contain HTML markup. * @param {string} message The message to display in the error dialog. Can contain HTML markup. + * @param {function=} callback The optional callback to be called when the dialog is dismissed. Called with + * the value of the data-button-id of the clicked button. */ -brackets.showErrorDialog = function(title, message) { - var dlg = $("#error-dialog"); +brackets.showDialog = function(id, title, message, callback) { + var dlg = $("#" + id); // Set title and message - $("#error-dialog-title").html(title); - $("#error-dialog-message").html(message); + $(".dialog-title", dlg).html(title); + $(".dialog-message", dlg).html(message); - // Click handler for OK button - dlg.delegate("#error-dialog-ok", "click", function(e) { + // Click handler for buttons + dlg.on("click", ".dialog-button", function(e) { + if (callback) { + callback($(this).attr("data-button-id")); + } dlg.modal(true).hide(); }); @@ -33,7 +48,7 @@ brackets.showErrorDialog = function(title, message) { , show: true } ); -} +}; $(document).ready(function() { @@ -149,6 +164,8 @@ $(document).ready(function() { // This should be 0, but just to be safe... _savedUndoPosition = editor.historySize().undo; updateDirty(); + + editor.focus(); }; reader.onerror = function(event) { @@ -200,23 +217,63 @@ $(document).ready(function() { _savedUndoPosition = editor.historySize().undo; updateDirty(); } + editor.focus(); }); } }); CommandManager.register(Commands.FILE_CLOSE, function() { if (_currentFilePath && _isDirty) { - // *** TODO: prompt to save + brackets.showDialog( + brackets.DIALOG_ID_SAVE_CLOSE + , brackets.strings.SAVE_CLOSE_TITLE + , brackets.strings.format(brackets.strings.SAVE_CLOSE_MESSAGE, _currentTitlePath) + , function(id) { + if (id !== brackets.DIALOG_BTN_CANCEL) { + if (id === brackets.DIALOG_BTN_OK) { + CommandManager.execute(Commands.FILE_SAVE); + } + + // TODO: When we implement multiple files being open, this will probably change to just + // dispose of the editor for the current file (and will later change again if we choose to + // limit the number of open editors). + editor.setValue(""); + editor.clearHistory(); + _currentFilePath = _currentTitlePath = null; + _savedUndoPosition = 0; + _isDirty = false; + updateTitle(); + editor.focus(); + } + } + ); + } + }); + + // Register keymaps and install the keyboard handler + // TODO: show keyboard equivalents in the menus + var KEYMAP_GLOBAL = "global"; + KeyBindingManager.registerKeymap(new KeyMap(KEYMAP_GLOBAL, 0, + { "Ctrl-O": Commands.FILE_OPEN + , "Ctrl-S": Commands.FILE_SAVE + , "Ctrl-W": Commands.FILE_CLOSE + })); + KeyBindingManager.activateKeymap(KEYMAP_GLOBAL); + + $(document.body).keydown(function(event) { + var keyDescriptor = []; + if (event.metaKey || event.ctrlKey) { + keyDescriptor.push("Ctrl"); + } + if (event.altKey) { + keyDescriptor.push("Alt"); + } + if (event.shiftKey) { + keyDescriptor.push("Shift"); + } + keyDescriptor.push(String.fromCharCode(event.keyCode).toUpperCase()); + if (KeyBindingManager.handleKey(keyDescriptor.join("-"))) { + event.preventDefault(); } - - // TODO: When we implement multiple files being open, this will probably change to just - // dispose of the editor for the current file (and will later change again if we choose to - // limit the number of open editors). - editor.setValue(""); - editor.clearHistory(); - _currentFilePath = _currentTitlePath = null; - _savedUndoPosition = 0; - _isDirty = false; - updateTitle(); }); }); diff --git a/src/index.html b/src/index.html index d2456289d3a..560764cb0e0 100644 --- a/src/index.html +++ b/src/index.html @@ -23,6 +23,7 @@ <script src="NativeFileSystem.js"></script> <script src="CommandManager.js"></script> <script src="Commands.js"></script> + <script src="KeyBindingManager.js"></script> <script src="ProjectManager.js"></script> <script src="brackets.js"></script> <script src="strings.js"></script> @@ -86,13 +87,28 @@ <div id="error-dialog" class="modal hide fade"> <div class="modal-header"> <a href="#" class="close">×</a> - <h3 id="error-dialog-title">Error</h3> + <h3 class="dialog-title">Error</h3> </div> <div class="modal-body"> - <p id="error-dialog-message">Message goes here</p> + <p class="dialog-message">Message goes here</p> </div> <div class="modal-footer"> - <a id="error-dialog-ok" href="#" class="btn primary">OK</a> + <a href="#" class="dialog-button btn primary" data-button-id="ok">OK</a> + </div> + </div> + <div id="save-close-dialog" class="modal hide fade"> + <div class="modal-header"> + <a href="#" class="close">×</a> + <h3 class="dialog-title">Save Changes</h3> + </div> + <div class="modal-body"> + <p class="dialog-message">Message goes here</p> + </div> + <div class="modal-footer"> + <a href="#" class="dialog-button btn left" data-button-id="dontsave">Don't Save</a> + <!-- TODO: switch buttons on Windows --> + <a href="#" class="dialog-button btn" data-button-id="cancel">Cancel</a> + <a href="#" class="dialog-button btn primary" data-button-id="ok">Save</a> </div> </div> </body> diff --git a/src/strings.js b/src/strings.js index a45d57ce935..102bf9c8117 100644 --- a/src/strings.js +++ b/src/strings.js @@ -33,6 +33,9 @@ if (!brackets.strings) // Project error strings s.ERROR_LOADING_PROJECT = "Error loading project"; s.OPEN_DIALOG_ERROR = "An error occurred when showing the open file dialog. (error {0})"; - s.REQUEST_NATIVE_FILE_SYSTEM_ERROR = "An error occurred when trying to load the directory '{0}'. (error {1})"; - s.READ_DIRECTORY_ENTRIES_ERROR = "An error occurred when reading the contents of the directory '{0}'. (error {1})"; + s.REQUEST_NATIVE_FILE_SYSTEM_ERROR = "An error occurred when trying to load the directory \"{0}\". (error {1})"; + s.READ_DIRECTORY_ENTRIES_ERROR = "An error occurred when reading the contents of the directory \"{0}\". (error {1})"; + + s.SAVE_CLOSE_TITLE = "Save Changes"; + s.SAVE_CLOSE_MESSAGE = "Do you want to save the changes you made in the document \"{0}\"?"; })(); \ No newline at end of file diff --git a/src/styles/brackets.less b/src/styles/brackets.less index 6ec96e6b059..d835b912e4b 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -73,6 +73,10 @@ .box-shadow(0 1px 3px 0 rgba(0, 0, 0, 0.3)); } +.modal-footer .btn.left { + float: left; +} + /* Overall layout */ html, body { From 72d998c8efb338f1d98e4b910b830a4ca4a6328a Mon Sep 17 00:00:00 2001 From: njadbe <njaramil@adobe.com> Date: Thu, 15 Dec 2011 22:52:34 -0800 Subject: [PATCH 07/14] Made commands rely on $.Deferred() to handle asynchronicity, and updated logic of the open/save/close commands to take advantage of this. --- src/CommandManager.js | 16 ++++++-- src/brackets.js | 96 ++++++++++++++++++++++++++++++++----------- 2 files changed, 83 insertions(+), 29 deletions(-) diff --git a/src/CommandManager.js b/src/CommandManager.js index 5849cc0cefc..ae34211e04c 100644 --- a/src/CommandManager.js +++ b/src/CommandManager.js @@ -11,7 +11,9 @@ CommandManager._commands = {}; * * @param {string} id The ID of the command. * @param {function} command The function to call when the command is executed. Any arguments passed to - * execute() (after the id) are passed as arguments to the function. + * execute() (after the id) are passed as arguments to the function. If the function is asynchronous, + * it must return a jQuery Deferred that is resolved when the command completes. Otherwise, the + * CommandManager will assume it is synchronous, and return a Deferred that is already resolved. */ CommandManager.register = function(id, command) { if (CommandManager._commands[id]) { @@ -24,15 +26,21 @@ CommandManager.register = function(id, command) { * Runs a global command. Additional arguments are passed to the command. * * @param {string} id The ID of the command to run. + * @return {Deferred} a jQuery Deferred that will be resolved when the command completes. */ CommandManager.execute = function(id) { - // TODO: are we guaranteed that all commands are synchronous? If not, we should - // take a callback, or (perhaps better) return a Deferred. var command = CommandManager._commands[id]; if (command) { - command.apply(null, Array.prototype.slice.call(arguments, 1)); + var result = command.apply(null, Array.prototype.slice.call(arguments, 1)); + if (result === undefined) { + return $.Deferred().resolve(); + } + else { + return result; + } } else { console.log("Attempted to call unregistered command: " + id); + return $.Deferred().reject(); } } diff --git a/src/brackets.js b/src/brackets.js index 84829412991..45f8dab5e71 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -128,7 +128,24 @@ $(document).ready(function() { $("#main-toolbar .title").text(_currentTitlePath ? (_currentTitlePath + (_isDirty ? " \u2022" : "")) : "Untitled"); } - function doOpen(fullPath) { + function doOpenWithOptionalPath(fullPath) { + if (!fullPath) { + // Prompt the user with a dialog + // TODO: we're relying on this to not be asynchronous--is that safe? + NativeFileSystem.showOpenDialog(false, false, "Open File", ProjectManager.getProjectRoot().fullPath, + ["htm", "html", "js", "css"], function(files) { + if (files.length > 0) { + return doOpen(files[0]); + } + }); + } + else { + return doOpen(fullPath); + } + } + + function doOpen(fullPath) { + var result = $.Deferred(); if (fullPath) { var reader = new NativeFileSystem.FileReader(); @@ -166,10 +183,12 @@ $(document).ready(function() { updateDirty(); editor.focus(); + result.resolve(); }; reader.onerror = function(event) { // TODO: display meaningful error + result.reject(); } reader.readAsText(file, "utf8"); @@ -178,6 +197,20 @@ $(document).ready(function() { // TODO: display meaningful error }); } + return result; + } + + function doClose() { + // TODO: When we implement multiple files being open, this will probably change to just + // dispose of the editor for the current file (and will later change again if we choose to + // limit the number of open editors). + editor.setValue(""); + editor.clearHistory(); + _currentFilePath = _currentTitlePath = null; + _savedUndoPosition = 0; + _isDirty = false; + updateTitle(); + editor.focus(); } // Register global commands @@ -188,65 +221,78 @@ $(document).ready(function() { // prompts the user to save it in the close command, where it belongs. When we implement multiple open files, // we can remove this here. if (_currentFilePath) { - CommandManager.execute(Commands.FILE_CLOSE); - } - - if (!fullPath) { - // Prompt the user with a dialog - NativeFileSystem.showOpenDialog(false, false, "Open File", ProjectManager.getProjectRoot().fullPath, - ["htm", "html", "js", "css"], function(files) { - if (files.length > 0) { - doOpen(files[0]); - } + var result = $.Deferred(); + CommandManager.execute(Commands.FILE_CLOSE).done(function() { + doOpenWithOptionalPath(fullPath).done(function() { + result.resolve(); + }) + .fail(function() { + result.reject(); }); + }) + .fail(function() { + result.reject(); + }); + return result; } else { - doOpen(fullPath); + return doOpenWithOptionalPath(fullPath); } }); CommandManager.register(Commands.FILE_SAVE, function() { if (_currentFilePath && _isDirty) { + var result = $.Deferred(); brackets.fs.writeFile(_currentFilePath, editor.getValue(), "utf8", function(err) { if (err) { // TODO--this will change with the real file API implementation + result.reject(); } else { // Remember which position in the undo stack we're at as of the last save. // When we're exactly at that position again, we know we're not dirty. _savedUndoPosition = editor.historySize().undo; updateDirty(); + result.resolve(); } editor.focus(); }); + return result; } }); CommandManager.register(Commands.FILE_CLOSE, function() { if (_currentFilePath && _isDirty) { + var result = $.Deferred(); brackets.showDialog( brackets.DIALOG_ID_SAVE_CLOSE , brackets.strings.SAVE_CLOSE_TITLE , brackets.strings.format(brackets.strings.SAVE_CLOSE_MESSAGE, _currentTitlePath) , function(id) { - if (id !== brackets.DIALOG_BTN_CANCEL) { + if (id === brackets.DIALOG_BTN_CANCEL) { + result.reject(); + } + else { if (id === brackets.DIALOG_BTN_OK) { - CommandManager.execute(Commands.FILE_SAVE); + CommandManager.execute(Commands.FILE_SAVE).done(function() { + doClose(); + result.resolve(); + }) + .fail(function() { + result.reject(); + }); } - - // TODO: When we implement multiple files being open, this will probably change to just - // dispose of the editor for the current file (and will later change again if we choose to - // limit the number of open editors). - editor.setValue(""); - editor.clearHistory(); - _currentFilePath = _currentTitlePath = null; - _savedUndoPosition = 0; - _isDirty = false; - updateTitle(); - editor.focus(); + else { + doClose(); + result.resolve(); + } } } ); + return result; + } + else { + doClose(); } }); From ef8d96c6ab884a07a38efa53d672271713ceeac6 Mon Sep 17 00:00:00 2001 From: njadbe <njaramil@adobe.com> Date: Thu, 15 Dec 2011 23:12:03 -0800 Subject: [PATCH 08/14] Added clarifying comment in FILE_CLOSE command --- src/brackets.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/brackets.js b/src/brackets.js index 45f8dab5e71..d539871d315 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -283,6 +283,7 @@ $(document).ready(function() { }); } else { + // This is the "Don't Save" case--we can just go ahead and close the file. doClose(); result.resolve(); } From b91566c24735f5b705a9aaf8e996e867ed94ab4d Mon Sep 17 00:00:00 2001 From: njadbe <njaramil@adobe.com> Date: Fri, 16 Dec 2011 09:45:03 -0800 Subject: [PATCH 09/14] Trying different indentation style for Deferred callbacks --- src/brackets.js | 58 ++++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/brackets.js b/src/brackets.js index d539871d315..6d33d0308cb 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -24,10 +24,11 @@ brackets.DIALOG_ID_SAVE_CLOSE = "save-close-dialog"; * @param {string} id The ID of the dialog node in the HTML. * @param {string} title The title of the error dialog. Can contain HTML markup. * @param {string} message The message to display in the error dialog. Can contain HTML markup. - * @param {function=} callback The optional callback to be called when the dialog is dismissed. Called with - * the value of the data-button-id of the clicked button. + * @return {Deferred} a $.Deferred() that will be resolved with the ID of the clicked button when the dialog + * is dismissed. */ brackets.showDialog = function(id, title, message, callback) { + var result = $.Deferred(); var dlg = $("#" + id); // Set title and message @@ -36,9 +37,7 @@ brackets.showDialog = function(id, title, message, callback) { // Click handler for buttons dlg.on("click", ".dialog-button", function(e) { - if (callback) { - callback($(this).attr("data-button-id")); - } + result.resolve($(this).attr("data-button-id")); dlg.modal(true).hide(); }); @@ -48,6 +47,7 @@ brackets.showDialog = function(id, title, message, callback) { , show: true } ); + return result; }; $(document).ready(function() { @@ -222,17 +222,20 @@ $(document).ready(function() { // we can remove this here. if (_currentFilePath) { var result = $.Deferred(); - CommandManager.execute(Commands.FILE_CLOSE).done(function() { - doOpenWithOptionalPath(fullPath).done(function() { - result.resolve(); + CommandManager + .execute(Commands.FILE_CLOSE) + .done(function() { + doOpenWithOptionalPath(fullPath) + .done(function() { + result.resolve(); + }) + .fail(function() { + result.reject(); + }); }) .fail(function() { result.reject(); }); - }) - .fail(function() { - result.reject(); - }); return result; } else { @@ -245,7 +248,7 @@ $(document).ready(function() { var result = $.Deferred(); brackets.fs.writeFile(_currentFilePath, editor.getValue(), "utf8", function(err) { if (err) { - // TODO--this will change with the real file API implementation + // TODO: display meaningful error result.reject(); } else { @@ -268,28 +271,29 @@ $(document).ready(function() { brackets.DIALOG_ID_SAVE_CLOSE , brackets.strings.SAVE_CLOSE_TITLE , brackets.strings.format(brackets.strings.SAVE_CLOSE_MESSAGE, _currentTitlePath) - , function(id) { - if (id === brackets.DIALOG_BTN_CANCEL) { - result.reject(); - } - else { - if (id === brackets.DIALOG_BTN_OK) { - CommandManager.execute(Commands.FILE_SAVE).done(function() { + ).done(function(id) { + if (id === brackets.DIALOG_BTN_CANCEL) { + result.reject(); + } + else { + if (id === brackets.DIALOG_BTN_OK) { + CommandManager + .execute(Commands.FILE_SAVE) + .done(function() { doClose(); result.resolve(); }) .fail(function() { result.reject(); }); - } - else { - // This is the "Don't Save" case--we can just go ahead and close the file. - doClose(); - result.resolve(); - } + } + else { + // This is the "Don't Save" case--we can just go ahead and close the file. + doClose(); + result.resolve(); } } - ); + }); return result; } else { From 43a0b989c716266282caa2bfef7d801abc3ed129 Mon Sep 17 00:00:00 2001 From: njadbe <njaramil@adobe.com> Date: Fri, 16 Dec 2011 09:49:47 -0800 Subject: [PATCH 10/14] Added another reject() case to doOpen() --- src/brackets.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/brackets.js b/src/brackets.js index 6d33d0308cb..c0454110c79 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -195,6 +195,7 @@ $(document).ready(function() { }, function (error) { // TODO: display meaningful error + result.reject(); }); } return result; @@ -246,6 +247,7 @@ $(document).ready(function() { CommandManager.register(Commands.FILE_SAVE, function() { if (_currentFilePath && _isDirty) { var result = $.Deferred(); + var writer = brackets.fs.writeFile(_currentFilePath, editor.getValue(), "utf8", function(err) { if (err) { // TODO: display meaningful error From adb37d0d72919f8d39ec094ea7ff5ab9ec634fe6 Mon Sep 17 00:00:00 2001 From: njadbe <njaramil@adobe.com> Date: Fri, 16 Dec 2011 10:13:17 -0800 Subject: [PATCH 11/14] Hooked up to HTML file save API --- src/brackets.js | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/brackets.js b/src/brackets.js index c0454110c79..093d9a7d4b0 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -245,25 +245,35 @@ $(document).ready(function() { }); CommandManager.register(Commands.FILE_SAVE, function() { + var result = $.Deferred(); if (_currentFilePath && _isDirty) { - var result = $.Deferred(); - var writer = - brackets.fs.writeFile(_currentFilePath, editor.getValue(), "utf8", function(err) { - if (err) { - // TODO: display meaningful error - result.reject(); - } - else { - // Remember which position in the undo stack we're at as of the last save. - // When we're exactly at that position again, we know we're not dirty. + // TODO: we should implement something like NativeFileSystem.resolveNativeFileSystemURL() (similar + // to what's in the standard file API) to get a FileEntry, rather than manually constructing it + var fileEntry = new NativeFileSystem.FileEntry(_currentFilePath); + + fileEntry.createWriter(function(writer) { + writer.onwrite = function() { _savedUndoPosition = editor.historySize().undo; updateDirty(); result.resolve(); } - editor.focus(); + writer.onerror = function() { + result.reject(); + } + writer.write(editor.getValue()); + }, + function(error) { + // TODO: display meaningful error + result.reject(); }); - return result; } + else { + result.resolve(); + } + result.always(function() { + editor.focus(); + }); + return result; }); CommandManager.register(Commands.FILE_CLOSE, function() { From 3e7c89a5dd71e3ceb1a5e9ee2034960d89b34194 Mon Sep 17 00:00:00 2001 From: njadbe <njaramil@adobe.com> Date: Fri, 16 Dec 2011 12:55:16 -0800 Subject: [PATCH 12/14] Renamed showDialog() to showModalDialog() --- src/ProjectManager.js | 6 +++--- src/brackets.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ProjectManager.js b/src/ProjectManager.js index ac0dfe6d951..5d52364be8e 100644 --- a/src/ProjectManager.js +++ b/src/ProjectManager.js @@ -34,7 +34,7 @@ ProjectManager.openProject = function() { ProjectManager.loadProject( files[0] ); }, function(error) { - brackets.showDialog( + brackets.showModalDialog( brackets.DIALOG_ID_ERROR , brackets.strings.ERROR_LOADING_PROJECT , brackets.strings.format(brackets.strings.OPEN_DIALOG_ERROR, error.code) @@ -90,7 +90,7 @@ ProjectManager.loadProject = function(rootPath) { ProjectManager._renderTree(ProjectManager._treeDataProvider); }, function(error) { - brackets.showDialog( + brackets.showModalDialog( brackets.DIALOG_ID_ERROR , brackets.strings.ERROR_LOADING_PROJECT , brackets.strings.format(brackets.strings.REQUEST_NATIVE_FILE_SYSTEM_ERROR, rootPath, error.code) @@ -128,7 +128,7 @@ ProjectManager._treeDataProvider = function(treeNode, jsTreeCallback) { jsTreeCallback(subtreeJSON); }, function(error) { - brackets.showDialog( + brackets.showModalDialog( brackets.DIALOG_ID_ERROR , brackets.strings.ERROR_LOADING_PROJECT , brackets.strings.format(brackets.strings.READ_DIRECTORY_ENTRIES_ERROR, dirEntry.fullPath, error.code) diff --git a/src/brackets.js b/src/brackets.js index 093d9a7d4b0..dc7c47f8c12 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -27,7 +27,7 @@ brackets.DIALOG_ID_SAVE_CLOSE = "save-close-dialog"; * @return {Deferred} a $.Deferred() that will be resolved with the ID of the clicked button when the dialog * is dismissed. */ -brackets.showDialog = function(id, title, message, callback) { +brackets.showModalDialog = function(id, title, message, callback) { var result = $.Deferred(); var dlg = $("#" + id); @@ -279,7 +279,7 @@ $(document).ready(function() { CommandManager.register(Commands.FILE_CLOSE, function() { if (_currentFilePath && _isDirty) { var result = $.Deferred(); - brackets.showDialog( + brackets.showModalDialog( brackets.DIALOG_ID_SAVE_CLOSE , brackets.strings.SAVE_CLOSE_TITLE , brackets.strings.format(brackets.strings.SAVE_CLOSE_MESSAGE, _currentTitlePath) From b01a80b685f0fbc4d6b07ef7490879e32ffb9819 Mon Sep 17 00:00:00 2001 From: njadbe <njaramil@adobe.com> Date: Fri, 16 Dec 2011 14:17:16 -0800 Subject: [PATCH 13/14] Refactored brackets.js to make the boot sequence cleaner, and pulled out the file handling code into a separate file. Simplified KeyBindingManager. Changed $.Deferred() to new $.Deferred(). --- src/CommandManager.js | 8 +- src/FileCommandHandlers.js | 248 ++++++++++++++++++++++++++ src/KeyBindingManager.js | 86 ++------- src/NativeFileSystem.js | 11 +- src/brackets.js | 356 ++++++++----------------------------- src/index.html | 1 + 6 files changed, 356 insertions(+), 354 deletions(-) create mode 100644 src/FileCommandHandlers.js diff --git a/src/CommandManager.js b/src/CommandManager.js index ae34211e04c..cc6e6763648 100644 --- a/src/CommandManager.js +++ b/src/CommandManager.js @@ -2,6 +2,10 @@ * Copyright 2011 Adobe Systems Incorporated. All Rights Reserved. */ +/** + * Manages global application commands that can be called from menu items, key bindings, or subparts + * of the application. + */ var CommandManager = {}; CommandManager._commands = {}; @@ -33,7 +37,7 @@ CommandManager.execute = function(id) { if (command) { var result = command.apply(null, Array.prototype.slice.call(arguments, 1)); if (result === undefined) { - return $.Deferred().resolve(); + return (new $.Deferred()).resolve(); } else { return result; @@ -41,6 +45,6 @@ CommandManager.execute = function(id) { } else { console.log("Attempted to call unregistered command: " + id); - return $.Deferred().reject(); + return (new $.Deferred()).reject(); } } diff --git a/src/FileCommandHandlers.js b/src/FileCommandHandlers.js new file mode 100644 index 00000000000..e78647397c9 --- /dev/null +++ b/src/FileCommandHandlers.js @@ -0,0 +1,248 @@ +/* + * Copyright 2011 Adobe Systems Incorporated. All Rights Reserved. + */ + +/** + * Handlers for commands related to file handling (opening, saving, etc.) + */ +var FileCommandHandlers = (function() { + // TODO: remove this and use the real exports variable when we switch to modules. + var exports = {}; + + var _editor, _title, _currentFilePath, _currentTitlePath, + _isDirty = false, + _savedUndoPosition = 0; + + exports.init = function init(editor, title) { + _editor = editor; + _title = title; + + _editor.setOption("onChange", function() { + updateDirty(); + }); + + // Register global commands + CommandManager.register(Commands.FILE_OPEN, handleFileOpen); + CommandManager.register(Commands.FILE_SAVE, handleFileSave); + CommandManager.register(Commands.FILE_CLOSE, handleFileClose); + }; + + function updateDirty() { + // If we've undone past the undo position at the last save, and there is no redo stack, + // then we can never get back to a non-dirty state. + var historySize = _editor.historySize(); + if (historySize.undo < _savedUndoPosition && historySize.redo == 0) { + _savedUndoPosition = -1; + } + var newIsDirty = (_editor.historySize().undo != _savedUndoPosition); + if (_isDirty != newIsDirty) { + _isDirty = newIsDirty; + updateTitle(); + } + } + + function updateTitle() { + _title.text( + _currentTitlePath + ? (_currentTitlePath + (_isDirty ? " \u2022" : "")) + : "Untitled" + ); + } + + function handleFileOpen(fullPath) { + // TODO: In the future, when we implement multiple open files, we won't close the previous file when opening + // a new one. However, for now, since we only support a single open document, I'm pretending as if we're + // closing the existing file first. This is so that I can put the code that checks for an unsaved file and + // prompts the user to save it in the close command, where it belongs. When we implement multiple open files, + // we can remove this here. + var result; + if (_currentFilePath) { + result = new $.Deferred(); + CommandManager + .execute(Commands.FILE_CLOSE) + .done(function() { + doOpenWithOptionalPath(fullPath) + .done(function() { + result.resolve(); + }) + .fail(function() { + result.reject(); + }); + }) + .fail(function() { + result.reject(); + }); + } + else { + result = doOpenWithOptionalPath(fullPath); + } + result.always(function() { + _editor.focus(); + }); + return result; + } + + function doOpenWithOptionalPath(fullPath) { + if (!fullPath) { + // Prompt the user with a dialog + // TODO: we're relying on this to not be asynchronous--is that safe? + NativeFileSystem.showOpenDialog(false, false, "Open File", ProjectManager.getProjectRoot().fullPath, + ["htm", "html", "js", "css"], function(files) { + if (files.length > 0) { + return doOpen(files[0]); + } + }); + } + else { + return doOpen(fullPath); + } + } + + function doOpen(fullPath) { + var result = new $.Deferred(); + if (fullPath) { + var reader = new NativeFileSystem.FileReader(); + + // TODO: we should implement something like NativeFileSystem.resolveNativeFileSystemURL() (similar + // to what's in the standard file API) to get a FileEntry, rather than manually constructing it + var fileEntry = new NativeFileSystem.FileEntry(fullPath); + + // TODO: it's weird to have to construct a FileEntry just to get a File. + fileEntry.file(function(file) { + reader.onload = function(event) { + _currentFilePath = _currentTitlePath = fullPath; + + // TODO: have a real controller object for the editor + _editor.setValue(event.target.result); + _editor.clearHistory(); + + // In the main toolbar, show the project-relative path (if the file is inside the current project) + // or the full absolute path (if it's not in the project). + var projectRootPath = ProjectManager.getProjectRoot().fullPath; + if (projectRootPath.length > 0 && projectRootPath.charAt(projectRootPath.length - 1) != "/") { + projectRootPath += "/"; + } + if (fullPath.indexOf(projectRootPath) == 0) { + _currentTitlePath = fullPath.slice(projectRootPath.length); + if (_currentTitlePath.charAt(0) == '/') { + _currentTitlePath = _currentTitlePath.slice(1); + } + } + + // Make sure we can't undo back to the previous content. + _editor.clearHistory(); + + // This should be 0, but just to be safe... + _savedUndoPosition = _editor.historySize().undo; + updateDirty(); + + _editor.focus(); + result.resolve(); + }; + + reader.onerror = function(event) { + // TODO: display meaningful error + result.reject(); + } + + reader.readAsText(file, "utf8"); + }, + function (error) { + // TODO: display meaningful error + result.reject(); + }); + } + return result; + } + + function handleFileSave() { + var result = new $.Deferred(); + if (_currentFilePath && _isDirty) { + // TODO: we should implement something like NativeFileSystem.resolveNativeFileSystemURL() (similar + // to what's in the standard file API) to get a FileEntry, rather than manually constructing it + var fileEntry = new NativeFileSystem.FileEntry(_currentFilePath); + + fileEntry.createWriter( + function(writer) { + writer.onwrite = function() { + _savedUndoPosition = _editor.historySize().undo; + updateDirty(); + result.resolve(); + } + writer.onerror = function() { + result.reject(); + } + writer.write(_editor.getValue()); + }, + function(error) { + // TODO: display meaningful error + result.reject(); + } + ); + } + else { + result.resolve(); + } + result.always(function() { + _editor.focus(); + }); + return result; + } + + function handleFileClose() { + if (_currentFilePath && _isDirty) { + var result = new $.Deferred(); + brackets.showModalDialog( + brackets.DIALOG_ID_SAVE_CLOSE + , brackets.strings.SAVE_CLOSE_TITLE + , brackets.strings.format(brackets.strings.SAVE_CLOSE_MESSAGE, _currentTitlePath) + ).done(function(id) { + if (id === brackets.DIALOG_BTN_CANCEL) { + result.reject(); + } + else { + if (id === brackets.DIALOG_BTN_OK) { + CommandManager + .execute(Commands.FILE_SAVE) + .done(function() { + doClose(); + result.resolve(); + }) + .fail(function() { + result.reject(); + }); + } + else { + // This is the "Don't Save" case--we can just go ahead and close the file. + doClose(); + result.resolve(); + } + } + }); + result.always(function() { + _editor.focus(); + }); + return result; + } + else { + doClose(); + _editor.focus(); + } + } + + function doClose() { + // TODO: When we implement multiple files being open, this will probably change to just + // dispose of the editor for the current file (and will later change again if we choose to + // limit the number of open editors). + _editor.setValue(""); + _editor.clearHistory(); + _currentFilePath = _currentTitlePath = null; + _savedUndoPosition = 0; + _isDirty = false; + updateTitle(); + _editor.focus(); + } + + return exports; +})(); + diff --git a/src/KeyBindingManager.js b/src/KeyBindingManager.js index a5333b4ac86..fec89b639c5 100644 --- a/src/KeyBindingManager.js +++ b/src/KeyBindingManager.js @@ -2,63 +2,22 @@ * Copyright 2011 Adobe Systems Incorporated. All Rights Reserved. */ +/** + * Manages the mapping of keyboard inputs to commands. + */ var KeyBindingManager = { /** - * The map of all registered keymaps. - */ - _keymaps: {}, - - /** - * The map of currently active keymaps. As new keymaps come into effect, they are pushed onto the end of - * the array. Later keymaps take precedence over earlier ones. - */ - _activeKeymaps: {}, - - /** - * The maximum possible keymap level. - */ - _maxLevel: -1, - - /** - * Registers the specified keymap based on its id. - * - * TODO: do Closure annotations support user-defined types? - * @param {KeyMap} keymap The keymap to register. + * The currently installed keymap. */ - registerKeymap: function(keymap) { - KeyBindingManager._keymaps[keymap.id] = keymap; - if (keymap.level > KeyBindingManager._maxLevel) { - KeyBindingManager._maxLevel = keymap.level; - } - }, + _keymap: null, /** - * Activate the specified keymap. This installs it at the appropriate level, replacing any other keymap - * at that level. + * Install the specified keymap as the current keymap, overwriting the existing keymap. * - * TODO: does the Closure annotation syntax support user-created types? - * @param {string} keymap The ID of the registered keymap to install. + * @param {KeyMap} keymap The keymap to install. */ - activateKeymap: function(id) { - var keymap = KeyBindingManager._keymaps[id]; - if (!keymap) { - throw new Error("Keymap " + id + " not registered"); - } - - KeyBindingManager._activeKeymaps[keymap.level] = keymap; - }, - - /** - * Deactivate the keymap with the given ID. If it's not already active, does nothing. - * - * @param {string} id The ID of the keymap to deactivate. - */ - deactivateKeymap: function(id) { - for (var i = 0; i < KeyBindingManager._maxLevel; i++) { - if (KeyBindingManager._activeKeymaps[i] && KeyBindingManager._activeKeymaps[i].id === id) { - delete KeyBindingManager._activeKeymaps[i]; - } - } + installKeymap: function(keymap) { + this._keymap = keymap; }, /** @@ -68,12 +27,9 @@ var KeyBindingManager = { * @return {boolean} true if the key was processed, false otherwise */ handleKey: function(key) { - for (var i = KeyBindingManager._maxLevel; i >= 0; i--) { - var keymap = KeyBindingManager._activeKeymaps[i]; - if (keymap && keymap.map[key]) { - CommandManager.execute(keymap.map[key]); - return true; - } + if (this._keymap && this._keymap.map[key]) { + CommandManager.execute(this._keymap.map[key]); + return true; } return false; } @@ -81,11 +37,8 @@ var KeyBindingManager = { /** class Keymap * - * A keymap specifies how keys are mapped to commands. The KeyBindingManager allows for a hierarchy of - * keymaps, specified by a numeric level. It does not dictate the semantics of these levels, but in - * practice, level 0 is a global keymap, level 1 is a file-type-specific keymap, and deeper levels can - * be used for more specific contexts. When a new keymap is activated, it replaces any other keymap at - * its same level. + * A keymap specifies how keys are mapped to commands. This currently just holds the map, but in future + * it will likely be extended to include other metadata about the keymap. * * Keys are described by strings of the form "[modifier-modifier-...-]key", where modifier is one of * Ctrl, Alt, or Shift. If multiple modifiers are specified, they must be specified in that order @@ -97,16 +50,11 @@ var KeyBindingManager = { * down, you must specifically include Shift. * * @constructor - * @param {string} id A unique ID for this keymap. - * @param {number} level The level of this keymap. Must be an integer >= 0. * @param {map} map An object mapping key-description strings to command IDs. */ -var KeyMap = function(id, level, map) { - if (id === undefined || level === undefined || map === undefined) { +var KeyMap = function(map) { + if (map === undefined) { throw new Error("All parameters to the KeyMap constructor must be specified"); - } - - this.id = id; - this.level = level; + } this.map = map; }; diff --git a/src/NativeFileSystem.js b/src/NativeFileSystem.js index 0ec1411166e..1ea2f33b962 100644 --- a/src/NativeFileSystem.js +++ b/src/NativeFileSystem.js @@ -186,12 +186,13 @@ NativeFileSystem.FileEntry.prototype.createWriter = function( successCallback, e var self = this; brackets.fs.writeFile( fileEntry.fullPath, data, "utf8", function( err ) { - if ( self.onerror ) { - self.onerror ( NativeFileSystem._nativeToFileError( err ) ); - - // TODO (jasonsj): partial write, update length and position + if ( err ) { + if ( self.onerror ) { + self.onerror ( NativeFileSystem._nativeToFileError( err ) ); + } } else { + // TODO (jasonsj): partial write, update length and position // successful completetion of a write self.position += data.size; } @@ -204,7 +205,7 @@ NativeFileSystem.FileEntry.prototype.createWriter = function( successCallback, e self.onwrite(); } - if ( this.onwriteend ) { + if ( self.onwriteend ) { // TODO (jasonsj): progressevent self.onwriteend(); } diff --git a/src/brackets.js b/src/brackets.js index dc7c47f8c12..254e73ce207 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -53,290 +53,90 @@ brackets.showModalDialog = function(id, title, message, callback) { $(document).ready(function() { var editor = CodeMirror($('#editor').get(0)); - - // Load a default project into the tree - if (brackets.inBrowser) { - // In browser: dummy folder tree (hardcoded in ProjectManager) - ProjectManager.loadProject("DummyProject"); - } else { - // In app shell: load Brackets itself - var loadedPath = window.location.pathname; - var bracketsSrc = loadedPath.substr(0, loadedPath.lastIndexOf("/")); - ProjectManager.loadProject(bracketsSrc); - } - - // Open project button - $("#btn-open-project").click(function() { - ProjectManager.openProject(); - }); - - // Implements the File menu items - $("#menu-file-open").click(function() { - CommandManager.execute(Commands.FILE_OPEN); - }); - $("#menu-file-close").click(function() { - CommandManager.execute(Commands.FILE_CLOSE); - }); - $("#menu-file-save").click(function() { - CommandManager.execute(Commands.FILE_SAVE); - }); - - // Implements the 'Run Tests' menu to bring up the Jasmine unit test window - var testWindow = null; - $("#menu-runtests").click(function(){ - if (!(testWindow === null)) { - try { - testWindow.location.reload(); - } catch(e) { - testWindow = null; // the window was probably closed - } - } - - if (testWindow === null) { - testWindow = window.open("../test/SpecRunner.html"); - testWindow.location.reload(); // if it was opened before, we need to reload because it will be cached - } - }); - // Application state - // TODO: factor this stuff out into a real app controller - var _currentFilePath = null; - var _currentTitlePath = null; - var _isDirty = false; - var _savedUndoPosition = 0; - - editor.setOption("onChange", function() { - updateDirty(); - }); - - // Utility functions - function updateDirty() { - // If we've undone past the undo position at the last save, and there is no redo stack, - // then we can never get back to a non-dirty state. - var historySize = editor.historySize(); - if (historySize.undo < _savedUndoPosition && historySize.redo == 0) { - _savedUndoPosition = -1; - } - var newIsDirty = (editor.historySize().undo != _savedUndoPosition); - if (_isDirty != newIsDirty) { - _isDirty = newIsDirty; - updateTitle(); - } - } - - function updateTitle() { - $("#main-toolbar .title").text(_currentTitlePath ? (_currentTitlePath + (_isDirty ? " \u2022" : "")) : "Untitled"); - } - - function doOpenWithOptionalPath(fullPath) { - if (!fullPath) { - // Prompt the user with a dialog - // TODO: we're relying on this to not be asynchronous--is that safe? - NativeFileSystem.showOpenDialog(false, false, "Open File", ProjectManager.getProjectRoot().fullPath, - ["htm", "html", "js", "css"], function(files) { - if (files.length > 0) { - return doOpen(files[0]); - } - }); - } - else { - return doOpen(fullPath); - } + initProject(); + initMenus(); + initCommandHandlers(); + initKeyBindings(); + + function initProject() { + // Load a default project into the tree + if (brackets.inBrowser) { + // In browser: dummy folder tree (hardcoded in ProjectManager) + ProjectManager.loadProject("DummyProject"); + } else { + // In app shell: load Brackets itself + var loadedPath = window.location.pathname; + var bracketsSrc = loadedPath.substr(0, loadedPath.lastIndexOf("/")); + ProjectManager.loadProject(bracketsSrc); + } + + // Open project button + $("#btn-open-project").click(function() { + ProjectManager.openProject(); + }); } + + function initMenus() { + // Implements the File menu items + $("#menu-file-open").click(function() { + CommandManager.execute(Commands.FILE_OPEN); + }); + $("#menu-file-close").click(function() { + CommandManager.execute(Commands.FILE_CLOSE); + }); + $("#menu-file-save").click(function() { + CommandManager.execute(Commands.FILE_SAVE); + }); - function doOpen(fullPath) { - var result = $.Deferred(); - if (fullPath) { - var reader = new NativeFileSystem.FileReader(); - - // TODO: we should implement something like NativeFileSystem.resolveNativeFileSystemURL() (similar - // to what's in the standard file API) to get a FileEntry, rather than manually constructing it - var fileEntry = new NativeFileSystem.FileEntry(fullPath); - - // TODO: it's weird to have to construct a FileEntry just to get a File. - fileEntry.file(function(file) { - reader.onload = function(event) { - _currentFilePath = _currentTitlePath = fullPath; - - // TODO: have a real controller object for the editor - editor.setValue(event.target.result); - editor.clearHistory(); - - // In the main toolbar, show the project-relative path (if the file is inside the current project) - // or the full absolute path (if it's not in the project). - var projectRootPath = ProjectManager.getProjectRoot().fullPath; - if (projectRootPath.length > 0 && projectRootPath.charAt(projectRootPath.length - 1) != "/") { - projectRootPath += "/"; - } - if (fullPath.indexOf(projectRootPath) == 0) { - _currentTitlePath = fullPath.slice(projectRootPath.length); - if (_currentTitlePath.charAt(0) == '/') { - _currentTitlePath = _currentTitlePath.slice(1); - } - } - - // Make sure we can't undo back to the previous content. - editor.clearHistory(); - - // This should be 0, but just to be safe... - _savedUndoPosition = editor.historySize().undo; - updateDirty(); - - editor.focus(); - result.resolve(); - }; - - reader.onerror = function(event) { - // TODO: display meaningful error - result.reject(); - } - - reader.readAsText(file, "utf8"); - }, - function (error) { - // TODO: display meaningful error - result.reject(); - }); - } - return result; + // Implements the 'Run Tests' menu to bring up the Jasmine unit test window + var testWindow = null; + $("#menu-runtests").click(function(){ + if (!(testWindow === null)) { + try { + testWindow.location.reload(); + } catch(e) { + testWindow = null; // the window was probably closed + } + } + + if (testWindow === null) { + testWindow = window.open("../test/SpecRunner.html"); + testWindow.location.reload(); // if it was opened before, we need to reload because it will be cached + } + }); } - function doClose() { - // TODO: When we implement multiple files being open, this will probably change to just - // dispose of the editor for the current file (and will later change again if we choose to - // limit the number of open editors). - editor.setValue(""); - editor.clearHistory(); - _currentFilePath = _currentTitlePath = null; - _savedUndoPosition = 0; - _isDirty = false; - updateTitle(); - editor.focus(); + function initCommandHandlers() { + FileCommandHandlers.init(editor, $("#main-toolbar .title")); } - // Register global commands - CommandManager.register(Commands.FILE_OPEN, function(fullPath) { - // TODO: In the future, when we implement multiple open files, we won't close the previous file when opening - // a new one. However, for now, since we only support a single open document, I'm pretending as if we're - // closing the existing file first. This is so that I can put the code that checks for an unsaved file and - // prompts the user to save it in the close command, where it belongs. When we implement multiple open files, - // we can remove this here. - if (_currentFilePath) { - var result = $.Deferred(); - CommandManager - .execute(Commands.FILE_CLOSE) - .done(function() { - doOpenWithOptionalPath(fullPath) - .done(function() { - result.resolve(); - }) - .fail(function() { - result.reject(); - }); - }) - .fail(function() { - result.reject(); - }); - return result; - } - else { - return doOpenWithOptionalPath(fullPath); - } - }); - - CommandManager.register(Commands.FILE_SAVE, function() { - var result = $.Deferred(); - if (_currentFilePath && _isDirty) { - // TODO: we should implement something like NativeFileSystem.resolveNativeFileSystemURL() (similar - // to what's in the standard file API) to get a FileEntry, rather than manually constructing it - var fileEntry = new NativeFileSystem.FileEntry(_currentFilePath); - - fileEntry.createWriter(function(writer) { - writer.onwrite = function() { - _savedUndoPosition = editor.historySize().undo; - updateDirty(); - result.resolve(); - } - writer.onerror = function() { - result.reject(); - } - writer.write(editor.getValue()); - }, - function(error) { - // TODO: display meaningful error - result.reject(); - }); - } - else { - result.resolve(); - } - result.always(function() { - editor.focus(); + function initKeyBindings() { + // Register keymaps and install the keyboard handler + // TODO: show keyboard equivalents in the menus + var _globalKeymap = new KeyMap( + { "Ctrl-O": Commands.FILE_OPEN + , "Ctrl-S": Commands.FILE_SAVE + , "Ctrl-W": Commands.FILE_CLOSE + } + ); + KeyBindingManager.installKeymap(_globalKeymap); + + $(document.body).keydown(function(event) { + var keyDescriptor = []; + if (event.metaKey || event.ctrlKey) { + keyDescriptor.push("Ctrl"); + } + if (event.altKey) { + keyDescriptor.push("Alt"); + } + if (event.shiftKey) { + keyDescriptor.push("Shift"); + } + keyDescriptor.push(String.fromCharCode(event.keyCode).toUpperCase()); + if (KeyBindingManager.handleKey(keyDescriptor.join("-"))) { + event.preventDefault(); + } }); - return result; - }); - - CommandManager.register(Commands.FILE_CLOSE, function() { - if (_currentFilePath && _isDirty) { - var result = $.Deferred(); - brackets.showModalDialog( - brackets.DIALOG_ID_SAVE_CLOSE - , brackets.strings.SAVE_CLOSE_TITLE - , brackets.strings.format(brackets.strings.SAVE_CLOSE_MESSAGE, _currentTitlePath) - ).done(function(id) { - if (id === brackets.DIALOG_BTN_CANCEL) { - result.reject(); - } - else { - if (id === brackets.DIALOG_BTN_OK) { - CommandManager - .execute(Commands.FILE_SAVE) - .done(function() { - doClose(); - result.resolve(); - }) - .fail(function() { - result.reject(); - }); - } - else { - // This is the "Don't Save" case--we can just go ahead and close the file. - doClose(); - result.resolve(); - } - } - }); - return result; - } - else { - doClose(); - } - }); - - // Register keymaps and install the keyboard handler - // TODO: show keyboard equivalents in the menus - var KEYMAP_GLOBAL = "global"; - KeyBindingManager.registerKeymap(new KeyMap(KEYMAP_GLOBAL, 0, - { "Ctrl-O": Commands.FILE_OPEN - , "Ctrl-S": Commands.FILE_SAVE - , "Ctrl-W": Commands.FILE_CLOSE - })); - KeyBindingManager.activateKeymap(KEYMAP_GLOBAL); - - $(document.body).keydown(function(event) { - var keyDescriptor = []; - if (event.metaKey || event.ctrlKey) { - keyDescriptor.push("Ctrl"); - } - if (event.altKey) { - keyDescriptor.push("Alt"); - } - if (event.shiftKey) { - keyDescriptor.push("Shift"); - } - keyDescriptor.push(String.fromCharCode(event.keyCode).toUpperCase()); - if (KeyBindingManager.handleKey(keyDescriptor.join("-"))) { - event.preventDefault(); - } - }); + } }); diff --git a/src/index.html b/src/index.html index 560764cb0e0..48e786a8652 100644 --- a/src/index.html +++ b/src/index.html @@ -23,6 +23,7 @@ <script src="NativeFileSystem.js"></script> <script src="CommandManager.js"></script> <script src="Commands.js"></script> + <script src="FileCommandHandlers.js"></script> <script src="KeyBindingManager.js"></script> <script src="ProjectManager.js"></script> <script src="brackets.js"></script> From dbf5170ffea53a168a304be21d124e45250646cc Mon Sep 17 00:00:00 2001 From: njadbe <njaramil@adobe.com> Date: Fri, 16 Dec 2011 16:39:34 -0800 Subject: [PATCH 14/14] Code review cleanup --- src/FileCommandHandlers.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/FileCommandHandlers.js b/src/FileCommandHandlers.js index e78647397c9..2a0c9978380 100644 --- a/src/FileCommandHandlers.js +++ b/src/FileCommandHandlers.js @@ -112,10 +112,6 @@ var FileCommandHandlers = (function() { reader.onload = function(event) { _currentFilePath = _currentTitlePath = fullPath; - // TODO: have a real controller object for the editor - _editor.setValue(event.target.result); - _editor.clearHistory(); - // In the main toolbar, show the project-relative path (if the file is inside the current project) // or the full absolute path (if it's not in the project). var projectRootPath = ProjectManager.getProjectRoot().fullPath; @@ -129,6 +125,9 @@ var FileCommandHandlers = (function() { } } + // TODO: have a real controller object for the editor + _editor.setValue(event.target.result); + // Make sure we can't undo back to the previous content. _editor.clearHistory(); @@ -136,7 +135,6 @@ var FileCommandHandlers = (function() { _savedUndoPosition = _editor.historySize().undo; updateDirty(); - _editor.focus(); result.resolve(); }; @@ -190,8 +188,8 @@ var FileCommandHandlers = (function() { } function handleFileClose() { + var result = new $.Deferred(); if (_currentFilePath && _isDirty) { - var result = new $.Deferred(); brackets.showModalDialog( brackets.DIALOG_ID_SAVE_CLOSE , brackets.strings.SAVE_CLOSE_TITLE @@ -222,12 +220,13 @@ var FileCommandHandlers = (function() { result.always(function() { _editor.focus(); }); - return result; } else { doClose(); _editor.focus(); + result.resolve(); } + return result; } function doClose() {