1//////////////////////////////////////////////////////////////////////////////// 2// 3// ADOBE SYSTEMS INCORPORATED 4// Copyright 2007-2010 Adobe Systems Incorporated 5// All Rights Reserved. 6// 7// NOTICE: Adobe permits you to use, modify, and distribute this file 8// in accordance with the terms of the license agreement accompanying it. 9// 10//////////////////////////////////////////////////////////////////////////////// 11package flashx.textLayout.edit 12{ 13 import flash.display.DisplayObject; 14 import flash.display.DisplayObjectContainer; 15 import flash.display.InteractiveObject; 16 import flash.errors.IllegalOperationError; 17 import flash.events.Event; 18 import flash.events.FocusEvent; 19 import flash.events.IMEEvent; 20 import flash.events.KeyboardEvent; 21 import flash.events.MouseEvent; 22 import flash.events.TextEvent; 23 import flash.geom.Point; 24 import flash.geom.Rectangle; 25 import flash.system.Capabilities; 26 import flash.text.engine.TextLine; 27 import flash.ui.Keyboard; 28 import flash.utils.getQualifiedClassName; 29 30 import flashx.textLayout.compose.IFlowComposer; 31 import flashx.textLayout.container.ContainerController; 32 import flashx.textLayout.debug.Debugging; 33 import flashx.textLayout.debug.assert; 34 import flashx.textLayout.elements.Configuration; 35 import flashx.textLayout.elements.DivElement; 36 import flashx.textLayout.elements.FlowElement; 37 import flashx.textLayout.elements.FlowGroupElement; 38 import flashx.textLayout.elements.FlowLeafElement; 39 import flashx.textLayout.elements.GlobalSettings; 40 import flashx.textLayout.elements.InlineGraphicElement; 41 import flashx.textLayout.elements.LinkElement; 42 import flashx.textLayout.elements.ListElement; 43 import flashx.textLayout.elements.ListItemElement; 44 import flashx.textLayout.elements.ParagraphElement; 45 import flashx.textLayout.elements.SubParagraphGroupElement; 46 import flashx.textLayout.elements.TCYElement; 47 import flashx.textLayout.elements.TextFlow; 48 import flashx.textLayout.elements.TextRange; 49 import flashx.textLayout.events.FlowOperationEvent; 50 import flashx.textLayout.formats.IListMarkerFormat; 51 import flashx.textLayout.formats.ITextLayoutFormat; 52 import flashx.textLayout.formats.TextLayoutFormat; 53 import flashx.textLayout.operations.ApplyElementIDOperation; import flashx.textLayout.operations.ApplyElementTypeNameOperation; 54 import flashx.textLayout.operations.ApplyFormatOperation; 55 import flashx.textLayout.operations.ApplyFormatToElementOperation; 56 import flashx.textLayout.operations.ApplyLinkOperation; 57 import flashx.textLayout.operations.ApplyTCYOperation; 58 import flashx.textLayout.operations.ClearFormatOnElementOperation; 59 import flashx.textLayout.operations.ClearFormatOperation; 60 import flashx.textLayout.operations.CompositeOperation; 61 import flashx.textLayout.operations.CreateDivOperation; 62 import flashx.textLayout.operations.CreateListOperation; 63 import flashx.textLayout.operations.CreateSubParagraphGroupOperation; 64 import flashx.textLayout.operations.CutOperation; 65 import flashx.textLayout.operations.DeleteTextOperation; 66 import flashx.textLayout.operations.FlowOperation; 67 import flashx.textLayout.operations.InsertInlineGraphicOperation; 68 import flashx.textLayout.operations.InsertTextOperation; 69 import flashx.textLayout.operations.ModifyInlineGraphicOperation; 70 import flashx.textLayout.operations.MoveChildrenOperation; 71 import flashx.textLayout.operations.PasteOperation; 72 import flashx.textLayout.operations.RedoOperation; 73 import flashx.textLayout.operations.SplitElementOperation; 74 import flashx.textLayout.operations.SplitParagraphOperation; 75 import flashx.textLayout.operations.UndoOperation; 76 import flashx.textLayout.tlf_internal; 77 import flashx.textLayout.utils.CharacterUtil; 78 import flashx.textLayout.utils.GeometryUtil; 79 import flashx.textLayout.utils.NavigationUtil; 80 import flashx.undo.IOperation; 81 import flashx.undo.IUndoManager; 82 83 use namespace tlf_internal; 84 85 /** 86 * The EditManager class manages editing changes to a TextFlow. 87 * 88 * <p>To enable text flow editing, assign an EditManager object to the <code>interactionManager</code> 89 * property of the TextFlow object. The edit manager handles changes to the text (such as insertions, 90 * deletions, and format changes). Changes are reversible if the edit manager has an undo manager. The edit 91 * manager triggers the recomposition and display of the text flow, as necessary.</p> 92 * 93 * <p>The EditManager class supports the following keyboard shortcuts:</p> 94 * 95 * <table class="innertable" width="100%"> 96 * <tr><th>Keys</th><th>Result</th></tr> 97 * <tr><td>ctrl-z</td><td>undo</td></tr> 98 * <tr><td>ctrl-y</td><td>redo</td></tr> 99 * <tr><td>ctrl-backspace</td><td>deletePreviousWord</td></tr> 100 * <tr><td>ctrl-delete</td><td>deleteNextWord</td></tr> 101 * <tr><td>alt+delete</td><td>deleteNextWord</td></tr> 102 * <tr><td>ctrl+alt-delete</td><td>deleteNextWord</td></tr> 103 * <tr><td>ctrl-shift-hyphen</td><td>insert discretionary hyphen</td></tr> 104 * <tr><td>ctrl+backspace</td><td>deletePreviousWord</td></tr> 105 * <tr><td>alt+backspace</td><td>deletePreviousWord</td></tr> 106 * <tr><td>ctrl+alt-backspace</td><td>deletePreviousWord</td></tr> 107 * <tr><td>INSERT</td><td>toggles overWriteMode</td></tr> 108 * <tr><td>backspace</td><td>deletePreviousCharacter</td></tr> 109 * <tr><td>ENTER</td><td>if textFlow.configuration.manageEnterKey in a list it creates a new list item, otherwise creates a new paragraph</td></tr> 110 * <tr><td>shift-ENTER</td><td>if textFlow.configuration.manageEnterKey creates a new paragraph</td></tr> 111 * <tr><td>TAB</td><td>if textFlow.configuration.manageTabKey in a list it creates nested list, otherwise inserts a TAB or overwrites next character with a TAB</td></tr> 112 * <tr><td>shift-TAB</td><td>if textFlow.configuration.manageTabKey in the first item of a list it moves the item out of the list (promotes it)</td></tr> 113 * </table> 114 * 115 * <p><strong>Note:</strong> The following keys do not work on Windows: alt-backspace, alt-delete, ctrl+alt-backspace, 116 * and ctrl+alt-delete. These keys do not generate an event for the runtime.</p> 117 * 118 * @see flashx.textLayout.elements.TextFlow 119 * @see flashx.undo.UndoManager 120 * 121 * @includeExample examples\EditManager_example.as -noswf 122 * 123 * @playerversion Flash 10 124 * @playerversion AIR 1.5 125 * @langversion 3.0 126 */ 127 public class EditManager extends SelectionManager implements IEditManager 128 { 129 /** 130 * To minimize expensive recompositions during fast typing, inserts 131 * don't necessarily take place immediately. An insert operation that 132 * hasn't yet executed is held here. 133 */ 134 private var pendingInsert:InsertTextOperation; 135 136 /* 137 * The object that has the ENTER_FRAME event listener attached to perform pending inserts. 138 */ 139 private var enterFrameListener:DisplayObjectContainer; 140 141 /* True if updates get handled on enter_frame, and not immediately. */ 142 private var _delayUpdates:Boolean = false; 143 144 /* True if some operations (e.g., handling of text events) can be delayed to next enterFrame by default. False for immediate handling */ 145 private var _allowDelayedOperations:Boolean = true; 146 147 /* 148 * The object that has the ENTER_FRAME event listener attached to perform pending updates. 149 */ 150 private var redrawListener:DisplayObjectContainer; 151 152 /* 153 * Some operations can be undone & redone. The undoManager keeps track 154 * of the operations that have been done or undone so that they can be undone or 155 * redone. I'm not sure if only text operations can be undone. If so, the undoManager 156 * should probably be moved to EditManager. 157 */ 158 private var _undoManager:flashx.undo.IUndoManager; 159 160 private var _imeSession:IMEClient; 161 private var _imeOperationInProgress:Boolean; 162 163 /** 164 * Indicates whether overwrite mode is on or off. 165 * 166 * <p>If <code>true</code>, then a keystroke overwrites the character following the cursor. 167 * If <code>false</code>, then a keystroke is inserted at the cursor location.</p> 168 * 169 * @playerversion Flash 10 170 * @playerversion AIR 1.5 171 * @langversion 3.0 172 */ 173 public static var overwriteMode:Boolean = false; 174 175 /** 176 * Creates an EditManager object. 177 * 178 * <p>Assign an EditManager object to the <code>interactionManager</code> property 179 * of a text flow to enable editing of that text flow. </p> 180 * 181 * <p>To enable support for undoing and redoing changes, pass an 182 * IUndoManager instance to the EditManager constructor. You can use 183 * the <code>flashx.undo.UndoManager</code> class 184 * or create a custom IUndoManager instance. Use a custom IUndoManager instance 185 * to integrate Text Layout Framework changes with an existing 186 * undo manager that is not an instance of the UndoManager class. 187 * To create a custom IUndoManager instance, ensure that the class 188 * you use to define the undo manager 189 * implements the IUndoManager interface.</p> 190 * 191 * 192 * @param undo The UndoManager for the application 193 * 194 * @see flashx.textLayout.elements.TextFlow#interactionManager 195 * @see flashx.undo.IUndoManager 196 * 197 * @includeExample examples\EditManager_constructor.as -noswf 198 * @playerversion Flash 10 199 * @playerversion AIR 1.5 200 * @langversion 3.0 201 */ 202 public function EditManager(undoManager:flashx.undo.IUndoManager = null) 203 { 204 super(); 205 _undoManager = undoManager; 206 } 207 208 /** 209 * The IUndoManager assigned to this edit manager. 210 * 211 * <p>To allow edits to be undone (and redone), pass an IUndoManager instance to the EditManager 212 * constructor. The undo manager maintains a stack of operations that have been executed, and it can 213 * undo or redo individual operations. </p> 214 * 215 * <p><b>Note:</b> If the TextFlow is modified directly (not via 216 * calls to the EditManager, but directly via calls to the managed FlowElement objects), then the EditManager 217 * clears the undo stack to prevent the stack from getting out of sync with the current state.</p> 218 * 219 * @playerversion Flash 10 220 * @playerversion AIR 1.5 221 * @langversion 3.0 222 */ 223 public function get undoManager():flashx.undo.IUndoManager 224 { 225 return _undoManager; 226 } 227 228 // Backdoor provided so that IMEClient can temporarily use an undo manager to maintain the IME session state. 229 /** @private */ 230 tlf_internal function setUndoManager(undoManager:flashx.undo.IUndoManager):void 231 { 232 _undoManager = undoManager; 233 } 234 235 /** @private */ 236 override public function editHandler(event:Event):void 237 { 238 if (event.isDefaultPrevented()) 239 return; 240 241 super.editHandler(event); 242 switch (event.type) 243 { 244 case Event.CUT: 245 case Event.CLEAR: 246 if (activePosition != anchorPosition) 247 { 248 if (event.type == Event.CUT) 249 TextClipboard.setContents(cutTextScrap()); 250 else 251 deleteText(null); 252 event.preventDefault(); 253 } 254 break; 255 case Event.PASTE: 256 pasteTextScrap(TextClipboard.getContents()); 257 event.preventDefault(); 258 break; 259 } 260 } 261 262 // /////////////////////////////////// 263 // keyboard methods 264 // /////////////////////////////////// 265 266 /** @private */ 267 public override function keyDownHandler(event:KeyboardEvent):void 268 { 269 var listItem:ListItemElement; 270 var operationState:SelectionState; 271 272 if (!hasSelection() || event.isDefaultPrevented()) 273 return; 274 275 if (redrawListener) // pending redraw; handle this before anything else so TextLines on screen will be up to date 276 updateAllControllers(); 277 278 super.keyDownHandler(event); 279 280 if (event.ctrlKey) 281 { 282 // The player subsequently sends a text input event (which should be ignored) as listed below: 283 // CTRL/CMD+z: Only on Mac when using a pre-Argo player version 284 // CTRL/CMD+y: On all platforms (the exact char code for the text input event is platform dependent) 285 if (!event.altKey) 286 { 287 if (_imeSession != null && ((event.charCode == 122) || (event.charCode == 121))) 288 _imeSession.compositionAbandoned(); // must be done before undo or redo start b/c IME session uses undo also for its own rollback 289 // do Operation will also cancel the session. 290 291 switch(event.charCode) 292 { 293 case 122: // small z 294 /* pre-Argo and on the mac then ignoreNextTextEvent */ 295 if (!Configuration.versionIsAtLeast(10,1) && (Capabilities.os.search("Mac OS") > -1)) 296 ignoreNextTextEvent = true; 297 undo(); 298 event.preventDefault(); 299 break; 300 case 121: // small y 301 ignoreNextTextEvent = true; 302 redo(); 303 event.preventDefault(); 304 break; 305 case Keyboard.BACKSPACE: 306 if (_imeSession) 307 _imeSession.compositionAbandoned(); 308 deletePreviousWord(); 309 event.preventDefault(); 310 break; 311 } 312 if (event.keyCode == Keyboard.DELETE) 313 { 314 if (_imeSession) 315 _imeSession.compositionAbandoned(); 316 deleteNextWord(); 317 event.preventDefault(); 318 } 319 320 if (event.shiftKey) 321 { 322 // detect ctrl-shift-"-" (cnd-shift-"-" on mac) and insert a DH 323 if (event.charCode == 95) 324 { 325 if (_imeSession) 326 _imeSession.compositionAbandoned(); 327 328 //a discretionary hyphen is being inserted. 329 var discretionaryHyphenString:String = String.fromCharCode(0x000000AD); 330 overwriteMode ? overwriteText(discretionaryHyphenString) : insertText(discretionaryHyphenString); 331 event.preventDefault(); 332 } 333 } 334 } 335 } 336 else if (event.altKey) 337 { 338 if (event.charCode == Keyboard.BACKSPACE) 339 { 340 deletePreviousWord(); 341 event.preventDefault(); 342 } 343 else if (event.keyCode == Keyboard.DELETE) 344 { 345 deleteNextWord(); 346 event.preventDefault(); 347 } 348 } 349 // not ctrl key or alt key 350 else if (event.keyCode == Keyboard.DELETE) //del 351 { 352 deleteNextCharacter(); 353 event.preventDefault(); 354 } 355 else if (event.keyCode == Keyboard.INSERT) //insert 356 { 357 overwriteMode = !overwriteMode; 358 event.preventDefault(); 359 } 360 else switch(event.charCode) { 361 case Keyboard.BACKSPACE: 362 deletePreviousCharacter(); 363 event.preventDefault(); 364 break; 365 case Keyboard.ENTER: 366 if (textFlow.configuration.manageEnterKey) 367 { 368 var firstLeaf:FlowLeafElement = textFlow.findLeaf(absoluteStart); 369 listItem = firstLeaf.getParentByType(ListItemElement) as ListItemElement; 370 // if the listitem has a ListElement child then treat this as a regular Paragraph 371 if (listItem && firstLeaf.getParentByType(ListElement) != listItem.getParentByType(ListElement)) 372 listItem = null; 373 374 // inside a list shift-enter splits a paragraph and shift splits the listitem 375 if (listItem && !event.shiftKey) 376 { 377 // if on last item of list and it's empty, remove it and put cursor on a new para immediatly following the list (new para should be wrapped in a new list item if parent of list is another list). 378 if(listItem.textLength == 1 && listItem.parent.getChildIndex(listItem) == listItem.parent.numChildren - 1) 379 { 380 operationState = defaultOperationState(); 381 if (!operationState) 382 return; 383 384 doOperation(new MoveChildrenOperation(operationState, listItem.parent, listItem.parent.getChildIndex(listItem), 1, listItem.parent.parent, listItem.parent.parent.getChildIndex(listItem.parent) + 1)); 385 } 386 else 387 { 388 splitElement(listItem); 389 // push cursor past the marker 390 selectRange(absoluteStart+1,absoluteStart+1); 391 refreshSelection(); 392 } 393 } 394 else 395 splitParagraph(); 396 event.preventDefault(); 397 event.stopImmediatePropagation(); 398 } 399 break; 400 case Keyboard.TAB: 401 if (textFlow.configuration.manageTabKey) 402 { 403 listItem = textFlow.findLeaf(absoluteStart).getParentByType(ListItemElement) as ListItemElement; 404 if (listItem && listItem.getAbsoluteStart() == absoluteStart) 405 { 406 operationState = defaultOperationState(); 407 if (!operationState) 408 return; 409 410 if(event.shiftKey) 411 { 412 // unindent by moving list element(s) out of parent into grandparent 413 414 // first make sure there is a grandparent 415 if(listItem.parent.parent is ListElement && listItem.parent.getChildIndex(listItem) == 0) 416 { 417 var source:FlowGroupElement; 418 var target:FlowGroupElement; 419 var numElements:int; 420 var sourceIndex:int; 421 var targetIndex:int; 422 423 { 424 source = listItem.parent; 425 target = listItem.parent.parent; 426 numElements = listItem.parent.numChildren; 427 sourceIndex = 0; 428 targetIndex = listItem.parent.parent.getChildIndex(listItem.parent); 429 } 430 doOperation(new MoveChildrenOperation(operationState, source, sourceIndex, numElements, target, targetIndex)); 431 } 432 } 433 else 434 { 435 // create new list from list element(s) 436 var element:FlowGroupElement = listItem; 437 if(listItem.parent.getChildIndex(listItem) == 0) 438 element = listItem.parent; 439 440 doOperation(new CreateListOperation(new SelectionState(textFlow, element.getAbsoluteStart(), element.getAbsoluteStart() + element.textLength), listItem.parent)); 441 } 442 } 443 else 444 { 445 overwriteMode ? overwriteText(String.fromCharCode(event.charCode)) : insertText(String.fromCharCode(event.charCode)); 446 } 447 event.preventDefault(); 448 } 449 break; 450 } 451 } 452 453 /** @private */ 454 public override function keyUpHandler(event:KeyboardEvent):void 455 { 456 if (!hasSelection() || event.isDefaultPrevented()) 457 return; 458 459 super.keyUpHandler(event); 460 461 if ((textFlow.configuration.manageEnterKey && event.charCode == Keyboard.ENTER) || (textFlow.configuration.manageTabKey && event.charCode == Keyboard.TAB)) { 462 event.stopImmediatePropagation(); 463 } 464 } 465 466 /** @private */ 467 public override function keyFocusChangeHandler(event:FocusEvent):void 468 { 469 if (textFlow.configuration.manageTabKey) 470 event.preventDefault(); 471 } 472 473 /** @private */ 474 public override function mouseDownHandler(event:MouseEvent):void 475 { 476 if (redrawListener) 477 updateAllControllers(); 478 super.mouseDownHandler(event); 479 } 480 481 /** @private */ 482 public override function textInputHandler(event:TextEvent):void 483 { 484 if (!ignoreNextTextEvent) 485 { 486 var charCode:int = event.text.charCodeAt(0); 487 // only if its a space or larger - ignore control characters here 488 if (charCode >= 32) 489 overwriteMode ? overwriteText(event.text) : insertText(event.text); 490 } 491 ignoreNextTextEvent = false; 492 } 493 494 /** @private */ 495 override public function focusOutHandler(event:FocusEvent):void 496 { 497 super.focusOutHandler(event); 498 if (_imeSession && selectionFormatState != SelectionFormatState.FOCUSED) 499 _imeSession.compositionAbandoned(); 500 } 501 502 /** @private */ 503 override public function deactivateHandler(event:Event):void 504 { 505 super.deactivateHandler(event); 506 if (_imeSession) 507 _imeSession.compositionAbandoned(); 508 } 509 510 /** @private */ 511 override public function imeStartCompositionHandler(event:IMEEvent):void 512 { 513 CONFIG::debug{ assert(!_imeSession, "IME session already in progress: IME not reentrant!"); } 514 // CONFIG::debug { Debugging.traceOut("imeStartComposition event"); } 515 516 // any pending operations must be executed first, to 517 // preserve operation order. 518 flushPendingOperations(); 519 520 // Coded to avoid dependency on Argo (10.1). 521 if (!(event["imeClient"])) 522 { 523 _imeSession = new IMEClient(this); 524 _imeOperationInProgress = false; 525 event["imeClient"] = _imeSession; 526 } 527 } 528 529 /** @private */ 530 override public function setFocus():void 531 { 532 var flowComposer:IFlowComposer = textFlow ? textFlow.flowComposer : null; 533 if (_imeSession && flowComposer && flowComposer.numControllers > 1) 534 { 535 // container with the ime start position gets the key focus 536 _imeSession.setFocus(); 537 538 setSelectionFormatState(SelectionFormatState.FOCUSED); 539 } 540 else 541 super.setFocus(); 542 543 /* CONFIG::debug 544 { 545 if (textFlow.flowComposer.getControllerAt(0).container.stage) 546 { 547 var focusDI:DisplayObject = textFlow.flowComposer.getControllerAt(0).container.stage.focus; 548 trace("set focus to ", focusDI.hasOwnProperty("name") ? focusDI["name"] : focusDI.toString()); 549 } 550 } */ 551 } 552 /** @private */ 553 tlf_internal function endIMESession():void 554 { 555 _imeSession = null; 556 var flowComposer:IFlowComposer = textFlow ? textFlow.flowComposer : null; 557 if (flowComposer && flowComposer.numControllers > 1) 558 setFocus(); 559 } 560 /** @private */ 561 tlf_internal function beginIMEOperation():void 562 { 563 _imeOperationInProgress = true; 564 beginCompositeOperation(); 565 } 566 /** @private */ 567 tlf_internal function endIMEOperation():void 568 { 569 endCompositeOperation(); 570 _imeOperationInProgress = false; 571 } 572 573 /** @private We track the nesting level of the doOperation, because in finalize we need to know if 574 we are at the outermost level and need to push the operation on the undo stack and redraw 575 the screen, or if we're in a nested level and need to append the operation to the next 576 level up. */ 577 tlf_internal var captureLevel:int = 0; 578 579 /** 580 * @copy IEditManager#doOperation() 581 * 582 * @includeExample examples\EditManager_doOperation.as -noswf 583 * 584 * @playerversion Flash 10 585 * @playerversion AIR 1.5 586 * @langversion 3.0 587 */ 588 public override function doOperation(operation:FlowOperation):void 589 { 590 CONFIG::debug { assert(operation.textFlow == this.textFlow,"Operation from a different TextFlow"); } 591 592 // If we get any operation during an IME session that is not owned by the session, we cancel the IME 593 if (_imeSession && !_imeOperationInProgress) 594 _imeSession.compositionAbandoned(); 595 596 // any pending operations must be executed first, to 597 // preserve operation order. 598 flushPendingOperations(); 599 600 try 601 { 602 captureLevel++; 603 operation = doInternal(operation); 604 } 605 catch(e:Error) 606 { 607 captureLevel--; 608 throw(e); 609 } 610 captureLevel--; 611 612 if (operation) // don't finalize if operation was cancelled 613 finalizeDo(operation); 614 } 615 616 private function finalizeDo(op:FlowOperation):void 617 { 618 // Handle operation if we're in a beginCompositeOperation/endCompositeOperation context 619 // In this case any nested commands we do will get added to the composite operation when 620 // they're done instead of added to the undo stack. 621 var parentOperation:CompositeOperation; 622 if (parentStack && parentStack.length > 0) 623 { 624 var parent:Object = parentStack[parentStack.length - 1]; 625 if (parent.captureLevel == captureLevel) 626 parentOperation = parent.operation as CompositeOperation; 627 } 628 629 // CONFIG::debug { assert(captureLevel == 0 || parentOperation != null, "missing parent for nested operation"); } 630 631 if (parentOperation) 632 parentOperation.addOperation(op); 633 634 else if (captureLevel == 0) 635 { 636 captureOperations.length = 0; 637 if (_undoManager) 638 { 639 if (_undoManager.canUndo() && allowOperationMerge) 640 { 641 var lastOp:FlowOperation = _undoManager.peekUndo() as FlowOperation; 642 if (lastOp) 643 { 644 // Try to merge the last operation on the stack with the current 645 // operation. This may modify lastOp, or return a new operation 646 var combinedOp:FlowOperation = lastOp.merge(op); 647 if (combinedOp) 648 { 649 CONFIG::debug { assert(combinedOp.endGeneration == textFlow.generation,"Who did what?"); } 650 CONFIG::debug { assert(combinedOp.canUndo() && combinedOp.endGeneration == op.endGeneration,"Bad operation merge in EditManager") }; 651 _undoManager.popUndo(); 652 op = combinedOp; 653 } 654 } 655 } 656 if (op.canUndo()) 657 _undoManager.pushUndo(op); 658 allowOperationMerge = true; 659 660 // following operations are no longer redoable 661 _undoManager.clearRedo(); 662 } 663 664 handleUpdate(); 665 666 if (!_imeSession) 667 { 668 var opEvent:FlowOperationEvent = new FlowOperationEvent(FlowOperationEvent.FLOW_OPERATION_COMPLETE,false,false,op,0,null); 669 textFlow.dispatchEvent(opEvent); 670 } 671 } 672 } 673 674 private var captureOperations:Array = []; 675 676 /** Internal guts of a dooperation - Execute a FlowOperation. This function proceeds in steps. 677 * <p>Step 2. Send a canceallable OperationEvent. If cancelled this method returns immediately.</p> 678 * If it is not cancelled, the listener may "do" other operations by calling back into the EditManager. This will result 679 * in a nested call to do which will post additional commands to the captureOperations array. 680 * <p>Step 3. Execute the operation. The operation returns true or false. false indicates no changes were made.</p> 681 * <p>Step 7. Send a OperationEvent. </p> 682 * The listener may "do" other operations by calling back into the EditManager. This will result 683 * in a nested call to do which will post additional commands to the captureOperations array. 684 * <p>Exception handling. If the operation throws the exception is caught and the error is attached to the event dispatched 685 * at step 7. If the event is not cancelled the error is rethrown.</p> 686 */ 687 private function doInternal(op:FlowOperation):FlowOperation 688 { 689 CONFIG::debug { assert(op.textFlow == this.textFlow,"Operation from a different TextFlow"); } 690 691 var captureStart:int = captureOperations.length; 692 var success:Boolean = false; 693 var opEvent:FlowOperationEvent; 694 695 // tell any listeners about the operation 696 if (!_imeSession) 697 { 698 opEvent = new FlowOperationEvent(FlowOperationEvent.FLOW_OPERATION_BEGIN,false,true,op,captureLevel-1,null); 699 textFlow.dispatchEvent(opEvent); 700 if (opEvent.isDefaultPrevented()) 701 return null; 702 // user may replace the operation - TODO: WHAT IF SWITCH TO UNDO/REDO???? 703 op = opEvent.operation; 704 if ((op is UndoOperation) || (op is RedoOperation)) 705 throw new IllegalOperationError(GlobalSettings.resourceStringFunction("illegalOperation",[ getQualifiedClassName(op) ])); 706 } 707 708 var opError:Error = null; 709 try 710 { 711 // begin this op after pending ops are flushed 712 CONFIG::debug 713 { 714 if (captureLevel <= 1) 715 debugCheckTextFlow(); 716 } 717 718 // null return implies no operation was done - just discard it 719 var beforeGeneration:uint = textFlow.generation; 720 op.setGenerations(beforeGeneration,0); 721 722 captureOperations.push(op); 723 success = op.doOperation(); 724 if (success) // operation succeeded 725 { 726 textFlow.normalize(); //force normalization at this point. Don't compose unless the captureLevel is 0 727 728 // This has to be done after the normalize, because normalize increments the generation number 729 op.setGenerations(beforeGeneration,textFlow.generation); 730 } 731 else 732 { 733 var index:int = captureOperations.indexOf(op); 734 if (index >= 0) 735 captureOperations.splice(index, 1); 736 } 737 } 738 catch(e:Error) 739 { 740 opError = e; 741 } 742 743 // operation completed - send event whether it succeeded or not. 744 // client can check generation number for changes 745 if (!_imeSession) 746 { 747 opEvent = new FlowOperationEvent(FlowOperationEvent.FLOW_OPERATION_END,false,true,op,captureLevel-1,opError); 748 textFlow.dispatchEvent(opEvent); 749 opError = opEvent.isDefaultPrevented() ? null : opEvent.error; 750 } 751 752 if (opError) 753 throw (opError); 754 755 // If we fired off any subsidiary operations, create a composite operation to hold them all 756 if (captureOperations.length - captureStart > 1) 757 { 758 op = new CompositeOperation(captureOperations.slice(captureStart)); 759 op.setGenerations(FlowOperation(CompositeOperation(op).operations[0]).beginGeneration,textFlow.generation); 760 allowOperationMerge = false; 761 captureOperations.length = captureStart; 762 } 763 764 return success ? op : null; 765 } 766 767 /** @private **/ 768 override public function set textFlow(value:TextFlow):void 769 { 770 flushPendingOperations(); 771 if (redrawListener) // detach handler if there is one 772 updateAllControllers(); 773 super.textFlow = value; 774 } 775 776 /** 777 * @copy IEditManager.delayUpdates 778 */ 779 public function get delayUpdates():Boolean 780 { 781 return _delayUpdates; 782 } 783 public function set delayUpdates(value:Boolean):void 784 { 785 _delayUpdates = value; 786 } 787 788 private function redrawHandler(e:Event):void 789 { 790 // This is here because it has to take an argument 791 updateAllControllers(); 792 } 793 794 /** @copy IEditManager.updateAllControllers 795 */ 796 public function updateAllControllers():void 797 { 798 flushPendingOperations(); 799 800 if (redrawListener) // detach handler if there is one 801 { 802 redrawListener.removeEventListener(Event.ENTER_FRAME, redrawHandler); 803 redrawListener = null; 804 } 805 806 if (textFlow.flowComposer) 807 { 808 textFlow.flowComposer.updateAllControllers(); 809 810 // Scroll to selection 811 if (hasSelection()) 812 { 813 var controllerIndex:int = textFlow.flowComposer.findControllerIndexAtPosition(activePosition); 814 if (controllerIndex >= 0) 815 textFlow.flowComposer.getControllerAt(controllerIndex).scrollToRange(activePosition,anchorPosition); 816 } 817 } 818 819 selectionChanged(true, false); 820 821 CONFIG::debug { debugCheckTextFlow(); } 822 } 823 824 // By default, the EditManager will update in response to a model change immediately. 825 // Client may also request a delayed update; in this case, we schedule an update on the 826 // next enter frame event. 827 private function handleUpdate():void 828 { 829 if (_delayUpdates) 830 { 831 if (!redrawListener) // only need to attach if we're not already 832 { 833 var controller:ContainerController = textFlow.flowComposer.getControllerAt(0); 834 if (controller) 835 { 836 redrawListener = controller.container; 837 if (redrawListener) 838 redrawListener.addEventListener(Event.ENTER_FRAME, redrawHandler, false, 1.0, true); 839 } 840 } 841 } 842 else // redraw now 843 { 844 updateAllControllers(); 845 } 846 } 847 848 /** @copy IEditManager#allowDelayedOperations() */ 849 public function get allowDelayedOperations():Boolean 850 { 851 return _allowDelayedOperations; 852 } 853 public function set allowDelayedOperations(value:Boolean):void 854 { 855 if (!value) 856 flushPendingOperations(); 857 _allowDelayedOperations = value; 858 } 859 860 /** @private */ 861 public override function flushPendingOperations():void 862 { 863 super.flushPendingOperations(); 864 if (pendingInsert) 865 { 866 var pi0:InsertTextOperation = pendingInsert; 867 pendingInsert = null; 868 if (enterFrameListener) 869 { 870 enterFrameListener.removeEventListener(Event.ENTER_FRAME, enterFrameHandler); 871 enterFrameListener = null; 872 } 873 doOperation(pi0); 874 } 875 } 876 877 /** 878 * @copy IEditManager#undo() 879 * @includeExample examples\EditManager_undo.as -noswf 880 * 881 * @see flashx.undo.IUndoManager#undo() 882 * 883 * @playerversion Flash 10 884 * @playerversion AIR 1.5 885 * @langversion 3.0 886 */ 887 public function undo():void 888 { 889 // Cancel out of an IME session if there is one. 890 // Some IMEs are on all the time, and so the undo has to win over the IME, 891 // otherwise you would never be able to undo in Korean. 892 if (_imeSession) 893 _imeSession.compositionAbandoned(); 894 895 if (undoManager) 896 undoManager.undo(); 897 } 898 899 /** @private */ 900 public function performUndo(theop:IOperation):void 901 { 902 var operation:FlowOperation = theop as FlowOperation; 903 if ((!operation) || (operation.textFlow != textFlow)) 904 return; 905 // tell any listeners about the operation 906 if (!_imeSession) 907 { 908 var undoPsuedoOp:UndoOperation = new UndoOperation(operation); 909 var opEvent:FlowOperationEvent = new FlowOperationEvent(FlowOperationEvent.FLOW_OPERATION_BEGIN,false,true,undoPsuedoOp,0,null); 910 textFlow.dispatchEvent(opEvent); 911 if (opEvent.isDefaultPrevented()) 912 { 913 //operation cancelled by user. Push the operation back onto the undo stack 914 undoManager.pushUndo(operation); 915 return; 916 } 917 undoPsuedoOp = opEvent.operation as UndoOperation; 918 if (!undoPsuedoOp) 919 throw new IllegalOperationError(GlobalSettings.resourceStringFunction("illegalOperation",[ getQualifiedClassName(opEvent.operation) ])); 920 operation = undoPsuedoOp.operation; 921 } 922 923 if (operation.endGeneration != textFlow.generation) 924 { 925 //CONFIG::debug { trace("EditManager.undo: skipping undo due to mismatched generation numbers. textFlow",textFlow.generation,flash.utils.getQualifiedClassName(operation),operation.endGeneration); } 926 return; 927 } 928 929 var opError:Error = null; 930 try 931 { 932 CONFIG::debug { debugCheckTextFlow(); } 933 934 var rslt:SelectionState; 935 rslt = operation.undo(); 936 937 CONFIG::debug { assert(rslt != null,"undoable operations must return a SelectionState"); } 938 setSelectionState(rslt); 939 if (_undoManager) 940 _undoManager.pushRedo(operation); 941 942 } 943 catch(e:Error) 944 { 945 opError = e; 946 } 947 948 // tell user its complete and give them a chance to cancel the rethrow 949 if (!_imeSession) 950 { 951 opEvent = new FlowOperationEvent(FlowOperationEvent.FLOW_OPERATION_END,false,true,undoPsuedoOp,0,opError); 952 textFlow.dispatchEvent(opEvent); 953 opError = opEvent.isDefaultPrevented() ? null : opEvent.error; 954 } 955 956 if (opError) 957 throw (opError); 958 959 handleUpdate(); 960 961 // push the generation of the textFlow backwards - must be done after update which does a normalize 962 textFlow.setGeneration(operation.beginGeneration); 963 964 if (!_imeSession) 965 { 966 opEvent = new FlowOperationEvent(FlowOperationEvent.FLOW_OPERATION_COMPLETE,false,false,undoPsuedoOp,0,null); 967 textFlow.dispatchEvent(opEvent); 968 } 969 } 970 971 /** 972 * @copy IEditManager#redo() 973 * @includeExample examples\EditManager_redo.as -noswf 974 * 975 * @see flashx.undo.IUndoManager#redo() 976 * 977 * @playerversion Flash 10 978 * @playerversion AIR 1.5 979 * @langversion 3.0 980 */ 981 public function redo():void 982 { 983 // Cancel out of an IME session if there is one. 984 // Some IMEs are on all the time, and so the undo has to win over the IME, 985 // otherwise you would never be able to undo in Korean. 986 if (_imeSession) 987 _imeSession.compositionAbandoned(); 988 989 if (undoManager) 990 undoManager.redo(); 991 } 992 993 /** @private */ 994 public function performRedo(theop:IOperation):void 995 { 996 var opEvent:FlowOperationEvent; 997 var op:FlowOperation = theop as FlowOperation; 998 if ((!op) || (op.textFlow != textFlow)) 999 return; 1000 // tell any listeners about the operation 1001 if (!_imeSession) 1002 { 1003 var redoPsuedoOp:RedoOperation = new RedoOperation(op); 1004 opEvent = new FlowOperationEvent(FlowOperationEvent.FLOW_OPERATION_BEGIN,false,true,redoPsuedoOp,0,null); 1005 textFlow.dispatchEvent(opEvent); 1006 if (opEvent.isDefaultPrevented() && _undoManager) 1007 { 1008 //user cancelled the event. Push the operation back onto the redo stack 1009 _undoManager.pushRedo(op); 1010 return; 1011 } 1012 redoPsuedoOp = opEvent.operation as RedoOperation; 1013 if (!redoPsuedoOp) 1014 throw new IllegalOperationError(GlobalSettings.resourceStringFunction("illegalOperation",[ getQualifiedClassName(opEvent.operation) ])); 1015 op = redoPsuedoOp.operation; 1016 } 1017 1018 if (op.beginGeneration != textFlow.generation) 1019 { 1020 //CONFIG::debug { trace("EditManager.redo: skipping redo due to mismatched generation numbers."); } 1021 return; 1022 } 1023 1024 var opError:Error = null; 1025 try 1026 { 1027 CONFIG::debug { debugCheckTextFlow(); } 1028 var rslt:SelectionState; 1029 rslt = op.redo(); 1030 1031 CONFIG::debug { assert(rslt != null,"redoable operations must return a SelectionState"); } 1032 setSelectionState(rslt); 1033 if (_undoManager) 1034 _undoManager.pushUndo(op); 1035 1036 1037 } 1038 catch(e:Error) 1039 { 1040 opError = e; 1041 } 1042 1043 // tell user its complete and give them a chance to cancel the rethrow 1044 if (!_imeSession) 1045 { 1046 opEvent = new FlowOperationEvent(FlowOperationEvent.FLOW_OPERATION_END,false,true,redoPsuedoOp,0,opError); 1047 textFlow.dispatchEvent(opEvent); 1048 opError = opEvent.isDefaultPrevented() ? null : opEvent.error; 1049 } 1050 if (opError) 1051 throw (opError); 1052 1053 handleUpdate(); 1054 1055 // push the generation of the textFlow backwards - must be done after update which does a normalize 1056 // set the generation of the textFlow to end of redoOp. 1057 textFlow.setGeneration(op.endGeneration); 1058 1059 if (hasSelection()) 1060 { 1061 var controllerIndex:int = textFlow.flowComposer.findControllerIndexAtPosition(activePosition); 1062 if (controllerIndex >= 0) 1063 textFlow.flowComposer.getControllerAt(controllerIndex).scrollToRange(activePosition,anchorPosition); 1064 } 1065 if (!_imeSession) 1066 { 1067 opEvent = new FlowOperationEvent(FlowOperationEvent.FLOW_OPERATION_COMPLETE,false,false,redoPsuedoOp,0,null); 1068 textFlow.dispatchEvent(opEvent); 1069 } 1070 } 1071 1072 /** 1073 * @private 1074 * Returns the editing mode (READ_ONLY, READ_SELECT, or READ_WRITE) of the EditManager. 1075 * @playerversion Flash 10 1076 * @playerversion AIR 1.5 1077 * @langversion 3.0 1078 * @see flashx.textLayout.edit.EditingMode. 1079 */ 1080 public override function get editingMode():String 1081 { 1082 return EditingMode.READ_WRITE; 1083 } 1084 1085 // Resolve the operationState. 1086 // If the operation state is null... 1087 // Return the active selection 1088 // If there's no active selection, return null. The caller will have to check 1089 // Otherwise (operation not null) 1090 // just return it 1091 /** @private */ 1092 tlf_internal function defaultOperationState(operationState:SelectionState = null):SelectionState 1093 { 1094 if (operationState) 1095 { 1096 // flush any pending operations and use marks to preserve the operationState positions 1097 var markActive:Mark = createMark(); 1098 var markAnchor:Mark = createMark(); 1099 try 1100 { 1101 markActive.position = operationState.activePosition; 1102 markAnchor.position = operationState.anchorPosition; 1103 flushPendingOperations(); 1104 } 1105 finally 1106 { 1107 removeMark(markActive); 1108 removeMark(markAnchor); 1109 operationState.activePosition = markActive.position; 1110 operationState.anchorPosition = markAnchor.position; 1111 } 1112 } 1113 else 1114 { 1115 flushPendingOperations(); 1116 if (hasSelection()) 1117 { 1118 // tell the operation that the state is from the SelectionManager so it will update pending point formats 1119 operationState = getSelectionState(); 1120 operationState.selectionManagerOperationState = true; 1121 } 1122 } 1123 return operationState; 1124 } 1125 1126 /** 1127 * @copy IEditManager#splitParagraph() 1128 * @includeExample examples\EditManager_splitParagraph.as -noswf 1129 * 1130 * @playerversion Flash 10 1131 * @playerversion AIR 1.5 1132 * @langversion 3.0 1133 */ 1134 public function splitParagraph(operationState:SelectionState = null):ParagraphElement 1135 { 1136 operationState = defaultOperationState(operationState); 1137 if (!operationState) 1138 return null; 1139 1140 var op:SplitElementOperation = new SplitParagraphOperation(operationState); 1141 doOperation(op); 1142 return op.newElement as ParagraphElement; 1143 } 1144 1145 1146 /** @copy IEditManager#splitElement() */ 1147 public function splitElement(target:FlowGroupElement, operationState:SelectionState = null):FlowGroupElement 1148 { 1149 operationState = defaultOperationState(operationState); 1150 if (!operationState) 1151 return null; 1152 var op:SplitElementOperation = new SplitElementOperation(operationState, target); 1153 doOperation(op); 1154 return op.newElement; 1155 } 1156 1157 /** @copy IEditManager#createDiv() */ 1158 public function createDiv(parent:FlowGroupElement = null, format:ITextLayoutFormat = null, operationState:SelectionState = null):DivElement 1159 { 1160 operationState = defaultOperationState(operationState); 1161 if (!operationState) 1162 return null; 1163 1164 var operation:CreateDivOperation = new CreateDivOperation(operationState, parent, format); 1165 doOperation(operation); 1166 return operation.newDivElement; 1167 } 1168 1169 /** @copy IEditManager#createList() */ 1170 public function createList(parent:FlowGroupElement = null, format:ITextLayoutFormat = null, operationState:SelectionState = null):ListElement 1171 { 1172 operationState = defaultOperationState(operationState); 1173 if (!operationState) 1174 return null; 1175 1176 var operation:CreateListOperation = new CreateListOperation(operationState, parent, format); 1177 doOperation(operation); 1178 return operation.newListElement; 1179 } 1180 1181 /** @copy IEditManager#moveChildren() */ 1182 public function moveChildren(source:FlowGroupElement, sourceIndex:int, numChildren:int, destination:FlowGroupElement, destinationIndex:int, selectionState:SelectionState = null):void 1183 { 1184 selectionState = defaultOperationState(selectionState); 1185 if (!selectionState) 1186 return; 1187 1188 var operation:MoveChildrenOperation = new MoveChildrenOperation(selectionState, source, sourceIndex, numChildren, destination, destinationIndex); 1189 doOperation(operation); 1190 } 1191 1192 /** @copy IEditManager#createSubParagraphGroup() */ 1193 public function createSubParagraphGroup(parent:FlowGroupElement = null, format:ITextLayoutFormat = null, operationState:SelectionState = null):SubParagraphGroupElement 1194 { 1195 operationState = defaultOperationState(operationState); 1196 if (!operationState) 1197 return null; 1198 1199 var operation:CreateSubParagraphGroupOperation = new CreateSubParagraphGroupOperation(operationState, parent, format); 1200 doOperation(operation); 1201 return operation.newSubParagraphGroupElement; 1202 } 1203 1204 /** 1205 * @copy IEditManager#deleteText() 1206 * @includeExample examples\EditManager_deleteText.as -noswf 1207 * 1208 * @playerversion Flash 10 1209 * @playerversion AIR 1.5 1210 * @langversion 3.0 1211 */ 1212 public function deleteText(operationState:SelectionState = null):void 1213 { 1214 1215 operationState = defaultOperationState(operationState); 1216 if (!operationState) 1217 return; 1218 1219 doOperation(new DeleteTextOperation(operationState, operationState, false /* don't allow merge when deleting by range */)); 1220 } 1221 1222 /** 1223 * @copy IEditManager#deleteNextCharacter() 1224 * @includeExample examples\EditManager_deleteNextCharacter.as -noswf 1225 * 1226 * @playerversion Flash 10 1227 * @playerversion AIR 1.5 1228 * @langversion 3.0 1229 */ 1230 public function deleteNextCharacter(operationState:SelectionState = null):void 1231 { 1232 operationState = defaultOperationState(operationState); 1233 if (!operationState) 1234 return; 1235 1236 // Delete the next character if it's a caret selection, and allow adejacent delete next's to merge 1237 // If it's a range selection, delete the range and disallow merge 1238 var deleteOp:DeleteTextOperation; 1239 if (operationState.absoluteStart == operationState.absoluteEnd) 1240 { 1241 var nextPosition:int = NavigationUtil.nextAtomPosition(textFlow, absoluteStart); 1242 deleteOp = new DeleteTextOperation(operationState, new SelectionState(textFlow, absoluteStart, nextPosition, pointFormat), true /* allowMerge for deleteForward */); 1243 } 1244 else 1245 deleteOp = new DeleteTextOperation(operationState, operationState, false /* don't allow merge when deleting by range */); 1246 doOperation(deleteOp); 1247 1248 } 1249 1250 /** 1251 * @copy IEditManager#deleteNextWord() 1252 * @includeExample examples\EditManager_deleteNextWord.as -noswf 1253 * 1254 * @playerversion Flash 10 1255 * @playerversion AIR 1.5 1256 * @langversion 3.0 1257 */ 1258 public function deleteNextWord(operationState:SelectionState = null):void 1259 { 1260 operationState = defaultOperationState(operationState); 1261 if ((!operationState) || ((operationState.anchorPosition == operationState.activePosition) && (operationState.anchorPosition >= textFlow.textLength - 1))) 1262 return; 1263 1264 var nextWordSelState:SelectionState = getNextWordForDelete(operationState.absoluteStart); 1265 if (nextWordSelState.anchorPosition == nextWordSelState.activePosition) 1266 //nothing to delete. No operation required. 1267 return; 1268 1269 setSelectionState(new SelectionState(textFlow, operationState.absoluteStart, operationState.absoluteStart, new TextLayoutFormat(textFlow.findLeaf(operationState.absoluteStart).format))); 1270 doOperation(new DeleteTextOperation(operationState, nextWordSelState, false)); 1271 } 1272 1273 // Sadly, this is NOT the same as the cursor key movement - specialized for delete forward one word 1274 private function getNextWordForDelete(absoluteStart:int):SelectionState 1275 { 1276 var leafEl:FlowLeafElement = textFlow.findLeaf(absoluteStart); 1277 var paraEl:ParagraphElement = leafEl.getParagraph(); 1278 var paraElAbsStart:int = paraEl.getAbsoluteStart(); 1279 1280 var nextPosition:int = -1; 1281 1282 if ((absoluteStart - paraElAbsStart) >= (paraEl.textLength - 1)) 1283 { 1284 // We're at the end of the paragraph, delete the following newline 1285 nextPosition = NavigationUtil.nextAtomPosition(textFlow, absoluteStart); 1286 } 1287 else 1288 { 1289 var curPos:int = absoluteStart - paraElAbsStart; 1290 var curPosCharCode:int = paraEl.getCharCodeAtPosition(curPos); 1291 var prevPosCharCode:int = -1; 1292 if (curPos > 0) prevPosCharCode = paraEl.getCharCodeAtPosition(curPos - 1); 1293 var nextPosCharCode:int = paraEl.getCharCodeAtPosition(curPos + 1); 1294 if (!CharacterUtil.isWhitespace(curPosCharCode) && ((curPos == 0) || ((curPos > 0) && CharacterUtil.isWhitespace(prevPosCharCode)))) { 1295 nextPosition = NavigationUtil.nextWordPosition(textFlow, absoluteStart); 1296 } else { 1297 if (CharacterUtil.isWhitespace(curPosCharCode) && ((curPos > 0) && !CharacterUtil.isWhitespace(prevPosCharCode))) { 1298 //if at beginning of space word then get through all the spaces 1299 curPos = paraEl.findNextWordBoundary(curPos); 1300 } 1301 nextPosition = paraElAbsStart + paraEl.findNextWordBoundary(curPos); 1302 } 1303 } 1304 return new SelectionState(textFlow, absoluteStart, nextPosition, pointFormat); 1305 } 1306 1307 /** 1308 * @copy IEditManager#deletePreviousCharacter() 1309 * @includeExample examples\EditManager_deletePreviousCharacter.as -noswf 1310 * 1311 * @playerversion Flash 10 1312 * @playerversion AIR 1.5 1313 * @langversion 3.0 1314 */ 1315 public function deletePreviousCharacter(operationState:SelectionState = null):void 1316 { 1317 operationState = defaultOperationState(operationState); 1318 if (!operationState) 1319 return; 1320 1321 var deleteOp:DeleteTextOperation; 1322 if (operationState.absoluteStart == operationState.absoluteEnd) 1323 { 1324 // with a caret selection, generally delete the previous character, but also check whether to move the paragraph out of its parent chain (like backspacing at the beginning of a list) 1325 var leaf:FlowLeafElement = textFlow.findLeaf(operationState.absoluteStart); 1326 var para:ParagraphElement = leaf.getParagraph(); 1327 var parent:FlowGroupElement = para.parent; 1328 1329 var movePara:Boolean = false; 1330 if(!(parent is TextFlow)) 1331 { 1332 if(operationState.absoluteStart == para.getAbsoluteStart() && parent.getChildIndex(para) == 0 && // cursor is at start of this paragraph AND para is at beginning of parent AND 1333 (!(parent is ListItemElement) || parent.parent.getChildIndex(parent) == 0)) // if parent is a listItem, it's the first item in the list 1334 { 1335 movePara = true; 1336 } 1337 } 1338 if(movePara) 1339 { 1340 var source:FlowGroupElement; 1341 var target:FlowGroupElement; 1342 var numElementsToMove:int; 1343 var targetIndex:int; 1344 1345 if(parent is ListItemElement) 1346 { 1347 if(parent.parent.parent is ListElement) 1348 { 1349 // move the whole list item to grandparent list 1350 source = parent.parent; 1351 numElementsToMove = 1; 1352 target = parent.parent.parent; 1353 targetIndex = parent.parent.parent.getChildIndex(parent.parent); 1354 } 1355 else 1356 { 1357 // move everything inside the list item out into grandparent 1358 source = para.parent; 1359 numElementsToMove = para.parent.numChildren; 1360 target = parent.parent.parent; 1361 targetIndex = parent.parent.parent.getChildIndex(parent.parent); 1362 } 1363 } 1364 else 1365 { 1366 // move just the first paragraph out into grandparent 1367 source = para.parent; 1368 numElementsToMove = 1; 1369 target = parent.parent; 1370 targetIndex = parent.parent.getChildIndex(parent); 1371 } 1372 doOperation(new MoveChildrenOperation(operationState, source, 0, numElementsToMove, target, targetIndex)); 1373 } 1374 else 1375 { 1376 var beginPrevious:int = NavigationUtil.previousAtomPosition(textFlow, operationState.absoluteStart); 1377 deleteOp = new DeleteTextOperation(operationState, new SelectionState(textFlow, beginPrevious, operationState.absoluteStart), true /* allowMerge */); 1378 doOperation(deleteOp); 1379 } 1380 } 1381 else // just delete the range 1382 { 1383 deleteOp = new DeleteTextOperation(operationState); 1384 doOperation(deleteOp); 1385 } 1386 } 1387 1388 /** 1389 * @copy IEditManager#deletePreviousWord() 1390 * @includeExample examples\EditManager_deletePreviousWord.as -noswf 1391 * 1392 * @playerversion Flash 10 1393 * @playerversion AIR 1.5 1394 * @langversion 3.0 1395 */ 1396 public function deletePreviousWord(operationState:SelectionState = null):void 1397 { 1398 operationState = defaultOperationState(operationState); 1399 if (!operationState) 1400 return; 1401 1402 var prevWordSelState:SelectionState = getPreviousWordForDelete(operationState.absoluteStart); 1403 if (prevWordSelState.anchorPosition == prevWordSelState.activePosition) 1404 //there is nothing to delete. No operation required 1405 return; 1406 1407 setSelectionState(new SelectionState(textFlow, operationState.absoluteStart, operationState.absoluteStart, new TextLayoutFormat(textFlow.findLeaf(operationState.absoluteStart).format))); 1408 doOperation(new DeleteTextOperation(operationState, prevWordSelState, false /* don't allow merge */)); 1409 } 1410 1411 // Sadly, this is NOT the same as the cursor key movement - specialized for delete backward one word 1412 private function getPreviousWordForDelete(absoluteStart:int):SelectionState 1413 { 1414 var leafEl:FlowLeafElement = textFlow.findLeaf(absoluteStart); 1415 var paraEl:ParagraphElement = leafEl.getParagraph(); 1416 var paraElAbsStart:int = paraEl.getAbsoluteStart(); 1417 1418 if (absoluteStart == paraElAbsStart) // at the start of the paragraph, delete the previous newline. Should insert a space after punctuation. 1419 { 1420 var beginPrevious:int = NavigationUtil.previousAtomPosition(textFlow, absoluteStart); 1421 return new SelectionState(textFlow, beginPrevious, absoluteStart); 1422 } 1423 1424 var curPos:int = absoluteStart - paraElAbsStart; 1425 var curPosCharCode:int = paraEl.getCharCodeAtPosition(curPos); 1426 var prevPosCharCode:int = paraEl.getCharCodeAtPosition(curPos - 1); 1427 var curAbsStart:int = absoluteStart; 1428 1429 if (CharacterUtil.isWhitespace(curPosCharCode) && (curPos != (paraEl.textLength - 1))) 1430 { 1431 if (CharacterUtil.isWhitespace(prevPosCharCode)) //this will get you past the spaces 1432 { 1433 curPos = paraEl.findPreviousWordBoundary(curPos); 1434 } 1435 if (curPos > 0) { 1436 curPos = paraEl.findPreviousWordBoundary(curPos); //this will get you to the beginning of the word before the space. 1437 if (curPos > 0) { 1438 prevPosCharCode = paraEl.getCharCodeAtPosition(curPos - 1); 1439 if (CharacterUtil.isWhitespace(prevPosCharCode)) { 1440 curPos = paraEl.findPreviousWordBoundary(curPos); 1441 } 1442 } 1443 } 1444 } else { //you are here if you are not on a space 1445 if (CharacterUtil.isWhitespace(prevPosCharCode)) 1446 { 1447 curPos = paraEl.findPreviousWordBoundary(curPos); //this will get you past the spaces 1448 if (curPos > 0) { 1449 curPos = paraEl.findPreviousWordBoundary(curPos); 1450 if (curPos > 0) { 1451 prevPosCharCode = paraEl.getCharCodeAtPosition(curPos - 1); 1452 if (!CharacterUtil.isWhitespace(prevPosCharCode)) { 1453 curAbsStart--; //Microsoft Word insists on keeping the original space 1454 //if the ending position does not have a space. 1455 } 1456 } 1457 } 1458 } else { //just delete to the previous word boundary 1459 curPos = paraEl.findPreviousWordBoundary(curPos); 1460 } 1461 } 1462 return new SelectionState(textFlow, paraElAbsStart + curPos, curAbsStart); 1463 } 1464 1465 /** 1466 * @copy IEditManager#insertText() 1467 * @includeExample examples\EditManager_insertText.as -noswf 1468 * 1469 * @playerversion Flash 10 1470 * @playerversion AIR 1.5 1471 * @langversion 3.0 1472 */ 1473 public function insertText(text:String, origOperationState:SelectionState = null):void 1474 { 1475 // if there's another insert operation waiting to be executed, 1476 // just add to it, if possible 1477 if (origOperationState == null && pendingInsert) 1478 pendingInsert.text += text; 1479 else 1480 { 1481 var operationState:SelectionState = defaultOperationState(origOperationState); 1482 if (!operationState) 1483 return; 1484 1485 // rather than execute the insert immediately, create 1486 // it and wait for the next frame, in order to batch 1487 // keystrokes. 1488 pendingInsert = new InsertTextOperation(operationState, text); 1489 1490 var controller:ContainerController = textFlow.flowComposer.getControllerAt(0); 1491 if (captureLevel == 0 && origOperationState == null && controller && controller.container && allowDelayedOperations) 1492 { 1493 enterFrameListener = controller.container; 1494 enterFrameListener.addEventListener(Event.ENTER_FRAME, enterFrameHandler, false, 1.0, true); 1495 } 1496 else 1497 flushPendingOperations(); 1498 } 1499 } 1500 1501 1502 1503 /** 1504 * @copy IEditManager#overwriteText() 1505 * 1506 * @includeExample examples\EditManager_overwriteText.as -noswf 1507 * 1508 * @playerversion Flash 10 1509 * @playerversion AIR 1.5 1510 * @langversion 3.0 1511 */ 1512 public function overwriteText(text:String, operationState:SelectionState = null):void 1513 { 1514 operationState = defaultOperationState(operationState); 1515 if (!operationState) 1516 return; 1517 var selState:SelectionState = getSelectionState(); 1518 NavigationUtil.nextCharacter(selState,true); 1519 doOperation(new InsertTextOperation(operationState, text, selState)); 1520 } 1521 1522 /** 1523 * @copy IEditManager#insertInlineGraphic() 1524 * Returns the new InlineGraphicElement that was created. 1525 * @includeExample examples\EditManager_insertInlineGraphic.as -noswf 1526 * 1527 * @playerversion Flash 10 + 10.2 1528 * @playerversion AIR 1.5 1529 * @langversion 3.0 1530 * @see flash.text.engine.TextRotation 1531 */ 1532 public function insertInlineGraphic(source:Object, width:Object, height:Object, options:Object = null, operationState:SelectionState = null):InlineGraphicElement 1533 { 1534 operationState = defaultOperationState(operationState); 1535 if (!operationState) 1536 return null; 1537 1538 var operation:InsertInlineGraphicOperation = new InsertInlineGraphicOperation(operationState, source, width, height, options); 1539 doOperation(operation); 1540 return operation.newInlineGraphicElement; 1541 } 1542 1543 /** 1544 * @copy IEditManager#modifyInlineGraphic() 1545 * @includeExample examples\EditManager_modifyInlineGraphic.as -noswf 1546 * 1547 * @playerversion Flash 10 1548 * @playerversion AIR 1.5 1549 * @langversion 3.0 1550 */ 1551 public function modifyInlineGraphic(source:Object, width:Object, height:Object, options:Object = null, operationState:SelectionState = null):void 1552 { 1553 operationState = defaultOperationState(operationState); 1554 if (!operationState) 1555 return; 1556 1557 doOperation(new ModifyInlineGraphicOperation(operationState, source, width, height, options)); 1558 } 1559 1560 /** 1561 * @copy IEditManager#applyFormat() 1562 * 1563 * @includeExample examples\EditManager_applyFormat.as -noswf 1564 * @playerversion Flash 10 1565 * @playerversion AIR 1.5 1566 * @langversion 3.0 1567 */ 1568 public function applyFormat(leafFormat:ITextLayoutFormat, paragraphFormat:ITextLayoutFormat, containerFormat:ITextLayoutFormat, operationState:SelectionState = null):void 1569 { 1570 operationState = defaultOperationState(operationState); 1571 if (!operationState) 1572 return; 1573 1574 // apply to the current selection else remember new format for next char typed 1575 doOperation(new ApplyFormatOperation(operationState, leafFormat, paragraphFormat, containerFormat)); 1576 } 1577 /** 1578 * @copy IEditManager#clearFormat() 1579 * 1580 * Known issue is that undefines of leafFormat values with a point selection are not applied at the next insertion. 1581 * 1582 * @playerversion Flash 10 1583 * @playerversion AIR 1.5 1584 * @langversion 3.0 1585 */ 1586 public function clearFormat(leafFormat:ITextLayoutFormat, paragraphFormat:ITextLayoutFormat, containerFormat:ITextLayoutFormat, operationState:SelectionState = null):void 1587 { 1588 operationState = defaultOperationState(operationState); 1589 if (!operationState) 1590 return; 1591 1592 // apply to the current selection else remember new format for next char typed 1593 doOperation(new ClearFormatOperation(operationState, leafFormat, paragraphFormat, containerFormat)); 1594 } 1595 /** 1596 * @copy IEditManager#applyLeafFormat() 1597 * 1598 * @includeExample examples\EditManager_applyLeafFormat.as -noswf 1599 * 1600 * @playerversion Flash 10 1601 * @playerversion AIR 1.5 1602 * @langversion 3.0 1603 */ 1604 public function applyLeafFormat(characterFormat:ITextLayoutFormat, operationState:SelectionState = null):void 1605 { 1606 applyFormat(characterFormat, null, null, operationState); 1607 } 1608 1609 /** 1610 * @copy IEditManager#applyParagraphFormat() 1611 * 1612 * @includeExample examples\EditManager_applyParagraphFormat.as -noswf 1613 * 1614 * @playerversion Flash 10 1615 * @playerversion AIR 1.5 1616 * @langversion 3.0 1617 */ 1618 public function applyParagraphFormat(paragraphFormat:ITextLayoutFormat, operationState:SelectionState = null):void 1619 { 1620 applyFormat(null, paragraphFormat, null, operationState); 1621 } 1622 1623 /** 1624 * @copy IEditManager#applyContainerFormat() 1625 * 1626 * @includeExample examples\EditManager_applyContainerFormat.as -noswf 1627 * 1628 * @playerversion Flash 10 1629 * @playerversion AIR 1.5 1630 * @langversion 3.0 1631 */ 1632 public function applyContainerFormat(containerFormat:ITextLayoutFormat, operationState:SelectionState = null):void 1633 { 1634 applyFormat(null, null, containerFormat, operationState); 1635 } 1636 1637 /** 1638 * @copy IEditManager#applyFormatToElement() 1639 * 1640 * @playerversion Flash 10 1641 * @playerversion AIR 1.5 1642 * @langversion 3.0 1643 */ 1644 public function applyFormatToElement(targetElement:FlowElement, format:ITextLayoutFormat, relativeStart:int = 0, relativeEnd:int = -1, operationState:SelectionState = null):void 1645 { 1646 operationState = defaultOperationState(operationState); 1647 if (!operationState) 1648 return; 1649 1650 doOperation(new ApplyFormatToElementOperation(operationState, targetElement, format, relativeStart, relativeEnd)); 1651 } 1652 1653 /** 1654 * @copy IEditManager#clearFormatOnElement() 1655 * 1656 * @playerversion Flash 10 1657 * @playerversion AIR 1.5 1658 * @langversion 3.0 1659 */ 1660 public function clearFormatOnElement(targetElement:FlowElement, format:ITextLayoutFormat, operationState:SelectionState = null):void 1661 { 1662 operationState = defaultOperationState(operationState); 1663 if (!operationState) 1664 return; 1665 1666 doOperation(new ClearFormatOnElementOperation(operationState, targetElement, format)); 1667 } 1668 1669 /** 1670 * @copy IEditManager#cutTextScrap() 1671 * @includeExample examples\EditManager_cutTextScrap.as -noswf 1672 * 1673 * @playerversion Flash 10 1674 * @playerversion AIR 1.5 1675 * @langversion 3.0 1676 * 1677 * @see flashx.textLayout.edit.TextScrap 1678 */ 1679 public function cutTextScrap(operationState:SelectionState = null):TextScrap 1680 { 1681 operationState = defaultOperationState(operationState); 1682 if (!operationState) 1683 return null; 1684 1685 if (operationState.anchorPosition == operationState.activePosition) 1686 return null; 1687 1688 var tScrap:TextScrap = TextScrap.createTextScrap(operationState); 1689 var beforeOpLen:int = textFlow.textLength; 1690 doOperation(new CutOperation(operationState, tScrap)); 1691 if (operationState.textFlow.textLength != beforeOpLen) 1692 { 1693 return tScrap; 1694 } 1695 return null; 1696 } 1697 1698 /** 1699 * @copy IEditManager#pasteTextScrap() 1700 * @includeExample examples\EditManager_pasteTextScrap.as -noswf 1701 * 1702 * @playerversion Flash 10 1703 * @playerversion AIR 1.5 1704 * @langversion 3.0 1705 * 1706 * @see flashx.textLayout.edit.TextScrap 1707 */ 1708 public function pasteTextScrap(scrapToPaste:TextScrap, operationState:SelectionState = null):void 1709 { 1710 operationState = defaultOperationState(operationState); 1711 if (!operationState) 1712 return; 1713 1714 doOperation(new PasteOperation(operationState, scrapToPaste)); 1715 } 1716 1717 /** 1718 * @copy IEditManager#applyTCY() 1719 * Returns the new TCYElement that was created. 1720 * @includeExample examples\EditManager_applyTCY.as -noswf 1721 * 1722 * @playerversion Flash 10 + 10.2 1723 * @playerversion AIR 1.5 1724 * @langversion 3.0 1725 */ 1726 public function applyTCY(tcyOn:Boolean, operationState:SelectionState = null):TCYElement 1727 { 1728 operationState = defaultOperationState(operationState); 1729 if (!operationState) 1730 return null; 1731 1732 var operation:ApplyTCYOperation = new ApplyTCYOperation(operationState, tcyOn); 1733 doOperation(operation); 1734 return operation.newTCYElement; 1735 } 1736 1737 /** 1738 * @copy IEditManager#applyLink() 1739 * Returns the new LinkElement that was created. 1740 * @includeExample examples\EditManager_applyLink.as -noswf 1741 * 1742 * @playerversion Flash 10 + 10.2 1743 * @playerversion AIR 1.5 1744 * @langversion 3.0 1745 */ 1746 public function applyLink(href:String, targetString:String = null, extendToLinkBoundary:Boolean=false, operationState:SelectionState = null):LinkElement 1747 { 1748 operationState = defaultOperationState(operationState); 1749 if (!operationState) 1750 return null; 1751 1752 if (operationState.absoluteStart == operationState.absoluteEnd) 1753 return null; 1754 1755 var operation:ApplyLinkOperation = new ApplyLinkOperation(operationState, href, targetString, extendToLinkBoundary); 1756 doOperation(operation); 1757 return operation.newLinkElement; 1758 } 1759 1760 /** 1761 * @copy IEditManager#changeElementID() 1762 * @includeExample examples\EditManager_changeElementID.as -noswf 1763 * 1764 * @playerversion Flash 10 1765 * @playerversion AIR 1.5 1766 * @langversion 3.0 1767 */ 1768 public function changeElementID(newID:String, targetElement:FlowElement, relativeStart:int = 0, relativeEnd:int = -1, operationState:SelectionState = null):void 1769 { 1770 operationState = defaultOperationState(operationState); 1771 if (!operationState) 1772 return; 1773 1774 if (operationState.absoluteStart == operationState.absoluteEnd) 1775 return; 1776 1777 doOperation(new ApplyElementIDOperation(operationState, targetElement, newID, relativeStart, relativeEnd)); 1778 } 1779 1780 [Deprecated(replacement="applyFormatToElement", deprecatedSince="2.0")] 1781 /** 1782 * @copy IEditManager#changeStyleName() 1783 * @includeExample examples\EditManager_changeStyleName.as -noswf 1784 * 1785 * @playerversion Flash 10 1786 * @playerversion AIR 1.5 1787 * @langversion 3.0 1788 */ 1789 public function changeStyleName(newName:String, targetElement:FlowElement, relativeStart:int = 0, relativeEnd:int = -1, operationState:SelectionState = null):void 1790 { 1791 operationState = defaultOperationState(operationState); 1792 if (!operationState) 1793 return; 1794 1795 var format:TextLayoutFormat = new TextLayoutFormat(); 1796 format.styleName = newName; 1797 doOperation(new ApplyFormatToElementOperation(operationState, targetElement, format, relativeStart, relativeEnd)); 1798 } 1799 1800 /** 1801 * @copy IEditManager#changeTypeName() 1802 * 1803 * @playerversion Flash 10 1804 * @playerversion AIR 1.5 1805 * @langversion 3.0 1806 */ 1807 public function changeTypeName(newName:String, targetElement:FlowElement, relativeStart:int = 0, relativeEnd:int = -1, operationState:SelectionState = null):void 1808 { 1809 operationState = defaultOperationState(operationState); 1810 if (!operationState) 1811 return; 1812 1813 doOperation(new ApplyElementTypeNameOperation(operationState, targetElement, newName, relativeStart, relativeEnd)); 1814 } 1815 1816 /* CompositeOperations 1817 Normally when you call doOperation, it gets executed immediately. By calling beginCompositeOperation, you can instead accumulate the 1818 operations into a CompositeOperation. The CompositeOperation is completed and returned when you call endCompositeOperation, and 1819 processing returns to normal state. The client code can then either call doOperation on the CompositeOperation that was returned, 1820 or just drop it if the operation should be aborted. 1821 1822 The parentStack is a stack of pending CompositeOperations. 1823 */ 1824 private var parentStack:Array; 1825 1826 /** 1827 * @copy IEditManager#beginCompositeOperation() 1828 * 1829 * @playerversion Flash 10 1830 * @playerversion AIR 1.5 1831 * @langversion 3.0 1832 * 1833 * @includeExample examples\EditManager_beginCompositeOperation.as -noswf 1834 */ 1835 public function beginCompositeOperation():void 1836 { 1837 flushPendingOperations(); 1838 1839 if (!parentStack) 1840 parentStack = []; 1841 var operation:CompositeOperation = new CompositeOperation(); 1842 1843 if (!_imeSession) 1844 { 1845 var opEvent:FlowOperationEvent = new FlowOperationEvent(FlowOperationEvent.FLOW_OPERATION_BEGIN,false,false,operation,captureLevel,null); 1846 textFlow.dispatchEvent(opEvent); 1847 } 1848 1849 CONFIG::debug { assert(!operation.operations || operation.operations.length == 0, "opening a composite operation that already has operations"); } 1850 operation.setGenerations(textFlow.generation, 0); 1851 ++captureLevel; 1852 var parent:Object = new Object(); 1853 parent.operation = operation; 1854 parent.captureLevel = captureLevel; 1855 parentStack.push(parent); 1856 } 1857 1858 /** 1859 * @copy IEditManager#endCompositeOperation() 1860 * 1861 * @playerversion Flash 10 1862 * @playerversion AIR 1.5 1863 * @langversion 3.0 1864 * 1865 * @includeExample examples\EditManager_beginCompositeOperation.as -noswf 1866 */ 1867 public function endCompositeOperation():void 1868 { 1869 CONFIG::debug { assert( parentStack.length > 0 || captureLevel <= 0, "EditManager.endOperation - no composite operation in progress"); } 1870 1871 --captureLevel; 1872 1873 var parent:Object = parentStack.pop(); 1874 var operation:FlowOperation = parent.operation; 1875 if (!_imeSession) 1876 { 1877 var opEvent:FlowOperationEvent = new FlowOperationEvent(FlowOperationEvent.FLOW_OPERATION_END,false,false,operation,captureLevel,null); 1878 textFlow.dispatchEvent(opEvent); 1879 } 1880 operation.setGenerations(operation.beginGeneration, textFlow.generation); 1881 finalizeDo(operation); 1882 } 1883 1884 /** @private 1885 * Handler function called when the selection has been changed. 1886 * @playerversion Flash 10 1887 * @playerversion AIR 1.5 1888 * @langversion 3.0 1889 * @param doDispatchEvent true if a selection changed event will be sent 1890 * @param resetPointFormat true if the attributes associated with the caret should be discarded 1891 */ 1892 tlf_internal override function selectionChanged(doDispatchEvent:Boolean = true, resetPointFormat:Boolean=true):void 1893 { 1894 if (_imeSession) 1895 _imeSession.selectionChanged(); 1896 1897 super.selectionChanged(doDispatchEvent, resetPointFormat); 1898 } 1899 1900 1901 } 1902} 1903