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.modules.xml.text.completion;
043:
044: import java.awt.Container;
045: import java.awt.event.ActionListener;
046: import java.lang.reflect.InvocationTargetException;
047: import javax.swing.JEditorPane;
048: import javax.swing.Timer;
049: import javax.swing.event.CaretEvent;
050: import javax.swing.event.CaretListener;
051: import javax.swing.text.BadLocationException;
052: import javax.swing.text.Caret;
053: import javax.swing.text.Document;
054:
055: import org.netbeans.editor.BaseDocument;
056: import org.netbeans.modules.xml.api.model.GrammarQuery;
057: import org.netbeans.modules.xml.api.model.HintContext;
058: import org.netbeans.modules.xml.text.completion.XMLCompletionQuery;
059: import org.netbeans.modules.xml.text.completion.GrammarManager;
060: import org.netbeans.modules.xml.text.syntax.XMLSyntaxSupport;
061: import org.openide.nodes.FilterNode;
062: import org.openide.nodes.Node;
063: import org.openide.nodes.Sheet;
064: import org.openide.windows.TopComponent;
065: import org.openide.util.NbBundle;
066: import org.w3c.dom.DOMException;
067:
068: /**
069: * Finds SyntaxNode at the Carat position in the text editor
070: * and asks grammars to provide customizer and properties.
071: * The customizer and properties are added to the selected node in the editors
072: * TopComponent.
073: *
074: * @author asgeir@dimonsoftware.com
075: */
076: public final class NodeSelector {
077:
078: /** Listener on caret movements */
079: private CaretListener caretListener;
080:
081: /** Timer which countdowns the "update selected element node" time. */
082: // NOI18N
083: private Timer timerSelNodes;
084:
085: /** The last caret offset position. */
086: private int lastCaretOffset = -1;
087:
088: /** Default delay between cursor movement and updating selected element nodes. */
089: private static final int SELECTED_NODES_DELAY = 200;
090:
091: private JEditorPane pane;
092:
093: private XMLSyntaxSupport syntaxSupport;
094:
095: private Node originalUINode;
096:
097: HintContext hintContext;
098:
099: /** Creates a new instance of NodeSelector */
100: public NodeSelector(final JEditorPane pane) {
101: this .pane = pane;
102:
103: caretListener = new CaretListener() {
104: public void caretUpdate(CaretEvent e) {
105: restartTimerSelNodes(e.getDot());
106: }
107: };
108:
109: timerSelNodes = new Timer(100,
110: new java.awt.event.ActionListener() {
111: public void actionPerformed(
112: java.awt.event.ActionEvent e) {
113: if (lastCaretOffset == -1 && pane != null) {
114: Caret caret = pane.getCaret();
115: if (caret != null)
116: lastCaretOffset = caret.getDot();
117: }
118: selectElementsAtOffset(lastCaretOffset);
119: }
120: });
121: timerSelNodes.setInitialDelay(100);
122: timerSelNodes.setRepeats(false);
123: timerSelNodes.restart();
124:
125: pane.addCaretListener(caretListener);
126:
127: //!!! It should also listen on current TopComponent
128: }
129:
130: /** Restart the timer which updates the selected nodes after the specified delay from
131: * last caret movement.
132: */
133: void restartTimerSelNodes(int pos) {
134: timerSelNodes.setInitialDelay(SELECTED_NODES_DELAY);
135: lastCaretOffset = pos;
136: timerSelNodes.restart();
137: }
138:
139: /** Selects element at the given position. */
140: synchronized void selectElementsAtOffset(final int offset) {
141: if (syntaxSupport == null) {
142: Document doc = pane.getDocument();
143: if (doc instanceof BaseDocument) {
144: syntaxSupport = (XMLSyntaxSupport) ((BaseDocument) doc)
145: .getSyntaxSupport();
146: }
147: if (syntaxSupport == null) {
148: return;
149: }
150: }
151:
152: Container parent = pane.getParent();
153: while (parent != null && !(parent instanceof TopComponent)) {
154: parent = parent.getParent();
155: }
156: if (parent == null) {
157: return;
158: }
159:
160: TopComponent topComp = (TopComponent) parent;
161: Node activeNodes[] = topComp.getActivatedNodes();
162: if (activeNodes == null || activeNodes.length == 0) {
163: return; // No nodes active
164: }
165:
166: if (originalUINode == null) {
167: originalUINode = activeNodes[0];
168: }
169:
170: //it must be called from separate thread, it may the block UI thread
171:
172: GrammarQuery grammarQuery = XMLCompletionQuery.getPerformer(
173: pane.getDocument(), syntaxSupport);
174: if (grammarQuery == null) {
175: return;
176: }
177:
178: SyntaxQueryHelper helper = null;
179: try {
180: helper = new SyntaxQueryHelper(syntaxSupport, offset);
181: } catch (BadLocationException e) {
182: topComp.setActivatedNodes(new Node[] { new DelegatingNode(
183: originalUINode, null, null) });
184: return;
185: }
186:
187: Node newUiNode = new DelegatingNode(originalUINode,
188: grammarQuery, helper.getContext());
189:
190: topComp.setActivatedNodes(new Node[] { newUiNode });
191: }
192:
193: private class DelegatingNode extends FilterNode {
194:
195: GrammarQuery grammarQuery;
196:
197: HintContext hintContext;
198:
199: Sheet propSheet;
200:
201: public DelegatingNode(Node peer, GrammarQuery grammarQuery,
202: HintContext hintContext) {
203: super (peer);
204: this .grammarQuery = grammarQuery;
205: this .hintContext = hintContext;
206: }
207:
208: public java.awt.Component getCustomizer() {
209: if (grammarQuery == null || hintContext == null) {
210: return super .getCustomizer();
211: } else {
212: return grammarQuery.getCustomizer(hintContext);
213: }
214: }
215:
216: public boolean hasCustomizer() {
217: if (grammarQuery == null || hintContext == null) {
218: return super .hasCustomizer();
219: } else {
220: return grammarQuery.hasCustomizer(hintContext);
221: }
222: }
223:
224: public Node.PropertySet[] getPropertySets() {
225: if (propSheet == null) {
226: propSheet = Sheet.createDefault();
227: Sheet.Set nodePropertySet = propSheet
228: .get(Sheet.PROPERTIES);
229:
230: if (grammarQuery != null && hintContext != null) {
231: Node.Property[] nodeProperties = grammarQuery
232: .getProperties(hintContext);
233: if (nodeProperties != null
234: && nodeProperties.length > 0) {
235: // The GrammarQuery controls the properties
236: nodePropertySet.put(nodeProperties);
237: return propSheet.toArray();
238: }
239: }
240:
241: // By default, we try to create properties from the attributes of the
242: // selected element.
243: org.w3c.dom.Element attributeOwningElem = null;
244: if (hintContext != null) {
245: if (hintContext.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
246: attributeOwningElem = (org.w3c.dom.Element) hintContext;
247: } else if (hintContext.getNodeType() == org.w3c.dom.Node.ATTRIBUTE_NODE) {
248: attributeOwningElem = (org.w3c.dom.Element) ((org.w3c.dom.Attr) hintContext)
249: .getOwnerElement();
250: }
251: }
252:
253: if (attributeOwningElem != null) {
254: // We have a selected element that might have attributes
255: org.w3c.dom.NamedNodeMap attributes = attributeOwningElem
256: .getAttributes();
257: for (int ind = 0; ind < attributes.getLength(); ind++) {
258: org.w3c.dom.Node node = attributes.item(ind);
259: nodePropertySet
260: .put(new AttributeProperty(
261: attributeOwningElem, node
262: .getNodeName()));
263: }
264:
265: }
266: }
267:
268: return propSheet.toArray();
269: }
270: }
271:
272: /**
273: * It models attribute as node property.
274: */
275: private class AttributeProperty extends
276: org.openide.nodes.PropertySupport {
277: private final String propName;
278: private final org.w3c.dom.Element ownerElem;
279: private boolean canWrite = true;
280:
281: public AttributeProperty(org.w3c.dom.Element ownerElem,
282: String propName) {
283: super (propName, String.class, propName, propName, true,
284: true);
285: this .ownerElem = ownerElem;
286: this .propName = propName;
287: }
288:
289: public void setValue(Object value) {
290: try {
291: ownerElem.setAttribute(propName, (String) value);
292: } catch (DOMException ex) {
293: canWrite = false;
294: }
295: }
296:
297: public Object getValue() {
298: try {
299: return ownerElem.getAttribute(propName);
300: } catch (DOMException ex) {
301: // #29618 lifetime problem
302: canWrite = false;
303: return NbBundle
304: .getMessage(NodeSelector.class, "BK0001");
305: }
306: }
307:
308: /**
309: * Writeability can change during lifetime.
310: * So nobody can cache this flag.
311: */
312: public boolean canWrite() {
313: return canWrite;
314: }
315:
316: }
317: }
|