001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.api.editor;
043:
044: import java.awt.Component;
045: import java.awt.event.ActionEvent;
046: import java.awt.event.ActionListener;
047: import java.awt.event.FocusEvent;
048: import java.awt.event.FocusListener;
049: import java.beans.PropertyChangeEvent;
050: import java.beans.PropertyChangeListener;
051: import java.beans.PropertyChangeSupport;
052: import java.lang.ref.WeakReference;
053: import java.util.ArrayList;
054: import java.util.Collections;
055: import java.util.List;
056: import java.util.logging.Level;
057: import java.util.logging.Logger;
058: import javax.swing.JComponent;
059: import javax.swing.SwingUtilities;
060: import javax.swing.Timer;
061: import javax.swing.event.AncestorEvent;
062: import javax.swing.event.AncestorListener;
063: import javax.swing.text.Document;
064: import javax.swing.text.JTextComponent;
065: import org.netbeans.lib.editor.util.ArrayUtilities;
066: import org.netbeans.modules.editor.lib2.EditorApiPackageAccessor;
067:
068: /**
069: * Registry maintaining {@link JTextComponent}s in most-recently-used order.
070: * <br/>
071: * The particular text component needs to register itself first (to avoid dealing
072: * with all the JTextFields etc.). Then the registry will attach
073: * a focus listener to the text component and once the component gains
074: * the focus it will move to the head of the components list.
075: * <br/>
076: * The registry will also fire a change in case a document property
077: * of the focused component changes (by calling component.setDocument()).
078: *
079: * @author Miloslav Metelka
080: */
081: public final class EditorRegistry {
082:
083: private EditorRegistry() {
084: // No instances
085: }
086:
087: static {
088: EditorApiPackageAccessor.register(new PackageAccessor());
089: }
090:
091: // -J-Dorg.netbeans.api.editor.EditorRegistry.level=FINEST
092: private static final Logger LOG = Logger
093: .getLogger(EditorRegistry.class.getName());
094:
095: /**
096: * Fired when focus was delivered to a registered text component.
097: * <br/>
098: * The focused component will become the first in the components list.
099: * <br/>
100: * The {@link java.beans.PropertyChangeEvent#getOldValue()} will be a component
101: * losing the focus {@link FocusEvent#getOppositeComponent()}.
102: * The {@link java.beans.PropertyChangeEvent#getNewValue()} will be the text component gaining the focus.
103: */
104: public static final String FOCUS_GAINED_PROPERTY = "focusGained";
105:
106: /**
107: * Fired when a registered focused component has lost the focus.
108: * <br/>
109: * The focused component will remain the first in the components list.
110: * <br/>
111: * The {@link java.beans.PropertyChangeEvent#getOldValue()} will be the text component
112: * losing the focus and the {@link java.beans.PropertyChangeEvent#getNewValue()}
113: * will be the component gaining the focus {@link FocusEvent#getOppositeComponent()}.
114: */
115: public static final String FOCUS_LOST_PROPERTY = "focusLost";
116:
117: /**
118: * Fired when document property of the focused component changes
119: * i.e. someone has called {@link JTextComponent#setDocument(Document)}.
120: * <br/>
121: * The {@link java.beans.PropertyChangeEvent#getOldValue()} will be the original document
122: * of the focused text component and the {@link java.beans.PropertyChangeEvent#getNewValue()}
123: * will be the new document set to the focused text component.
124: */
125: public static final String FOCUSED_DOCUMENT_PROPERTY = "focusedDocument";
126:
127: /**
128: * Fired when the last focused component (returned previously from {@link #lastFocusedComponent()})
129: * was removed from component hierarchy (so it's likely that the component will be released completely
130: * and garbage-collected).
131: * <br/>
132: * Such component will no longer be returned from {@link #componentList()}
133: * or {@link #lastFocusedComponent()}.
134: * <br/>
135: * The {@link java.beans.PropertyChangeEvent#getOldValue()} will be the removed
136: * last focused component and the {@link java.beans.PropertyChangeEvent#getNewValue()}
137: * will be the component that would currently be returned from {@link #lastFocusedComponent()}.
138: * <br/>
139: * If {@link java.beans.PropertyChangeEvent#getNewValue()} returns <code>null</code>
140: * then there are no longer any registered components
141: * ({@link #componentList()} would return empty list). If the client
142: * holds per-last-focused-component data it should clear them.
143: */
144: public static final String LAST_FOCUSED_REMOVED_PROPERTY = "lastFocusedRemoved";
145:
146: /**
147: * Double linked list of weak references to text components.
148: */
149: private static Item items;
150:
151: private static final PropertyChangeSupport pcs = new PropertyChangeSupport(
152: EditorRegistry.class);
153:
154: private static Class ignoredAncestorClass;
155:
156: /**
157: * Return last focused text component (from the ones included in the registry).
158: * <br/>
159: * It may or may not currently have a focus.
160: *
161: * @return last focused text component or null if no text components
162: * were registered yet or all the registered components were closed.
163: */
164: public static synchronized JTextComponent lastFocusedComponent() {
165: return firstValidComponent();
166: }
167:
168: /**
169: * Return the last focused component if it currently has a focus
170: * or return null if none of the registered components currently have the focus.
171: * <br/>
172: * @return focused component or null if none of the registered components
173: * is currently focused.
174: */
175: public static synchronized JTextComponent focusedComponent() {
176: JTextComponent c = firstValidComponent();
177: return (c != null && c.isFocusOwner()) ? c : null;
178: }
179:
180: /**
181: * Get list of all components present in the registry starting with the most active
182: * and ending with least active component.
183: * <br/>
184: * The list is a snapshot of the current state and it may be modified
185: * by the caller if desired.
186: *
187: * @return non-null list containing all the registered components in MRU order.
188: */
189: public static synchronized List<? extends JTextComponent> componentList() {
190: List<JTextComponent> l;
191: JTextComponent c = firstValidComponent();
192: if (c != null) {
193: l = new ArrayList<JTextComponent>();
194: l.add(c);
195: // Add remaining ones (eliminate empty items)
196: Item item = items.next;
197: while (item != null) {
198: c = item.get();
199: if (c != null) {
200: l.add(c);
201: item = item.next;
202: } else
203: item = removeFromItemList(item);
204: }
205:
206: } else
207: // No valid items
208: l = Collections.emptyList();
209: return l;
210: }
211:
212: /**
213: * Add a property change listener for either of the following properties:
214: * <ul>
215: * <li>{@link #FOCUS_GAINED_PROPERTY}</li>
216: * <li>{@link #FOCUS_LOST_PROPERTY}</li>
217: * <li>{@link #FOCUSED_DOCUMENT_PROPERTY}</li>
218: * </ul>.
219: * <br/>
220: * All the firing should occur in AWT thread only
221: * (assuming the JTextComponent.setDocument() is done properly in AWT).
222: *
223: * @param l non-null listener to add.
224: */
225: public static void addPropertyChangeListener(
226: PropertyChangeListener l) {
227: pcs.addPropertyChangeListener(l);
228: }
229:
230: public static void removePropertyChangeListener(
231: PropertyChangeListener l) {
232: pcs.removePropertyChangeListener(l);
233: }
234:
235: /**
236: * Add a given text component to the registry. The registry will weakly
237: * reference the given component for its whole lifetime
238: * until it will be garbage collected.
239: *
240: * @param c non-null text component to be registered.
241: */
242: static synchronized void register(JTextComponent c) {
243: assert (c != null);
244: if (item(c) == null) { // Not registered yet
245: Item item = new Item(c);
246: c.putClientProperty(Item.class, item);
247: c.addFocusListener(FocusL.INSTANCE);
248: c.addAncestorListener(AncestorL.INSTANCE);
249: if (LOG.isLoggable(Level.FINE)) {
250: LOG.log(Level.FINE, "EditorRegistry.register(): "
251: + dumpComponent(c) + '\n');
252: }
253: // By default do not add the component to be last in the item list
254: // at this point since e.g. the component from warmup task(s) would show up
255: // in the item list and they would never be removed
256: // since they have no ancestor and they do not become focused ever.
257: if (c.isFocusOwner()) { // If the focus owner then simulate the focus was gained
258: focusGained(c, null); // opposite could eventually be got from Focus Manager
259: } else if (c.isDisplayable()) { // Simulate that addNotify() was called
260: itemMadeDisplayable(item);
261: }
262: }
263: }
264:
265: static synchronized void setIgnoredAncestorClass(
266: Class ignoredAncestorClass) {
267: EditorRegistry.ignoredAncestorClass = ignoredAncestorClass;
268: }
269:
270: static synchronized void notifyClose(JComponent c) {
271: // Go through the present items and remove those that have the "c" as parent.
272: Item item = items;
273: while (item != null) {
274: JTextComponent textComponent = item.get();
275: if (textComponent == null
276: || (item.ignoreAncestorChange && c
277: .isAncestorOf(textComponent))) {
278: // Explicitly call focusLost() before physical removal from the registry.
279: // In practice this notification happens first before focusLost() from focus listener.
280: if (textComponent != null) {
281: focusLost(textComponent, null); // Checks if the component is focused and does nothing otherwise
282: item = removeFromRegistry(item);
283: } else { // Null text component - just remove the item
284: item = removeFromItemList(item);
285: }
286: } else {
287: item = item.next;
288: }
289: }
290: }
291:
292: static synchronized void focusGained(JTextComponent c,
293: Component origFocused) {
294: Item item = item(c);
295: assert (item != null) : "Not registered!"; // NOI18N
296:
297: // Move the item to head of the list
298: removeFromItemList(item);
299: addToItemListAsFirst(item);
300: item.focused = true;
301:
302: c.addPropertyChangeListener(PropertyDocL.INSTANCE);
303: if (LOG.isLoggable(Level.FINE)) {
304: LOG.log(Level.FINE, FOCUS_GAINED_PROPERTY + ": "
305: + dumpComponent(c) + '\n');
306: logItemListFinest();
307: }
308: firePropertyChange(FOCUS_GAINED_PROPERTY, origFocused, c);
309: }
310:
311: static void focusLost(JTextComponent c, Component newFocused) {
312: Item item = item(c);
313: assert (item != null) : "Not registered!"; // NOI18N
314: // For explicit close notifications: in practice the closing comes first before focus lost.
315: if (item.focused) {
316: item.focused = false;
317: if (!item.ignoreAncestorChange
318: && firstValidComponent() != c) {
319: throw new IllegalStateException(
320: "Invalid ordering of focusLost()");
321: }
322: c.removePropertyChangeListener(PropertyDocL.INSTANCE);
323: if (LOG.isLoggable(Level.FINE)) {
324: LOG.log(Level.FINE, FOCUS_LOST_PROPERTY + ": "
325: + dumpComponent(c) + '\n');
326: logItemListFinest();
327: }
328: firePropertyChange(FOCUS_LOST_PROPERTY, c, newFocused);
329: }
330: }
331:
332: static void itemMadeDisplayable(Item item) {
333: // If the component was removed from the component hierarchy and then
334: // returned back to the hierarchy it will be readded to the end of the component list.
335: // If the item is not removed yet then the addAsLast() will do nothing.
336: addToItemListAsLast(item);
337: JTextComponent c = item.get();
338: if (c == null)
339: throw new IllegalStateException(
340: "Component should be non-null");
341:
342: // Remember whether component should not be removed from registry upon removeNotify()
343: item.ignoreAncestorChange = (SwingUtilities.getAncestorOfClass(
344: ignoredAncestorClass, c) != null);
345: if (LOG.isLoggable(Level.FINER)) {
346: LOG.fine("ancestorAdded: " + dumpComponent(item.get())
347: + '\n');
348: logItemListFinest();
349: }
350: }
351:
352: static void focusedDocumentChange(JTextComponent c,
353: Document oldDoc, Document newDoc) {
354: if (LOG.isLoggable(Level.FINE)) {
355: LOG.log(Level.FINE, FOCUSED_DOCUMENT_PROPERTY + ": "
356: + dumpComponent(c) + "\n OLDDoc=" + oldDoc
357: + "\n NEWDoc=" + newDoc + '\n');
358: }
359: firePropertyChange(FOCUSED_DOCUMENT_PROPERTY, oldDoc, newDoc);
360: }
361:
362: private static JTextComponent firstValidComponent() {
363: JTextComponent c = null;
364: while (items != null && (c = items.get()) == null) {
365: removeFromItemList(items);
366: }
367: return c;
368: }
369:
370: static Item item(JComponent c) {
371: return (Item) c.getClientProperty(Item.class);
372:
373: }
374:
375: private static void addToItemListAsLast(Item item) {
376: if (item.linked)
377: return;
378: item.linked = true;
379: if (items == null) {
380: items = item;
381: } else {
382: Item i = items;
383: while (i.next != null)
384: i = i.next;
385: i.next = item;
386: item.previous = i;
387: }
388: // Assuming item.next == null (done in removeItem() too).
389: if (LOG.isLoggable(Level.FINEST)) { // Consistency checking
390: checkItemListConsistency();
391: }
392: }
393:
394: private static void addToItemListAsFirst(Item item) {
395: if (item.linked)
396: return;
397: item.linked = true;
398: item.next = items;
399: if (items != null)
400: items.previous = item;
401: items = item;
402: if (LOG.isLoggable(Level.FINEST)) { // Consistency checking
403: checkItemListConsistency();
404: }
405: }
406:
407: /**
408: * Remove given entry and return a next one.
409: */
410: private static Item removeFromItemList(Item item) {
411: if (!item.linked)
412: return null;
413: item.linked = false;
414: Item next = item.next;
415: if (item.previous == null) { // Head
416: assert (items == item);
417: items = next;
418: } else { // Not head
419: item.previous.next = next;
420: }
421: if (next != null)
422: next.previous = item.previous;
423: item.next = item.previous = null;
424: if (LOG.isLoggable(Level.FINEST)) { // Consistency checking
425: checkItemListConsistency();
426: }
427: return next;
428: }
429:
430: /**
431: * Remove the given item from registry and return the next one.
432: *
433: * @param item item to remove.
434: * @return next item in registry.
435: */
436: static Item removeFromRegistry(Item item) {
437: boolean lastFocused = (items == item);
438: // Remove component from item chain
439: JTextComponent component = item.get();
440: item = removeFromItemList(item);
441: if (component != null) {
442: if (LOG.isLoggable(Level.FINEST)) {
443: LOG.fine("Component removed: "
444: + dumpComponent(component) + '\n');
445: logItemListFinest();
446: }
447: if (lastFocused) {
448: firePropertyChange(LAST_FOCUSED_REMOVED_PROPERTY,
449: component, lastFocusedComponent());
450: if (LOG.isLoggable(Level.FINE)) {
451: LOG.fine("Fired LAST_FOCUSED_REMOVED_PROPERTY for "
452: + dumpComponent(component) + '\n');
453: }
454: }
455: }
456: return item;
457: }
458:
459: static void checkItemListConsistency() {
460: Item item = items;
461: Item previous = null;
462: while (item != null) {
463: if (!item.linked)
464: throw new IllegalStateException("item=" + item
465: + " is in list but item.linked is false.");
466: if (item.previous != previous)
467: throw new IllegalStateException(
468: "Invalid previous of item=" + item);
469: if (item.ignoreAncestorChange
470: && (item.runningTimer != null))
471: throw new IllegalStateException("item=" + item
472: + " has running timer.");
473: if (item.focused && item != items)
474: throw new IllegalStateException(
475: "Non-first component has focused flag.");
476:
477: previous = item;
478: item = item.next;
479: }
480: }
481:
482: static void firePropertyChange(String propertyName,
483: Object oldValue, Object newValue) {
484: pcs.firePropertyChange(propertyName, oldValue, newValue);
485: }
486:
487: static void logItemListFinest() {
488: if (LOG.isLoggable(Level.FINEST)) {
489: LOG.finest(dumpItemList());
490: }
491: }
492:
493: private static String dumpItemList() {
494: StringBuilder sb = new StringBuilder(256);
495: sb.append("---------- EditorRegistry Dump START ----------\n");
496: int i = 0;
497: Item item = items;
498: while (item != null) {
499: ArrayUtilities.appendBracketedIndex(sb, i, 1);
500: sb.append(' ');
501: if (item.focused)
502: sb.append("Focused, ");
503: if (item.ignoreAncestorChange)
504: sb.append("IgnoreAncestorChange, ");
505: sb.append(dumpComponent(item.get()));
506: sb.append('\n');
507: item = item.next;
508: i++;
509: }
510: sb.append("---------- EditorRegistry Dump END ----------\n");
511: return sb.toString();
512: }
513:
514: static String dumpComponent(JComponent c) {
515: Object streamDesc = null;
516: if (c instanceof JTextComponent) {
517: Document doc = ((JTextComponent) c).getDocument();
518: if (doc != null) {
519: streamDesc = doc
520: .getProperty(Document.StreamDescriptionProperty);
521: }
522: }
523: return "component[IHC=" + System.identityHashCode(c) + "]:"
524: + ((streamDesc != null) ? streamDesc : c);
525: }
526:
527: /**
528: * Item of a single linked list of text component references.
529: */
530: private static final class Item extends
531: WeakReference<JTextComponent> {
532:
533: Item(JTextComponent c) {
534: super (c);
535: }
536:
537: /**
538: * Whether the item is contained in the item list - used for quicker checking
539: * than checking next/last vars (and possibly items var).
540: */
541: boolean linked;
542:
543: /**
544: * Whether this item is currently treated as focused.
545: */
546: boolean focused;
547:
548: /**
549: * Next item in items double linked list.
550: */
551: Item next;
552:
553: /**
554: * Previous item in items double linked list.
555: */
556: Item previous;
557:
558: /**
559: * Whether component should not be removed from registry upon removeNotify()
560: * since TabbedAdapter in NB winsys removes the component upon tab switching.
561: */
562: boolean ignoreAncestorChange;
563:
564: /**
565: * Timer for removal of component from registry after removeNotify() was called on component.
566: */
567: Timer runningTimer;
568:
569: @Override
570: public String toString() {
571: return "component=" + get() + ", linked=" + linked
572: + ", hasPrevious=" + (previous != null)
573: + ", hasNext=" + (next != null)
574: + ", ignoreAncestorChange=" + ignoreAncestorChange
575: + ", hasTimer=" + (runningTimer != null);
576: }
577:
578: }
579:
580: private static final class FocusL implements FocusListener {
581:
582: static final FocusL INSTANCE = new FocusL();
583:
584: public void focusGained(FocusEvent e) {
585: EditorRegistry.focusGained((JTextComponent) e.getSource(),
586: e.getOppositeComponent());
587:
588: }
589:
590: public void focusLost(FocusEvent e) {
591: EditorRegistry.focusLost((JTextComponent) e.getSource(), e
592: .getOppositeComponent());
593: }
594:
595: }
596:
597: private static final class PropertyDocL implements
598: PropertyChangeListener {
599:
600: static final PropertyDocL INSTANCE = new PropertyDocL();
601:
602: public void propertyChange(PropertyChangeEvent evt) {
603: if ("document".equals(evt.getPropertyName())) {
604: focusedDocumentChange((JTextComponent) evt.getSource(),
605: (Document) evt.getOldValue(), (Document) evt
606: .getNewValue());
607: }
608: }
609:
610: }
611:
612: private static final class AncestorL implements AncestorListener {
613:
614: static final AncestorL INSTANCE = new AncestorL();
615:
616: private static final int BEFORE_REMOVE_DELAY = 2000; // 2000ms delay
617:
618: public void ancestorAdded(AncestorEvent event) {
619: Item item = item(event.getComponent());
620: if (item.runningTimer != null) {
621: item.runningTimer.stop();
622: item.runningTimer = null;
623: }
624: itemMadeDisplayable(item);
625: }
626:
627: public void ancestorMoved(AncestorEvent event) {
628: }
629:
630: public void ancestorRemoved(AncestorEvent event) {
631: final JComponent component = event.getComponent();
632: Item item = item(component);
633: // In case the ancestor has class of certain type
634: // the ancestor removal is not significant and the registry expects
635: // that the closing of the component will be notified explicitly.
636: if (LOG.isLoggable(Level.FINER)) {
637: LOG.fine("ancestorRemoved for "
638: + dumpComponent(component)
639: + "; ignoreAncestorChange="
640: + item.ignoreAncestorChange + '\n');
641: }
642: if (!item.ignoreAncestorChange) {
643: // Only start timer when ancestor changes are not ignored.
644: item.runningTimer = new Timer(BEFORE_REMOVE_DELAY,
645: new ActionListener() {
646: public void actionPerformed(ActionEvent e) {
647: Item item = item(component);
648: item.runningTimer.stop();
649: item.runningTimer = null;
650: removeFromRegistry(item);
651: }
652: });
653: item.runningTimer.start();
654: }
655: }
656:
657: }
658:
659: private static final class PackageAccessor extends
660: EditorApiPackageAccessor {
661:
662: @Override
663: public void register(JTextComponent c) {
664: EditorRegistry.register(c);
665: }
666:
667: @Override
668: public void setIgnoredAncestorClass(Class ignoredAncestorClass) {
669: EditorRegistry
670: .setIgnoredAncestorClass(ignoredAncestorClass);
671: }
672:
673: @Override
674: public void notifyClose(JComponent c) {
675: EditorRegistry.notifyClose(c);
676: }
677:
678: }
679: }
|