Permalink
Browse files

Gadgets support reimplemented and working (some bugs left)

  • Loading branch information...
1 parent 1929e34 commit 1111c430bfdd93550f6457826e199deb6fc31d93 @p2k p2k committed Aug 27, 2009
View
68 amqp_rpc_server.py
@@ -43,13 +43,16 @@
# WAVELET_OPEN (sync)
# PARTICIPANT_INFO (sync)
# PARTICIPANT_SEARCH (sync)
+# GADGET_LIST (sync)
# WAVELET_ADD_PARTICIPANT (sync)
# WAVELET_REMOVE_SELF (sync)
-# DOCUMENT_ELEMENT_REPLACE (sync)
-# DOCUMENT_ELEMENT_DELTA (async)
# OPERATION_MESSAGE_BUNDLE (OT)
# DOCUMENT_INSERT
# DOCUMENT_DELETE
+# DOCUMENT_ELEMENT_INSERT
+# DOCUMENT_ELEMENT_DELETE
+# DOCUMENT_ELEMENT_DELTA
+# DOCUMENT_ELEMENT_SETPREF
# -> OPERATION_MESSAGE_BUNDLE_ACK
class PyGoWaveMessageProcessor(object):
@@ -115,6 +118,7 @@ def broadcast(self, wavelet, type, property, except_connections=[]):
"type": type,
"property": property
}
+ logger.debug("Broadcasting Message:\n" + repr(msg_dict))
for p in wavelet.participants.all():
for conn in p.connections.all():
if not conn in except_connections:
@@ -133,6 +137,7 @@ def emit(self, to, type, property, except_connections=[]):
"type": type,
"property": property
}
+ logger.debug("Emiting Message to %s/%d:\n%s" % (to.participant.name, to.id, repr(msg_dict)))
if self.out_queue.has_key(to.rx_key):
self.out_queue[to.rx_key].append(msg_dict)
else:
@@ -224,6 +229,11 @@ def handle_participant_message(self, wavelet, pconn, message):
lst.append(p.id)
self.emit(pconn, "PARTICIPANT_SEARCH", {"result": "OK", "data": lst})
+ elif message["type"] == "GADGET_LIST":
+ all_gadgets = map(lambda g: {"id": g.id, "uploaded_by": g.by_user.participants.all()[0].name, "name": g.title, "descr": g.description, "url": g.url}, Gadget.objects.all())
+ logger.info("[%s/%d@%s] Sending Gadget list" % (participant.name, pconn.id, wavelet.wave.id))
+ self.emit(pconn, "GADGET_LIST", all_gadgets)
+
elif message["type"] == "WAVELET_ADD_PARTICIPANT":
# Find participant
try:
@@ -249,60 +259,6 @@ def handle_participant_message(self, wavelet, pconn, message):
wavelet.wave.delete()
return False
- elif message["type"] == "DOCUMENT_ELEMENT_REPLACE":
- # Find Gadget
- try:
- g = Gadget.objects.get(pk=message["property"])
- except ObjectDoesNotExist:
- logger.error("[%s/%d@%s] Gadget #%s not found" % (participant.name, pconn.id, wavelet.wave.id, message["property"]))
- return # Fail silently (TODO: report error to user)
-
- blip = wavelet.root_blip
- if blip.elements.count() > 0:
- blip.elements.all().delete()
-
- ge = g.instantiate()
- ge.blip = blip
- ge.position = 0
- ge.save()
-
- logger.info("[%s/%d@%s] Gadget #%s (%s) set -> GadgetElement #%d" % (participant.name, pconn.id, wavelet.wave.id, message["property"], g.title, ge.id))
-
- self.broadcast(wavelet, "DOCUMENT_ELEMENT_REPLACE", {"url": ge.url, "id": ge.id, "data": {}})
-
- elif message["type"] == "DOCUMENT_ELEMENT_DELTA":
- elt_id = int(message["property"]["id"])
- delta = message["property"]["delta"]
- # Find GadgetElement
- try:
- ge = GadgetElement.objects.get(pk=elt_id, blip=wavelet.root_blip)
- except ObjectDoesNotExist:
- logger.error("[%s/%d@%s] GadgetElement #%d not found (or not accessible)" % (participant.name, pconn.id, wavelet.wave.id, elt_id))
- return # Fail silently (TODO: report error to user)
-
- ge.apply_delta(delta) # Apply delta and save
- logger.info("[%s/%d@%s] Applied delta to GadgetElement #%d" % (participant.name, pconn.id, wavelet.wave.id, elt_id))
-
- # Asynchronous event, so send to all part. except the sender
- self.broadcast(wavelet, "DOCUMENT_ELEMENT_DELTA", {"id": elt_id, "delta": delta}, [pconn])
-
- elif message["type"] == "DOCUMENT_ELEMENT_SETPREF":
- elt_id = int(message["property"]["id"])
- key = message["property"]["key"]
- value = message["property"]["value"]
- # Find GadgetElement
- try:
- ge = GadgetElement.objects.get(pk=elt_id, blip=wavelet.root_blip)
- except ObjectDoesNotExist:
- logger.error("[%s/%d@%s] GadgetElement #%d not found (or not accessible)" % (participant.name, pconn.id, wavelet.wave.id, elt_id))
- return # Fail silently (TODO: report error to user)
-
- ge.set_userpref(key, value)
- logger.info("[%s/%d@%s] Set UserPref '%s' on GadgetElement #%d" % (participant.name, pconn.id, wavelet.wave.id, key, elt_id))
-
- # Asynchronous event, so send to all part. except the sender
- self.broadcast(wavelet, "DOCUMENT_ELEMENT_SETPREF", {"id": elt_id, "key": key, "value": value}, [pconn])
-
elif message["type"] == "OPERATION_MESSAGE_BUNDLE":
# Build OpManager
newdelta = OpManager(wavelet.wave.id, wavelet.id)
View
44 media/css/pygowave-client-style.css
@@ -265,18 +265,28 @@
overflow: auto;
font-size: 1.3em;
outline: none;
+ padding-bottom: 2px;
border-bottom: 1px solid #B8C6D9;
}
.blip_editor_widget p
{
line-height: normal !important;
margin-left: 5px;
- margin-bottom: 10px;
- margin-top: 5px;
+ margin-bottom: 0;
+ margin-top: 2px;
padding: 0;
}
+.blip_editor_widget .gadget_element
+{
+ width: 90%;
+ height: 400px;
+ margin-left: 5px;
+ margin-top: 2px;
+ border: 1px solid black;
+}
+
.search_widget
{
height: 24px;
@@ -432,7 +442,7 @@
background-color: #CCCCCC;
width: 100%;
height: 31px;
- background-image: url(../images/blip_error.png);
+ background-image: url(../images/generic_error.png);
background-repeat: no-repeat;
background-position: 4px 4px;
}
@@ -451,14 +461,32 @@
margin-bottom: 0px;
}
-.wavelet_add_gadget_div div
+.wavelet_add_gadget .description
+{
+ font-size: 0.8em;
+}
+
+.wavelet_add_gadget .select_div
{
- padding-left: 10px;
padding-top: 10px;
- padding-right: 10px;
+ padding-bottom: 10px;
+ height: 26px;
}
-.wavelet_add_gadget_div select
+.wavelet_add_gadget select
{
- width: 100%;
+ float: left;
+ width: 260px;
+}
+
+.wavelet_add_gadget .refresh_button
+{
+ float: left;
+ height: 16px;
+ width: 16px;
+ padding: 2px;
+ margin-top: 4px;
+ margin-left: 6px;
+ background: url(../images/view-refresh.png) no-repeat;
+ cursor: pointer;
}
View
23 media/css/style.css
@@ -269,8 +269,31 @@ p
.gadget_loader_fail
{
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
+
+.gadget_loader_fail .bg_div
+{
+ width: 100%;
+ height: 100%;
+ background: url(../images/ui-bg_diagonals-thick_18_b81900_40x40.png) repeat scroll 50% 50%;
+ opacity: 0.5;
+}
+
+.gadget_loader_fail .msg_div
+{
color: #CC0000;
font-weight: bold;
+ background-image: url(../images/generic_error.png);
+ background-repeat: no-repeat;
+ background-position: 5px 8px;
+ background-color: #CCCCCC;
+ padding-left: 32px;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ font-size: 1.3em;
}
.button_box
View
BIN media/images/blip_editor_toolbarbuttons.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
0 media/images/blip_error.png → media/images/generic_error.png
File renamed without changes
View
BIN media/images/view-refresh.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
287 pygowave_client/cache/model/model.js
@@ -269,7 +269,7 @@ pygowave.model = (function() {
/**
* Element-objects are all the non-text elements in a Blip.
* An element has no physical presence in the text of a Blip, but it maintains
- * an implicit protected space (or newline) to keep positions distinct.
+ * an implicit protected newline character to keep positions distinct.
*
* Only special Wave Client elements are treated here.
* There are no HTML elements in any Blip. All markup is handled by Annotations.
@@ -283,13 +283,24 @@ pygowave.model = (function() {
* Called on instantiation. Documented for internal purposes.
* @constructor {private} initialize
* @param {Blip} blip The element's parent Blip
+ * @param {int} id ID of the element, setting this to null will assign
+ * a new temporaty ID
* @param {int} position Index where this element resides
* @param {int} type Type of the element
+ * @param {Object} properties The element's properties
*/
- initialize: function (blip, position, type) {
+ initialize: function (blip, id, position, type, properties) {
this._blip = blip;
+ if (id == null)
+ this._id = Element.newTempId();
+ else
+ this._id = id;
this._pos = position;
this._type = type;
+ if (properties == null)
+ this._properties = new Hash();
+ else
+ this._properties = new Hash(properties);
},
/**
@@ -302,6 +313,26 @@ pygowave.model = (function() {
},
/**
+ * Set the parent Blip to the given Blip.
+ *
+ * @function {public} setBlip
+ */
+ setBlip: function (blip) {
+ this._blip = blip;
+ },
+
+ /**
+ * Return the ID of the element. A negative ID represents a temporary ID,
+ * that is only valid for this session and will be replaced by a real ID
+ * on reload.
+ *
+ * @function {public int} id
+ */
+ id: function () {
+ return this._id;
+ },
+
+ /**
* Returns the type of the element.
*
* @function {public int} type
@@ -329,6 +360,12 @@ pygowave.model = (function() {
this._pos = pos;
}
});
+ Element.newTempId = function () {
+ Element.lastTempId--;
+ return Element.lastTempId;
+ };
+
+ Element.lastTempId = 0;
/**
* A gadget element.
@@ -355,14 +392,18 @@ pygowave.model = (function() {
* Called on instantiation. Documented for internal purposes.
* @constructor {private} initialize
* @param {Blip} blip The gadget's parent Blip
+ * @param {int} id ID of the element, setting this to null will assign
+ * a new temporaty ID
* @param {int} position Index where this gadget resides
- * @param {String} url URL of the gadget xml
- */
- initialize: function (blip, position, url) {
- this.parent(blip, position, ELEMENT_TYPE.GADGET);
- this._url = url;
- this._fields = new Hash();
- this._userprefs = new Hash();
+ * @param {Object} properties The gadget element's properties
+ * @param {int} id
+ */
+ initialize: function (blip, id, position, properties) {
+ this.parent(blip, id, position, ELEMENT_TYPE.GADGET, properties);
+ if (this._properties.has("fields"))
+ this._properties.set("fields", new Hash(this._properties.get("fields")));
+ if (this._properties.has("userprefs"))
+ this._properties.set("userprefs", new Hash(this._properties.get("userprefs")));
},
/**
@@ -371,16 +412,22 @@ pygowave.model = (function() {
* @function {public Object} fields
*/
fields: function () {
- return this._fields.getClean();
+ if (this._properties.has("fields"))
+ return this._properties.get("fields").getClean();
+ else
+ return {};
},
/**
- * Return all UserPrefs as object.
+ * Return all UserPrefs as hash.
*
- * @function {public Object} userPrefs
+ * @function {public Hash} userPrefs
*/
userPrefs: function () {
- return this._userprefs.getClean();
+ if (this._properties.has("userprefs"))
+ return this._properties.get("userprefs");
+ else
+ return {};
},
/**
@@ -389,7 +436,10 @@ pygowave.model = (function() {
* @function {public String} url
*/
url: function () {
- return this._url;
+ if (this._properties.has("url"))
+ return this._properties.get("url");
+ else
+ return "";
},
/**
@@ -400,12 +450,15 @@ pygowave.model = (function() {
* gadget state.
*/
applyDelta: function (delta) {
- this._fields.update(delta);
- for (var __iter0_ = new _Iterator(this._fields); __iter0_.hasNext();) {
+ if (!this._properties.has("fields"))
+ this._properties.set("fields", new Hash());
+ var fields = this._properties.get("fields");
+ fields.update(delta);
+ for (var __iter0_ = new _Iterator(this._properties); __iter0_.hasNext();) {
var value = __iter0_.next();
var key = __iter0_.key();
if (value == null)
- delete this._fields[key];
+ delete fields[key];
}
delete __iter0_;
this.fireEvent("stateChange");
@@ -418,9 +471,13 @@ pygowave.model = (function() {
* @param {String} key
* @param {Object} value
*/
- setUserPref: function (key, value) {
- this._userprefs[key] = value;
- this.fireEvent("setUserPref", [key, value]);
+ setUserPref: function (key, value, noevent) {
+ if (!$defined(noevent)) noevent = false;
+ if (!this._properties.has("userprefs"))
+ this._properties.set("userprefs", new Hash());
+ this._properties.get("userprefs").set(key, value);
+ if (!noevent)
+ this.fireEvent("setUserPref", [key, value]);
}
});
@@ -456,6 +513,20 @@ pygowave.model = (function() {
*/
/**
+ * Fired on element insertion.
+ *
+ * @event onInsertElement
+ * @param {int} index Offset where the element is inserted
+ */
+
+ /**
+ * Fired on element deletion.
+ *
+ * @event onDeleteElement
+ * @param {int} index Offset where the element is deleted
+ */
+
+ /**
* Fired if the Blip has gone out of sync with the server.
*
* @event onOutOfSync
@@ -476,17 +547,25 @@ pygowave.model = (function() {
* @... {int} version Version of the Blip
* @... {Boolean} submitted True if this Blip is submitted
* @param {options String} content Content of the Blip
+ * @param {optional Element[]} elements Element objects which initially
+ * reside in this Blip
* @param {optional Blip} parent Parent Blip if this is a nested Blip
*/
- initialize: function (wavelet, id, options, content, parent) {
+ initialize: function (wavelet, id, options, content, elements, parent) {
if (!$defined(content)) content = "";
+ if (!$defined(elements)) elements = [];
if (!$defined(parent)) parent = null;
this.setOptions(options);
this._wavelet = wavelet;
this._id = id;
this._parent = parent;
this._content = content;
- this._elements = [];
+ this._elements = elements;
+ for (var __iter0_ = new _Iterator(this._elements); __iter0_.hasNext();) {
+ var element = __iter0_.next();
+ element.setBlip(this);
+ }
+ delete __iter0_;
this._annotations = [];
this._outofsync = false;
},
@@ -518,6 +597,32 @@ pygowave.model = (function() {
},
/**
+ * Returns the Element object at the given position or null.
+ *
+ * @function {public Element} elementAt
+ * @param {int} index Index of the element to retrieve
+ */
+ elementAt: function (index) {
+ for (var __iter0_ = new XRange(len(this._elements)); __iter0_.hasNext();) {
+ var i = __iter0_.next();
+ var elt = this._elements[i];
+ if (elt.position() == index)
+ return elt;
+ }
+ delete __iter0_;
+ return null;
+ },
+
+ /**
+ * Returns all Elements of this Blip.
+ *
+ * @function {public Element[]} allElements
+ */
+ allElements: function () {
+ return this._elements;
+ },
+
+ /**
* Insert a text at the specified index. This moves annotations and
* elements as appropriate.<br/>
* Note: This sets the wavelet status to 'dirty'.
@@ -583,6 +688,95 @@ pygowave.model = (function() {
},
/**
+ * Insert an element at the specified index. This implicitly adds a
+ * protected newline character at the index.<br/>
+ * Note: This sets the wavelet status to 'dirty'.
+ *
+ * @function {public} insertElement
+ * @param {int} index Position of the new element
+ * @param {String} type Element type
+ * @param {Object} properties Element properties
+ * @param {optional Boolean} noevent Set to true if no event should be generated
+ */
+ insertElement: function (index, type, properties, noevent) {
+ if (!$defined(noevent)) noevent = false;
+ this.insertText(index, "\n", true);
+ if (type == 2)
+ var elt = new GadgetElement(this, null, index, properties);
+ else
+ elt = new Element(this, null, index, type, properties);
+ this._elements.append(elt);
+ this._wavelet._setStatus("dirty");
+ if (!noevent)
+ this.fireEvent("insertElement", index);
+ },
+
+ /**
+ * Delete an element at the specified index. This implicitly deletes the
+ * protected newline character at the index.<br/>
+ * Note: This sets the wavelet status to 'dirty'.
+ *
+ * @function {public} deleteElement
+ * @param {int} index Position of the element to delete
+ * @param {optional Boolean} noevent Set to true if no event should be generated
+ */
+ deleteElement: function (index, noevent) {
+ if (!$defined(noevent)) noevent = false;
+ for (var __iter0_ = new XRange(len(this._elements)); __iter0_.hasNext();) {
+ var i = __iter0_.next();
+ var elt = this._elements[i];
+ if (elt.position() == index) {
+ this._elements.pop(i);
+ break;
+ }
+ }
+ delete __iter0_;
+ this.deleteText(index, 1, true);
+ if (!noevent)
+ this.fireEvent("deleteElement", index);
+ },
+
+ /**
+ * Apply an element delta. Currently only for gadget elements.<br/>
+ * Note: This action always emits stateChange.
+ *
+ * @function {public} applyElementDelta
+ * @param {int} index Position of the element
+ * @param {Object} delta Delta to apply to the element
+ */
+ applyElementDelta: function (index, delta) {
+ for (var __iter0_ = new _Iterator(this._elements); __iter0_.hasNext();) {
+ var elt = __iter0_.next();
+ if (elt.position() == index) {
+ elt.applyDelta(delta);
+ break;
+ }
+ }
+ delete __iter0_;
+ },
+
+ /**
+ * Set an UserPref of an element. Currently only for gadget elements.
+ *
+ * @function {public} setElementUserpref
+ * param {int} index Position of the element
+ * @param {Object} key Name of the UserPref
+ * @param {Object} value Value of the UserPref
+ * @param {optional Boolean} noevent Set to true if no event should be generated
+ */
+ setElementUserpref: function (index, key, value, noevent) {
+ if (!$defined(noevent)) noevent = false;
+ for (var __iter0_ = new _Iterator(this._elements); __iter0_.hasNext();) {
+ var elt = __iter0_.next();
+ if (elt.position() == index) {
+ elt.setUserPref(key, value, noevent);
+ break;
+ }
+ }
+ delete __iter0_;
+ },
+
+ /**
* Returns the text content of this Blip.
* @function {public String} content
*/
@@ -707,6 +901,14 @@ pygowave.model = (function() {
},
/**
+ * Returns the parent WaveModel object.
+ * @function {public WaveModel} waveModel
+ */
+ waveModel: function () {
+ return this._wave;
+ },
+
+ /**
* Add a participant to this Wavelet.<br/>
* Note: Fires {@link pygowave.model.Wavelet.onParticipantsChanged onParticipantsChanged}
*
@@ -784,11 +986,14 @@ pygowave.model = (function() {
* @function {public Blip} appendBlip
* @param {String} id ID of the new Blip
* @param {Object} options Information about the Blip
- * @param {options String} content Content of the Blip
+ * @param {optional String} content Content of the Blip
+ * @param {optional Element[]} elements Element objects which initially
+ * reside in this Blip
*/
- appendBlip: function (id, options, content) {
+ appendBlip: function (id, options, content, elements) {
if (!$defined(content)) content = "";
- return this.insertBlip(len(this._blips), id, options, content);
+ if (!$defined(elements)) elements = [];
+ return this.insertBlip(len(this._blips), id, options, content, elements);
},
/**
@@ -800,11 +1005,14 @@ pygowave.model = (function() {
* @param {int} index Index where to insert the Blip
* @param {String} id ID of the new Blip
* @param {Object} options Information about the Blip
- * @param {options String} content Content of the Blip
+ * @param {optional String} content Content of the Blip
+ * @param {optional Element[]} elements Element objects which initially
+ * reside in this Blip
*/
- insertBlip: function (index, id, options, content) {
+ insertBlip: function (index, id, options, content, elements) {
if (!$defined(content)) content = "";
- var blip = new Blip(this, id, options, content);
+ if (!$defined(elements)) elements = [];
+ var blip = new Blip(this, id, options, content, elements);
this._blips.insert(index, blip);
this.fireEvent("blipInserted", [index, id]);
return blip;
@@ -927,6 +1135,15 @@ pygowave.model = (function() {
},
/**
+ * Returns the ID of the viewer.
+ *
+ * @function {public String} viewerId
+ */
+ viewerId: function () {
+ return this._viewerId;
+ },
+
+ /**
* Load the wave's contents from a JSON-serialized snapshot and a map of
* participant objects.
*
@@ -960,7 +1177,16 @@ pygowave.model = (function() {
version: blip.version,
submitted: blip.submitted
};
- var blipObj = rootWaveletObj.appendBlip(blip_id, blip_options, blip.content);
+ var blip_elements = [];
+ for (var __iter1_ = new _Iterator(blip.elements); __iter1_.hasNext();) {
+ var serialelement = __iter1_.next();
+ if (serialelement.type == ELEMENT_TYPE.GADGET)
+ blip_elements.append(new GadgetElement(null, serialelement.id, serialelement.index, serialelement.properties));
+ else
+ blip_elements.append(new Element(null, serialelement.id, serialelement.index, serialelement.type, serialelement.properties));
+ }
+ delete __iter1_;
+ var blipObj = rootWaveletObj.appendBlip(blip_id, blip_options, blip.content, blip_elements);
}
delete __iter0_;
},
@@ -1014,6 +1240,7 @@ pygowave.model = (function() {
return {
WaveModel: WaveModel,
- Participant: Participant
+ Participant: Participant,
+ ELEMENT_TYPE: ELEMENT_TYPE
};
})();
View
177 pygowave_client/cache/operations/operations.js
@@ -33,49 +33,13 @@ pygowave.operations = (function() {
var DOCUMENT_DELETE = "DOCUMENT_DELETE";
- var DOCUMENT_REPLACE = "DOCUMENT_REPLACE";
+ var DOCUMENT_ELEMENT_INSERT = "DOCUMENT_ELEMENT_INSERT";
- /**
- * Represents a start and end range with integers.
- *
- * Ranges map positions in the document. A range must have at least a length
- * of zero. If zero, the range is considered to be a single point (collapsed).
- *
- * @class {public} Range
- */
- var Range = new Class({
-
- /**
- * Initializes the range with a start and end position.
- *
- * @constructor {public} initialize
- *
- * @param {int} start Start index of the range.
- * @param {int} end End index of the range.
- *
- * #@throws ValueError Value error if the range is invalid (less than zero).
- */
- initialize: function (start, end) {
- if (!$defined(start)) start = 0;
- if (!$defined(end)) end = 1;
- this.start = start;
- this.end = end;
- },
+ var DOCUMENT_ELEMENT_DELETE = "DOCUMENT_ELEMENT_DELETE";
- __repr__: function () {
- return "Range(%d,%d)".sprintf(this.start, this.end);
- },
+ var DOCUMENT_ELEMENT_DELTA = "DOCUMENT_ELEMENT_DELTA";
- /**
- * "
- * Returns true if this represents a single point as opposed to a range.
- *
- * @function {public Boolean} isCollapsed
- */
- isCollapsed: function () {
- return this.end == this.start;
- }
- });
+ var DOCUMENT_ELEMENT_SETPREF = "DOCUMENT_ELEMENT_SETPREF";
/**
* Represents a generic operation applied on the server.
@@ -151,12 +115,9 @@ pygowave.operations = (function() {
* @param {Operation} other_op
*/
isCompatibleTo: function (other_op) {
- if ((this.type == DOCUMENT_INSERT || this.type == DOCUMENT_DELETE) && (other_op.type == DOCUMENT_INSERT || other_op.type == DOCUMENT_DELETE)) {
- if (this.wave_id != other_op.wave_id || this.wavelet_id != other_op.wavelet_id || this.blip_id != this.blip_id)
- return false;
- return true;
- }
- return false;
+ if (this.wave_id != other_op.wave_id || this.wavelet_id != other_op.wavelet_id || this.blip_id != this.blip_id)
+ return false;
+ return true;
},
/**
@@ -165,7 +126,7 @@ pygowave.operations = (function() {
* @function {public Boolean} isInsert
*/
isInsert: function () {
- return this.type == DOCUMENT_INSERT;
+ return this.type == DOCUMENT_INSERT || this.type == DOCUMENT_ELEMENT_INSERT;
},
/**
@@ -174,7 +135,16 @@ pygowave.operations = (function() {
* @function {public Boolean} isDelete
*/
isDelete: function () {
- return this.type == DOCUMENT_DELETE;
+ return this.type == DOCUMENT_DELETE || this.type == DOCUMENT_ELEMENT_DELETE;
+ },
+
+ /**
+ * Returns true, if this op is an (attribute) change operation.
+ *
+ * @function {public Boolean} isChange
+ */
+ isChange: function () {
+ return this.type == DOCUMENT_ELEMENT_DELTA || this.type == DOCUMENT_ELEMENT_SETPREF;
},
/**
@@ -189,6 +159,8 @@ pygowave.operations = (function() {
return len(this.property);
else if (this.type == DOCUMENT_DELETE)
return this.property;
+ else if (this.type == DOCUMENT_ELEMENT_INSERT || this.type == DOCUMENT_ELEMENT_DELETE)
+ return 1;
return 0;
},
@@ -416,6 +388,36 @@ pygowave.operations = (function() {
else
op.index += myop.length();
}
+ else if (op.isChange() && myop.isDelete()) {
+ if (op.index > myop.index) {
+ if (op.index <= (myop.index + myop.length()))
+ op.index = myop.index;
+ else
+ op.index -= myop.length();
+ }
+ }
+ else if (op.isChange() && myop.isInsert()) {
+ if (op.index >= myop.index)
+ op.index += myop.length();
+ }
+ else if (op.isDelete() && myop.isChange()) {
+ if (op.index < myop.index) {
+ if (myop.index <= (op.index + op.length())) {
+ myop.index = op.index;
+ this.fireEvent("operationChanged", i);
+ }
+ else {
+ myop.index -= op.length();
+ this.fireEvent("operationChanged", i);
+ }
+ }
+ }
+ else if (op.isInsert() && myop.isChange()) {
+ if (op.index <= myop.index) {
+ myop.index += op.length();
+ this.fireEvent("operationChanged", i);
+ }
+ }
j++;
}
i++;
@@ -499,9 +501,23 @@ pygowave.operations = (function() {
* @param {Operation} newop
*/
__insert: function (newop) {
- var i = len(this.operations) - 1;
+ var op = null;
+ var i = 0;
+ if (newop.type == DOCUMENT_ELEMENT_DELTA) {
+ for (var __iter0_ = new XRange(len(this.operations)); __iter0_.hasNext();) {
+ i = __iter0_.next();
+ op = this.operations[i];
+ if (op.type == DOCUMENT_ELEMENT_DELTA && newop.property.id == op.property.id) {
+ op.property.delta.update(newop.property.delta);
+ this.fireEvent("operationChanged", i);
+ return;
+ }
+ }
+ delete __iter0_;
+ }
+ i = len(this.operations) - 1;
if (i >= 0) {
- var op = this.operations[i];
+ op = this.operations[i];
if (newop.type == DOCUMENT_INSERT && op.type == DOCUMENT_INSERT) {
if (newop.index == op.index) {
op.property = newop.property + op.property;
@@ -586,14 +602,71 @@ pygowave.operations = (function() {
*/
documentDelete: function (blip_id, start, end) {
this.__insert(new Operation(DOCUMENT_DELETE, this.wave_id, this.wavelet_id, blip_id, start, end - start));
+ },
+
+ /**
+ * Requests to insert an element at the given position.
+ *
+ * @function {public} documentElementInsert
+ * @param {String} blip_id The blip id that this operation is applied to
+ * @param {int} index Position of the new element
+ * @param {String} type Element type
+ * @param {Object} properties Element properties
+ */
+ documentElementInsert: function (blip_id, index, type, properties) {
+ this.__insert(new Operation(DOCUMENT_ELEMENT_INSERT, this.wave_id, this.wavelet_id, blip_id, index, {
+ type: type,
+ properties: properties
+ }));
+ },
+
+ /**
+ * Requests to delete an element from the given position.
+ *
+ * @function {public} documentElementDelete
+ * @param {String} blip_id The blip id that this operation is applied to
+ * @param {int} index Position of the element to delete
+ */
+ documentElementDelete: function (blip_id, index) {
+ this.__insert(new Operation(DOCUMENT_ELEMENT_DELETE, this.wave_id, this.wavelet_id, blip_id, index, null));
+ },
+
+ /**
+ * Requests to apply a delta to the element at the given position.
+ *
+ * @function {public} documentElementDelta
+ * @param {String} blip_id The blip id that this operation is applied to
+ * @param {int} index Position of the element
+ * @param {Object} delta Delta to apply to the element
+ */
+ documentElementDelta: function (blip_id, index, delta) {
+ this.__insert(new Operation(DOCUMENT_ELEMENT_DELTA, this.wave_id, this.wavelet_id, blip_id, index, delta));
+ },
+
+ /**
+ * Requests to set a UserPref of the element at the given position.
+ *
+ * @function {public} documentElementSetpref
+ * @param {String} blip_id The blip id that this operation is applied to
+ * @param {int} index Position of the element
+ * @param {Object} key Name of the UserPref
+ * @param {Object} value Value of the UserPref
+ */
+ documentElementSetpref: function (blip_id, index, key, value) {
+ this.__insert(new Operation(DOCUMENT_ELEMENT_SETPREF, this.wave_id, this.wavelet_id, blip_id, index, {
+ key: key,
+ value: value
+ }));
}
});
return {
- Range: Range,
OpManager: OpManager,
DOCUMENT_INSERT: DOCUMENT_INSERT,
DOCUMENT_DELETE: DOCUMENT_DELETE,
- DOCUMENT_REPLACE: DOCUMENT_REPLACE
+ DOCUMENT_ELEMENT_INSERT: DOCUMENT_ELEMENT_INSERT,
+ DOCUMENT_ELEMENT_DELETE: DOCUMENT_ELEMENT_DELETE,
+ DOCUMENT_ELEMENT_DELTA: DOCUMENT_ELEMENT_DELTA,
+ DOCUMENT_ELEMENT_SETPREF: DOCUMENT_ELEMENT_SETPREF
};
})();
View
111 pygowave_client/src/controller/controller.js
@@ -94,16 +94,22 @@ pygowave.controller = $defined(pygowave.controller) ? pygowave.controller : new
this._iview = view;
this._iview.addEvent('textInserted', this._onTextInserted.bind(this));
this._iview.addEvent('textDeleted', this._onTextDeleted.bind(this));
+ this._iview.addEvent('elementInsert', this._onElementInsert.bind(this));
+ this._iview.addEvent('elementDelete', this._onElementDelete.bind(this));
+ this._iview.addEvent('elementDeltaSubmitted', this._onElementDeltaSubmitted.bind(this));
+ this._iview.addEvent('elementSetUserpref', this._onElementSetUserpref.bind(this));
this._iview.addEvent('searchForParticipant', this._onSearchForParticipant.bind(this));
this._iview.addEvent('addParticipant', this._onAddParticipant.bind(this));
this._iview.addEvent('leaveWavelet', this._onLeaveWavelet.bind(this));
+ this._iview.addEvent('refreshGadgetList', this._onRefreshGadgetList.bind(this));
this.waves = new Hash();
this.waves.set(model.id(), model);
this.wavelets = new Hash();
this.new_participants = new Array();
this.participants = new Hash();
+ this._cachedGadgetList = null;
// The connection object must be stored in this.conn and must have a sendJson and subscribeWavelet method (defined below).
this.conn = new STOMPClient(); // STOMP is used as communication protocol
@@ -240,6 +246,10 @@ pygowave.controller = $defined(pygowave.controller) ? pygowave.controller : new
else
wavelet_model.removeParticipant(msg.property);
break;
+ case "GADGET_LIST":
+ this._cachedGadgetList = msg.property;
+ this._iview.updateGadgetList(msg.property);
+ break;
}
}
},
@@ -439,17 +449,34 @@ pygowave.controller = $defined(pygowave.controller) ? pygowave.controller : new
var op = tr2.next();
if (op.isNull()) continue;
// Apply operation
- if (op.type == pygowave.operations.DOCUMENT_INSERT)
- wavelet.blipById(op.blip_id).insertText(op.index, op.property);
- else if (op.type == pygowave.operations.DOCUMENT_DELETE)
- wavelet.blipById(op.blip_id).deleteText(op.index, op.property);
+ switch (op.type) {
+ case pygowave.operations.DOCUMENT_INSERT:
+ wavelet.blipById(op.blip_id).insertText(op.index, op.property);
+ break;
+ case pygowave.operations.DOCUMENT_DELETE:
+ wavelet.blipById(op.blip_id).deleteText(op.index, op.property);
+ break;
+ case pygowave.operations.DOCUMENT_ELEMENT_INSERT:
+ wavelet.blipById(op.blip_id).insertElement(op.index, op.property.type, op.property.properties);
+ break;
+ case pygowave.operations.DOCUMENT_ELEMENT_DELETE:
+ wavelet.blipById(op.blip_id).deleteElement(op.index);
+ break;
+ case pygowave.operations.DOCUMENT_ELEMENT_DELTA:
+ wavelet.blipById(op.blip_id).applyElementDelta(op.index, op.property);
+ break;
+ case pygowave.operations.DOCUMENT_ELEMENT_SETPREF:
+ wavelet.blipById(op.blip_id).setElementUserpref(op.index, op.property.key, op.property.value);
+ break;
+ }
}
}
}
},
/**
- * Callback from view on text insertion.
+ * Callback from view on text insertion.<br/>
+ * Note: Does not generate an event in the model.
*
* @function {private} _onTextInserted
* @param {String} waveletId ID of the Wavelet
@@ -462,7 +489,8 @@ pygowave.controller = $defined(pygowave.controller) ? pygowave.controller : new
this.wavelets[waveletId].model.blipById(blipId).insertText(index, content, true);
},
/**
- * Callback from view on text deletion.
+ * Callback from view on text deletion.<br/>
+ * Note: Does not generate an event in the model.
*
* @event {private} _onTextDeleted
* @param {String} waveletId ID of the Wavelet
@@ -475,6 +503,59 @@ pygowave.controller = $defined(pygowave.controller) ? pygowave.controller : new
this.wavelets[waveletId].model.blipById(blipId).deleteText(start, end-start, true);
},
/**
+ * Callback from view on element insertion.<br/>
+ * Note: This generates an event in the model.
+ *
+ * @function {private} _onElementInsert
+ * @param {String} waveletId ID of the Wavelet
+ * @param {String} blipId ID of the Blip
+ * @param {int} index Position of the new element
+ * @param {String} type Element type
+ * @param {Object} properties Element properties
+ */
+ _onElementInsert: function (waveletId, blipId, index, type, properties) {
+ this.wavelets[waveletId].mcached.documentElementInsert(blipId, index, type, properties);
+ this.wavelets[waveletId].model.blipById(blipId).insertElement(index, type, properties);
+ },
+ /**
+ * Callback from view on element deletion.<br/>
+ * Note: This generates an event in the model.
+ *
+ * @event {private} _onElementDelete
+ * @param {String} waveletId ID of the Wavelet
+ * @param {String} blipId ID of the Blip
+ * @param {int} index Position of the element to delete
+ */
+ _onElementDelete: function (waveletId, blipId, index) {
+ this.wavelets[waveletId].mcached.documentElementDelete(blipId, index);
+ this.wavelets[waveletId].model.blipById(blipId).deleteElement(index);
+ },
+ /**
+ * Callback from view on element delta submission.
+ *
+ * @param {String} waveletId ID of the Wavelet
+ * @param {String} blipId ID of the Blip
+ * @param {int} index Position of the element
+ * @param {Object} delta Delta to apply to the element
+ */
+ _onElementDeltaSubmitted: function (waveletId, blipId, index, delta) {
+ this.wavelets[waveletId].mcached.documentElementDelta(blipId, index, delta);
+ this.wavelets[waveletId].model.blipById(blipId).applyElementDelta(index, delta);
+ },
+ /**
+ * Callback from view on element UserPref setting.
+ *
+ * @param {String} waveletId ID of the Wavelet
+ * @param {String} blipId ID of the Blip
+ * @param {int} index Position of the element
+ * @param {Object} key Name of the UserPref
+ * @param {Object} value Value of the UserPref
+ */
+ _onElementSetUserpref: function (waveletId, blipId, index, key, value) {
+ this.wavelets[waveletId].mcached.documentElementSetpref(blipId, index, key, value);
+ this.wavelets[waveletId].model.blipById(blipId).setElementUserpref(index, key, value, true);
+ },
+ /**
* Callback from view on searching.
*
* @function {private} _onSearchForParticipant
@@ -506,9 +587,23 @@ pygowave.controller = $defined(pygowave.controller) ? pygowave.controller : new
*/
_onLeaveWavelet: function (waveletId) {
this.conn.sendJson(waveletId, {
- type: "WAVELET_REMOVE_SELF",
- property: null
+ type: "WAVELET_REMOVE_SELF"
});
+ },
+ /**
+ * Callback from view on gadget adding.
+ * @function {private} _onRefreshGadgetList
+ * @param {String} waveletId ID of the Wavelet
+ * @param {Boolean} forced True if the user explicitly clicked refresh
+ */
+ _onRefreshGadgetList: function (waveletId, forced) {
+ if (forced || this._cachedGadgetList == null) {
+ this.conn.sendJson(waveletId, {
+ type: "GADGET_LIST"
+ });
+ }
+ else
+ this._iview.updateGadgetList(this._cachedGadgetList);
}
});
View
257 pygowave_client/src/model/model.py
@@ -26,7 +26,7 @@
from hashlib import sha1 as sha_constructor
-__all__ = ["WaveModel", "Participant"]
+__all__ = ["WaveModel", "Participant", "ELEMENT_TYPE"]
@Implements(Options, Events)
@Class
@@ -256,25 +256,36 @@ class Element(object):
"""
Element-objects are all the non-text elements in a Blip.
An element has no physical presence in the text of a Blip, but it maintains
- an implicit protected space (or newline) to keep positions distinct.
+ an implicit protected newline character to keep positions distinct.
Only special Wave Client elements are treated here.
There are no HTML elements in any Blip. All markup is handled by Annotations.
@class {private} pygowave.model.Element
"""
- def __init__(self, blip, position, type):
+ def __init__(self, blip, id, position, type, properties):
"""
Called on instantiation. Documented for internal purposes.
@constructor {private} initialize
@param {Blip} blip The element's parent Blip
+ @param {int} id ID of the element, setting this to null will assign
+ a new temporaty ID
@param {int} position Index where this element resides
@param {int} type Type of the element
+ @param {Object} properties The element's properties
"""
self._blip = blip
+ if id == None:
+ self._id = Element.newTempId()
+ else:
+ self._id = id
self._pos = position
self._type = type
+ if properties == None:
+ self._properties = Hash()
+ else:
+ self._properties = Hash(properties)
def blip(self):
"""
@@ -284,6 +295,24 @@ def blip(self):
"""
return self._blip
+ def setBlip(self, blip):
+ """
+ Set the parent Blip to the given Blip.
+
+ @function {public} setBlip
+ """
+ self._blip = blip
+
+ def id(self):
+ """
+ Return the ID of the element. A negative ID represents a temporary ID,
+ that is only valid for this session and will be replaced by a real ID
+ on reload.
+
+ @function {public int} id
+ """
+ return self._id
+
def type(self):
"""
Returns the type of the element.
@@ -309,6 +338,13 @@ def setPosition(self, pos):
"""
self._pos = pos
+ @staticmethod
+ def newTempId():
+ Element.lastTempId -= 1
+ return Element.lastTempId
+
+Element.lastTempId = 0
+
@Class
class GadgetElement(Element):
"""
@@ -334,42 +370,55 @@ class GadgetElement(Element):
"""
# ---------------------------
- def __init__(self, blip, position, url):
+ def __init__(self, blip, id, position, properties):
"""
Called on instantiation. Documented for internal purposes.
@constructor {private} initialize
@param {Blip} blip The gadget's parent Blip
+ @param {int} id ID of the element, setting this to null will assign
+ a new temporaty ID
@param {int} position Index where this gadget resides
- @param {String} url URL of the gadget xml
+ @param {Object} properties The gadget element's properties
+ @param {int} id
"""
- super(GadgetElement, self).__init__(blip, position, ELEMENT_TYPE["GADGET"])
- self._url = url
- self._fields = Hash()
- self._userprefs = Hash()
+ super(GadgetElement, self).__init__(blip, id, position, ELEMENT_TYPE["GADGET"], properties)
+ if self._properties.has("fields"): # Convert fields to hash
+ self._properties.set("fields", Hash(self._properties.get("fields")))
+ if self._properties.has("userprefs"): # Convert userprefs to hash
+ self._properties.set("userprefs", Hash(self._properties.get("userprefs")))
def fields(self):
"""
Return the gadget's state object i.e. the field map.
@function {public Object} fields
"""
- return self._fields.getClean()
+ if self._properties.has("fields"):
+ return self._properties.get("fields").getClean()
+ else:
+ return {}
def userPrefs(self):
"""
- Return all UserPrefs as object.
+ Return all UserPrefs as hash.
- @function {public Object} userPrefs
+ @function {public Hash} userPrefs
"""
- return self._userprefs.getClean()
+ if self._properties.has("userprefs"):
+ return self._properties.get("userprefs")
+ else:
+ return {}
def url(self):
"""
Returns the gadget xml URL.
@function {public String} url
"""
- return self._url
+ if self._properties.has("url"):
+ return self._properties.get("url")
+ else:
+ return ""
def applyDelta(self, delta):
"""
@@ -379,26 +428,33 @@ def applyDelta(self, delta):
@param {Object} delta An object whose fields will be merged into the
gadget state.
"""
- self._fields.update(delta)
+ if not self._properties.has("fields"):
+ self._properties.set("fields", Hash())
+
+ fields = self._properties.get("fields")
+ fields.update(delta)
# Delete keys with null values
- for key, value in self._fields.iteritems():
+ for key, value in self._properties.iteritems():
if value == None:
- del self._fields[key]
+ del fields[key]
self.fireEvent("stateChange")
- def setUserPref(self, key, value):
+ def setUserPref(self, key, value, noevent = False):
"""
Set a UserPref.
@function {public} setUserPref
@param {String} key
@param {Object} value
"""
+ if not self._properties.has("userprefs"):
+ self._properties.set("userprefs", Hash())
- self._userprefs[key] = value
- self.fireEvent("setUserPref", [key, value])
+ self._properties.get("userprefs").set(key, value)
+ if not noevent:
+ self.fireEvent("setUserPref", [key, value])
@Implements(Options, Events)
@Class
@@ -436,13 +492,27 @@ class Blip(object):
"""
"""
+ Fired on element insertion.
+
+ @event onInsertElement
+ @param {int} index Offset where the element is inserted
+ """
+
+ """
+ Fired on element deletion.
+
+ @event onDeleteElement
+ @param {int} index Offset where the element is deleted
+ """
+
+ """
Fired if the Blip has gone out of sync with the server.
@event onOutOfSync
"""
# ---------------------------
- def __init__(self, wavelet, id, options, content = "", parent = None):
+ def __init__(self, wavelet, id, options, content = "", elements = [], parent = None):
"""
Called on instantiation. Documented for internal purposes.
@constructor {private} initialize
@@ -457,6 +527,8 @@ def __init__(self, wavelet, id, options, content = "", parent = None):
@... {int} version Version of the Blip
@... {Boolean} submitted True if this Blip is submitted
@param {options String} content Content of the Blip
+ @param {optional Element[]} elements Element objects which initially
+ reside in this Blip
@param {optional Blip} parent Parent Blip if this is a nested Blip
"""
self.setOptions(options)
@@ -465,7 +537,10 @@ def __init__(self, wavelet, id, options, content = "", parent = None):
self._parent = parent
self._content = content
- self._elements = []
+ self._elements = elements
+ for element in self._elements:
+ element.setBlip(self)
+
self._annotations = []
self._outofsync = False
@@ -493,6 +568,30 @@ def isRoot(self):
"""
return self.options["is_root"]
+ def elementAt(self, index):
+ """
+ Returns the Element object at the given position or null.
+
+ @function {public Element} elementAt
+ @param {int} index Index of the element to retrieve
+ """
+
+ for i in xrange(len(self._elements)):
+ elt = self._elements[i]
+ if elt.position() == index:
+ return elt
+
+ return None
+
+ def allElements(self):
+ """
+ Returns all Elements of this Blip.
+
+ @function {public Element[]} allElements
+ """
+
+ return self._elements
+
def insertText(self, index, text, noevent = False):
"""
Insert a text at the specified index. This moves annotations and
@@ -549,6 +648,81 @@ def deleteText(self, index, length, noevent = False):
if not noevent:
self.fireEvent("deleteText", [index, length])
+ def insertElement(self, index, type, properties, noevent = False):
+ """
+ Insert an element at the specified index. This implicitly adds a
+ protected newline character at the index.<br/>
+ Note: This sets the wavelet status to 'dirty'.
+
+ @function {public} insertElement
+ @param {int} index Position of the new element
+ @param {String} type Element type
+ @param {Object} properties Element properties
+ @param {optional Boolean} noevent Set to true if no event should be generated
+ """
+
+ self.insertText(index, "\n", True)
+ if type == 2:
+ elt = GadgetElement(self, None, index, properties)
+ else:
+ elt = Element(self, None, index, type, properties)
+ self._elements.append(elt)
+
+ self._wavelet._setStatus("dirty")
+ if not noevent:
+ self.fireEvent("insertElement", index)
+
+ def deleteElement(self, index, noevent = False):
+ """
+ Delete an element at the specified index. This implicitly deletes the
+ protected newline character at the index.<br/>
+ Note: This sets the wavelet status to 'dirty'.
+
+ @function {public} deleteElement
+ @param {int} index Position of the element to delete
+ @param {optional Boolean} noevent Set to true if no event should be generated
+ """
+
+ for i in xrange(len(self._elements)):
+ elt = self._elements[i]
+ if elt.position() == index:
+ self._elements.pop(i)
+ break
+ self.deleteText(index, 1, True)
+ if not noevent:
+ self.fireEvent("deleteElement", index)
+
+ def applyElementDelta(self, index, delta):
+ """
+ Apply an element delta. Currently only for gadget elements.<br/>
+ Note: This action always emits stateChange.
+
+ @function {public} applyElementDelta
+ @param {int} index Position of the element
+ @param {Object} delta Delta to apply to the element
+ """
+
+ for elt in self._elements:
+ if elt.position() == index:
+ elt.applyDelta(delta)
+ break
+
+ def setElementUserpref(self, index, key, value, noevent = False):
+ """
+ Set an UserPref of an element. Currently only for gadget elements.
+
+ @function {public} setElementUserpref
+ param {int} index Position of the element
+ @param {Object} key Name of the UserPref
+ @param {Object} value Value of the UserPref
+ @param {optional Boolean} noevent Set to true if no event should be generated
+ """
+
+ for elt in self._elements:
+ if elt.position() == index:
+ elt.setUserPref(key, value, noevent)
+ break
+
def content(self):
"""
Returns the text content of this Blip.
@@ -668,6 +842,13 @@ def waveId(self):
"""
return self._wave.id()
+ def waveModel(self):
+ """
+ Returns the parent WaveModel object.
+ @function {public WaveModel} waveModel
+ """
+ return self._wave
+
def addParticipant(self, participant):
"""
Add a participant to this Wavelet.<br/>
@@ -726,7 +907,7 @@ def allParticipantsForGadget(self):
ret[id] = participant.toGadgetFormat()
return ret
- def appendBlip(self, id, options, content = ""):
+ def appendBlip(self, id, options, content = "", elements = []):
"""
Convenience function for inserting a new Blip at the end.
For options see the {@link pygowave.model.Blip.initialize Blip constructor}.<br/>
@@ -735,11 +916,13 @@ def appendBlip(self, id, options, content = ""):
@function {public Blip} appendBlip
@param {String} id ID of the new Blip
@param {Object} options Information about the Blip
- @param {options String} content Content of the Blip
+ @param {optional String} content Content of the Blip
+ @param {optional Element[]} elements Element objects which initially
+ reside in this Blip
"""
- return self.insertBlip(len(self._blips), id, options, content)
+ return self.insertBlip(len(self._blips), id, options, content, elements)
- def insertBlip(self, index, id, options, content = ""):
+ def insertBlip(self, index, id, options, content = "", elements = []):
"""
Insert a new Blip at the specified index.
For options see the {@link pygowave.model.Blip.initialize Blip constructor}.<br/>
@@ -749,9 +932,11 @@ def insertBlip(self, index, id, options, content = ""):
@param {int} index Index where to insert the Blip
@param {String} id ID of the new Blip
@param {Object} options Information about the Blip
- @param {options String} content Content of the Blip
+ @param {optional String} content Content of the Blip
+ @param {optional Element[]} elements Element objects which initially
+ reside in this Blip
"""
- blip = Blip(self, id, options, content)
+ blip = Blip(self, id, options, content, elements)
self._blips.insert(index, blip)
self.fireEvent('blipInserted', [index, id])
return blip
@@ -861,6 +1046,14 @@ def id(self):
"""
return self._waveId
+ def viewerId(self):
+ """
+ Returns the ID of the viewer.
+
+ @function {public String} viewerId
+ """
+ return self._viewerId
+
def loadFromSnapshot(self, obj, participants):
"""
Load the wave's contents from a JSON-serialized snapshot and a map of
@@ -896,7 +1089,13 @@ def loadFromSnapshot(self, obj, participants):
"version": blip["version"],
"submitted": blip["submitted"]
}
- blipObj = rootWaveletObj.appendBlip(blip_id, blip_options, blip["content"])
+ blip_elements = []
+ for serialelement in blip["elements"]:
+ if serialelement["type"] == ELEMENT_TYPE["GADGET"]:
+ blip_elements.append(GadgetElement(None, serialelement["id"], serialelement["index"], serialelement["properties"]))
+ else:
+ blip_elements.append(Element(None, serialelement["id"], serialelement["index"], serialelement["type"], serialelement["properties"]))
+ blipObj = rootWaveletObj.appendBlip(blip_id, blip_options, blip["content"], blip_elements)
def createWavelet(self, id, options):
"""
View
188 pygowave_client/src/operations/operations.py
@@ -27,47 +27,20 @@
DOCUMENT_INSERT = 'DOCUMENT_INSERT'
DOCUMENT_DELETE = 'DOCUMENT_DELETE'
-DOCUMENT_REPLACE = 'DOCUMENT_REPLACE'
+DOCUMENT_ELEMENT_INSERT = 'DOCUMENT_ELEMENT_INSERT'
+DOCUMENT_ELEMENT_DELETE = 'DOCUMENT_ELEMENT_DELETE'
+DOCUMENT_ELEMENT_DELTA = 'DOCUMENT_ELEMENT_DELTA'
+DOCUMENT_ELEMENT_SETPREF = 'DOCUMENT_ELEMENT_SETPREF'
-__all__ = ["Range", "OpManager", "DOCUMENT_INSERT", "DOCUMENT_DELETE", "DOCUMENT_REPLACE"]
-
-@Class
-class Range(object):
- """
- Represents a start and end range with integers.
-
- Ranges map positions in the document. A range must have at least a length
- of zero. If zero, the range is considered to be a single point (collapsed).
-
- @class {public} Range
- """
-
- def __init__(self, start=0, end=1):
- """
- Initializes the range with a start and end position.
-
- @constructor {public} initialize
-
- @param {int} start Start index of the range.
- @param {int} end End index of the range.
-
- #@throws ValueError Value error if the range is invalid (less than zero).
- """
- self.start = start
- self.end = end
- #if self.end - self.start < 0:
- # raise ValueError('Range cannot be less than 0')
-
- def __repr__(self):
- return 'Range(%d,%d)' % (self.start, self.end)
-
- def isCollapsed(self):
- """"
- Returns true if this represents a single point as opposed to a range.
-
- @function {public Boolean} isCollapsed
- """
- return self.end == self.start
+__all__ = [
+ "OpManager",
+ "DOCUMENT_INSERT",
+ "DOCUMENT_DELETE",
+ "DOCUMENT_ELEMENT_INSERT",
+ "DOCUMENT_ELEMENT_DELETE",
+ "DOCUMENT_ELEMENT_DELTA",
+ "DOCUMENT_ELEMENT_SETPREF",
+]
@Class
class Operation(object):
@@ -139,30 +112,38 @@ def isCompatibleTo(self, other_op):
@function {public Boolean} isCompatibleTo
@param {Operation} other_op
"""
- if (self.type == DOCUMENT_INSERT or self.type == DOCUMENT_DELETE) and \
- (other_op.type == DOCUMENT_INSERT or other_op.type == DOCUMENT_DELETE):
- if self.wave_id != other_op.wave_id \
- or self.wavelet_id != other_op.wavelet_id \
- or self.blip_id != self.blip_id:
- return False
- return True
- return False
+
+ # Currently all supported operations are compatible to each other (if on the same blip)
+ # DOCUMENT_INSERT DOCUMENT_DELETE DOCUMENT_ELEMENT_INSERT DOCUMENT_ELEMENT_DELETE DOCUMENT_ELEMENT_DELTA DOCUMENT_ELEMENT_SETPREF
+ if self.wave_id != other_op.wave_id \
+ or self.wavelet_id != other_op.wavelet_id \
+ or self.blip_id != self.blip_id:
+ return False
+ return True
def isInsert(self):
"""
Returns true, if this op is an insertion operation.
@function {public Boolean} isInsert
"""
- return self.type == DOCUMENT_INSERT
+ return (self.type == DOCUMENT_INSERT or self.type == DOCUMENT_ELEMENT_INSERT)
def isDelete(self):
"""
Returns true, if this op is a deletion operation.
@function {public Boolean} isDelete
"""
- return self.type == DOCUMENT_DELETE
+ return (self.type == DOCUMENT_DELETE or self.type == DOCUMENT_ELEMENT_DELETE)
+
+ def isChange(self):
+ """
+ Returns true, if this op is an (attribute) change operation.
+
+ @function {public Boolean} isChange
+ """
+ return (self.type == DOCUMENT_ELEMENT_DELTA or self.type == DOCUMENT_ELEMENT_SETPREF)
def length(self):
"""
@@ -176,6 +157,8 @@ def length(self):
return len(self.property)
elif self.type == DOCUMENT_DELETE:
return self.property
+ elif self.type == DOCUMENT_ELEMENT_INSERT or self.type == DOCUMENT_ELEMENT_DELETE:
+ return 1
return 0
def resize(self, value):
@@ -401,6 +384,27 @@ def transform(self, input_op):
self.fireEvent("operationChanged", i)
else: # op.index > myop.index
op.index += myop.length()
+ elif op.isChange() and myop.isDelete():
+ if op.index > myop.index:
+ if op.index <= myop.index + myop.length():
+ op.index = myop.index
+ else:
+ op.index -= myop.length()
+ elif op.isChange() and myop.isInsert():
+ if op.index >= myop.index:
+ op.index += myop.length()
+ elif op.isDelete() and myop.isChange():
+ if op.index < myop.index:
+ if myop.index <= op.index + op.length():
+ myop.index = op.index
+ self.fireEvent("operationChanged", i)
+ else:
+ myop.index -= op.length()
+ self.fireEvent("operationChanged", i)
+ elif op.isInsert() and myop.isChange():
+ if op.index <= myop.index:
+ myop.index += op.length()
+ self.fireEvent("operationChanged", i)
j += 1
@@ -480,7 +484,18 @@ def __insert(self, newop):
@param {Operation} newop
"""
- # Only merge with the last op (otherwise this may get a bit complicated)
+ # Element delta's can always merge with predecessors
+ op = None
+ i = 0
+ if newop.type == DOCUMENT_ELEMENT_DELTA:
+ for i in xrange(len(self.operations)):
+ op = self.operations[i]
+ if op.type == DOCUMENT_ELEMENT_DELTA and newop.property["id"] == op.property["id"]:
+ op.property["delta"].update(newop.property["delta"])
+ self.fireEvent("operationChanged", i)
+ return
+
+ # Others: Only merge with the last op (otherwise this may get a bit complicated)
i = len(self.operations) - 1
if i >= 0:
op = self.operations[i]
@@ -568,3 +583,74 @@ def documentDelete(self, blip_id, start, end):
start,
end-start # = length
))
+
+ def documentElementInsert(self, blip_id, index, type, properties):
+ """
+ Requests to insert an element at the given position.
+
+ @function {public} documentElementInsert
+ @param {String} blip_id The blip id that this operation is applied to
+ @param {int} index Position of the new element
+ @param {String} type Element type
+ @param {Object} properties Element properties
+ """
+ self.__insert(Operation(
+ DOCUMENT_ELEMENT_INSERT,
+ self.wave_id, self.wavelet_id, blip_id,
+ index,
+ {
+ "type": type,
+ "properties": properties
+ }
+ ))
+
+ def documentElementDelete(self, blip_id, index):
+ """
+ Requests to delete an element from the given position.
+
+ @function {public} documentElementDelete
+ @param {String} blip_id The blip id that this operation is applied to
+ @param {int} index Position of the element to delete
+ """
+ self.__insert(Operation(
+ DOCUMENT_ELEMENT_DELETE,
+ self.wave_id, self.wavelet_id, blip_id,
+ index,
+ None
+ ))
+
+ def documentElementDelta(self, blip_id, index, delta):
+ """
+ Requests to apply a delta to the element at the given position.
+
+ @function {public} documentElementDelta
+ @param {String} blip_id The blip id that this operation is applied to
+ @param {int} index Position of the element
+ @param {Object} delta Delta to apply to the element
+ """
+ self.__insert(Operation(
+ DOCUMENT_ELEMENT_DELTA,
+ self.wave_id, self.wavelet_id, blip_id,
+ index,
+ delta
+ ))
+
+ def documentElementSetpref(self, blip_id, index, key, value):
+ """
+ Requests to set a UserPref of the element at the given position.
+
+ @function {public} documentElementSetpref
+ @param {String} blip_id The blip id that this operation is applied to
+ @param {int} index Position of the element
+ @param {Object} key Name of the UserPref
+ @param {Object} value Value of the UserPref
+ """
+ self.__insert(Operation(
+ DOCUMENT_ELEMENT_SETPREF,
+ self.wave_id, self.wavelet_id, blip_id,
+ index,
+ {
+ "key": key,
+ "value": value
+ }
+ ))
View
139 pygowave_client/src/view/blip_editor.js
@@ -30,6 +30,7 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
/* Imports */
var Widget = pygowave.view.Widget;
var Selection = pygowave.view.Selection;
+ var GadgetElementWidget = pygowave.view.GadgetElementWidget;
/**
* An editable div which generates events for the controller and is able to
@@ -73,6 +74,7 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
this._onInsertText = this._onInsertText.bind(this);
this._onDeleteText = this._onDeleteText.bind(this);
+ this._onInsertElement = this._onInsertElement.bind(this);
this._onKeyDown = this._onKeyDown.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
this._onKeyUp = this._onKeyUp.bind(this);
@@ -104,13 +106,15 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
this._blip = blip;
else
blip = this._blip;
+ this._elements = new Array();
- this.reloadContent();
+ var ok = this.reloadContent();
blip.addEvents({
insertText: this._onInsertText,
deleteText: this._onDeleteText,
- outOfSync: this._onOutOfSync
+ outOfSync: this._onOutOfSync,
+ insertElement: this._onInsertElement
});
this.contentElement.addEvents({
@@ -124,7 +128,8 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
this._processing = false;
window.addEvent('resize', this._onWindowResize);
- this._onSyncCheckTimer = this._onSyncCheck.periodical(2000, this);
+ if (ok)
+ this._onSyncCheckTimer = this._onSyncCheck.periodical(2000, this);
return this;
},
@@ -143,7 +148,8 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
this._blip.removeEvents({
insertText: this._onInsertText,
deleteText: this._onDeleteText,
- outOfSync: this._onOutOfSync
+ outOfSync: this._onOutOfSync,
+ insertElement: this._onInsertElement
});
this.contentElement.removeEvents({
keydown: this._onKeyDown,
@@ -157,22 +163,40 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
},
/**
+ * Returns the Blip rendered through this editor.
+ *
+ * @function {public pygowave.model.Blip} blip
+ */
+ blip: function () {
+ return this._blip;
+ },
+
+ /**
* Build the content elements from the Blip's string and insert it
- * into the widget.
+ * into the widget. Returns true on success, false otherwise.
*
- * @function {private} reloadContent
+ * @function {private Boolean} reloadContent
*/
reloadContent: function () {
// Clean up first
+ for (var it = new _Iterator(this._elements); it.hasNext(); )
+ it.next().destroy();
+ this._elements.empty();
for (var it = new _Iterator(this.contentElement.getChildren()); it.hasNext(); )
it.next().destroy();
this.contentElement.setStyle("opacity", 1);
this._removeErrorOverlay();
+ var elementPosMap = new Hash();
+ for (var it = new _Iterator(this._blip.allElements()); it.hasNext(); ) {
+ var element = it.next();
+ elementPosMap.set(element.position(), element);
+ }
+
// Build up
var content = this._blip.content().replace(/ /g, "\u00a0");
- var lines = content.split("\n"), line, pg;
+ var lines = content.split("\n"), line, pg, pos = 0;
for (var i = 0; i < lines.length; i++) {
line = lines[i];
pg = new Element("p");
@@ -181,13 +205,23 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
if (!Browser.Engine.trident) // IE allows empty paragraphs
new Element("br").inject(pg); // Others use an implicit br
pg.inject(this.contentElement);
+ pos += line.length;
+ if (elementPosMap.has(pos)) {
+ this._elements.push(new GadgetElementWidget(this._view, elementPosMap.get(pos), this.contentElement));
+ i++;
+ }
+ pos++;
}
this._lastContent = this.contentToString();
- if (this._blip.content() != this._lastContent)
+ if (this._blip.content() != this._lastContent) {
+ $clear(this._onSyncCheckTimer);
this._displayErrorOverlay("render_fail");
- else
- this.contentElement.contentEditable = "true";
+ return false;
+ }
+
+ this.contentElement.contentEditable = "true";
+ return true;
},
/**
* Places an overlay over the editor and displays an error message with
@@ -264,8 +298,11 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
*/
contentToString: function () {
var cleaned = "", first = true;
- for (var it = new _Iterator(this.contentElement.getChildren("p")); it.hasNext(); ) {
+ for (var it = new _Iterator(this.contentElement.getChildren()); it.hasNext(); ) {
var elt = it.next();
+ if (elt.get('tag') != "p" && elt.get('tag') != "iframe")
+ continue;
+
if (first)
first = false;
else
@@ -296,6 +333,15 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
this.fireEvent("currentTextRangeChanged", [start, end]);
return [start, end];
},
+ /**
+ * Returns the last text range which was succesfully retrieved
+ * as [start, end].
+ *
+ * @function {public Array} lastTextRange
+ */
+ lastTextRange: function () {
+ return this._lastRange;
+ },
_onKeyDown: function (e) {
this._processing = true;
@@ -305,6 +351,13 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
this._processing = true;
},
+ /**
+ * Callback from underlying DOM element on key release. Generates events
+ * for the controller (which in turn generates operations).
+ *
+ * @function {private} _onKeyUp
+ * @param {Object} e Event object
+ */
_onKeyUp: function(e) {
this._processing = true;
var newContent = this.contentToString();
@@ -384,6 +437,13 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
return false; // Context menu is blocked
},
+ /**
+ * Callback from model on text insertion
+ *
+ * @function {private} _onInsertText
+ * @param {int} index Index of the inserion
+ * @param {String} text Text to be inserted
+ */
_onInsertText: function (index, text) {
//TODO: this function assumes no formatting elements
this._processing = true;
@@ -455,6 +515,13 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
}
this._processing = false;
},
+ /**
+ * Callback from model on text deletion
+ *
+ * @function {private} _onDeleteText
+ * @param {int} index Index of the deletion
+ * @param {String} length How many characters to delete
+ */
_onDeleteText: function (index, length) {
// Safe for formatting elements
this._processing = true;
@@ -494,11 +561,61 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
}
this._processing = false;
},
+ /**
+ * Callback from model if an element was inserted.
+ *
+ * @param {int} index Offset where the element is inserted
+ */
+ _onInsertElement: function (index) {
+ this._processing = true;
+
+ var elt = this._blip.elementAt(index);
+
+ // Note: There is always an implicit newline at the index, so fake
+ // a newline insertion here and replace the resulting empty paragraph
+ this._onInsertText(index, "\n");
+ var para = this._walkDown(this.contentElement, index+1)[0];
+
+ var parent = para.previousSibling, where = 'after';
+ para.destroy();
+
+ if (!$defined(parent)) {
+ parent = this.contentElement;
+ where = 'top';
+ }
+
+ //TODO other elements
+ var ew = new GadgetElementWidget(this._view, elt, parent, where);
+
+ this._elements.push(ew);
+
+ this._processing = false;
+ },
+ /**
+ * Callback from model if blip has gone out of sync with the server
+ *
+ * @function {private} _onOutOfSync
+ */
_onOutOfSync: function () {
this._displayErrorOverlay('checksum_fail');
},
/**
+ * Utility function. Splits up a textnode in a paragraph at the
+ * given position.
+ *
+ * @function {private} _spitPara
+ * @param {Element} elt The textnode to split
+ * @param {int} offset Spit offset
+ */
+ _spitPara: function (elt, offset) {
+ var parent = elt.parentNode;