Eliminate controllerstate abstraction

This commit is contained in:
Tom MacWright
2012-10-23 14:33:22 -04:00
parent 1744cc195b
commit 948a5c600c
17 changed files with 155 additions and 262 deletions
+1 -1
View File
@@ -77,7 +77,7 @@ table th {
}
#modebuttons {
width:300px;
width:500px;
position:absolute;
left:0px;
top:0px;
+14 -1
View File
@@ -12,6 +12,7 @@
<div id="appLayout" class="demoLayout">
<script type="text/javascript" src="js/lib/underscore-min.js"></script>
<script type="text/javascript" src="js/lib/jquery-1.8.2.min.js"></script>
<script type="text/javascript" src="js/iD/actions/UndoStack.js"></script>
<script type="text/javascript" src="js/iD/Util.js"></script>
<script type="text/javascript" src="js/iD/Taginfo.js"></script>
<script type="text/javascript" src="js/iD/Node.js"></script>
@@ -86,6 +87,14 @@ require(["dojo/dom-geometry","dojo/dom-class","dojo/on","dojo/dom","dojo/Evented
controller.setState(new iD.controller.shape.NoSelection());
});
$('#undo').click(function() {
controller.undoStack.undo();
});
$('#redo').click(function() {
controller.undoStack.redo();
});
// ----------------------------------------------------
// Map control handlers
@@ -118,7 +127,11 @@ require(["dojo/dom-geometry","dojo/dom-class","dojo/on","dojo/dom","dojo/Evented
<button id='add-place'>
+ Place</button><button id="add-road">
+ Road</button><button id="add-area">
+ Area</button>
+ Area</button><button id="undo">
&larr;</button><button id="redo">
&rarr;</button>
<div id='addPOI'>
<table id='dndgrid'>
</table>
+4 -6
View File
@@ -14,7 +14,7 @@ declare("iD.Controller", [Evented], {
constructor:function(map) {
// summary: The Controller marshalls ControllerStates and passes events to them.
this.map = map;
this.undoStack = new iD.actions.UndoStack();
this.undoStack = new iD.UndoStack();
this.editorCache = {};
},
@@ -29,17 +29,15 @@ declare("iD.Controller", [Evented], {
if (this.state) {
this.emit("exitState", {
bubbles: true,
cancelable: true,
state: this.state.stateNameAsArray()
cancelable: true
});
}
newState.setController(this);
newState.controller = this;
this.state = newState;
newState.enterState();
this.emit("enterState", {
bubbles: true,
cancelable: true,
state: this.state.stateNameAsArray()
cancelable: true
});
},
+2 -3
View File
@@ -16,7 +16,7 @@ declare("iD.actions.CreateEntityAction", [iD.actions.UndoableEntityAction], {
this.setName("Create " + entity.entityType);
},
doAction: function() {
run: function() {
// summary: Call out to the specified method (in the Controller) to create the entity.
// See undoAction for explanation of special redo handling.
if (this.deleteAction!==null) {
@@ -28,7 +28,7 @@ declare("iD.actions.CreateEntityAction", [iD.actions.UndoableEntityAction], {
return this.SUCCESS;
},
undoAction: function() {
undo: function() {
// summary: Special handling for undoing a create. When undo is called, instead
// of simply removing the entity, we work through to make a Delete[Entity]Action,
// call that, and store it for later. Then, when this action is called again
@@ -43,7 +43,6 @@ declare("iD.actions.CreateEntityAction", [iD.actions.UndoableEntityAction], {
// summary: Set the associated delete action (see undoAction for explanation).
deleteAction = action;
}
});
// ----------------------------------------------------------------------
+6 -9
View File
@@ -17,34 +17,31 @@ declare("iD.actions.CreatePOIAction", [iD.actions.CompositeUndoableAction], {
constructor: function(connection, tags, lat, lon) {
// summary: Create a new node and set it as a POI. Used by drag-and-drop. Note that the
// node is remembered, so that on redo we can just reinstate it.
this.setName('Create POI');
this.setName('Create POI: ' + iD.Util.friendlyName(tags));
this.connection = connection;
this.tags = tags;
this.lat = lat;
this.lon = lon;
},
doAction:function() {
run: function() {
if (this.newNode === null) {
this.newNode = this.connection.doCreateNode(this.tags,
this.lat, this.lon,
_.bind(this.push, this));
}
this.inherited(arguments);
this.connection.registerPOI(this.newNode);
return this.SUCCESS;
return true;
},
undoAction:function() {
this.inherited(arguments);
undo: function() {
this.connection.unregisterPOI(this.newNode);
return this.SUCCESS;
return true;
},
getNode:function() {
getNode: function() {
return this.newNode;
}
});
// ----------------------------------------------------------------------
+10 -8
View File
@@ -15,7 +15,7 @@ declare("iD.actions.MoveNodeAction", [iD.actions.UndoableEntityAction], {
newLon: NaN,
setLatLon: null,
constructor:function(node, newLat, newLon, setLatLon) {
constructor: function(node, newLat, newLon, setLatLon) {
// summary: Move a node to a new position.
this.entity = node;
this.newLat = newLat;
@@ -24,25 +24,28 @@ declare("iD.actions.MoveNodeAction", [iD.actions.UndoableEntityAction], {
this.createTime = new Date().getTime();
},
doAction:function() {
run: function() {
var node = this.entity;
this.oldLat = node.lat;
this.oldLon = node.lon;
if (this.oldLat==this.newLat && this.oldLon==this.newLon) { return NO_CHANGE; }
if (this.oldLat === this.newLat &&
this.oldLon === this.newLon) {
return NO_CHANGE;
}
this.setLatLon(this.newLat, this.newLon);
this.markDirty();
node.refresh();
return this.SUCCESS;
return true;
},
undoAction:function() {
undo: function() {
this.setLatLon(this.oldLat, this.oldLon);
this.markClean();
this.refresh();
return this.SUCCESS;
return true;
},
mergePrevious:function(prev) {
mergePrevious: function(prev) {
if (prev.declaredClass!=this.declaredClass) { return false; }
if (prev.entity == this.entity && prev.createTime+1000>this.createTime) {
@@ -52,7 +55,6 @@ declare("iD.actions.MoveNodeAction", [iD.actions.UndoableEntityAction], {
}
return false;
}
});
// ----------------------------------------------------------------------
+85 -105
View File
@@ -1,124 +1,104 @@
// iD/actions/UndoStack.js
// ** FIXME: a couple of AS3-isms in undoIfAction/removeLastIfAction
define(['dojo/_base/declare'], function(declare){
// ----------------------------------------------------------------------
// UndoStack base class
declare("iD.actions.UndoStack", null, {
undoActions: null,
redoActions: null,
if (typeof iD === 'undefined') iD = {};
FAIL: 0,
SUCCESS: 1,
NO_CHANGE: 2,
iD.UndoStack = function() {
constructor: function() {
// summary: An undo stack. There can be any number of these, but almost all operations will
// take place on the global undo stack - implemented as a singleton-like property of
// the Controller.
this.undoActions=[];
this.redoActions=[];
},
addAction: function(_action) {
// summary: Do an action, and add it to the undo stack if it succeeded.
var result = _action.doAction();
switch (result) {
case this.FAIL:
// do something bad
break;
var stack = {},
undoActions = [],
redoActions = [];
case this.NO_CHANGE:
break;
var FAIL = 0,
SUCCESS = 1,
NO_CHANGE = 2;
case this.SUCCESS:
default:
if (this.undoActions.length>0) {
var previous = this.undoActions[this.undoActions.length-1];
if (_action.mergePrevious(previous)) {
_action.wasDirty = previous.wasDirty;
_action.connectionWasDirty = previous.connectionWasDirty;
this.undoActions.pop();
}
}
this.undoActions.push(_action);
this.redoActions=[];
break;
}
},
stack.add = function(action) {
// summary: Add an action to the undo stack
if (undoActions.length > 0) {
var previous = undoActions[undoActions.length - 1];
if (action.mergePrevious(previous)) {
action.wasDirty = previous.wasDirty;
action.connectionWasDirty = previous.connectionWasDirty;
undoActions.pop();
}
}
undoActions.push(action);
redoActions = [];
};
breakUndo: function() {
// summary: Wipe the undo stack - typically used after saving.
this.undoActions = [];
this.redoActions = [];
},
stack.breakUndo = function() {
// summary: Wipe the undo stack - typically used after saving.
undoActions = [];
redoActions = [];
};
canUndo: function() {
// summary: Are there any items on the undo stack?
return this.undoActions.length > 0;
},
stack.canUndo = function() {
// summary: Are there any items on the undo stack?
return undoActions.length > 0;
};
canRedo: function() {
// summary: Are there any redoable actions?
return this.redoActions.length > 0;
},
stack.canRedo = function() {
// summary: Are there any redoable actions?
return redoActions.length > 0;
};
undo: function() {
// summary: Undo the most recent action, and add it to the top of the redo stack.
if (!this.undoActions.length) { return; }
var action = undoActions.pop();
action.undoAction();
redoActions.push(action);
},
undoIfAction: function(_action) {
// summary: Undo the most recent action _only_ if it was of a certain type.
// Fixme: isInstanceOf needs to be made into JavaScript.
if (!this.undoActions.length) { return; }
if (this.undoActions[this.undoActions.length-1].isInstanceOf(_action)) {
this.undo();
return true;
}
return false;
},
removeLastIfAction: function(_action) {
// summary: Remove the most recent action from the stack _only_ if it was of a certain type.
// Fixme: isInstanceOf needs to be made into JavaScript.
if (this.undoActions.length && this.undoActions[this.undoActions.length-1].isInstanceOf(_action)) {
this.undoActions.pop();
}
},
stack.undo = function() {
// summary: Undo the most recent action, and add it to the top of the redo stack.
if (!stack.canUndo()) { return; }
var action = undoActions.pop();
action.undo();
redoActions.push(action);
};
getUndoDescription: function() {
// summary: Get the name of the topmost item on the undo stack.
if (!this.undoActions.length) return null;
if (this.undoActions[this.undoActions.length-1].name) {
return this.undoActions[this.undoActions.length-1].name;
}
return null;
},
stack.undoIfAction = function(_action) {
// summary: Undo the most recent action _only_ if it was of a certain type.
// Fixme: isInstanceOf needs to be made into JavaScript.
if (!undoActions.length) { return; }
if (undoActions[undoActions.length-1].isInstanceOf(_action)) {
undo();
return true;
}
return false;
};
getRedoDescription: function() {
// summary: Get the name of the topmost item on the redo stack.
if (!this.redoActions.length) return null;
if (this.redoActions[this.redoActions.length-1].name) {
return this.redoActions[this.redoActions.length-1].name;
}
return null;
},
stack.removeLastIfAction = function(_action) {
// summary: Remove the most recent action from the stack _only_ if it was of a certain type.
// Fixme: isInstanceOf needs to be made into JavaScript.
if (undoActions.length &&
undoActions[undoActions.length - 1].isInstanceOf(_action)) {
undoActions.pop();
}
};
redo: function() {
// summary: Takes the action most recently undone, does it, and adds it to the undo stack.
if (!this.redoActions.length) { return; }
var action = this.redoActions.pop();
action.doAction();
this.undoActions.push(action);
}
stack.getUndoDescription = function() {
// summary: Get the name of the topmost item on the undo stack.
if (!undoActions.length) return null;
if (undoActions[undoActions.length - 1].name) {
return undoActions[undoActions.length - 1].name;
}
return null;
};
});
stack.getRedoDescription = function() {
// summary: Get the name of the topmost item on the redo stack.
if (!redoActions.length) return null;
if (redoActions[redoActions.length - 1].name) {
return redoActions[redoActions.length - 1].name;
}
return null;
};
// ----------------------------------------------------------------------
// End of module
});
stack.redo = function() {
// summary: Takes the action most recently undone, does it, and adds it to the undo stack.
if (!stack.canRedo()) { return; }
var action = redoActions.pop();
action.run();
undoActions.push(action);
};
return stack;
};
+7 -36
View File
@@ -7,42 +7,14 @@ define(['dojo/_base/declare'], function(declare){
// UndoableAction base class
declare("iD.actions.UndoableAction", null, {
FAIL: 0,
SUCCESS: 1,
NO_CHANGE: 2,
name: "",
constructor:function() {
// summary: An UndoableAction is a user-induced change to the map data (held within the Connection)
// which can be undone. The UndoableAction doesn't actually change the data: it provides
// the logic around the change, but the change itself is generally made by the model.
// Therefore the UndoableAction constructor needs to be passed a reference
// to a method that actually makes the change - for example, MoveNodeAction will be
// passed a reference to Node._setLatLonImmediate.
},
doAction:function() {
// summary: Do the action.
return FAIL;
},
undoAction:function() {
// summary: Undo the action.
return FAIL;
},
mergePrevious:function() {
// summary: If two successive actions can be merged (in particular, two successive node moves), do that.
return false;
},
setName:function(_name) {
// summary: Set the name of an action. For UI and debugging purposes.
this.name=_name;
}
});
// ----------------------------------------------------------------------
@@ -63,14 +35,14 @@ declare("iD.actions.UndoableEntityAction", [iD.actions.UndoableAction], {
},
markDirty:function() {
// summary: Mark a change to the entity ('dirtying' it).
// summary: Mark a change to the entity ('dirtying' it).
if (!this.initialised) this.init();
if (!this.wasDirty) this.entity._markDirty();
if (!this.connectionWasDirty) this.entity.connection.modified = true;
},
markClean:function() {
// summary: If the entity was clean before, revert the dirty flag to that state.
// summary: If the entity was clean before, revert the dirty flag to that state.
if (!this.initialised) this.init();
if (!this.wasDirty) this.entity._markClean();
if (!this.connectionWasDirty) this.entity.connection.modified = false;
@@ -102,7 +74,7 @@ declare("iD.actions.CompositeUndoableAction", [iD.actions.UndoableAction], {
// summary: A CompositeUndoableAction is a group of user-induced changes to the map data
// (held within the Connection) which are executed, and therefore undone,
// in a batch. For example, creating a new node and a new way containing it.
this.actions=[];
this.actions = [];
},
push:function(action) {
@@ -112,14 +84,14 @@ declare("iD.actions.CompositeUndoableAction", [iD.actions.UndoableAction], {
clearActions:function() {
// summary: Clear the list of actions.
this.actions=[];
this.actions = [];
},
doAction:function() {
// summary: Execute all the actions one-by-one as a transaction (i.e. roll them all back if one fails).
if (this.actionsDone) { return this.FAIL; }
var somethingDone=false;
for (var i=0; i<this.actions.length; i++) {
var somethingDone = false;
for (var i = 0; i < this.actions.length; i++) {
var action = this.actions[i];
var result = action.doAction();
switch (result) {
@@ -144,7 +116,7 @@ declare("iD.actions.CompositeUndoableAction", [iD.actions.UndoableAction], {
// summary: Roll back all the actions one-by-one.
if (!this.actionsDone) { return this.FAIL; }
this._undoFrom(this.actions.length);
return this.SUCCESS;
return true;
},
_undoFrom:function(index) {
@@ -154,7 +126,6 @@ declare("iD.actions.CompositeUndoableAction", [iD.actions.UndoableAction], {
}
this.actionsDone=false;
}
});
// ----------------------------------------------------------------------
-62
View File
@@ -1,62 +0,0 @@
// iD/controller/ControllerState.js
define(['dojo/_base/declare'], function(declare) {
// ----------------------------------------------------------------------
// ControllerState base class
declare("iD.controller.ControllerState", null, {
controller: null, // parent Controller
constructor:function() {
// summary: Base class for ControllerStates.
},
setController:function(_controller) {
// summary: Set a reference to the parent Controller.
this.controller=_controller;
},
processMouseEvent:function(event,entityUI) {
// summary: Process mouse events. Most of the UI handling goes on in here.
// returns: iD.controller.ControllerState The ControllerState to move to as a result of the user's actions (or just 'this' for no change).
},
enterState:function() {
// summary: Do any work required for entering the ControllerState, such as highlighting the selected entity.
},
exitState:function(newState) {
// summary: Do any work required to leave the ControllerState clearly, such as unhighlighting the selected entity.
},
stateName:function() {
// summary: Return the name of this state as a string, e.g. 'edit.NoSelection'.
// return: String
return this.stateNameAsArray.join('.');
},
stateNameAsArray:function() {
// summary: Return the name of this state as an array, e.g. ['edit','NoSelection'].
// return: Array
return this.__proto__.declaredClass.split('.').slice(2);
},
getConnection: function() {
// summary: Shorthand to return the Connection associated with this Controller (via its Map object).
// return: iD.Connection
return this.controller.map.connection;
},
undoAdder:function() {
// summary: Shorthand for adding an action to the global undo stack, setting the scope correctly.
// return: Function
return _.bind(this.controller.undoStack.addAction,
this.controller.undoStack);
}
});
// ----------------------------------------------------------------------
// End of module
});
+4 -6
View File
@@ -1,13 +1,13 @@
// iD/controller/edit/EditBaseState.js
define(['dojo/_base/declare','dojo/_base/lang','dojo/_base/array','dojo/on',
'dijit/registry','dijit/TooltipDialog','dijit/Dialog','dijit/popup',
'iD/controller/ControllerState'], function(declare,lang,array,on,registry){
'dijit/registry'
], function(declare,lang,array,on,registry){
// ----------------------------------------------------------------------
// EditBaseState class - provides shared UI functions to edit mode states
declare("iD.controller.edit.EditBaseState", [iD.controller.ControllerState], {
declare("iD.controller.edit.EditBaseState", null, {
editortooltip: null,
@@ -16,10 +16,8 @@ declare("iD.controller.edit.EditBaseState", [iD.controller.ControllerState], {
// object is selected and the user is making changes to it.
},
openEditorTooltip: function(x, y, entity) {
openEditorTooltip: function(entity) {
// summary: Open the initial 'Edit tags/Edit shape' tooltip.
// x: Number Screen co-ordinate.
// y: Number Screen co-ordinate.
// entity: iD.Entity The entity to be edited.
$('.edit-pane h2').text(iD.Util.friendlyName(entity));
$('.edit-pane').show().addClass('active');
+1 -1
View File
@@ -13,7 +13,7 @@ define(['dojo/_base/declare',
declare("iD.controller.edit.NoSelection", [iD.controller.edit.EditBaseState], {
constructor: function() {
// summary: In 'Edit object' mode but nothing selected.
// summary: In 'Edit object' mode but nothing selected.
},
enterState: function() {
+4 -6
View File
@@ -20,9 +20,7 @@ define(['dojo/_base/declare','iD/controller/edit/EditBaseState'], function(decla
this.nodeUI = map.getUI(this.node);
this.nodeUI.setStateClass('selected')
.redraw();
this.openEditorTooltip(
map.lon2screen(this.node.lon),
map.lat2screen(this.node.lat), this.node);
this.openEditorTooltip(this.node);
return this;
},
@@ -33,10 +31,10 @@ define(['dojo/_base/declare','iD/controller/edit/EditBaseState'], function(decla
return this;
},
processMouseEvent: function(event,entityUI) {
processMouseEvent: function(event, entityUI) {
if (event.type !== 'click') return this;
var entity=entityUI ? entityUI.entity : null;
var entityType=entity ? entity.entityType : null;
var entity = entityUI ? entityUI.entity : null;
var entityType = entity ? entity.entityType : null;
switch (entityType) {
case null: return new iD.controller.edit.NoSelection();
case 'node': return new iD.controller.edit.SelectedPOINode(entityUI.entity);
+2 -2
View File
@@ -14,13 +14,13 @@
*/
define(['dojo/_base/declare', 'dojo/_base/lang', 'dojox/gfx/shape', 'iD/controller/ControllerState'],
define(['dojo/_base/declare', 'dojo/_base/lang', 'dojox/gfx/shape'],
function(declare, lang, shape){
// ----------------------------------------------------------------------
// DrawWay class
declare("iD.controller.shape.DrawWay", [iD.controller.ControllerState], {
declare("iD.controller.shape.DrawWay", null, {
way: null,
wayUI: null,
+10 -9
View File
@@ -15,7 +15,6 @@
define(['dojo/_base/declare',
'iD/actions/UndoableAction',
'iD/controller/ControllerState',
'iD/controller/shape/DrawWay',
'iD/controller/shape/SelectedWay',
'iD/controller/shape/SelectedPOINode'
@@ -24,7 +23,7 @@ define(['dojo/_base/declare',
// ----------------------------------------------------------------------
// ControllerState base class
declare("iD.controller.shape.NoSelection", [iD.controller.ControllerState], {
declare("iD.controller.shape.NoSelection", null, {
constructor: function(intent) {
// summary: In 'Draw shape' mode but nothing is selected.
@@ -77,13 +76,15 @@ declare("iD.controller.shape.NoSelection", [iD.controller.ControllerState], {
var action = new iD.actions.CreatePOIAction(this.getConnection(), {},
map.coord2lat(map.mouseY(event)),
map.coord2lon(map.mouseX(event)));
this.controller.undoStack.addAction(action);
var node = action.getNode();
this.controller.map.createUI(node);
var state = new iD.controller.edit.SelectedPOINode(node);
state.controller = this.controller;
state.enterState();
return state;
if (action.doAction()) {
this.controller.undoStack.add(action);
var node = action.getNode();
this.controller.map.createUI(node);
var state = new iD.controller.edit.SelectedPOINode(node);
state.controller = this.controller;
state.enterState();
return state;
}
}
}
}
+2 -3
View File
@@ -8,14 +8,13 @@
*/
define(['dojo/_base/declare',
'iD/actions/UndoableAction',
'iD/controller/ControllerState'
'iD/actions/UndoableAction'
], function(declare){
// ----------------------------------------------------------------------
// SelectedPOINode class
declare("iD.controller.shape.SelectedPOINode", [iD.controller.ControllerState], {
declare("iD.controller.shape.SelectedPOINode", null, {
constructor:function() {
// summary: In 'Draw shape' mode and a POI node is selected, to be converted into a way.
+2 -3
View File
@@ -7,14 +7,13 @@
// to.
define(['dojo/_base/declare',
'iD/actions/UndoableAction',
'iD/controller/ControllerState'
'iD/actions/UndoableAction'
], function(declare) {
// ----------------------------------------------------------------------
// SelectedWayNode class
declare("iD.controller.shape.SelectedWay", [iD.controller.ControllerState], {
declare("iD.controller.shape.SelectedWay", null, {
way: null,
wayUI: null,
+1 -1
View File
@@ -176,7 +176,7 @@ declare("iD.renderer.Map", null, {
return sub; // dojox.gfx.Group
},
createUI: function(entity,stateClasses) {
createUI: function(entity, stateClasses) {
// summary: Create a UI (sprite) for an entity, assigning any specified state classes
// (temporary attributes such as ':hover' or ':selected')
var id = entity.id;