Bug 700893 - API for tracking unsaved/saved state in source editor; r=rcampbell f=fayearthur

This commit is contained in:
Mihai Sucan 2012-02-17 19:11:17 +02:00
Родитель 30a7711db3
Коммит a25f68694c
7 изменённых файлов: 325 добавлений и 104 удалений

Просмотреть файл

@ -75,6 +75,8 @@ const BUTTON_POSITION_DONT_SAVE = 2;
* The scratchpad object handles the Scratchpad window functionality.
*/
var Scratchpad = {
_initialWindowTitle: document.title,
/**
* The script execution context. This tells Scratchpad in which context the
* script shall execute.
@ -151,7 +153,22 @@ var Scratchpad = {
*/
setFilename: function SP_setFilename(aFilename)
{
document.title = this.filename = aFilename;
this.filename = aFilename;
this._updateTitle();
},
/**
* Update the Scratchpad window title based on the current state.
* @private
*/
_updateTitle: function SP__updateTitle()
{
if (this.filename) {
document.title = (this.editor && this.editor.dirty ? "*" : "") +
this.filename;
} else {
document.title = this._initialWindowTitle;
}
},
/**
@ -168,7 +185,7 @@ var Scratchpad = {
filename: this.filename,
text: this.getText(),
executionContext: this.executionContext,
saved: this.saved
saved: !this.editor.dirty,
};
},
@ -184,7 +201,9 @@ var Scratchpad = {
if (aState.filename) {
this.setFilename(aState.filename);
}
this.saved = aState.saved;
if (this.editor) {
this.editor.dirty = !aState.saved;
}
if (aState.executionContext == SCRATCHPAD_CONTEXT_BROWSER) {
this.setBrowserContext();
@ -638,7 +657,7 @@ var Scratchpad = {
fp.defaultString = "";
if (fp.show() != Ci.nsIFilePicker.returnCancel) {
this.setFilename(fp.file.path);
this.importFromFile(fp.file, false, this.onTextSaved.bind(this));
this.importFromFile(fp.file, false);
}
},
@ -658,7 +677,9 @@ var Scratchpad = {
file.initWithPath(this.filename);
this.exportToFile(file, true, false, function(aStatus) {
this.onTextSaved();
if (Components.isSuccessCode(aStatus)) {
this.editor.dirty = false;
}
if (aCallback) {
aCallback(aStatus);
}
@ -681,7 +702,9 @@ var Scratchpad = {
this.setFilename(fp.file.path);
this.exportToFile(fp.file, true, false, function(aStatus) {
this.onTextSaved();
if (Components.isSuccessCode(aStatus)) {
this.editor.dirty = false;
}
if (aCallback) {
aCallback(aStatus);
}
@ -783,7 +806,6 @@ var Scratchpad = {
if (aEvent.target != document) {
return;
}
let chrome = Services.prefs.getBoolPref(DEVTOOLS_CHROME_ENABLED);
if (chrome) {
let environmentMenu = document.getElementById("sp-environment-menu");
@ -794,10 +816,11 @@ var Scratchpad = {
errorConsoleCommand.removeAttribute("disabled");
}
let state = null;
let initialText = this.strings.GetStringFromName("scratchpadIntro");
if ("arguments" in window &&
window.arguments[0] instanceof Ci.nsIDialogParamBlock) {
let state = JSON.parse(window.arguments[0].GetString(0));
state = JSON.parse(window.arguments[0].GetString(0));
this.setState(state);
initialText = state.text;
}
@ -811,29 +834,32 @@ var Scratchpad = {
};
let editorPlaceholder = document.getElementById("scratchpad-editor");
this.editor.init(editorPlaceholder, config, this.onEditorLoad.bind(this));
this.editor.init(editorPlaceholder, config,
this._onEditorLoad.bind(this, state));
},
/**
* The load event handler for the source editor. This method does post-load
* editor initialization.
*
* @private
* @param object aState
* The initial Scratchpad state object.
*/
onEditorLoad: function SP_onEditorLoad()
_onEditorLoad: function SP__onEditorLoad(aState)
{
this.editor.addEventListener(SourceEditor.EVENTS.CONTEXT_MENU,
this.onContextMenu);
this.editor.addEventListener(SourceEditor.EVENTS.DIRTY_CHANGED,
this._onDirtyChanged);
this.editor.focus();
this.editor.setCaretOffset(this.editor.getCharCount());
if (aState) {
this.editor.dirty = !aState.saved;
}
this.initialized = true;
if (this.filename && !this.saved) {
this.onTextChanged();
}
else if (this.filename && this.saved) {
this.onTextSaved();
}
this._triggerObservers("Ready");
},
@ -866,6 +892,20 @@ var Scratchpad = {
}
},
/**
* The Source Editor DirtyChanged event handler. This function updates the
* Scratchpad window title to show an asterisk when there are unsaved changes.
*
* @private
* @see SourceEditor.EVENTS.DIRTY_CHANGED
* @param object aEvent
* The DirtyChanged event object.
*/
_onDirtyChanged: function SP__onDirtyChanged(aEvent)
{
Scratchpad._updateTitle();
},
/**
* The popupshowing event handler for the Edit menu. This method updates the
* enabled/disabled state of the Undo and Redo commands, based on the editor
@ -899,36 +939,6 @@ var Scratchpad = {
this.editor.redo();
},
/**
* This method adds a listener to the editor for text changes. Called when
* a scratchpad is saved, opened from file, or restored from a saved file.
*/
onTextSaved: function SP_onTextSaved(aStatus)
{
if (aStatus && !Components.isSuccessCode(aStatus)) {
return;
}
if (!document || !this.initialized) {
return; // file saved to disk after window has closed
}
document.title = document.title.replace(/^\*/, "");
this.saved = true;
this.editor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED,
this.onTextChanged);
},
/**
* The scratchpad handler for editor text change events. This handler
* indicates that there are unsaved changes in the UI.
*/
onTextChanged: function SP_onTextChanged()
{
document.title = "*" + document.title;
Scratchpad.saved = false;
Scratchpad.editor.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED,
Scratchpad.onTextChanged);
},
/**
* The Scratchpad window unload event handler. This method unloads/destroys
* the source editor.
@ -942,6 +952,8 @@ var Scratchpad = {
}
this.resetContext();
this.editor.removeEventListener(SourceEditor.EVENTS.DIRTY_CHANGED,
this._onDirtyChanged);
this.editor.removeEventListener(SourceEditor.EVENTS.CONTEXT_MENU,
this.onContextMenu);
this.editor.destroy();
@ -953,13 +965,18 @@ var Scratchpad = {
* Prompt to save scratchpad if it has unsaved changes.
*
* @param function aCallback
* Optional function you want to call when file is saved
* Optional function you want to call when file is saved. The callback
* receives three arguments:
* - toClose (boolean) - tells if the window should be closed.
* - saved (boolen) - tells if the file has been saved.
* - status (number) - the file save status result (if the file was
* saved).
* @return boolean
* Whether the window should be closed
*/
promptSave: function SP_promptSave(aCallback)
{
if (this.filename && !this.saved) {
if (this.filename && this.editor.dirty) {
let ps = Services.prompt;
let flags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_SAVE +
ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL +
@ -971,12 +988,25 @@ var Scratchpad = {
flags, null, null, null, null, {});
if (button == BUTTON_POSITION_CANCEL) {
if (aCallback) {
aCallback(false, false);
}
return false;
}
if (button == BUTTON_POSITION_SAVE) {
this.saveFile(aCallback);
this.saveFile(function(aStatus) {
if (aCallback) {
aCallback(true, true, aStatus);
}
});
return true;
}
}
if (aCallback) {
aCallback(true, false);
}
return true;
},
@ -988,10 +1018,22 @@ var Scratchpad = {
*/
onClose: function SP_onClose(aEvent)
{
let toClose = this.promptSave();
if (!toClose) {
aEvent.preventDefault();
if (this._skipClosePrompt) {
return;
}
this.promptSave(function(aShouldClose, aSaved, aStatus) {
let shouldClose = aShouldClose;
if (aSaved && !Components.isSuccessCode(aStatus)) {
shouldClose = false;
}
if (shouldClose) {
this._skipClosePrompt = true;
window.close();
}
}.bind(this));
aEvent.preventDefault();
},
/**
@ -1003,10 +1045,20 @@ var Scratchpad = {
*/
close: function SP_close(aCallback)
{
let toClose = this.promptSave(aCallback);
if (toClose) {
window.close();
}
this.promptSave(function(aShouldClose, aSaved, aStatus) {
let shouldClose = aShouldClose;
if (aSaved && !Components.isSuccessCode(aStatus)) {
shouldClose = false;
}
if (shouldClose) {
this._skipClosePrompt = true;
window.close();
}
if (aCallback) {
aCallback();
}
}.bind(this));
},
_observers: [],

Просмотреть файл

@ -46,9 +46,10 @@ function test()
function testNew()
{
openScratchpad(function(win) {
win.Scratchpad.close();
ok(win.closed, "new scratchpad window should close without prompting")
done();
win.Scratchpad.close(function() {
ok(win.closed, "new scratchpad window should close without prompting")
done();
});
}, {noFocus: true});
}
@ -56,11 +57,11 @@ function testSavedFile()
{
openScratchpad(function(win) {
win.Scratchpad.filename = "test.js";
win.Scratchpad.saved = true;
win.Scratchpad.close();
ok(win.closed, "scratchpad from file with no changes should close")
done();
win.Scratchpad.editor.dirty = false;
win.Scratchpad.close(function() {
ok(win.closed, "scratchpad from file with no changes should close")
done();
});
}, {noFocus: true});
}
@ -74,17 +75,16 @@ function testUnsaved()
function testUnsavedFileCancel()
{
openScratchpad(function(win) {
win.Scratchpad.filename = "test.js";
win.Scratchpad.saved = false;
win.Scratchpad.setFilename("test.js");
win.Scratchpad.editor.dirty = true;
promptButton = win.BUTTON_POSITION_CANCEL;
win.Scratchpad.close();
ok(!win.closed, "cancelling dialog shouldn't close scratchpad");
win.close();
done();
win.Scratchpad.close(function() {
ok(!win.closed, "cancelling dialog shouldn't close scratchpad");
win.close();
done();
});
}, {noFocus: true});
}
@ -92,8 +92,7 @@ function testUnsavedFileSave()
{
openScratchpad(function(win) {
win.Scratchpad.importFromFile(gFile, true, function(status, content) {
win.Scratchpad.filename = gFile.path;
win.Scratchpad.onTextSaved();
win.Scratchpad.setFilename(gFile.path);
let text = "new text";
win.Scratchpad.setText(text);
@ -101,13 +100,12 @@ function testUnsavedFileSave()
promptButton = win.BUTTON_POSITION_SAVE;
win.Scratchpad.close(function() {
ok(win.closed, 'pressing "Save" in dialog should close scratchpad');
readFile(gFile, function(savedContent) {
is(savedContent, text, 'prompted "Save" worked when closing scratchpad');
done();
});
});
ok(win.closed, 'pressing "Save" in dialog should close scratchpad');
});
}, {noFocus: true});
}
@ -115,15 +113,15 @@ function testUnsavedFileSave()
function testUnsavedFileDontSave()
{
openScratchpad(function(win) {
win.Scratchpad.filename = gFile.path;
win.Scratchpad.saved = false;
win.Scratchpad.setFilename(gFile.path);
win.Scratchpad.editor.dirty = true;
promptButton = win.BUTTON_POSITION_DONT_SAVE;
win.Scratchpad.close();
ok(win.closed, 'pressing "Don\'t Save" in dialog should close scratchpad');
done();
win.Scratchpad.close(function() {
ok(win.closed, 'pressing "Don\'t Save" in dialog should close scratchpad');
done();
});
}, {noFocus: true});
}

Просмотреть файл

@ -3,7 +3,7 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
// only finish() when correct number of tests are done
const expected = 5;
const expected = 4;
var count = 0;
function done()
{
@ -20,7 +20,6 @@ function test()
waitForExplicitFinish();
testListeners();
testErrorStatus();
testRestoreNotFromFile();
testRestoreFromFileSaved();
testRestoreFromFileUnsaved();
@ -34,36 +33,30 @@ function testListeners()
aScratchpad.setText("new text");
ok(!isStar(aWin), "no star if scratchpad isn't from a file");
aScratchpad.onTextSaved();
aScratchpad.editor.dirty = false;
ok(!isStar(aWin), "no star before changing text");
aScratchpad.setFilename("foo.js");
aScratchpad.setText("new text2");
ok(isStar(aWin), "shows star if scratchpad text changes");
aScratchpad.onTextSaved();
aScratchpad.editor.dirty = false;
ok(!isStar(aWin), "no star if scratchpad was just saved");
aScratchpad.setText("new text3");
ok(isStar(aWin), "shows star if scratchpad has more changes");
aScratchpad.undo();
ok(isStar(aWin), "star if scratchpad undo");
ok(!isStar(aWin), "no star if scratchpad undo to save point");
aScratchpad.undo();
ok(isStar(aWin), "star if scratchpad undo past save point");
aWin.close();
done();
}, {noFocus: true});
}
function testErrorStatus()
{
openScratchpad(function(aWin, aScratchpad) {
aScratchpad.onTextSaved(Components.results.NS_ERROR_FAILURE);
aScratchpad.setText("new text");
ok(!isStar(aWin), "no star if file save failed");
aWin.close();
done();
}, {noFocus: true});
}
function testRestoreNotFromFile()
{
let session = [{

Просмотреть файл

@ -146,6 +146,7 @@ function SourceEditor() {
Services.prefs.getBoolPref(SourceEditor.PREFS.EXPAND_TAB);
this._onOrionSelection = this._onOrionSelection.bind(this);
this._onTextChanged = this._onTextChanged.bind(this);
this._eventTarget = {};
this._eventListenersQueue = [];
@ -172,6 +173,7 @@ SourceEditor.prototype = {
_iframeWindow: null,
_eventTarget: null,
_eventListenersQueue: null,
_dirty: false,
/**
* The Source Editor user interface manager.
@ -279,8 +281,11 @@ SourceEditor.prototype = {
this._view.addEventListener("Load", onOrionLoad);
if (config.highlightCurrentLine || Services.appinfo.OS == "Linux") {
this._view.addEventListener("Selection", this._onOrionSelection);
this.addEventListener(SourceEditor.EVENTS.SELECTION,
this._onOrionSelection);
}
this.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED,
this._onTextChanged);
let KeyBinding = window.require("orion/textview/keyBinding").KeyBinding;
let TextDND = window.require("orion/textview/textDND").TextDND;
@ -587,6 +592,28 @@ SourceEditor.prototype = {
}
},
/**
* The TextChanged event handler which tracks the dirty state of the editor.
*
* @see SourceEditor.EVENTS.TEXT_CHANGED
* @see SourceEditor.EVENTS.DIRTY_CHANGED
* @see SourceEditor.dirty
* @private
*/
_onTextChanged: function SE__onTextChanged()
{
this._updateDirty();
},
/**
* Update the dirty state of the editor based on the undo stack.
* @private
*/
_updateDirty: function SE__updateDirty()
{
this.dirty = !this._undoStack.isClean();
},
/**
* Update the X11 PRIMARY buffer to hold the current selection.
* @private
@ -903,11 +930,53 @@ SourceEditor.prototype = {
},
/**
* Reset the Undo stack
* Reset the Undo stack.
*/
resetUndo: function SE_resetUndo()
{
this._undoStack.reset();
this._updateDirty();
},
/**
* Set the "dirty" state of the editor. Set this to false when you save the
* text being edited. The dirty state will become true once the user makes
* changes to the text.
*
* @param boolean aNewValue
* The new dirty state: true if the text is not saved, false if you
* just saved the text.
*/
set dirty(aNewValue)
{
if (aNewValue == this._dirty) {
return;
}
let event = {
type: SourceEditor.EVENTS.DIRTY_CHANGED,
oldValue: this._dirty,
newValue: aNewValue,
};
this._dirty = aNewValue;
if (!this._dirty && !this._undoStack.isClean()) {
this._undoStack.markClean();
}
this._dispatchEvent(event);
},
/**
* Get the editor "dirty" state. This tells if the text is considered saved or
* not.
*
* @see SourceEditor.EVENTS.DIRTY_CHANGED
* @return boolean
* True if there are changes which are not saved, false otherwise.
*/
get dirty()
{
return this._dirty;
},
/**
@ -1326,10 +1395,15 @@ SourceEditor.prototype = {
destroy: function SE_destroy()
{
if (this._config.highlightCurrentLine || Services.appinfo.OS == "Linux") {
this._view.removeEventListener("Selection", this._onOrionSelection);
this.removeEventListener(SourceEditor.EVENTS.SELECTION,
this._onOrionSelection);
}
this._onOrionSelection = null;
this.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED,
this._onTextChanged);
this._onTextChanged = null;
if (this._primarySelectionTimeout) {
let window = this.parentElement.ownerDocument.defaultView;
window.clearTimeout(this._primarySelectionTimeout);

Просмотреть файл

@ -282,6 +282,15 @@ SourceEditor.EVENTS = {
* condition.
*/
BREAKPOINT_CHANGE: "BreakpointChange",
/**
* The DirtyChanged event is fired when the dirty state of the editor is
* changed. The dirty state of the editor tells if the are text changes that
* have not been saved yet. Event object properties: oldValue and newValue.
* Both are booleans telling the old dirty state and the new state,
* respectively.
*/
DIRTY_CHANGED: "DirtyChanged",
};
/**

Просмотреть файл

@ -58,6 +58,7 @@ _BROWSER_TEST_FILES = \
browser_bug725388_mouse_events.js \
browser_bug707987_debugger_breakpoints.js \
browser_bug712982_line_ruler_click.js \
browser_bug700893_dirty_state.js \
head.js \
libs:: $(_BROWSER_TEST_FILES)

Просмотреть файл

@ -0,0 +1,94 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
function test() {
let temp = {};
Cu.import("resource:///modules/source-editor.jsm", temp);
let SourceEditor = temp.SourceEditor;
let component = Services.prefs.getCharPref(SourceEditor.PREFS.COMPONENT);
if (component == "textarea") {
ok(true, "skip test for bug 700893: only applicable for non-textarea components");
return;
}
waitForExplicitFinish();
let editor;
const windowUrl = "data:text/xml,<?xml version='1.0'?>" +
"<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
" title='test for bug 700893' width='600' height='500'><hbox flex='1'/></window>";
const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
let testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
testWin.addEventListener("load", function onWindowLoad() {
testWin.removeEventListener("load", onWindowLoad, false);
waitForFocus(initEditor, testWin);
}, false);
function initEditor()
{
let hbox = testWin.document.querySelector("hbox");
editor = new SourceEditor();
editor.init(hbox, {initialText: "foobar"}, editorLoaded);
}
function editorLoaded()
{
editor.focus();
is(editor.dirty, false, "editory is not dirty");
let event = null;
let eventHandler = function(aEvent) {
event = aEvent;
};
editor.addEventListener(SourceEditor.EVENTS.DIRTY_CHANGED, eventHandler);
editor.setText("omg");
is(editor.dirty, true, "editor is dirty");
ok(event, "DirtyChanged event fired")
is(event.oldValue, false, "event.oldValue is correct");
is(event.newValue, true, "event.newValue is correct");
event = null;
editor.setText("foo 2");
ok(!event, "no DirtyChanged event fired");
editor.dirty = false;
is(editor.dirty, false, "editor marked as clean");
ok(event, "DirtyChanged event fired")
is(event.oldValue, true, "event.oldValue is correct");
is(event.newValue, false, "event.newValue is correct");
event = null;
editor.setText("foo 3");
is(editor.dirty, true, "editor is dirty after changes");
ok(event, "DirtyChanged event fired")
is(event.oldValue, false, "event.oldValue is correct");
is(event.newValue, true, "event.newValue is correct");
editor.undo();
is(editor.dirty, false, "editor is not dirty after undo");
ok(event, "DirtyChanged event fired")
is(event.oldValue, true, "event.oldValue is correct");
is(event.newValue, false, "event.newValue is correct");
editor.removeEventListener(SourceEditor.EVENTS.DIRTY_CHANGED, eventHandler);
editor.destroy();
testWin.close();
testWin = editor = null;
waitForFocus(finish, window);
}
}