| GNU Classpath (0.95) | |
| Frames | No Frames |
1: /* AbstractDocument.java -- 2: Copyright (C) 2002, 2004, 2005, 2006 Free Software Foundation, Inc. 3: 4: This file is part of GNU Classpath. 5: 6: GNU Classpath is free software; you can redistribute it and/or modify 7: it under the terms of the GNU General Public License as published by 8: the Free Software Foundation; either version 2, or (at your option) 9: any later version. 10: 11: GNU Classpath is distributed in the hope that it will be useful, but 12: WITHOUT ANY WARRANTY; without even the implied warranty of 13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14: General Public License for more details. 15: 16: You should have received a copy of the GNU General Public License 17: along with GNU Classpath; see the file COPYING. If not, write to the 18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 19: 02110-1301 USA. 20: 21: Linking this library statically or dynamically with other modules is 22: making a combined work based on this library. Thus, the terms and 23: conditions of the GNU General Public License cover the whole 24: combination. 25: 26: As a special exception, the copyright holders of this library give you 27: permission to link this library with independent modules to produce an 28: executable, regardless of the license terms of these independent 29: modules, and to copy and distribute the resulting executable under 30: terms of your choice, provided that you also meet, for each linked 31: independent module, the terms and conditions of the license of that 32: module. An independent module is a module which is not derived from 33: or based on this library. If you modify this library, you may extend 34: this exception to your version of the library, but you are not 35: obligated to do so. If you do not wish to do so, delete this 36: exception statement from your version. */ 37: 38: 39: package javax.swing.text; 40: 41: import java.awt.font.TextAttribute; 42: import java.io.PrintStream; 43: import java.io.Serializable; 44: import java.text.Bidi; 45: import java.util.ArrayList; 46: import java.util.Dictionary; 47: import java.util.Enumeration; 48: import java.util.EventListener; 49: import java.util.HashMap; 50: import java.util.Hashtable; 51: import java.util.Vector; 52: 53: import javax.swing.event.DocumentEvent; 54: import javax.swing.event.DocumentListener; 55: import javax.swing.event.EventListenerList; 56: import javax.swing.event.UndoableEditEvent; 57: import javax.swing.event.UndoableEditListener; 58: import javax.swing.text.DocumentFilter; 59: import javax.swing.tree.TreeNode; 60: import javax.swing.undo.AbstractUndoableEdit; 61: import javax.swing.undo.CompoundEdit; 62: import javax.swing.undo.UndoableEdit; 63: 64: /** 65: * An abstract base implementation for the {@link Document} interface. 66: * This class provides some common functionality for all <code>Element</code>s, 67: * most notably it implements a locking mechanism to make document modification 68: * thread-safe. 69: * 70: * @author original author unknown 71: * @author Roman Kennke (roman@kennke.org) 72: */ 73: public abstract class AbstractDocument implements Document, Serializable 74: { 75: /** The serialization UID (compatible with JDK1.5). */ 76: private static final long serialVersionUID = 6842927725919637215L; 77: 78: /** 79: * Standard error message to indicate a bad location. 80: */ 81: protected static final String BAD_LOCATION = "document location failure"; 82: 83: /** 84: * Standard name for unidirectional <code>Element</code>s. 85: */ 86: public static final String BidiElementName = "bidi level"; 87: 88: /** 89: * Standard name for content <code>Element</code>s. These are usually 90: * {@link LeafElement}s. 91: */ 92: public static final String ContentElementName = "content"; 93: 94: /** 95: * Standard name for paragraph <code>Element</code>s. These are usually 96: * {@link BranchElement}s. 97: */ 98: public static final String ParagraphElementName = "paragraph"; 99: 100: /** 101: * Standard name for section <code>Element</code>s. These are usually 102: * {@link DefaultStyledDocument.SectionElement}s. 103: */ 104: public static final String SectionElementName = "section"; 105: 106: /** 107: * Attribute key for storing the element name. 108: */ 109: public static final String ElementNameAttribute = "$ename"; 110: 111: /** 112: * Standard name for the bidi root element. 113: */ 114: private static final String BidiRootName = "bidi root"; 115: 116: /** 117: * Key for storing the asynchronous load priority. 118: */ 119: private static final String AsyncLoadPriority = "load priority"; 120: 121: /** 122: * Key for storing the I18N state. 123: */ 124: private static final String I18N = "i18n"; 125: 126: /** 127: * The actual content model of this <code>Document</code>. 128: */ 129: Content content; 130: 131: /** 132: * The AttributeContext for this <code>Document</code>. 133: */ 134: AttributeContext context; 135: 136: /** 137: * The currently installed <code>DocumentFilter</code>. 138: */ 139: DocumentFilter documentFilter; 140: 141: /** 142: * The documents properties. 143: */ 144: Dictionary properties; 145: 146: /** 147: * Manages event listeners for this <code>Document</code>. 148: */ 149: protected EventListenerList listenerList = new EventListenerList(); 150: 151: /** 152: * Stores the current writer thread. Used for locking. 153: */ 154: private Thread currentWriter = null; 155: 156: /** 157: * The number of readers. Used for locking. 158: */ 159: private int numReaders = 0; 160: 161: /** 162: * The number of current writers. If this is > 1 then the same thread entered 163: * the write lock more than once. 164: */ 165: private int numWriters = 0; 166: 167: /** An instance of a DocumentFilter.FilterBypass which allows calling 168: * the insert, remove and replace method without checking for an installed 169: * document filter. 170: */ 171: private DocumentFilter.FilterBypass bypass; 172: 173: /** 174: * The bidi root element. 175: */ 176: private BidiRootElement bidiRoot; 177: 178: /** 179: * True when we are currently notifying any listeners. This is used 180: * to detect illegal situations in writeLock(). 181: */ 182: private transient boolean notifyListeners; 183: 184: /** 185: * Creates a new <code>AbstractDocument</code> with the specified 186: * {@link Content} model. 187: * 188: * @param doc the <code>Content</code> model to be used in this 189: * <code>Document<code> 190: * 191: * @see GapContent 192: * @see StringContent 193: */ 194: protected AbstractDocument(Content doc) 195: { 196: this(doc, StyleContext.getDefaultStyleContext()); 197: } 198: 199: /** 200: * Creates a new <code>AbstractDocument</code> with the specified 201: * {@link Content} model and {@link AttributeContext}. 202: * 203: * @param doc the <code>Content</code> model to be used in this 204: * <code>Document<code> 205: * @param ctx the <code>AttributeContext</code> to use 206: * 207: * @see GapContent 208: * @see StringContent 209: */ 210: protected AbstractDocument(Content doc, AttributeContext ctx) 211: { 212: content = doc; 213: context = ctx; 214: 215: // FIXME: Fully implement bidi. 216: bidiRoot = new BidiRootElement(); 217: 218: // FIXME: This is determined using a Mauve test. Make the document 219: // actually use this. 220: putProperty(I18N, Boolean.FALSE); 221: 222: // Add one child to the bidi root. 223: writeLock(); 224: try 225: { 226: Element[] children = new Element[1]; 227: children[0] = new BidiElement(bidiRoot, 0, 1, 0); 228: bidiRoot.replace(0, 0, children); 229: } 230: finally 231: { 232: writeUnlock(); 233: } 234: } 235: 236: /** Returns the DocumentFilter.FilterBypass instance for this 237: * document and create it if it does not exist yet. 238: * 239: * @return This document's DocumentFilter.FilterBypass instance. 240: */ 241: private DocumentFilter.FilterBypass getBypass() 242: { 243: if (bypass == null) 244: bypass = new Bypass(); 245: 246: return bypass; 247: } 248: 249: /** 250: * Returns the paragraph {@link Element} that holds the specified position. 251: * 252: * @param pos the position for which to get the paragraph element 253: * 254: * @return the paragraph {@link Element} that holds the specified position 255: */ 256: public abstract Element getParagraphElement(int pos); 257: 258: /** 259: * Returns the default root {@link Element} of this <code>Document</code>. 260: * Usual <code>Document</code>s only have one root element and return this. 261: * However, there may be <code>Document</code> implementations that 262: * support multiple root elements, they have to return a default root element 263: * here. 264: * 265: * @return the default root {@link Element} of this <code>Document</code> 266: */ 267: public abstract Element getDefaultRootElement(); 268: 269: /** 270: * Creates and returns a branch element with the specified 271: * <code>parent</code> and <code>attributes</code>. Note that the new 272: * <code>Element</code> is linked to the parent <code>Element</code> 273: * through {@link Element#getParentElement}, but it is not yet added 274: * to the parent <code>Element</code> as child. 275: * 276: * @param parent the parent <code>Element</code> for the new branch element 277: * @param attributes the text attributes to be installed in the new element 278: * 279: * @return the new branch <code>Element</code> 280: * 281: * @see BranchElement 282: */ 283: protected Element createBranchElement(Element parent, 284: AttributeSet attributes) 285: { 286: return new BranchElement(parent, attributes); 287: } 288: 289: /** 290: * Creates and returns a leaf element with the specified 291: * <code>parent</code> and <code>attributes</code>. Note that the new 292: * <code>Element</code> is linked to the parent <code>Element</code> 293: * through {@link Element#getParentElement}, but it is not yet added 294: * to the parent <code>Element</code> as child. 295: * 296: * @param parent the parent <code>Element</code> for the new branch element 297: * @param attributes the text attributes to be installed in the new element 298: * 299: * @return the new branch <code>Element</code> 300: * 301: * @see LeafElement 302: */ 303: protected Element createLeafElement(Element parent, AttributeSet attributes, 304: int start, int end) 305: { 306: return new LeafElement(parent, attributes, start, end); 307: } 308: 309: /** 310: * Creates a {@link Position} that keeps track of the location at the 311: * specified <code>offset</code>. 312: * 313: * @param offset the location in the document to keep track by the new 314: * <code>Position</code> 315: * 316: * @return the newly created <code>Position</code> 317: * 318: * @throws BadLocationException if <code>offset</code> is not a valid 319: * location in the documents content model 320: */ 321: public synchronized Position createPosition(final int offset) 322: throws BadLocationException 323: { 324: return content.createPosition(offset); 325: } 326: 327: /** 328: * Notifies all registered listeners when the document model changes. 329: * 330: * @param event the <code>DocumentEvent</code> to be fired 331: */ 332: protected void fireChangedUpdate(DocumentEvent event) 333: { 334: notifyListeners = true; 335: try 336: { 337: DocumentListener[] listeners = getDocumentListeners(); 338: for (int index = 0; index < listeners.length; ++index) 339: listeners[index].changedUpdate(event); 340: } 341: finally 342: { 343: notifyListeners = false; 344: } 345: } 346: 347: /** 348: * Notifies all registered listeners when content is inserted in the document 349: * model. 350: * 351: * @param event the <code>DocumentEvent</code> to be fired 352: */ 353: protected void fireInsertUpdate(DocumentEvent event) 354: { 355: notifyListeners = true; 356: try 357: { 358: DocumentListener[] listeners = getDocumentListeners(); 359: for (int index = 0; index < listeners.length; ++index) 360: listeners[index].insertUpdate(event); 361: } 362: finally 363: { 364: notifyListeners = false; 365: } 366: } 367: 368: /** 369: * Notifies all registered listeners when content is removed from the 370: * document model. 371: * 372: * @param event the <code>DocumentEvent</code> to be fired 373: */ 374: protected void fireRemoveUpdate(DocumentEvent event) 375: { 376: notifyListeners = true; 377: try 378: { 379: DocumentListener[] listeners = getDocumentListeners(); 380: for (int index = 0; index < listeners.length; ++index) 381: listeners[index].removeUpdate(event); 382: } 383: finally 384: { 385: notifyListeners = false; 386: } 387: } 388: 389: /** 390: * Notifies all registered listeners when an <code>UndoableEdit</code> has 391: * been performed on this <code>Document</code>. 392: * 393: * @param event the <code>UndoableEditEvent</code> to be fired 394: */ 395: protected void fireUndoableEditUpdate(UndoableEditEvent event) 396: { 397: UndoableEditListener[] listeners = getUndoableEditListeners(); 398: 399: for (int index = 0; index < listeners.length; ++index) 400: listeners[index].undoableEditHappened(event); 401: } 402: 403: /** 404: * Returns the asynchronous loading priority. Returns <code>-1</code> if this 405: * document should not be loaded asynchronously. 406: * 407: * @return the asynchronous loading priority 408: */ 409: public int getAsynchronousLoadPriority() 410: { 411: Object val = getProperty(AsyncLoadPriority); 412: int prio = -1; 413: if (val != null) 414: prio = ((Integer) val).intValue(); 415: return prio; 416: } 417: 418: /** 419: * Returns the {@link AttributeContext} used in this <code>Document</code>. 420: * 421: * @return the {@link AttributeContext} used in this <code>Document</code> 422: */ 423: protected final AttributeContext getAttributeContext() 424: { 425: return context; 426: } 427: 428: /** 429: * Returns the root element for bidirectional content. 430: * 431: * @return the root element for bidirectional content 432: */ 433: public Element getBidiRootElement() 434: { 435: return bidiRoot; 436: } 437: 438: /** 439: * Returns the {@link Content} model for this <code>Document</code> 440: * 441: * @return the {@link Content} model for this <code>Document</code> 442: * 443: * @see GapContent 444: * @see StringContent 445: */ 446: protected final Content getContent() 447: { 448: return content; 449: } 450: 451: /** 452: * Returns the thread that currently modifies this <code>Document</code> 453: * if there is one, otherwise <code>null</code>. This can be used to 454: * distinguish between a method call that is part of an ongoing modification 455: * or if it is a separate modification for which a new lock must be aquired. 456: * 457: * @return the thread that currently modifies this <code>Document</code> 458: * if there is one, otherwise <code>null</code> 459: */ 460: protected final synchronized Thread getCurrentWriter() 461: { 462: return currentWriter; 463: } 464: 465: /** 466: * Returns the properties of this <code>Document</code>. 467: * 468: * @return the properties of this <code>Document</code> 469: */ 470: public Dictionary<Object, Object> getDocumentProperties() 471: { 472: // FIXME: make me thread-safe 473: if (properties == null) 474: properties = new Hashtable(); 475: 476: return properties; 477: } 478: 479: /** 480: * Returns a {@link Position} which will always mark the end of the 481: * <code>Document</code>. 482: * 483: * @return a {@link Position} which will always mark the end of the 484: * <code>Document</code> 485: */ 486: public final Position getEndPosition() 487: { 488: Position p; 489: try 490: { 491: p = createPosition(content.length()); 492: } 493: catch (BadLocationException ex) 494: { 495: // Shouldn't really happen. 496: p = null; 497: } 498: return p; 499: } 500: 501: /** 502: * Returns the length of this <code>Document</code>'s content. 503: * 504: * @return the length of this <code>Document</code>'s content 505: */ 506: public int getLength() 507: { 508: // We return Content.getLength() -1 here because there is always an 509: // implicit \n at the end of the Content which does count in Content 510: // but not in Document. 511: return content.length() - 1; 512: } 513: 514: /** 515: * Returns all registered listeners of a given listener type. 516: * 517: * @param listenerType the type of the listeners to be queried 518: * 519: * @return all registered listeners of the specified type 520: */ 521: public <T extends EventListener> T[] getListeners(Class<T> listenerType) 522: { 523: return listenerList.getListeners(listenerType); 524: } 525: 526: /** 527: * Returns a property from this <code>Document</code>'s property list. 528: * 529: * @param key the key of the property to be fetched 530: * 531: * @return the property for <code>key</code> or <code>null</code> if there 532: * is no such property stored 533: */ 534: public final Object getProperty(Object key) 535: { 536: // FIXME: make me thread-safe 537: Object value = null; 538: if (properties != null) 539: value = properties.get(key); 540: 541: return value; 542: } 543: 544: /** 545: * Returns all root elements of this <code>Document</code>. By default 546: * this just returns the single root element returned by 547: * {@link #getDefaultRootElement()}. <code>Document</code> implementations 548: * that support multiple roots must override this method and return all roots 549: * here. 550: * 551: * @return all root elements of this <code>Document</code> 552: */ 553: public Element[] getRootElements() 554: { 555: Element[] elements = new Element[2]; 556: elements[0] = getDefaultRootElement(); 557: elements[1] = getBidiRootElement(); 558: return elements; 559: } 560: 561: /** 562: * Returns a {@link Position} which will always mark the beginning of the 563: * <code>Document</code>. 564: * 565: * @return a {@link Position} which will always mark the beginning of the 566: * <code>Document</code> 567: */ 568: public final Position getStartPosition() 569: { 570: Position p; 571: try 572: { 573: p = createPosition(0); 574: } 575: catch (BadLocationException ex) 576: { 577: // Shouldn't really happen. 578: p = null; 579: } 580: return p; 581: } 582: 583: /** 584: * Returns a piece of this <code>Document</code>'s content. 585: * 586: * @param offset the start offset of the content 587: * @param length the length of the content 588: * 589: * @return the piece of content specified by <code>offset</code> and 590: * <code>length</code> 591: * 592: * @throws BadLocationException if <code>offset</code> or <code>offset + 593: * length</code> are invalid locations with this 594: * <code>Document</code> 595: */ 596: public String getText(int offset, int length) throws BadLocationException 597: { 598: return content.getString(offset, length); 599: } 600: 601: /** 602: * Fetches a piece of this <code>Document</code>'s content and stores 603: * it in the given {@link Segment}. 604: * 605: * @param offset the start offset of the content 606: * @param length the length of the content 607: * @param segment the <code>Segment</code> to store the content in 608: * 609: * @throws BadLocationException if <code>offset</code> or <code>offset + 610: * length</code> are invalid locations with this 611: * <code>Document</code> 612: */ 613: public void getText(int offset, int length, Segment segment) 614: throws BadLocationException 615: { 616: content.getChars(offset, length, segment); 617: } 618: 619: /** 620: * Inserts a String into this <code>Document</code> at the specified 621: * position and assigning the specified attributes to it. 622: * 623: * <p>If a {@link DocumentFilter} is installed in this document, the 624: * corresponding method of the filter object is called.</p> 625: * 626: * <p>The method has no effect when <code>text</code> is <code>null</code> 627: * or has a length of zero.</p> 628: * 629: * 630: * @param offset the location at which the string should be inserted 631: * @param text the content to be inserted 632: * @param attributes the text attributes to be assigned to that string 633: * 634: * @throws BadLocationException if <code>offset</code> is not a valid 635: * location in this <code>Document</code> 636: */ 637: public void insertString(int offset, String text, AttributeSet attributes) 638: throws BadLocationException 639: { 640: // Bail out if we have a bogus insertion (Behavior observed in RI). 641: if (text == null || text.length() == 0) 642: return; 643: 644: writeLock(); 645: try 646: { 647: if (documentFilter == null) 648: insertStringImpl(offset, text, attributes); 649: else 650: documentFilter.insertString(getBypass(), offset, text, attributes); 651: } 652: finally 653: { 654: writeUnlock(); 655: } 656: } 657: 658: void insertStringImpl(int offset, String text, AttributeSet attributes) 659: throws BadLocationException 660: { 661: // Just return when no text to insert was given. 662: if (text == null || text.length() == 0) 663: return; 664: DefaultDocumentEvent event = 665: new DefaultDocumentEvent(offset, text.length(), 666: DocumentEvent.EventType.INSERT); 667: 668: UndoableEdit undo = content.insertString(offset, text); 669: if (undo != null) 670: event.addEdit(undo); 671: 672: // Check if we need bidi layout. 673: if (getProperty(I18N).equals(Boolean.FALSE)) 674: { 675: Object dir = getProperty(TextAttribute.RUN_DIRECTION); 676: if (TextAttribute.RUN_DIRECTION_RTL.equals(dir)) 677: putProperty(I18N, Boolean.TRUE); 678: else 679: { 680: char[] chars = text.toCharArray(); 681: if (Bidi.requiresBidi(chars, 0, chars.length)) 682: putProperty(I18N, Boolean.TRUE); 683: } 684: } 685: 686: insertUpdate(event, attributes); 687: 688: fireInsertUpdate(event); 689: 690: if (undo != null) 691: fireUndoableEditUpdate(new UndoableEditEvent(this, undo)); 692: } 693: 694: /** 695: * Called to indicate that text has been inserted into this 696: * <code>Document</code>. The default implementation does nothing. 697: * This method is executed within a write lock. 698: * 699: * @param chng the <code>DefaultDocumentEvent</code> describing the change 700: * @param attr the attributes of the changed content 701: */ 702: protected void insertUpdate(DefaultDocumentEvent chng, AttributeSet attr) 703: { 704: if (Boolean.TRUE.equals(getProperty(I18N))) 705: updateBidi(chng); 706: } 707: 708: /** 709: * Called after some content has been removed from this 710: * <code>Document</code>. The default implementation does nothing. 711: * This method is executed within a write lock. 712: * 713: * @param chng the <code>DefaultDocumentEvent</code> describing the change 714: */ 715: protected void postRemoveUpdate(DefaultDocumentEvent chng) 716: { 717: if (Boolean.TRUE.equals(getProperty(I18N))) 718: updateBidi(chng); 719: } 720: 721: /** 722: * Stores a property in this <code>Document</code>'s property list. 723: * 724: * @param key the key of the property to be stored 725: * @param value the value of the property to be stored 726: */ 727: public final void putProperty(Object key, Object value) 728: { 729: // FIXME: make me thread-safe 730: if (properties == null) 731: properties = new Hashtable(); 732: 733: if (value == null) 734: properties.remove(key); 735: else 736: properties.put(key, value); 737: 738: // Update bidi structure if the RUN_DIRECTION is set. 739: if (TextAttribute.RUN_DIRECTION.equals(key)) 740: { 741: if (TextAttribute.RUN_DIRECTION_RTL.equals(value) 742: && Boolean.FALSE.equals(getProperty(I18N))) 743: putProperty(I18N, Boolean.TRUE); 744: 745: if (Boolean.TRUE.equals(getProperty(I18N))) 746: { 747: writeLock(); 748: try 749: { 750: DefaultDocumentEvent ev = 751: new DefaultDocumentEvent(0, getLength(), 752: DocumentEvent.EventType.INSERT); 753: updateBidi(ev); 754: } 755: finally 756: { 757: writeUnlock(); 758: } 759: } 760: } 761: } 762: 763: /** 764: * Updates the bidi element structure. 765: * 766: * @param ev the document event for the change 767: */ 768: private void updateBidi(DefaultDocumentEvent ev) 769: { 770: // Determine start and end offset of the paragraphs to be scanned. 771: int start = 0; 772: int end = 0; 773: DocumentEvent.EventType type = ev.getType(); 774: if (type == DocumentEvent.EventType.INSERT 775: || type == DocumentEvent.EventType.CHANGE) 776: { 777: int offs = ev.getOffset(); 778: int endOffs = offs + ev.getLength(); 779: start = getParagraphElement(offs).getStartOffset(); 780: end = getParagraphElement(endOffs).getEndOffset(); 781: } 782: else if (type == DocumentEvent.EventType.REMOVE) 783: { 784: Element par = getParagraphElement(ev.getOffset()); 785: start = par.getStartOffset(); 786: end = par.getEndOffset(); 787: } 788: else 789: assert false : "Unknown event type"; 790: 791: // Determine the bidi levels for the affected range. 792: Bidi[] bidis = getBidis(start, end); 793: 794: int removeFrom = 0; 795: int removeTo = 0; 796: 797: int offs = 0; 798: int lastRunStart = 0; 799: int lastRunEnd = 0; 800: int lastRunLevel = 0; 801: ArrayList newEls = new ArrayList(); 802: for (int i = 0; i < bidis.length; i++) 803: { 804: Bidi bidi = bidis[i]; 805: int numRuns = bidi.getRunCount(); 806: for (int r = 0; r < numRuns; r++) 807: { 808: if (r == 0 && i == 0) 809: { 810: if (start > 0) 811: { 812: // Try to merge with the previous element if it has the 813: // same bidi level as the first run. 814: int prevElIndex = bidiRoot.getElementIndex(start - 1); 815: removeFrom = prevElIndex; 816: Element prevEl = bidiRoot.getElement(prevElIndex); 817: AttributeSet atts = prevEl.getAttributes(); 818: int prevElLevel = StyleConstants.getBidiLevel(atts); 819: if (prevElLevel == bidi.getRunLevel(r)) 820: { 821: // Merge previous element with current run. 822: lastRunStart = prevEl.getStartOffset() - start; 823: lastRunEnd = bidi.getRunLimit(r); 824: lastRunLevel = bidi.getRunLevel(r); 825: } 826: else if (prevEl.getEndOffset() > start) 827: { 828: // Split previous element and replace by 2 new elements. 829: lastRunStart = 0; 830: lastRunEnd = bidi.getRunLimit(r); 831: lastRunLevel = bidi.getRunLevel(r); 832: newEls.add(new BidiElement(bidiRoot, 833: prevEl.getStartOffset(), 834: start, prevElLevel)); 835: } 836: else 837: { 838: // Simply start new run at start location. 839: lastRunStart = 0; 840: lastRunEnd = bidi.getRunLimit(r); 841: lastRunLevel = bidi.getRunLevel(r); 842: removeFrom++; 843: } 844: } 845: else 846: { 847: // Simply start new run at start location. 848: lastRunStart = 0; 849: lastRunEnd = bidi.getRunLimit(r); 850: lastRunLevel = bidi.getRunLevel(r); 851: removeFrom = 0; 852: } 853: } 854: if (i == bidis.length - 1 && r == numRuns - 1) 855: { 856: if (end <= getLength()) 857: { 858: // Try to merge last element with next element. 859: int nextIndex = bidiRoot.getElementIndex(end); 860: Element nextEl = bidiRoot.getElement(nextIndex); 861: AttributeSet atts = nextEl.getAttributes(); 862: int nextLevel = StyleConstants.getBidiLevel(atts); 863: int level = bidi.getRunLevel(r); 864: if (lastRunLevel == level && level == nextLevel) 865: { 866: // Merge runs together. 867: if (lastRunStart + start == nextEl.getStartOffset()) 868: removeTo = nextIndex - 1; 869: else 870: { 871: newEls.add(new BidiElement(bidiRoot, start + lastRunStart, 872: nextEl.getEndOffset(), level)); 873: removeTo = nextIndex; 874: } 875: } 876: else if (lastRunLevel == level) 877: { 878: // Merge current and last run. 879: int endOffs = offs + bidi.getRunLimit(r); 880: newEls.add(new BidiElement(bidiRoot, start + lastRunStart, 881: