diff --git a/org.flowerplatform.editor.text/flex_src/org/flowerplatform/editor/text/CodeMirrorEditorFrontend.mxml b/org.flowerplatform.editor.text/flex_src/org/flowerplatform/editor/text/CodeMirrorEditorFrontend.mxml index 38b36131..a54d065b 100644 --- a/org.flowerplatform.editor.text/flex_src/org/flowerplatform/editor/text/CodeMirrorEditorFrontend.mxml +++ b/org.flowerplatform.editor.text/flex_src/org/flowerplatform/editor/text/CodeMirrorEditorFrontend.mxml @@ -32,15 +32,21 @@ license-end TextChangedEventBuffer + * if the user is saving his changes. This is to ensure that all the + * changes have arrived on the server before the text file is saved. + */ + public function emptyKeystrokeAggregationBuffer():void { + if (keystrokeBuffer) + keystrokeBuffer.aggregateEvents(); + } + + public function setKeystrokeAggregationBufferDirtyState(dirty:Boolean):void { + if (keystrokeBuffer) + keystrokeBuffer.dirty = dirty; + } + protected function initializeHandler(event:FlexEvent):void { if (FlexUtilGlobals.getInstance().isMobile) { editor = new MobileCodeMirrorEditor(); @@ -65,6 +114,8 @@ license-end ICodeMirrorEditor(editor).addViewCompleteHandler(orionEditor_frameLoadHandler); creationComplete = true; + setKeystrokeAggregationInterval(_keystrokeAggregationInterval); + addEventListener(BufferedTextChangesEvent.BUFFERED_TEXT_CHANGES, sendTextEditorChangesToServer); } protected function getURL():String { @@ -78,12 +129,12 @@ license-end } protected function orionEditor_frameLoadHandler(event:Event):void { - if (content != null) { - // initialize code mirror editor - ICodeMirrorEditor(editor).callJavaScriptMethod("initialize", null, escape(content), editorStatefulClient.getStatefulClientId(), readOnly); - // listen for text changes - ICodeMirrorEditor(editor).addCallbackHandler("codeMirrorEditorChangedHandler", textEditorChangedHandler); - } + // initialize code mirror editor + ICodeMirrorEditor(editor).callJavaScriptMethod("initialize", null, + content != null ? escape(content) : null, editorStatefulClient.getStatefulClientId(), frontendId, readOnly); + + // listen for text changes + ICodeMirrorEditor(editor).addCallbackHandler("codeMirrorEditorChangedHandler", textEditorChangedHandler); } public function setContent(content:String):void { @@ -128,26 +179,36 @@ license-end } } - protected function textEditorChangedHandler(statefulClientId:String, offset:String, oldText:String, newText:String):void { - var updates:ArrayCollection = new ArrayCollection(); - - var update:TextEditorUpdate = new TextEditorUpdate(); - update.offset = int(offset); - update.oldTextLength = oldText.length; - update.newText = newText; - updates.addItem(update); - + protected function textEditorChangedHandler(statefulClientId:String, frontendId:String, offset:String, oldText:String, newText:String):void { // here we must know where to send those updates // ind the statefulClient based on the id sent at code mirror initialisation var editorStatefulClient:EditorStatefulClient = EditorStatefulClient(CommunicationPlugin.getInstance().statefulClientRegistry.getStatefulClientById(statefulClientId)); - // for the moment the last editorFontend is used -> problems when using multiple editorFrontends - // TO DISCUSS: how can we know the exact editorFrontend - editorStatefulClient.attemptUpdateContent( - editorStatefulClient.editorFrontends[editorStatefulClient.editorFrontends.length - 1], updates); + for each (var editorFrontend:EditorFrontend in editorStatefulClient.editorFrontends) { + var codeMirrorEditor:CodeMirrorEditorFrontend = CodeMirrorEditorFrontend(editorFrontend); + if (codeMirrorEditor.frontendId == frontendId) { + codeMirrorEditor.keystrokeBuffer.add(new TextChangedEvent(int(offset), oldText.length, newText)); + break; + } + } } + private function sendTextEditorChangesToServer(evt:BufferedTextChangesEvent):void { + var buffer:ArrayCollection = evt.buffer; + + var updates:ArrayCollection = new ArrayCollection(); + for each (var textChangedEvent:TextChangedEvent in buffer) { + var update:TextEditorUpdate = new TextEditorUpdate(); + update.offset = textChangedEvent.offset; + update.oldTextLength = textChangedEvent.oldTextLength; + update.newText = textChangedEvent.newText; + updates.addItem(update); + } + + editorStatefulClient.attemptUpdateContent(this, updates); + } + ]]> diff --git a/org.flowerplatform.editor.text/flex_src/org/flowerplatform/editor/text/TextEditorFrontend.mxml b/org.flowerplatform.editor.text/flex_src/org/flowerplatform/editor/text/TextEditorFrontend.mxml index 49cbcdac..290c8f36 100644 --- a/org.flowerplatform.editor.text/flex_src/org/flowerplatform/editor/text/TextEditorFrontend.mxml +++ b/org.flowerplatform.editor.text/flex_src/org/flowerplatform/editor/text/TextEditorFrontend.mxml @@ -64,7 +64,7 @@ private static const CLIENT_KEYSTROKE_AGGREGATION_INTERVAL:String = "client.keystroke.aggregation.interval"; - private static var _keystrokeAggregationInterval:int = 0; + private static var _keystrokeAggregationInterval:int = 3000; /** * The interval (in ms) when buffered text events are aggregated diff --git a/org.flowerplatform.editor.text/flex_src/org/flowerplatform/editor/text/remote/CodeMirrorEditorStatefulClient.as b/org.flowerplatform.editor.text/flex_src/org/flowerplatform/editor/text/remote/CodeMirrorEditorStatefulClient.as index 5a8d03bb..08233c2a 100644 --- a/org.flowerplatform.editor.text/flex_src/org/flowerplatform/editor/text/remote/CodeMirrorEditorStatefulClient.as +++ b/org.flowerplatform.editor.text/flex_src/org/flowerplatform/editor/text/remote/CodeMirrorEditorStatefulClient.as @@ -31,15 +31,13 @@ package org.flowerplatform.editor.text.remote { } override protected function copyLocalDataFromExistingEditorToNewEditor(existingEditor:EditorFrontend, newEditor:EditorFrontend):void { - if (editableResourceStatus) + if (editableResourceStatus) { newEditor.editableResourceStatusUpdated(); + } CodeMirrorEditorFrontend(existingEditor).getContent(function(value:String):void { CodeMirrorEditorFrontend(newEditor).setContent(value); }); } - - override public function updateDirtyState(editorInput:Object, dirtyState:Boolean):void { - } - + } } \ No newline at end of file diff --git a/org.flowerplatform.editor.text/public-resources/codemirror/codeMirrorEditor.html b/org.flowerplatform.editor.text/public-resources/codemirror/codeMirrorEditor.html index 17fc1f4c..4dc0a6f4 100644 --- a/org.flowerplatform.editor.text/public-resources/codemirror/codeMirrorEditor.html +++ b/org.flowerplatform.editor.text/public-resources/codemirror/codeMirrorEditor.html @@ -93,24 +93,33 @@ } var myCodeMirror; - var editorStatefulClientId; - - function initialize(args) { - myCodeMirror.setValue(unescape(args[0])); - editorStatefulClientId = args[1]; - if (args[2]) { // readOnly = true + var statefulClientId; + var frontendId; + var sendTextChangesToFlexClient = true; + + function initialize(args) { + if (args[0] != null) { + myCodeMirror.setValue(unescape(args[0])); + } + + statefulClientId = args[1]; + frontendId = args[2]; + if (args[3]) { // readOnly = true disableEditing(); } - myCodeMirror.on("change", function (instance, changeObj) { + myCodeMirror.on("change", function (instance, changeObj) { + if (!sendTextChangesToFlexClient) { + return; + } while(changeObj) { // works like an iterator var offset = myCodeMirror.indexFromPos(changeObj.from); var oldText = changeObj.removed.join('\n'); var newText = changeObj.text.join('\n'); if (isMobile == "true") { - StageWebViewBridge.call("codeMirrorEditorChangedHandler", null, editorStatefulClientId, offset, oldText, newText); + StageWebViewBridge.call("codeMirrorEditorChangedHandler", null, statefulClientId, frontendId, offset, oldText, newText); } else { - parent.getFlexApp().codeMirrorEditorChangedHandler(editorStatefulClientId, offset, oldText, newText); + parent.getFlexApp().codeMirrorEditorChangedHandler(statefulClientId, frontendId, offset, oldText, newText); } changeObj = changeObj.next; } @@ -123,7 +132,7 @@ function getContent() { var result = myCodeMirror.getValue(); - if (isMobile) { + if (isMobile == "true") { document.location = result; } else { return result; @@ -138,8 +147,13 @@ myCodeMirror.readOnly = false; } - function updateText(args) { - myCodeMirror.replaceRange(unescape(args[2]), myCodeMirror.posFromIndex(args[0]), myCodeMirror.posFromIndex(args[0] + args[1])); + function updateText(args) { + sendTextChangesToFlexClient = false; + try { + myCodeMirror.replaceRange(unescape(args[2]), myCodeMirror.posFromIndex(args[0]), myCodeMirror.posFromIndex(args[0] + args[1])); + } finally { + sendTextChangesToFlexClient = true; + } } @@ -150,7 +164,7 @@ myCodeMirror = CodeMirror(document.body, { mode: CodeMirror.mimeModes[ext2Mime[getURLParam('extension')]], lineNumbers: true - }); + }); \ No newline at end of file diff --git a/org.flowerplatform.flextexteditor_legacy/.flexLibProperties b/org.flowerplatform.flextexteditor_legacy/.flexLibProperties index 2f5494bd..e908e502 100644 --- a/org.flowerplatform.flextexteditor_legacy/.flexLibProperties +++ b/org.flowerplatform.flextexteditor_legacy/.flexLibProperties @@ -30,6 +30,7 @@ + diff --git a/org.flowerplatform.flextexteditor_legacy/src/com/crispico/flower/texteditor/SyntaxTextArea.as b/org.flowerplatform.flextexteditor_legacy/src/com/crispico/flower/texteditor/SyntaxTextArea.as index 416f5e4e..94aff377 100644 --- a/org.flowerplatform.flextexteditor_legacy/src/com/crispico/flower/texteditor/SyntaxTextArea.as +++ b/org.flowerplatform.flextexteditor_legacy/src/com/crispico/flower/texteditor/SyntaxTextArea.as @@ -405,6 +405,7 @@ import flashx.textLayout.edit.SelectionFormat; import flashx.textLayout.edit.SelectionState; import flashx.textLayout.edit.TextScrap; import flashx.textLayout.elements.GlobalSettings; +import flashx.textLayout.elements.ParagraphElement; import flashx.textLayout.elements.TextFlow; import flashx.textLayout.events.FlowOperationEvent; import flashx.textLayout.operations.ApplyLinkOperation; @@ -589,164 +590,3 @@ class CustomUndoManager extends UndoManager { } } } - -import flash.utils.Timer; -import flash.events.TimerEvent; -import mx.collections.ArrayCollection; -import org.osmf.events.TimeEvent; -import com.crispico.flower.texteditor.events.TextChangedEvent; -import com.crispico.flower.texteditor.events.BufferedTextChangesEvent; -import com.crispico.flower.texteditor.SyntaxTextArea; -import flash.text.TextDisplayMode; -import flashx.textLayout.elements.ParagraphElement; - -/** - * Collects TextChangedEvents from the SyntaxTextArea and dispatches - * these events (as a list, not individually) at fixed intervals, - * so they can be applied to other paired editors. - * - *

- * - * If the aggregating mechanism is used, then consecutive events are - * merged to create less events, thus reducing the amount of data to - * be sent to other editors. - */ -class TextChangedEventBuffer { - - /** - * A list that collects the TextChangedEvents - * dispatched by this editor. - */ - private var buffer:ArrayCollection; - - /** - * Controls dispatching events at a certain interval. - */ - private var timer:Timer; - - /** - * The time interval (in ms) when the buffered events - * are aggregated and then dispatched and the buffer is cleared. - * - *

- * - * If the interval is 0, then there is no buffering; each event - * is dispatched immediately instead of being added to the buffer. - */ - private var interval:Number; - - /** - * If buffering is used, events are aggregated and added to this, - * list in order to be dispatched. - */ - private var aggregatedEvents:ArrayCollection; - - /** - * The SyntaxTextArea that provides the TextChangedEvents. - */ - private var textArea:SyntaxTextArea - - /** - * If the dirty flag is false, then updates are dispatched - * immediately, to avoid the case when the user closes the - * editor before modification are sent. - */ - public var dirty:Boolean = false; - - public function TextChangedEventBuffer(textArea:SyntaxTextArea, interval:int = 0):void { - this.textArea = textArea; - this.interval = interval; - this.buffer = new ArrayCollection(); - this.aggregatedEvents = new ArrayCollection(); - initializeTimer(); - } - - public function setInterval(value:int):void { - this.interval = value; - initializeTimer(); - } - - private function initializeTimer():void { - if (interval > 0) { - timer = new Timer(interval); - timer.addEventListener(TimerEvent.TIMER, aggregateEvents); - timer.start(); - } - } - - /** - * Adds the event to the buffer, if the buffering mechanism - * is used; otherwise the event is dispatched immediately. - */ - public function add(evt:TextChangedEvent):void { - buffer.addItem(evt); - if (interval == 0 || !dirty) { - dispatchEvents(); - } - } - - /** - * Aggregates consecutive events in the buffer, thus creating - * a list of fewer aggregated events. - * - *

- * - * Note: two events are consecutive if the one of them ends - * at the beginning of the other. - */ - public function aggregateEvents(evt:TimerEvent = null):void { - if (buffer.length > 0) { - aggregatedEvents = new ArrayCollection(); - var offset:int = (buffer.getItemAt(0) as TextChangedEvent).offset; - var oldTextLength:int = (buffer.getItemAt(0) as TextChangedEvent).oldTextLength; - var newText:String = (buffer.getItemAt(0) as TextChangedEvent).newText; - for (var i:int = 1; i < buffer.length; i++) { - var prev:TextChangedEvent = buffer.getItemAt(i - 1) as TextChangedEvent; - var crt:TextChangedEvent = buffer.getItemAt(i) as TextChangedEvent; - - // check if the events are consecutive - if (crt.offset == prev.offset - prev.oldTextLength + prev.newText.length) { - if (crt.offset < prev.offset) { - // both edits are actually delete events - // the aggregated event will now start at the start of the second one - offset = crt.offset; - oldTextLength = crt.oldTextLength + oldTextLength; - } - else { - oldTextLength += crt.oldTextLength; - } - newText += crt.newText; - } - else { - // add the new event to the list of aggregated events - aggregatedEvents.addItem(new TextChangedEvent(offset, oldTextLength, newText)); - offset = crt.offset; - oldTextLength = crt.oldTextLength; - newText = crt.newText; - } - } - - // the last event is not added in the for loop, so we add it now - aggregatedEvents.addItem(new TextChangedEvent(offset, oldTextLength, newText)); - - dispatchEvents(); - } - } - - /** - * Dispatches a BufferedTextChangesEvent that contains - * a list of events to be applied to other editors. - */ - private function dispatchEvents(evt:TimerEvent = null):void { - // send the list of events only if there are any events - if (buffer.length > 0) { - // check if the aggregating mechanism was used to decide which of - // the lists will be dispatched - var events:ArrayCollection = (interval == 0 || !dirty) ? buffer : aggregatedEvents; - textArea.dispatchEvent(new BufferedTextChangesEvent(events)); - // clear the buffer - buffer = new ArrayCollection(); - dirty = true; - } - } -} \ No newline at end of file diff --git a/org.flowerplatform.flextexteditor_legacy/src/com/crispico/flower/texteditor/TextChangedEventBuffer.as b/org.flowerplatform.flextexteditor_legacy/src/com/crispico/flower/texteditor/TextChangedEventBuffer.as new file mode 100644 index 00000000..b22418fd --- /dev/null +++ b/org.flowerplatform.flextexteditor_legacy/src/com/crispico/flower/texteditor/TextChangedEventBuffer.as @@ -0,0 +1,170 @@ +package com.crispico.flower.texteditor { + + import com.crispico.flower.texteditor.SyntaxTextArea; + import com.crispico.flower.texteditor.events.BufferedTextChangesEvent; + import com.crispico.flower.texteditor.events.TextChangedEvent; + + import flash.events.IEventDispatcher; + import flash.events.TimerEvent; + import flash.text.TextDisplayMode; + import flash.utils.Timer; + + import flashx.textLayout.elements.ParagraphElement; + + import mx.collections.ArrayCollection; + + import org.osmf.events.TimeEvent; + + import spark.core.IDisplayText; + + /** + * Collects TextChangedEvents from the SyntaxTextArea and dispatches + * these events (as a list, not individually) at fixed intervals, + * so they can be applied to other paired editors. + * + *

+ * + * If the aggregating mechanism is used, then consecutive events are + * merged to create less events, thus reducing the amount of data to + * be sent to other editors. + */ + public class TextChangedEventBuffer { + + /** + * A list that collects the TextChangedEvents + * dispatched by this editor. + */ + private var buffer:ArrayCollection; + + /** + * Controls dispatching events at a certain interval. + */ + private var timer:Timer; + + /** + * The time interval (in ms) when the buffered events + * are aggregated and then dispatched and the buffer is cleared. + * + *

+ * + * If the interval is 0, then there is no buffering; each event + * is dispatched immediately instead of being added to the buffer. + */ + private var interval:Number; + + /** + * If buffering is used, events are aggregated and added to this, + * list in order to be dispatched. + */ + private var aggregatedEvents:ArrayCollection; + + /** + * The SyntaxTextArea that provides the TextChangedEvents. + */ + private var eventDispatcher:IEventDispatcher; + + /** + * If the dirty flag is false, then updates are dispatched + * immediately, to avoid the case when the user closes the + * editor before modification are sent. + */ + public var dirty:Boolean = false; + + public function TextChangedEventBuffer(eventDispatcher:IEventDispatcher, interval:int = 0):void { + this.eventDispatcher = eventDispatcher; + this.interval = interval; + this.buffer = new ArrayCollection(); + this.aggregatedEvents = new ArrayCollection(); + initializeTimer(); + } + + public function setInterval(value:int):void { + this.interval = value; + initializeTimer(); + } + + private function initializeTimer():void { + if (interval > 0) { + timer = new Timer(interval); + timer.addEventListener(TimerEvent.TIMER, aggregateEvents); + timer.start(); + } + } + + /** + * Adds the event to the buffer, if the buffering mechanism + * is used; otherwise the event is dispatched immediately. + */ + public function add(evt:TextChangedEvent):void { + buffer.addItem(evt); + if (interval == 0 || !dirty) { + dispatchEvents(); + } + } + + /** + * Aggregates consecutive events in the buffer, thus creating + * a list of fewer aggregated events. + * + *

+ * + * Note: two events are consecutive if the one of them ends + * at the beginning of the other. + */ + public function aggregateEvents(evt:TimerEvent = null):void { + if (buffer.length > 0) { + aggregatedEvents = new ArrayCollection(); + var offset:int = (buffer.getItemAt(0) as TextChangedEvent).offset; + var oldTextLength:int = (buffer.getItemAt(0) as TextChangedEvent).oldTextLength; + var newText:String = (buffer.getItemAt(0) as TextChangedEvent).newText; + for (var i:int = 1; i < buffer.length; i++) { + var prev:TextChangedEvent = buffer.getItemAt(i - 1) as TextChangedEvent; + var crt:TextChangedEvent = buffer.getItemAt(i) as TextChangedEvent; + + // check if the events are consecutive + if (crt.offset == prev.offset - prev.oldTextLength + prev.newText.length) { + if (crt.offset < prev.offset) { + // both edits are actually delete events + // the aggregated event will now start at the start of the second one + offset = crt.offset; + oldTextLength = crt.oldTextLength + oldTextLength; + } + else { + oldTextLength += crt.oldTextLength; + } + newText += crt.newText; + } + else { + // add the new event to the list of aggregated events + aggregatedEvents.addItem(new TextChangedEvent(offset, oldTextLength, newText)); + offset = crt.offset; + oldTextLength = crt.oldTextLength; + newText = crt.newText; + } + } + + // the last event is not added in the for loop, so we add it now + aggregatedEvents.addItem(new TextChangedEvent(offset, oldTextLength, newText)); + + dispatchEvents(); + } + } + + /** + * Dispatches a BufferedTextChangesEvent that contains + * a list of events to be applied to other editors. + */ + private function dispatchEvents(evt:TimerEvent = null):void { + // send the list of events only if there are any events + if (buffer.length > 0) { + // check if the aggregating mechanism was used to decide which of + // the lists will be dispatched + var events:ArrayCollection = (interval == 0 || !dirty) ? buffer : aggregatedEvents; + eventDispatcher.dispatchEvent(new BufferedTextChangesEvent(events)); + // clear the buffer + buffer = new ArrayCollection(); + dirty = true; + } + } + } +} \ No newline at end of file