Support quick undo of the last operation (Ctrl-Z).

git-svn-id: http://google-refine.googlecode.com/svn/trunk@338 7d457c2a-affb-35e4-300a-418c747d4874
This commit is contained in:
David Huynh 2010-03-23 00:26:28 +00:00
parent 6d8776953d
commit 3dc4db020f
19 changed files with 174 additions and 52 deletions

View File

@ -18,7 +18,9 @@ import org.json.JSONWriter;
import com.metaweb.gridworks.Jsonizable;
import com.metaweb.gridworks.ProjectManager;
import com.metaweb.gridworks.browsing.Engine;
import com.metaweb.gridworks.history.HistoryEntry;
import com.metaweb.gridworks.model.Project;
import com.metaweb.gridworks.process.Process;
import com.metaweb.gridworks.util.ParsingUtilities;
/**
@ -115,6 +117,26 @@ public abstract class Command {
return null;
}
static protected void performProcessAndRespond(
HttpServletRequest request,
HttpServletResponse response,
Project project,
Process process
) throws Exception {
HistoryEntry historyEntry = project.processManager.queueProcess(process);
if (historyEntry != null) {
JSONWriter writer = new JSONWriter(response.getWriter());
Properties options = new Properties();
writer.object();
writer.key("code"); writer.value("ok");
writer.key("historyEntry"); historyEntry.write(writer, options);
writer.endObject();
} else {
respond(response, "{ \"code\" : \"pending\" }");
}
}
static protected void respond(HttpServletResponse response, String content)
throws IOException {

View File

@ -45,10 +45,7 @@ abstract public class EngineDependentCommand extends Command {
AbstractOperation op = createOperation(project, request, getEngineConfig(request));
Process process = op.createProcess(project, new Properties());
boolean done = project.processManager.queueProcess(process);
respond(response, "{ \"code\" : " + (done ? "\"ok\"" : "\"pending\"") + " }");
performProcessAndRespond(request, response, project, process);
} catch (Exception e) {
respondException(response, e);
}

View File

@ -6,8 +6,6 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONWriter;
import com.metaweb.gridworks.commands.Command;
import com.metaweb.gridworks.history.HistoryEntry;
import com.metaweb.gridworks.model.Project;
@ -39,16 +37,7 @@ public class AnnotateOneRowCommand extends Command {
starred
);
boolean done = project.processManager.queueProcess(process);
if (done) {
JSONWriter writer = new JSONWriter(response.getWriter());
writer.object();
writer.key("code"); writer.value("ok");
writer.endObject();
} else {
respond(response, "{ \"code\" : \"pending\" }");
}
performProcessAndRespond(request, response, project, process);
} else {
respond(response, "{ \"code\" : \"error\", \"message\" : \"invalid command parameters\" }");
}

View File

@ -53,16 +53,19 @@ public class EditOneCellCommand extends Command {
value
);
boolean done = project.processManager.queueProcess(process);
if (done) {
HistoryEntry historyEntry = project.processManager.queueProcess(process);
if (historyEntry != null) {
/*
* If the operation has been done, return the new cell's data
* so the client side can update the cell's rendering right away.
*/
JSONWriter writer = new JSONWriter(response.getWriter());
Properties options = new Properties();
writer.object();
writer.key("code"); writer.value("ok");
writer.key("cell"); process.newCell.write(writer, new Properties());
writer.key("historyEntry"); historyEntry.write(writer, options);
writer.key("cell"); process.newCell.write(writer, options);
writer.endObject();
} else {
respond(response, "{ \"code\" : \"pending\" }");

View File

@ -1,6 +1,6 @@
package com.metaweb.gridworks.commands.edit;
import java.io.IOException;
import java.io.IOException;
import java.util.Properties;
import javax.servlet.ServletException;
@ -28,10 +28,7 @@ public class JoinMultiValueCellsCommand extends Command {
AbstractOperation op = new MultiValuedCellJoinOperation(columnName, keyColumnName, separator);
Process process = op.createProcess(project, new Properties());
boolean done = project.processManager.queueProcess(process);
respond(response, "{ \"code\" : " + (done ? "\"ok\"" : "\"pending\"") + " }");
performProcessAndRespond(request, response, project, process);
} catch (Exception e) {
respondException(response, e);
}

View File

@ -26,10 +26,7 @@ public class RemoveColumnCommand extends Command {
AbstractOperation op = new ColumnRemovalOperation(columnName);
Process process = op.createProcess(project, new Properties());
boolean done = project.processManager.queueProcess(process);
respond(response, "{ \"code\" : " + (done ? "\"ok\"" : "\"pending\"") + " }");
performProcessAndRespond(request, response, project, process);
} catch (Exception e) {
respondException(response, e);
}

View File

@ -32,10 +32,7 @@ public class SaveProtographCommand extends Command {
AbstractOperation op = new SaveProtographOperation(protograph);
Process process = op.createProcess(project, new Properties());
boolean done = project.processManager.queueProcess(process);
respond(response, "{ \"code\" : " + (done ? "\"ok\"" : "\"pending\"") + " }");
performProcessAndRespond(request, response, project, process);
} catch (Exception e) {
respondException(response, e);
}

View File

@ -29,10 +29,7 @@ public class SplitMultiValueCellsCommand extends Command {
AbstractOperation op = new MultiValuedCellSplitOperation(columnName, keyColumnName, separator, mode);
Process process = op.createProcess(project, new Properties());
boolean done = project.processManager.queueProcess(process);
respond(response, "{ \"code\" : " + (done ? "\"ok\"" : "\"pending\"") + " }");
performProcessAndRespond(request, response, project, process);
} catch (Exception e) {
respondException(response, e);
}

View File

@ -17,10 +17,23 @@ public class UndoRedoCommand extends Command {
throws ServletException, IOException {
Project project = getProject(request);
long lastDoneID = Long.parseLong(request.getParameter("lastDoneID"));
boolean done = project.processManager.queueProcess(
new HistoryProcess(project, lastDoneID));
long lastDoneID = -1;
String lastDoneIDString = request.getParameter("lastDoneID");
if (lastDoneIDString != null) {
lastDoneID = Long.parseLong(lastDoneIDString);
} else {
String undoIDString = request.getParameter("undoID");
if (undoIDString != null) {
long undoID = Long.parseLong(undoIDString);
lastDoneID = project.history.getPrecedingEntryID(undoID);
}
}
boolean done = lastDoneID == -1 ||
project.processManager.queueProcess(
new HistoryProcess(project, lastDoneID));
respond(response, "{ \"code\" : " + (done ? "\"ok\"" : "\"pending\"") + " }");
}

View File

@ -59,16 +59,19 @@ public class ReconJudgeOneCellCommand extends Command {
match
);
boolean done = project.processManager.queueProcess(process);
if (done) {
HistoryEntry historyEntry = project.processManager.queueProcess(process);
if (historyEntry != null) {
/*
* If the process is done, write back the cell's data so that the
* client side can update its UI right away.
*/
JSONWriter writer = new JSONWriter(response.getWriter());
Properties options = new Properties();
writer.object();
writer.key("code"); writer.value("ok");
writer.key("cell"); process.newCell.write(writer, new Properties());
writer.key("historyEntry"); historyEntry.write(writer, options);
writer.key("cell"); process.newCell.write(writer, options);
writer.endObject();
} else {
respond(response, "{ \"code\" : \"pending\" }");

View File

@ -114,6 +114,31 @@ public class History implements Jsonizable {
}
}
public long getPrecedingEntryID(long entryID) {
if (entryID == 0) {
return -1;
} else {
for (int i = 0; i < _pastEntries.size(); i++) {
if (_pastEntries.get(i).id == entryID) {
return i == 0 ? 0 : _pastEntries.get(i - 1).id;
}
}
for (int i = 0; i < _futureEntries.size(); i++) {
if (_futureEntries.get(i).id == entryID) {
if (i > 0) {
return _futureEntries.get(i - 1).id;
} else if (_pastEntries.size() > 0) {
return _pastEntries.get(_pastEntries.size() - 1).id;
} else {
return 0;
}
}
}
}
return -1;
}
protected HistoryEntry getEntry(long entryID) {
for (int i = 0; i < _pastEntries.size(); i++) {
if (_pastEntries.get(i).id == entryID) {

View File

@ -40,9 +40,11 @@ public class HistoryProcess extends Process {
return true;
}
public void performImmediate() {
public HistoryEntry performImmediate() {
_project.history.undoRedo(_lastDoneID);
_done = true;
return null;
}
public void startPerforming(ProcessManager manager) {

View File

@ -5,6 +5,8 @@ import java.util.Properties;
import org.json.JSONException;
import org.json.JSONWriter;
import com.metaweb.gridworks.history.HistoryEntry;
abstract public class LongRunningProcess extends Process {
final protected String _description;
protected ProcessManager _manager;
@ -51,7 +53,7 @@ abstract public class LongRunningProcess extends Process {
}
@Override
public void performImmediate() {
public HistoryEntry performImmediate() {
throw new RuntimeException("Not an immediate process");
}

View File

@ -1,6 +1,7 @@
package com.metaweb.gridworks.process;
import com.metaweb.gridworks.Jsonizable;
import com.metaweb.gridworks.history.HistoryEntry;
public abstract class Process implements Jsonizable {
abstract public boolean isImmediate();
@ -8,7 +9,7 @@ public abstract class Process implements Jsonizable {
abstract public boolean isRunning();
abstract public boolean isDone();
abstract public void performImmediate() throws Exception;
abstract public HistoryEntry performImmediate() throws Exception;
abstract public void startPerforming(ProcessManager manager);
abstract public void cancel();

View File

@ -8,6 +8,8 @@ import org.json.JSONException;
import org.json.JSONWriter;
import com.metaweb.gridworks.Jsonizable;
import com.metaweb.gridworks.history.HistoryEntry;
import com.metaweb.gridworks.history.HistoryProcess;
public class ProcessManager implements Jsonizable {
protected List<Process> _processes = new LinkedList<Process>();
@ -29,22 +31,36 @@ public class ProcessManager implements Jsonizable {
writer.endObject();
}
public boolean queueProcess(Process process) {
public HistoryEntry queueProcess(Process process) {
if (process.isImmediate() && _processes.size() == 0) {
try {
process.performImmediate();
return process.performImmediate();
} catch (Exception e) {
// TODO: Not sure what to do yet
e.printStackTrace();
}
return true;
} else {
_processes.add(process);
update();
return false;
}
return null;
}
public boolean queueProcess(HistoryProcess process) {
if (process.isImmediate() && _processes.size() == 0) {
try {
return process.performImmediate() != null;
} catch (Exception e) {
// TODO: Not sure what to do yet
e.printStackTrace();
}
} else {
_processes.add(process);
update();
}
return false;
}
public boolean hasPending() {

View File

@ -31,12 +31,14 @@ abstract public class QuickHistoryEntryProcess extends Process {
throw new RuntimeException("Not a long-running process");
}
public void performImmediate() throws Exception {
public HistoryEntry performImmediate() throws Exception {
if (_historyEntry == null) {
_historyEntry = createHistoryEntry();
}
_project.history.addEntry(_historyEntry);
_done = true;
return _historyEntry;
}
public void startPerforming(ProcessManager manager) {

View File

@ -215,6 +215,10 @@ Gridworks.postProcess = function(command, params, body, updateOptions, callbacks
if (o.code == "ok") {
Gridworks.update(updateOptions, callbacks["onFinallyDone"]);
if ("historyEntry" in o) {
ui.processWidget.showUndo(o.historyEntry);
}
} else if (o.code == "pending") {
if ("onPending" in callbacks) {
try {

View File

@ -5,6 +5,21 @@ function ProcessWidget(div) {
this._updateOptions = {};
this._onDones = [];
this._latestHistoryEntry = null;
var self = this;
$(window).keypress(function(evt) {
if (evt.ctrlKey || evt.metaKey) {
var t = evt.target;
if (t) {
var tagName = t.tagName.toLowerCase();
if (tagName == "textarea" || tagName == "input") {
return;
}
}
self.undo();
}
});
this.update({});
}
@ -13,6 +28,8 @@ ProcessWidget.prototype.resize = function() {
};
ProcessWidget.prototype.update = function(updateOptions, onDone) {
this._latestHistoryEntry = null;
for (var n in updateOptions) {
if (updateOptions.hasOwnProperty(n)) {
this._updateOptions[n] = updateOptions[n];
@ -30,11 +47,39 @@ ProcessWidget.prototype.update = function(updateOptions, onDone) {
Ajax.chainGetJSON(
"/command/get-processes?" + $.param({ project: theProject.id }), null,
function(data) {
self._latestHistoryEntry = null;
self._render(data);
}
);
};
ProcessWidget.prototype.showUndo = function(historyEntry) {
var self = this;
this._latestHistoryEntry = historyEntry;
this._div.empty().show().html(
'<div class="process-panel-inner"><div class="process-panel-undo">' +
'<a href="javascript:{}" bind="undo">Undo</a> <span bind="description"></span>' +
'</div></div>'
);
var elmts = DOM.bind(this._div);
elmts.description.text(historyEntry.description);
elmts.undo.click(function() { self.undo() });
};
ProcessWidget.prototype.undo = function() {
if (this._latestHistoryEntry != null) {
Gridworks.postProcess(
"undo-redo",
{ undoID: this._latestHistoryEntry.id },
null,
{ everythingChanged: true }
);
}
};
ProcessWidget.prototype._cancelAll = function() {
var self = this;
$.post(

View File

@ -7,11 +7,15 @@
}
.process-panel-inner {
border: 1px solid #ccc;
border: 1px solid #aaa;
width: 500px;
margin: 0 auto;
text-align: left;
background: #fffee0;
-moz-border-radius-bottomleft: 10px;
-webkit-border-bottom-left-radius: 10px;
-moz-border-radius-bottomright: 10px;
-webkit-border-bottom-right-radius: 10px;
}
.process-panel-head {
@ -24,3 +28,9 @@
overflow: auto;
padding: 10px;
}
.process-panel-undo {
max-height: 50px;
overflow: auto;
padding: 10px;
}