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.Jsonizable;
import com.metaweb.gridworks.ProjectManager; import com.metaweb.gridworks.ProjectManager;
import com.metaweb.gridworks.browsing.Engine; import com.metaweb.gridworks.browsing.Engine;
import com.metaweb.gridworks.history.HistoryEntry;
import com.metaweb.gridworks.model.Project; import com.metaweb.gridworks.model.Project;
import com.metaweb.gridworks.process.Process;
import com.metaweb.gridworks.util.ParsingUtilities; import com.metaweb.gridworks.util.ParsingUtilities;
/** /**
@ -115,6 +117,26 @@ public abstract class Command {
return null; 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) static protected void respond(HttpServletResponse response, String content)
throws IOException { throws IOException {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,10 +17,23 @@ public class UndoRedoCommand extends Command {
throws ServletException, IOException { throws ServletException, IOException {
Project project = getProject(request); Project project = getProject(request);
long lastDoneID = Long.parseLong(request.getParameter("lastDoneID"));
boolean done = project.processManager.queueProcess( long lastDoneID = -1;
new HistoryProcess(project, lastDoneID)); 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\"") + " }"); respond(response, "{ \"code\" : " + (done ? "\"ok\"" : "\"pending\"") + " }");
} }

View File

@ -59,16 +59,19 @@ public class ReconJudgeOneCellCommand extends Command {
match match
); );
boolean done = project.processManager.queueProcess(process); HistoryEntry historyEntry = project.processManager.queueProcess(process);
if (done) { if (historyEntry != null) {
/* /*
* If the process is done, write back the cell's data so that the * If the process is done, write back the cell's data so that the
* client side can update its UI right away. * client side can update its UI right away.
*/ */
JSONWriter writer = new JSONWriter(response.getWriter()); JSONWriter writer = new JSONWriter(response.getWriter());
Properties options = new Properties();
writer.object(); writer.object();
writer.key("code"); writer.value("ok"); 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(); writer.endObject();
} else { } else {
respond(response, "{ \"code\" : \"pending\" }"); 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) { protected HistoryEntry getEntry(long entryID) {
for (int i = 0; i < _pastEntries.size(); i++) { for (int i = 0; i < _pastEntries.size(); i++) {
if (_pastEntries.get(i).id == entryID) { if (_pastEntries.get(i).id == entryID) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,21 @@ function ProcessWidget(div) {
this._updateOptions = {}; this._updateOptions = {};
this._onDones = []; 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({}); this.update({});
} }
@ -13,6 +28,8 @@ ProcessWidget.prototype.resize = function() {
}; };
ProcessWidget.prototype.update = function(updateOptions, onDone) { ProcessWidget.prototype.update = function(updateOptions, onDone) {
this._latestHistoryEntry = null;
for (var n in updateOptions) { for (var n in updateOptions) {
if (updateOptions.hasOwnProperty(n)) { if (updateOptions.hasOwnProperty(n)) {
this._updateOptions[n] = updateOptions[n]; this._updateOptions[n] = updateOptions[n];
@ -30,11 +47,39 @@ ProcessWidget.prototype.update = function(updateOptions, onDone) {
Ajax.chainGetJSON( Ajax.chainGetJSON(
"/command/get-processes?" + $.param({ project: theProject.id }), null, "/command/get-processes?" + $.param({ project: theProject.id }), null,
function(data) { function(data) {
self._latestHistoryEntry = null;
self._render(data); 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() { ProcessWidget.prototype._cancelAll = function() {
var self = this; var self = this;
$.post( $.post(

View File

@ -7,11 +7,15 @@
} }
.process-panel-inner { .process-panel-inner {
border: 1px solid #ccc; border: 1px solid #aaa;
width: 500px; width: 500px;
margin: 0 auto; margin: 0 auto;
text-align: left; text-align: left;
background: #fffee0; 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 { .process-panel-head {
@ -23,4 +27,10 @@
max-height: 70px; max-height: 70px;
overflow: auto; overflow: auto;
padding: 10px; padding: 10px;
}
.process-panel-undo {
max-height: 50px;
overflow: auto;
padding: 10px;
} }