Skip to content
This repository has been archived by the owner on Sep 6, 2021. It is now read-only.

Initial implementation of file save #16

Merged
merged 22 commits into from
Dec 17, 2011
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
14375eb
Initial implementation of File > Save and dirty bit management
Dec 14, 2011
f048b02
Removed call to temp debug function
Dec 15, 2011
c4493d7
Added comments around our dirty bit management
Dec 15, 2011
8941cd9
Beginning implementation of prompt when closing dirty file
Dec 15, 2011
8b769ba
Merged with latest file read API
Dec 16, 2011
2a3832d
Merge branch 'open-file-from-tree' into save-file
Dec 16, 2011
5701dd6
Merge latest file api and open-file code
Dec 16, 2011
0d169b9
Implementing file close--prompting to save not yet implemented
Dec 16, 2011
33e8cfe
Added dialog when closing dirty file. Refactored dialog code to make …
Dec 16, 2011
72d998c
Made commands rely on $.Deferred() to handle asynchronicity, and upda…
Dec 16, 2011
ef8d96c
Added clarifying comment in FILE_CLOSE command
Dec 16, 2011
b91566c
Trying different indentation style for Deferred callbacks
Dec 16, 2011
8db07b7
Merge branch 'initial-fileio-work' into save-file
Dec 16, 2011
43a0b98
Added another reject() case to doOpen()
Dec 16, 2011
adb37d0
Hooked up to HTML file save API
Dec 16, 2011
3e7c89a
Renamed showDialog() to showModalDialog()
Dec 16, 2011
b01a80b
Refactored brackets.js to make the boot sequence cleaner, and pulled …
Dec 16, 2011
ab3951c
Merge latest save file API
Dec 16, 2011
da5f24d
Merge remote-tracking branch 'origin/initial-fileio-work' into save-file
Dec 16, 2011
b2df1a1
Merge remote-tracking branch 'origin/master' into save-file
Dec 16, 2011
9a91763
Merge remote-tracking branch 'origin/master' into save-file
Dec 17, 2011
dbf5170
Code review cleanup
Dec 17, 2011
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/CommandManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand All @@ -11,7 +15,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]) {
Expand All @@ -24,13 +30,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) {
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 (new $.Deferred()).resolve();
}
else {
return result;
}
}
else {
console.log("Attempted to call unregistered command: " + id);
return (new $.Deferred()).reject();
}
}
4 changes: 3 additions & 1 deletion src/Commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
* List of constants for global command IDs.
*/
var Commands = {
FILE_OPEN: "file.open"
FILE_OPEN: "file.open",
FILE_SAVE: "file.save",
FILE_CLOSE: "file.close"
};
248 changes: 248 additions & 0 deletions src/FileCommandHandlers.js
Original file line number Diff line number Diff line change
@@ -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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_editor.clearHistory() is already done on line 117


// This should be 0, but just to be safe...
_savedUndoPosition = _editor.historySize().undo;
updateDirty();

_editor.focus();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleFileOpen() has a result.always() handler that calls _editor.focus(). This one is probably not necessary.

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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code path doesn't return a Deferred

}
}

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;
})();

60 changes: 60 additions & 0 deletions src/KeyBindingManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2011 Adobe Systems Incorporated. All Rights Reserved.
*/

/**
* Manages the mapping of keyboard inputs to commands.
*/
var KeyBindingManager = {
/**
* The currently installed keymap.
*/
_keymap: null,

/**
* Install the specified keymap as the current keymap, overwriting the existing keymap.
*
* @param {KeyMap} keymap The keymap to install.
*/
installKeymap: function(keymap) {
this._keymap = keymap;
},

/**
* 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) {
if (this._keymap && this._keymap.map[key]) {
CommandManager.execute(this._keymap.map[key]);
return true;
}
return false;
}
};

/** class Keymap
*
* 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
* (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 {map} map An object mapping key-description strings to command IDs.
*/
var KeyMap = function(map) {
if (map === undefined) {
throw new Error("All parameters to the KeyMap constructor must be specified");
}
this.map = map;
};
Loading