2
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4
* Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
6
* The contents of this file are subject to the terms of either the GNU
7
* General Public License Version 2 only ("GPL") or the Common
8
* Development and Distribution License("CDDL") (collectively, the
9
* "License"). You may not use this file except in compliance with the
10
* License. You can obtain a copy of the License at
11
* http://www.netbeans.org/cddl-gplv2.html
12
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13
* specific language governing permissions and limitations under the
14
* License. When distributing the software, include this License Header
15
* Notice in each file and include the License file at
16
* nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
17
* particular file as subject to the "Classpath" exception as provided
18
* by Sun in the GPL Version 2 section of the License file that
19
* accompanied this code. If applicable, add the following below the
20
* License Header, with the fields enclosed by brackets [] replaced by
21
* your own identifying information:
22
* "Portions Copyrighted [year] [name of copyright owner]"
26
* The Original Software is NetBeans. The Initial Developer of the Original
27
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
28
* Microsystems, Inc. All Rights Reserved.
30
* If you wish your version of this file to be governed by only the CDDL
31
* or only the GPL Version 2, indicate your decision by adding
32
* "[Contributor] elects to include this software in this distribution
33
* under the [CDDL or GPL Version 2] license." If you do not indicate a
34
* single choice of license, a recipient has the option to distribute
35
* your version of this file under either the CDDL, the GPL Version 2 or
36
* to extend the choice of license to its licensees as provided above.
37
* However, if you add GPL Version 2 code and therefore, elected the GPL
38
* Version 2 license, then the option applies only if the new code is
39
* made subject to such option by the copyright holder.
42
package org.netbeans.lib.editor.util.swing;
44
import java.lang.reflect.Field;
46
import javax.swing.event.DocumentEvent;
47
import javax.swing.event.DocumentListener;
48
import javax.swing.text.AbstractDocument;
49
import javax.swing.text.BadLocationException;
50
import javax.swing.text.Document;
51
import javax.swing.text.Element;
52
import javax.swing.text.Segment;
53
import javax.swing.text.StyledDocument;
54
import javax.swing.undo.CannotRedoException;
55
import javax.swing.undo.CannotUndoException;
56
import javax.swing.undo.UndoableEdit;
57
import org.netbeans.lib.editor.util.AbstractCharSequence;
58
import org.netbeans.lib.editor.util.CompactMap;
61
* Various utility methods related to swing text documents.
63
* @author Miloslav Metelka
67
public final class DocumentUtilities {
69
private static final Object TYPING_MODIFICATION_DOCUMENT_PROPERTY = new Object();
71
private static final Object TYPING_MODIFICATION_KEY = new Object();
73
private static Field numReadersField;
75
private static Field currWriterField;
78
private DocumentUtilities() {
83
* Add document listener to document with given priority
84
* or default to using regular {@link Document#addDocumentListener(DocumentListener)}
85
* if the given document is not listener priority aware.
87
* @param doc document to which the listener should be added.
88
* @param listener document listener to add.
89
* @param priority priority with which the listener should be added.
90
* If the document does not support document listeners ordering
91
* then the listener is added in a regular way by using
92
* {@link javax.swing.text.Document#addDocumentListener(
93
* javax.swing.event.DocumentListener)} method.
95
public static void addDocumentListener(Document doc, DocumentListener listener,
96
DocumentListenerPriority priority) {
97
if (!addPriorityDocumentListener(doc, listener, priority))
98
doc.addDocumentListener(listener);
102
* Suitable for document implementations - adds document listener
103
* to document with given priority and does not do anything
104
* if the given document is not listener priority aware.
106
* Using this method in the document impls and defaulting
107
* to super.addDocumentListener() in case it returns false
108
* will ensure that there won't be an infinite loop in case the super constructors
109
* would add some listeners prior initing of the priority listening.
111
* @param doc document to which the listener should be added.
112
* @param listener document listener to add.
113
* @param priority priority with which the listener should be added.
114
* @return true if the priority listener was added or false if the document
115
* does not support priority listening.
117
public static boolean addPriorityDocumentListener(Document doc, DocumentListener listener,
118
DocumentListenerPriority priority) {
119
PriorityDocumentListenerList priorityDocumentListenerList
120
= (PriorityDocumentListenerList)doc.getProperty(PriorityDocumentListenerList.class);
121
if (priorityDocumentListenerList != null) {
122
priorityDocumentListenerList.add(listener, priority.getPriority());
129
* Remove document listener that was previously added to the document
130
* with given priority or use default {@link Document#removeDocumentListener(DocumentListener)}
131
* if the given document is not listener priority aware.
133
* @param doc document from which the listener should be removed.
134
* @param listener document listener to remove.
135
* @param priority priority with which the listener should be removed.
136
* It should correspond to the priority with which the listener
137
* was added originally.
139
public static void removeDocumentListener(Document doc, DocumentListener listener,
140
DocumentListenerPriority priority) {
141
if (!removePriorityDocumentListener(doc, listener, priority))
142
doc.removeDocumentListener(listener);
146
* Suitable for document implementations - removes document listener
147
* from document with given priority and does not do anything
148
* if the given document is not listener priority aware.
150
* Using this method in the document impls and defaulting
151
* to super.removeDocumentListener() in case it returns false
152
* will ensure that there won't be an infinite loop in case the super constructors
153
* would remove some listeners prior initing of the priority listening.
155
* @param doc document from which the listener should be removed.
156
* @param listener document listener to remove.
157
* @param priority priority with which the listener should be removed.
158
* @return true if the priority listener was removed or false if the document
159
* does not support priority listening.
161
public static boolean removePriorityDocumentListener(Document doc, DocumentListener listener,
162
DocumentListenerPriority priority) {
163
PriorityDocumentListenerList priorityDocumentListenerList
164
= (PriorityDocumentListenerList)doc.getProperty(PriorityDocumentListenerList.class);
165
if (priorityDocumentListenerList != null) {
166
priorityDocumentListenerList.remove(listener, priority.getPriority());
173
* This method should be used by swing document implementations that
174
* want to support document listeners prioritization.
176
* It should be called from document's constructor in the following way:<pre>
178
* class MyDocument extends AbstractDocument {
181
* super.addDocumentListener(DocumentUtilities.initPriorityListening(this));
184
* public void addDocumentListener(DocumentListener listener) {
185
* if (!DocumentUtilities.addDocumentListener(this, listener, DocumentListenerPriority.DEFAULT))
186
* super.addDocumentListener(listener);
189
* public void removeDocumentListener(DocumentListener listener) {
190
* if (!DocumentUtilities.removeDocumentListener(this, listener, DocumentListenerPriority.DEFAULT))
191
* super.removeDocumentListener(listener);
197
* @param doc document to be initialized.
198
* @return the document listener instance that should be added as a document
199
* listener typically by using <code>super.addDocumentListener()</code>
200
* in document's constructor.
201
* @throws IllegalStateException when the document already has
202
* the property initialized.
204
public static DocumentListener initPriorityListening(Document doc) {
205
if (doc.getProperty(PriorityDocumentListenerList.class) != null) {
206
throw new IllegalStateException(
207
"PriorityDocumentListenerList already initialized for doc=" + doc); // NOI18N
209
PriorityDocumentListenerList listener = new PriorityDocumentListenerList();
210
doc.putProperty(PriorityDocumentListenerList.class, listener);
215
* Get total count of document listeners attached to a particular document
216
* (useful e.g. for logging).
218
* If the document uses priority listening then get the count of listeners
219
* at all levels. If the document is not {@link AbstractDocument} the method
222
* @param doc non-null document.
223
* @return total count of document listeners attached to the document.
225
public static int getDocumentListenerCount(Document doc) {
226
PriorityDocumentListenerList pdll;
227
return (pdll = (PriorityDocumentListenerList)doc.getProperty(PriorityDocumentListenerList.class)) != null
228
? pdll.getListenerCount()
229
: ((doc instanceof AbstractDocument)
230
? ((AbstractDocument)doc).getListeners(DocumentListener.class).length
235
* Mark that the ongoing document modification(s) will be caused
237
* It should be used by default-key-typed-action and the actions
238
* for backspace and delete keys.
240
* The document listeners being fired may
241
* query it by using {@link #isTypingModification(Document)}.
242
* This method should always be used in the following pattern:
244
* DocumentUtilities.setTypingModification(doc, true);
246
* doc.insertString(offset, typedText, null);
248
* DocumentUtilities.setTypingModification(doc, false);
252
* @see #isTypingModification(Document)
254
public static void setTypingModification(Document doc, boolean typingModification) {
255
doc.putProperty(TYPING_MODIFICATION_DOCUMENT_PROPERTY, Boolean.valueOf(typingModification));
259
* This method should be used by document listeners to check whether
260
* the just performed document modification was caused by user's typing.
262
* Certain functionality such as code completion or code templates
263
* may benefit from that information. For example the java code completion
264
* should only react to the typed "." but not if the same string was e.g.
265
* pasted from the clipboard.
267
* @see #setTypingModification(Document, boolean)
269
public static boolean isTypingModification(Document doc) {
270
Boolean b = (Boolean)doc.getProperty(TYPING_MODIFICATION_DOCUMENT_PROPERTY);
271
return (b != null) ? b.booleanValue() : false;
276
* @see #isTypingModification(Document)
278
public static boolean isTypingModification(DocumentEvent evt) {
279
return isTypingModification(evt.getDocument());
283
* Get text of the given document as char sequence.
286
* @param doc document for which the charsequence is being obtained.
287
* @return non-null character sequence.
289
* The returned character sequence should only be accessed under
290
* document's readlock (or writelock).
292
public static CharSequence getText(Document doc) {
293
CharSequence text = (CharSequence)doc.getProperty(CharSequence.class);
295
text = new DocumentCharSequence(doc);
296
doc.putProperty(CharSequence.class, text);
302
* Get a portion of text of the given document as char sequence.
305
* @param doc document for which the charsequence is being obtained.
306
* @param offset starting offset of the charsequence to obtain.
307
* @param length length of the charsequence to obtain
308
* @return non-null character sequence.
309
* @exception BadLocationException some portion of the given range
310
* was not a valid part of the document. The location in the exception
311
* is the first bad position encountered.
313
* The returned character sequence should only be accessed under
314
* document's readlock (or writelock).
316
public static CharSequence getText(Document doc, int offset, int length) throws BadLocationException {
317
CharSequence text = (CharSequence)doc.getProperty(CharSequence.class);
319
text = new DocumentCharSequence(doc);
320
doc.putProperty(CharSequence.class, text);
323
return text.subSequence(offset, offset + length);
324
} catch (IndexOutOfBoundsException e) {
325
int badOffset = offset;
326
if (offset >= 0 && offset + length > text.length()) {
329
throw new BadLocationException(e.getMessage(), badOffset);
334
* Document provider should call this method to allow for document event
335
* properties being stored in document events.
337
* @param evt document event to which the storage should be added.
338
* It must be an undoable edit allowing to add an edit.
340
public static void addEventPropertyStorage(DocumentEvent evt) {
341
// Parameter is DocumentEvent because it's more logical
342
if (!(evt instanceof UndoableEdit)) {
343
throw new IllegalStateException("evt not instanceof UndoableEdit: " + evt); // NOI18N
345
((UndoableEdit)evt).addEdit(new EventPropertiesElementChange());
349
* Get a property of a given document event.
351
* @param evt non-null document event from which the property should be retrieved.
352
* @param key non-null key of the property.
353
* @return value for the given property.
355
public static Object getEventProperty(DocumentEvent evt, Object key) {
356
EventPropertiesElementChange change = (EventPropertiesElementChange)
357
evt.getChange(EventPropertiesElement.INSTANCE);
358
return (change != null) ? change.getProperty(key) : null;
362
* Set a property of a given document event.
364
* @param evt non-null document event to which the property should be stored.
365
* @param key non-null key of the property.
366
* @param value for the given property.
368
public static void putEventProperty(DocumentEvent evt, Object key, Object value) {
369
EventPropertiesElementChange change = (EventPropertiesElementChange)
370
evt.getChange(EventPropertiesElement.INSTANCE);
371
if (change == null) {
372
throw new IllegalStateException("addEventPropertyStorage() not called for evt=" + evt); // NOI18N
374
change.putProperty(key, value);
378
* Set a property of a given document event by using the given map entry.
380
* The present implementation is able to directly store instances
381
* of <code>CompactMap.MapEntry</code>. Other map entry implementations
382
* will be delegated to {@link #putEventProperty(DocumentEvent, Object, Object)}.
384
* @param evt non-null document event to which the property should be stored.
385
* @param mapEntry non-null map entry which should be stored.
386
* Generally after this method finishes the {@link #getEventProperty(DocumentEvent, Object)}
387
* will return <code>mapEntry.getValue()</code> for <code>mapEntry.getKey()</code> key.
389
public static void putEventProperty(DocumentEvent evt, Map.Entry mapEntry) {
390
if (mapEntry instanceof CompactMap.MapEntry) {
391
EventPropertiesElementChange change = (EventPropertiesElementChange)
392
evt.getChange(EventPropertiesElement.INSTANCE);
393
if (change == null) {
394
throw new IllegalStateException("addEventPropertyStorage() not called for evt=" + evt); // NOI18N
396
change.putEntry((CompactMap.MapEntry)mapEntry);
399
putEventProperty(evt, mapEntry.getKey(), mapEntry.getValue());
404
* Fix the given offset according to the performed modification.
406
* @param offset >=0 offset in a document.
407
* @param evt document event describing change in the document.
408
* @return offset updated by applying the document change to the offset.
410
public static int fixOffset(int offset, DocumentEvent evt) {
411
int modOffset = evt.getOffset();
412
if (evt.getType() == DocumentEvent.EventType.INSERT) {
413
if (offset >= modOffset) {
414
offset += evt.getLength();
416
} else if (evt.getType() == DocumentEvent.EventType.REMOVE) {
417
if (offset > modOffset) {
418
offset = Math.min(offset - evt.getLength(), modOffset);
425
* Get text of the given document modification.
427
* It's implemented as retrieving of a <code>String.class</code>.
429
* @param evt document event describing either document insertion or removal
430
* (change event type events will produce null result).
431
* @return text that was inserted/removed from the document by the given
432
* document modification or null if that information is not provided
433
* by that document event.
435
public static String getModificationText(DocumentEvent evt) {
436
return (String)getEventProperty(evt, String.class);
440
* Check whether the given document is read-locked by at least one thread
441
* or whether it was write-locked by the current thread (write-locking
442
* grants the read-access automatically).
444
* The method currently only works for {@link javax.swing.text.AbstractDocument}
445
* based documents and it uses reflection.
447
* Unfortunately the AbstractDocument only records number of read-lockers
448
* but not the thread references that performed the read-locking. Thus it can't be verified
449
* whether current thread has performed read locking or another thread.
451
* @param doc non-null document instance.
452
* @return true if the document was read-locked by some thread
453
* or false if not (or if doc not-instanceof AbstractDocument).
456
public static boolean isReadLocked(Document doc) {
457
if (checkAbstractDoc(doc)) {
458
if (isWriteLocked(doc))
460
if (numReadersField == null) {
462
numReadersField = AbstractDocument.class.getDeclaredField("numReaders");
463
} catch (NoSuchFieldException ex) {
464
throw new IllegalStateException(ex);
466
numReadersField.setAccessible(true);
470
return numReadersField.getInt(doc) > 0;
472
} catch (IllegalAccessException ex) {
473
throw new IllegalStateException(ex);
480
* Check whether the given document is write-locked by the current thread.
482
* The method currently only works for {@link javax.swing.text.AbstractDocument}
483
* based documents and it uses reflection.
485
* @param doc non-null document instance.
486
* @return true if the document was write-locked by the current thread
487
* or false if not (or if doc not-instanceof AbstractDocument).
490
public static boolean isWriteLocked(Document doc) {
491
if (checkAbstractDoc(doc)) {
492
if (currWriterField == null) {
494
currWriterField = AbstractDocument.class.getDeclaredField("currWriter");
495
} catch (NoSuchFieldException ex) {
496
throw new IllegalStateException(ex);
498
currWriterField.setAccessible(true);
502
return currWriterField.get(doc) == Thread.currentThread();
504
} catch (IllegalAccessException ex) {
505
throw new IllegalStateException(ex);
508
return false; // not AbstractDocument
511
private static boolean checkAbstractDoc(Document doc) {
513
throw new IllegalArgumentException("document is null");
514
return (doc instanceof AbstractDocument);
518
* Get the paragraph element for the given document.
520
* @param doc non-null document instance.
521
* @param offset offset in the document >=0
522
* @return paragraph element containing the given offset.
524
public static Element getParagraphElement(Document doc, int offset) {
526
if (doc instanceof StyledDocument) {
527
paragraph = ((StyledDocument)doc).getParagraphElement(offset);
529
Element rootElem = doc.getDefaultRootElement();
530
int index = rootElem.getElementIndex(offset);
531
paragraph = rootElem.getElement(index);
532
if ((offset < paragraph.getStartOffset()) || (offset >= paragraph.getEndOffset())) {
540
* Get the root of the paragraph elements for the given document.
542
* @param doc non-null document instance.
543
* @return root element of the paragraph elements.
545
public static Element getParagraphRootElement(Document doc) {
546
if (doc instanceof StyledDocument) {
547
return ((StyledDocument)doc).getParagraphElement(0).getParentElement();
549
return doc.getDefaultRootElement().getElement(0).getParentElement();
554
* Implementation of the character sequence for a generic document
555
* that does not provide its own implementation of character sequence.
557
private static final class DocumentCharSequence extends AbstractCharSequence.StringLike {
559
private final Segment segment = new Segment();
561
private final Document doc;
563
DocumentCharSequence(Document doc) {
567
public int length() {
568
return doc.getLength();
571
public synchronized char charAt(int index) {
573
doc.getText(index, 1, segment);
574
} catch (BadLocationException e) {
575
throw new IndexOutOfBoundsException(e.getMessage()
576
+ " at offset=" + e.offsetRequested()); // NOI18N
578
char ch = segment.array[segment.offset];
579
segment.array = null; // Allow GC of large char arrays
586
* Helper element used as a key in searching for an element change
587
* being a storage of the additional properties in a document event.
589
private static final class EventPropertiesElement implements Element {
591
static final EventPropertiesElement INSTANCE = new EventPropertiesElement();
593
public int getStartOffset() {
597
public int getEndOffset() {
601
public int getElementCount() {
605
public int getElementIndex(int offset) {
609
public Element getElement(int index) {
613
public boolean isLeaf() {
617
public Element getParentElement() {
621
public String getName() {
622
return "Helper element for modification text providing"; // NOI18N
625
public Document getDocument() {
629
public javax.swing.text.AttributeSet getAttributes() {
633
public String toString() {
639
private static final class EventPropertiesElementChange
640
implements DocumentEvent.ElementChange, UndoableEdit {
642
private CompactMap eventProperties = new CompactMap();
644
public synchronized Object getProperty(Object key) {
645
return (eventProperties != null) ? eventProperties.get(key) : null;
648
@SuppressWarnings("unchecked")
649
public synchronized Object putProperty(Object key, Object value) {
650
return eventProperties.put(key, value);
653
@SuppressWarnings("unchecked")
654
public synchronized CompactMap.MapEntry putEntry(CompactMap.MapEntry entry) {
655
return eventProperties.putEntry(entry);
658
public int getIndex() {
662
public Element getElement() {
663
return EventPropertiesElement.INSTANCE;
666
public Element[] getChildrenRemoved() {
670
public Element[] getChildrenAdded() {
674
public boolean replaceEdit(UndoableEdit anEdit) {
678
public boolean addEdit(UndoableEdit anEdit) {
682
public void undo() throws CannotUndoException {
686
public void redo() throws CannotRedoException {
690
public boolean isSignificant() {
694
public String getUndoPresentationName() {
698
public String getRedoPresentationName() {
702
public String getPresentationName() {
710
public boolean canUndo() {
714
public boolean canRedo() {