Index: lib2/apichanges.xml =================================================================== RCS file: /cvs/editor/lib2/apichanges.xml,v retrieving revision 1.3 diff -u -r1.3 apichanges.xml --- lib2/apichanges.xml 26 Jan 2007 04:58:54 -0000 1.3 +++ lib2/apichanges.xml 4 Jun 2007 09:05:24 -0000 @@ -82,6 +82,19 @@ + + EditorRegistry added + + + + + + The EditorRegistry allows to get focused and last focused text component + and list of all registered components and it allows to listen for currently focused + component and changes of documents in it. + + + AttributesUtilities removed Index: lib2/nbproject/project.xml =================================================================== RCS file: /cvs/editor/lib2/nbproject/project.xml,v retrieving revision 1.3 diff -u -r1.3 project.xml --- lib2/nbproject/project.xml 26 Jan 2007 04:58:50 -0000 1.3 +++ lib2/nbproject/project.xml 4 Jun 2007 09:05:24 -0000 @@ -111,6 +111,7 @@ + org.netbeans.api.editor org.netbeans.spi.editor.highlighting org.netbeans.spi.editor.highlighting.support Index: lib2/src/org/netbeans/api/editor/EditorRegistry.java =================================================================== RCS file: lib2/src/org/netbeans/api/editor/EditorRegistry.java diff -N lib2/src/org/netbeans/api/editor/EditorRegistry.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ lib2/src/org/netbeans/api/editor/EditorRegistry.java 4 Jun 2007 09:05:24 -0000 @@ -0,0 +1,363 @@ +/* + * The contents of this file are subject to the terms of the Common Development + * and Distribution License (the License). You may not use this file except in + * compliance with the License. + * + * You can obtain a copy of the License at http://www.netbeans.org/cddl.html + * or http://www.netbeans.org/cddl.txt. + * + * When distributing Covered Code, include this CDDL Header Notice in each file + * and include the License file at http://www.netbeans.org/cddl.txt. + * If applicable, add the following below the CDDL Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun + * Microsystems, Inc. All Rights Reserved. + */ + +package org.netbeans.api.editor; + +import java.awt.Component; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import org.netbeans.lib.editor.util.ArrayUtilities; + +/** + * Registry maintaining {@link JTextComponent}s in most-recently-used order. + *
+ * The particular text component needs to register itself first (to avoid dealing + * with all the JTextFields etc.). Then the registry will attach + * a focus listener to the text component and once the component gains + * the focus it will move to the head of the components list. + *
+ * The registry will also fire a change in case a document property + * of the focused component changes (by calling component.setDocument()). + * + * @author Miloslav Metelka + */ +public class EditorRegistry { + + // -J-Dorg.netbeans.api.editor.EditorRegistry.level=FINEST + private static final Logger LOG = Logger.getLogger(EditorRegistry.class.getName()); + + /** + * Fired when focus was delivered to a registered text component. + *
+ * The focused component will become the first in the components list. + *
+ * The {@link PropertyEvent#getOldValue()} will be the a component + * losing the focus {@link FocusEvent#getOppositeComponent()}. + * The {@link PropertyEvent#getNewValue()} will be the text component gaining the focus. + */ + public static final String FOCUS_GAINED_PROPERTY = "focusGained"; + + /** + * Fired when a registered focused component has lost the focus. + *
+ * The focused component will remain the first in the components list. + *
+ * The {@link PropertyEvent#getOldValue()} will be the text component + * losing the focus and the {@link PropertyEvent#getNewValue()} + * will be the component gaining the focus {@link FocusEvent#getOppositeComponent()}. + */ + public static final String FOCUS_LOST_PROPERTY = "focusLost"; + + /** + * Fired when document property of the focused component changes + * i.e. someone has called {@link JTextComponent#setDocument(Document)}. + *
+ * The {@link PropertyEvent#getOldValue()} will be the original document + * of the focused text component and the {@link PropertyEvent#getNewValue()} + * will be the new document set to the focused text component. + */ + public static final String FOCUSED_DOCUMENT_PROPERTY = "focusedDocument"; + + /** + * Double linked list of weak references to text components. + */ + private static Item textComponentRefs; + + private static final PropertyChangeSupport pcs = new PropertyChangeSupport(EditorRegistry.class); + + + /** + * Return last focused text component (from the ones included in the registry). + *
+ * It may or may not currently have a focus. + * + * @return last focused text component or null if no text components + * were registered yet. + */ + public static synchronized JTextComponent lastFocusedComponent() { + return firstValidComponent(); + } + + /** + * Return the last focused component if it currently has a focus + * or return null if none of the registered components currently have the focus. + *
+ * @return focused component or null if none of the registered components + * is currently focused. + */ + public static synchronized JTextComponent focusedComponent() { + JTextComponent c = firstValidComponent(); + return (c != null && c.isFocusOwner()) ? c : null; + } + + /** + * Get list of all components present in the registry starting with the most active + * and ending with least active component. + *
+ * The list is a snapshot of the current state and it may be modified + * by the caller if desired. + * + * @return non-null list containing all the registered components in MRU order. + */ + public static synchronized List componentList() { + List l; + JTextComponent c = firstValidComponent(); + if (c != null) { + l = new ArrayList(); + l.add(c); + // Add remaining ones (eliminate empty items) + Item item = textComponentRefs.next; + while (item != null) { + c = item.get(); + if (c != null) { + l.add(c); + item = item.next; + } else + item = removeItem(item); + } + + } else // No valid items + l = Collections.emptyList(); + return l; + } + + /** + * Add a given text component to the registry. The registry will weakly + * reference the given component for its whole lifetime + * until it will be garbage collected. + * + * @param c non-null text component to be registered. + */ + public static synchronized void register(JTextComponent c) { + assert (c != null); + if (c.getClientProperty(Item.class) == null) { // Not registered yet + Item item = new Item(c); + c.putClientProperty(Item.class, item); + c.addFocusListener(FocusL.INSTANCE); + // Add to end of list + if (textComponentRefs == null) + textComponentRefs = item; + else { + Item i = textComponentRefs; + while (i.next != null) + i = i.next; + i.next = item; + item.previous = i; + } + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "REGISTERED new component as last item:\n" + dumpItemList()); + } + + // If focused (rare since usually registered early when not focused yet) + if (c.isFocusOwner()) { + focusGained(c, null); // opposite could eventually be got from Focus Manager + } + } + } + + /** + * Add a property change listener for either of the following properties: + *
    + *
  • {@link #FOCUS_GAINED_PROPERTY}
  • + *
  • {@link #FOCUS_LOST_PROPERTY}
  • + *
  • {@link #FOCUSED_DOCUMENT_PROPERTY}
  • + *
. + *
+ * All the firing should occur in AWT thread only + * (assuming the JTextComponent.setDocument() is done properly in AWT). + * + * @param l non-null listener to add. + */ + public static void addPropertyChangeListener(PropertyChangeListener l) { + pcs.addPropertyChangeListener(l); + } + + public static void removePropertyChangeListener(PropertyChangeListener l) { + pcs.removePropertyChangeListener(l); + } + + static synchronized void focusGained(JTextComponent c, Component origFocused) { + Item item = (Item)c.getClientProperty(Item.class); + assert (item != null) : "Not registered!"; // NOI18N + assert (item.next != null || item.previous != null || textComponentRefs == item) + : "Already released!"; // NOI18N + moveToHead(item); + c.addPropertyChangeListener(PropertyDocL.INSTANCE); + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, FOCUS_GAINED_PROPERTY + ": " + dumpComponent(c) + '\n'); + } + firePropertyChange(FOCUS_GAINED_PROPERTY, origFocused, c); + } + + static void focusLost(JTextComponent c, Component newFocused) { + c.removePropertyChangeListener(PropertyDocL.INSTANCE); + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, FOCUS_LOST_PROPERTY + ": " + dumpComponent(c) + '\n'); + } + firePropertyChange(FOCUS_LOST_PROPERTY, c, newFocused); + } + + static void focusedDocumentChange(JTextComponent c, Document oldDoc, Document newDoc) { + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, FOCUSED_DOCUMENT_PROPERTY + ": " + dumpComponent(c) + + "\n OLDDoc=" + oldDoc + "\n NEWDoc=" + newDoc + '\n'); + } + firePropertyChange(FOCUSED_DOCUMENT_PROPERTY, oldDoc, newDoc); + } + + private static JTextComponent firstValidComponent() { + JTextComponent c = null; + while (textComponentRefs != null && (c = textComponentRefs.get()) == null) { + removeItem(textComponentRefs); + } + return c; + } + + /** + * Remove given entry and return a next one. + */ + private static Item removeItem(Item item) { + Item next = item.next; + if (item.previous == null) { // Head + assert (textComponentRefs == item); + textComponentRefs = next; + } else { // Not head + item.previous.next = next; + } + if (next != null) + next.previous = item.previous; + item.next = item.previous = null; + return next; + } + + private static void moveToHead(Item item) { + if (LOG.isLoggable(Level.FINEST)) { // Debugging + isItemInList(item); + } + removeItem(item); + item.next = textComponentRefs; + if (textComponentRefs != null) + textComponentRefs.previous = item; + textComponentRefs = item; + if (LOG.isLoggable(Level.FINEST)) { // Debugging + isItemInList(item); + checkItemListConsistency(); + } + } + + private static void firePropertyChange(String propertyName, Object oldValue, Object newValue) { + pcs.firePropertyChange(propertyName, oldValue, newValue); + } + + private static boolean isItemInList(Item item) { + Item i = textComponentRefs; + while (i != null) { + if (i == item) + return true; + i = i.next; + } + return false; + } + + private static void checkItemListConsistency() { + Item item = textComponentRefs; + Item previous = null; + while (item != null) { + assert item.previous == previous; + previous = item; + item = item.next; + } + if (previous != null) + assert previous.next == null; + } + + private static String dumpItemList() { + StringBuilder sb = new StringBuilder(); + int i = 0; + Item item = textComponentRefs; + while (item != null) { + ArrayUtilities.appendBracketedIndex(sb, i, 1); + sb.append(dumpComponent(item.get())); + sb.append('\n'); + item = item.next; + i++; + } + sb.append('\n'); // One extra delimiting newline + return sb.toString(); + } + + private static String dumpComponent(JTextComponent c) { + return "c[IHC=" + System.identityHashCode(c) + + "]=" + c; + } + + /** + * Item of a single linked list of text component references. + */ + private static final class Item extends WeakReference { + + Item(JTextComponent c) { + super(c); + } + + Item next; + + Item previous; + + } + + private static final class FocusL implements FocusListener { + + static final FocusL INSTANCE = new FocusL(); + + public void focusGained(FocusEvent e) { + EditorRegistry.focusGained((JTextComponent)e.getSource(), e.getOppositeComponent()); + + } + + public void focusLost(FocusEvent e) { + EditorRegistry.focusLost((JTextComponent)e.getSource(), e.getOppositeComponent()); + } + + } + + private static final class PropertyDocL implements PropertyChangeListener { + + static final PropertyDocL INSTANCE = new PropertyDocL(); + + public void propertyChange(PropertyChangeEvent evt) { + if ("document".equals(evt.getPropertyName())) { + focusedDocumentChange((JTextComponent)evt.getSource(), (Document)evt.getOldValue(), (Document)evt.getNewValue()); + } + } + + } + +} Index: lib2/test/unit/src/org/netbeans/api/editor/EditorRegistryTest.java =================================================================== RCS file: lib2/test/unit/src/org/netbeans/api/editor/EditorRegistryTest.java diff -N lib2/test/unit/src/org/netbeans/api/editor/EditorRegistryTest.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ lib2/test/unit/src/org/netbeans/api/editor/EditorRegistryTest.java 4 Jun 2007 09:05:25 -0000 @@ -0,0 +1,146 @@ +/* + * The contents of this file are subject to the terms of the Common Development + * and Distribution License (the License). You may not use this file except in + * compliance with the License. + * + * You can obtain a copy of the License at http://www.netbeans.org/cddl.html + * or http://www.netbeans.org/cddl.txt. + * + * When distributing Covered Code, include this CDDL Header Notice in each file + * and include the License file at http://www.netbeans.org/cddl.txt. + * If applicable, add the following below the CDDL Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun + * Microsystems, Inc. All Rights Reserved. + */ + +package org.netbeans.api.editor; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.List; +import javax.swing.JEditorPane; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import org.netbeans.junit.NbTestCase; + +/** + * Tests of editor registry. + * + * @author Miloslav Metelka + */ +public class EditorRegistryTest extends NbTestCase { + + public EditorRegistryTest(String name) { + super(name); + } + + public void testRegistry() throws Exception { + // Start listening + EditorRegistry.addPropertyChangeListener(EditorRegistryListener.INSTANCE); + + // Test registration + JTextComponent c1 = new JEditorPane(); + EditorRegistry.register(c1); + JTextComponent c2 = new JEditorPane(); + EditorRegistry.register(c2); + List jtcList = EditorRegistry.componentList(); + assertSame(2, jtcList.size()); + assertSame(c1, jtcList.get(0)); + assertSame(c2, jtcList.get(1)); + + // Ignore repetitive registration + EditorRegistry.register(c2); + EditorRegistry.register(c2); + assertSame(2, EditorRegistry.componentList().size()); + + // Extra component + JTextComponent c3 = new JEditorPane(); + EditorRegistry.register(c3); + assertSame(3, EditorRegistry.componentList().size()); + + // Simulate focusGained + EditorRegistry.focusGained(c3, null); + assertSame(1, EditorRegistryListener.INSTANCE.firedCount); + assertSame(c3, EditorRegistryListener.INSTANCE.newValue); + assertSame(null, EditorRegistryListener.INSTANCE.oldValue); + EditorRegistryListener.INSTANCE.reset(); // Reset to 0 + + jtcList = EditorRegistry.componentList(); + assertSame(3, jtcList.size()); + assertSame(c3, jtcList.get(0)); + + // Simulate document change of focused component + Document oldDoc = c3.getDocument(); + Document newDoc = c3.getUI().getEditorKit(c3).createDefaultDocument(); + c3.setDocument(newDoc); + assertSame(1, EditorRegistryListener.INSTANCE.firedCount); + assertSame(newDoc, EditorRegistryListener.INSTANCE.newValue); + assertSame(oldDoc, EditorRegistryListener.INSTANCE.oldValue); + EditorRegistryListener.INSTANCE.reset(); // Reset to 0 + oldDoc = null; + newDoc = null; + + // Simulate focusLost + EditorRegistry.focusLost(c3, null); + assertSame(1, EditorRegistryListener.INSTANCE.firedCount); + assertSame(null, EditorRegistryListener.INSTANCE.newValue); + assertSame(c3, EditorRegistryListener.INSTANCE.oldValue); + EditorRegistryListener.INSTANCE.reset(); // Reset to 0 + + EditorRegistry.focusGained(c1, null); + assertSame(1, EditorRegistryListener.INSTANCE.firedCount); + assertSame(c1, EditorRegistryListener.INSTANCE.newValue); + assertSame(null, EditorRegistryListener.INSTANCE.oldValue); + EditorRegistryListener.INSTANCE.reset(); // Reset to 0 + + // Partial GC: c3 + c3 = null; + jtcList = null; + System.gc(); + assertSame(2, EditorRegistry.componentList().size()); + + // Test full GC + jtcList = null; + c1 = null; + c2 = null; + c3 = null; + EditorRegistryListener.INSTANCE.reset(); + System.gc(); + assertSame(0, EditorRegistry.componentList().size()); + + + } + + private static final class EditorRegistryListener implements PropertyChangeListener { + + static final EditorRegistryListener INSTANCE = new EditorRegistryListener(); + + int firedCount; + + String propertyName; + + Object oldValue; + + Object newValue; + + public void propertyChange(PropertyChangeEvent evt) { + firedCount++; + propertyName = evt.getPropertyName(); + oldValue = evt.getOldValue(); + newValue = evt.getNewValue(); + } + + public void reset() { + firedCount = 0; + propertyName = null; + oldValue = null; + newValue = null; + } + + } + +} Index: completion/nbproject/project.xml =================================================================== RCS file: /cvs/editor/completion/nbproject/project.xml,v retrieving revision 1.4 diff -u -r1.4 project.xml --- completion/nbproject/project.xml 30 Jun 2006 19:17:29 -0000 1.4 +++ completion/nbproject/project.xml 4 Jun 2007 09:05:25 -0000 @@ -40,7 +40,7 @@ - org.netbeans.modules.editor.util + org.netbeans.modules.editor.lib2 @@ -55,13 +55,14 @@ 1 - + - org.openide.util + org.netbeans.modules.editor.util - 6.2 + 1 + 1.3 @@ -74,6 +75,14 @@ org.openide.modules + + + + 6.2 + + + + org.openide.util Index: completion/src/org/netbeans/modules/editor/completion/CompletionImpl.java =================================================================== RCS file: /cvs/editor/completion/src/org/netbeans/modules/editor/completion/CompletionImpl.java,v retrieving revision 1.69 diff -u -r1.69 CompletionImpl.java --- completion/src/org/netbeans/modules/editor/completion/CompletionImpl.java 29 May 2007 14:18:30 -0000 1.69 +++ completion/src/org/netbeans/modules/editor/completion/CompletionImpl.java 4 Jun 2007 09:05:25 -0000 @@ -21,6 +21,8 @@ import java.awt.*; import java.awt.event.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; @@ -34,11 +36,11 @@ import javax.swing.*; import javax.swing.event.ListSelectionListener; import javax.swing.event.CaretListener; -import javax.swing.event.ChangeListener; import javax.swing.event.DocumentListener; import javax.swing.plaf.TextUI; import javax.swing.text.*; +import org.netbeans.api.editor.EditorRegistry; import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.mimelookup.MimePath; import org.netbeans.editor.BaseDocument; @@ -48,7 +50,6 @@ import org.netbeans.editor.SettingsNames; import org.netbeans.lib.editor.util.swing.DocumentUtilities; import org.netbeans.lib.editor.util.swing.DocumentListenerPriority; -import org.netbeans.editor.Registry; import org.netbeans.editor.Utilities; import org.netbeans.editor.ext.ExtKit; import org.netbeans.spi.editor.completion.*; @@ -72,7 +73,7 @@ */ public class CompletionImpl extends MouseAdapter implements DocumentListener, -CaretListener, KeyListener, FocusListener, ListSelectionListener, ChangeListener, SettingsChangeListener { +CaretListener, KeyListener, FocusListener, ListSelectionListener, PropertyChangeListener, SettingsChangeListener { private static final boolean debug = Boolean.getBoolean("org.netbeans.modules.editor.completion.debug"); private static final boolean alphaSort = Boolean.getBoolean("org.netbeans.modules.editor.completion.alphabeticalSort"); // [TODO] create an option @@ -176,7 +177,7 @@ private boolean pleaseWaitDisplayed = false; private CompletionImpl() { - Registry.addChangeListener(this); + EditorRegistry.addPropertyChangeListener(this); completionAutoPopupTimer = new Timer(0, new ActionListener() { public void actionPerformed(ActionEvent e) { Result localCompletionResult; @@ -361,11 +362,11 @@ /** * Expected to be called from the AWT only. */ - public void stateChanged(javax.swing.event.ChangeEvent e) { + public void propertyChange(PropertyChangeEvent e) { assert (SwingUtilities.isEventDispatchThread()); // expected in AWT only boolean cancel = false; - JTextComponent component = Registry.getMostActiveComponent(); + JTextComponent component = EditorRegistry.lastFocusedComponent(); if (component != getActiveComponent()) { activeProviders = getCompletionProvidersForComponent(component); if (debug) { @@ -401,8 +402,8 @@ installKeybindings(); cancel = true; } - Document document = Registry.getMostActiveDocument(); - if (component != null && document == component.getDocument() && document != getActiveDocument()) { + Document document = component.getDocument(); + if (component != null && document != getActiveDocument()) { activeProviders = getCompletionProvidersForComponent(component); if (debug) { StringBuffer sb = new StringBuffer("Completion PROVIDERS:\n"); // NOI18N