001: /*
002: * Sun Public License Notice
003: *
004: * The contents of this file are subject to the Sun Public License
005: * Version 1.0 (the "License"). You may not use this file except in
006: * compliance with the License. A copy of the License is available at
007: * http://www.sun.com/
008: *
009: * The Original Code is NetBeans. The Initial Developer of the Original
010: * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun
011: * Microsystems, Inc. All Rights Reserved.
012: */
013:
014: package org.netbeans.editor.ext;
015:
016: import java.awt.event.ActionEvent;
017: import java.awt.event.ActionListener;
018: import java.beans.PropertyChangeEvent;
019: import java.beans.PropertyChangeListener;
020:
021: import javax.swing.SwingUtilities;
022: import javax.swing.Timer;
023: import javax.swing.event.CaretEvent;
024: import javax.swing.event.CaretListener;
025: import javax.swing.event.DocumentEvent;
026: import javax.swing.event.DocumentListener;
027: import javax.swing.text.BadLocationException;
028: import javax.swing.text.JTextComponent;
029:
030: import org.netbeans.editor.BaseDocument;
031: import org.netbeans.editor.Settings;
032: import org.netbeans.editor.SettingsChangeEvent;
033: import org.netbeans.editor.SettingsChangeListener;
034: import org.netbeans.editor.SettingsUtil;
035: import org.netbeans.editor.Utilities;
036: import org.netbeans.editor.WeakTimerListener;
037:
038: /**
039: * General Completion display formatting and services
040: *
041: * @author Miloslav Metelka
042: * @version 1.00
043: */
044:
045: public class Completion implements PropertyChangeListener,
046: SettingsChangeListener, ActionListener {
047:
048: /** Editor UI supporting this completion */
049: protected ExtEditorUI extEditorUI;
050:
051: /** Completion query providing query support for this completion */
052: private CompletionQuery query;
053:
054: /**
055: * Last result retrieved for completion. It can become null if the document
056: * was modified so the replacement position would be invalid.
057: */
058: private CompletionQuery.Result lastResult;
059:
060: /** Completion view component displaying the completion help */
061: private CompletionView view;
062:
063: /**
064: * Component (usually scroll-pane) holding the view and the title and
065: * possibly other necessary components.
066: */
067: private CompletionPane pane;
068:
069: private boolean autoPopup;
070:
071: private int autoPopupDelay;
072:
073: private int refreshDelay;
074:
075: Timer timer;
076:
077: private DocumentListener docL;
078: private CaretListener caretL;
079:
080: private PropertyChangeListener docChangeL;
081:
082: private int caretPos = -1;
083:
084: public Completion(ExtEditorUI extEditorUI) {
085: this .extEditorUI = extEditorUI;
086:
087: // Initialize timer
088: timer = new Timer(0, new WeakTimerListener(this )); // delay will be set
089: // later
090: timer.setRepeats(false);
091:
092: // Create document listener
093: docL = new DocumentListener() {
094: public void insertUpdate(DocumentEvent evt) {
095: if (evt.getLength() > 0) {
096: invalidateLastResult();
097: refresh(false);
098: }
099: }
100:
101: public void removeUpdate(DocumentEvent evt) {
102: if (evt.getLength() > 0) {
103: invalidateLastResult();
104: refresh(false);
105: }
106: }
107:
108: public void changedUpdate(DocumentEvent evt) {
109: }
110: };
111:
112: caretL = new CaretListener() {
113: public void caretUpdate(CaretEvent e) {
114: if (!isPaneVisible()) {
115:
116: // cancel timer if caret moved
117: cancelRequest();
118: } else {
119:
120: // refresh completion only if a pane is already visible
121: refresh(true);
122: }
123: }
124: };
125:
126: Settings.addSettingsChangeListener(this );
127:
128: synchronized (extEditorUI.getComponentLock()) {
129: // if component already installed in ExtEditorUI simulate
130: // installation
131: JTextComponent component = extEditorUI.getComponent();
132: if (component != null) {
133: propertyChange(new PropertyChangeEvent(extEditorUI,
134: ExtEditorUI.COMPONENT_PROPERTY, null, component));
135: }
136:
137: extEditorUI.addPropertyChangeListener(this );
138: }
139: }
140:
141: public void settingsChange(SettingsChangeEvent evt) {
142: Class kitClass = Utilities.getKitClass(extEditorUI
143: .getComponent());
144:
145: if (kitClass != null) {
146: autoPopup = SettingsUtil.getBoolean(kitClass,
147: ExtSettingsNames.COMPLETION_AUTO_POPUP,
148: ExtSettingsDefaults.defaultCompletionAutoPopup);
149:
150: autoPopupDelay = SettingsUtil
151: .getInteger(
152: kitClass,
153: ExtSettingsNames.COMPLETION_AUTO_POPUP_DELAY,
154: ExtSettingsDefaults.defaultCompletionAutoPopupDelay);
155:
156: refreshDelay = SettingsUtil.getInteger(kitClass,
157: ExtSettingsNames.COMPLETION_REFRESH_DELAY,
158: ExtSettingsDefaults.defaultCompletionRefreshDelay);
159: }
160: }
161:
162: public void propertyChange(PropertyChangeEvent evt) {
163: String propName = evt.getPropertyName();
164:
165: if (ExtEditorUI.COMPONENT_PROPERTY.equals(propName)) {
166: JTextComponent component = (JTextComponent) evt
167: .getNewValue();
168: if (component != null) { // just installed
169:
170: settingsChange(null);
171:
172: BaseDocument doc = Utilities.getDocument(component);
173: if (doc != null) {
174: doc.addDocumentListener(docL);
175: }
176:
177: component.addCaretListener(caretL);
178: } else { // just deinstalled
179: component = (JTextComponent) evt.getOldValue();
180:
181: BaseDocument doc = Utilities.getDocument(component);
182: if (doc != null) {
183: doc.removeDocumentListener(docL);
184: }
185:
186: if (component != null) {
187: component.removeCaretListener(caretL);
188: }
189: }
190:
191: } else if ("document".equals(propName)) { // NOI18N
192: if (evt.getOldValue() instanceof BaseDocument) {
193: ((BaseDocument) evt.getOldValue())
194: .removeDocumentListener(docL);
195: }
196: if (evt.getNewValue() instanceof BaseDocument) {
197: ((BaseDocument) evt.getNewValue())
198: .addDocumentListener(docL);
199: }
200:
201: }
202:
203: }
204:
205: public CompletionPane getPane() {
206: if (pane == null) {
207: pane = new ScrollCompletionPane(extEditorUI);
208: }
209: return pane;
210: }
211:
212: protected CompletionView createView() {
213: return new ListCompletionView();
214: }
215:
216: public final CompletionView getView() {
217: if (view == null) {
218: view = createView();
219: }
220: return view;
221: }
222:
223: protected CompletionQuery createQuery() {
224: return null;
225: }
226:
227: public final CompletionQuery getQuery() {
228: if (query == null) {
229: query = createQuery();
230: }
231: return query;
232: }
233:
234: /**
235: * Get the result of the last valid completion query or null if there's no
236: * valid result available.
237: */
238: public synchronized final CompletionQuery.Result getLastResult() {
239: return lastResult;
240: }
241:
242: /**
243: * Reset the result of the last valid completion query. This is done for
244: * example after the document was modified.
245: */
246: public synchronized final void invalidateLastResult() {
247: lastResult = null;
248: }
249:
250: public synchronized Object getSelectedValue() {
251: if (lastResult != null) {
252: int index = getView().getSelectedIndex();
253: if (index >= 0) {
254: return lastResult.getData().get(index);
255: }
256: }
257: return null;
258: }
259:
260: /** Return true if the completion should popup automatically */
261: public boolean isAutoPopupEnabled() {
262: return autoPopup;
263: }
264:
265: /**
266: * Return true when the pane exists and is visible. This is the preferred
267: * method of testing the visibility of the pane instead of
268: * <tt>getPane().isVisible()</tt> that forces the creation of the pane.
269: */
270: public boolean isPaneVisible() {
271: return (pane != null && pane.isVisible());
272: }
273:
274: /**
275: * Set the visibility of the view. This method should be used mainly for
276: * hiding the completion pane. If used with visible set to true it calls the
277: * <tt>popup(false)</tt>.
278: */
279: public void setPaneVisible(boolean visible) {
280:
281: if (visible) {
282: if (extEditorUI.getComponent() != null) {
283: popup(false);
284: }
285: } else {
286: if (pane != null) {
287: cancelRequest();
288: invalidateLastResult();
289: pane.setVisible(false);
290: caretPos = -1;
291: }
292: }
293: }
294:
295: /**
296: * Refresh the contents of the view if it's currently visible.
297: *
298: * @param postRequest
299: * post the request instead of refreshing the view immediately.
300: * The <tt>ExtSettingsNames.COMPLETION_REFRESH_DELAY</tt>
301: * setting stores the number of milliseconds before the view is
302: * refreshed.
303: */
304: public synchronized void refresh(boolean postRequest) {
305: final boolean post = postRequest;
306:
307: SwingUtilities.invokeLater(new Runnable() {
308: public void run() {
309: if (isPaneVisible()) {
310: timer.stop();
311: if (post) {
312: timer.setInitialDelay(refreshDelay);
313: timer.setDelay(refreshDelay);
314: timer.start();
315: } else {
316: actionPerformed(null);
317: }
318: }
319: }
320: });
321: }
322:
323: /**
324: * Get the help and show it in the view. If the view is already visible
325: * perform the refresh of the view.
326: *
327: * @param postRequest
328: * post the request instead of displaying the view immediately.
329: * The <tt>ExtSettingsNames.COMPLETION_AUTO_POPUP_DELAY</tt>
330: * setting stores the number of milliseconds before the view is
331: * displayed. If the user presses a key until the delay expires
332: * nothing is shown. This guarantees that the user which knows
333: * what to write will not be annoyed with the unnecessary help.
334: */
335: public synchronized void popup(boolean postRequest) {
336: if (isPaneVisible()) {
337:
338: refresh(postRequest);
339: } else {
340: timer.stop();
341: if (postRequest) {
342:
343: timer.setInitialDelay(autoPopupDelay);
344: timer.setDelay(autoPopupDelay);
345: timer.start();
346: } else {
347: actionPerformed(null);
348: }
349: }
350: }
351:
352: /**
353: * Cancel last request for either displaying or refreshing the pane. It
354: * resets the internal timer.
355: */
356: public synchronized void cancelRequest() {
357:
358: timer.stop();
359: caretPos = -1;
360: }
361:
362: /**
363: * Called to do either displaying or refreshing of the view. This method can
364: * be called either directly or because of the timer has fired.
365: *
366: * @param evt
367: * event describing the timer firing or null if the method was
368: * called directly because of the synchronous showing/refreshing
369: * the view.
370: */
371: public synchronized void actionPerformed(ActionEvent evt) {
372: JTextComponent component = extEditorUI.getComponent();
373: BaseDocument doc = Utilities.getDocument(component);
374:
375: if (component != null && doc != null) {
376: if (evt != null) {
377: // AutoPopup performed, check whether the sources are prepared
378: // for completion
379: ExtSyntaxSupport sup = (ExtSyntaxSupport) doc
380: .getSyntaxSupport().get(ExtSyntaxSupport.class);
381: if (sup != null) {
382:
383: if (!sup.isPrepared()) {
384:
385: return;
386: }
387: }
388: }
389:
390: try {
391:
392: if (caretPos > doc.getLength())
393: caretPos = doc.getLength();
394:
395: // System.out.println( "caretPos = " + caretPos + "
396: // component.getCaret().getDot() = " +
397: // component.getCaret().getDot() );
398: // System.out.println( " Utilities.getRowStart1 = " +
399: // Utilities.getRowStart(component,component.getCaret().getDot()
400: // ) );
401: // if ( caretPos != -1 )
402: // System.out.println( " Utilities.getRowStart2 = " +
403: // Utilities.getRowStart(component,caretPos ) );
404:
405: if ((caretPos != -1)
406: && (Utilities.getRowStart(component, component
407: .getCaret().getDot()) != Utilities
408: .getRowStart(component, caretPos))) {
409:
410: getPane().setVisible(false);
411: caretPos = -1;
412: return;
413: }
414: } catch (BadLocationException ble) {
415: ble.printStackTrace();
416: }
417:
418: caretPos = component.getCaret().getDot();
419: lastResult = getQuery().query(component, caretPos,
420: doc.getSyntaxSupport());
421:
422: SwingUtilities.invokeLater(new Runnable() {
423: public void run() {
424: CompletionQuery.Result res = lastResult;
425: if (res != null) {
426: getPane().setTitle(res.getTitle());
427: getView().setResult(res);
428: if (isPaneVisible()) {
429: getPane().refresh();
430: } else {
431: getPane().setVisible(true);
432: }
433: } else {
434: getPane().setVisible(false);
435: caretPos = -1;
436: }
437: }
438: });
439: } else {
440: System.out
441: .println("Completion.actionPerformed null component or document ");
442: }
443: }
444:
445: /**
446: * Substitute the document's text with the text that is appopriate for the
447: * selection in the view. This function is usually triggered upon pressing
448: * the Enter key.
449: *
450: * @return true if the substitution was performed false if not.
451: */
452: public synchronized boolean substituteText(boolean shift) {
453: if (lastResult != null) {
454: int index = getView().getSelectedIndex();
455: if (index >= 0) {
456: lastResult.substituteText(index, shift);
457: }
458: return true;
459: } else {
460: return false;
461: }
462: }
463:
464: /**
465: * Substitute the text with the longest common part of all the entries
466: * appearing in the view. This function is usually triggered upon pressing
467: * the Tab key.
468: *
469: * @return true if the substitution was performed false if not.
470: */
471: public synchronized boolean substituteCommonText() {
472: if (lastResult != null) {
473: int index = getView().getSelectedIndex();
474: if (index >= 0) {
475: lastResult.substituteCommonText(index);
476: }
477: return true;
478: } else {
479: return false;
480: }
481: }
482:
483: }
|