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-2007 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: package org.netbeans.modules.openfile;
042:
043: import java.awt.Container;
044: import java.awt.EventQueue;
045: import java.awt.event.ActionEvent;
046: import java.beans.PropertyChangeEvent;
047: import java.beans.PropertyChangeListener;
048: import java.io.IOException;
049: import javax.swing.Action;
050: import javax.swing.JEditorPane;
051: import javax.swing.SwingUtilities;
052: import javax.swing.text.Element;
053: import javax.swing.text.StyledDocument;
054: import org.openide.DialogDisplayer;
055: import org.openide.ErrorManager;
056: import org.openide.NotifyDescriptor;
057: import org.openide.actions.FileSystemAction;
058: import org.openide.actions.ToolsAction;
059: import org.openide.awt.StatusDisplayer;
060: import org.openide.cookies.EditCookie;
061: import org.openide.cookies.EditorCookie;
062: import org.openide.cookies.OpenCookie;
063: import org.openide.cookies.ViewCookie;
064: import org.openide.filesystems.FileObject;
065: import org.openide.filesystems.FileUtil;
066: import org.openide.loaders.DataObject;
067: import org.openide.loaders.DataObjectNotFoundException;
068: import org.openide.nodes.Node;
069: import org.openide.nodes.NodeOperation;
070: import org.openide.text.NbDocument;
071: import org.openide.util.NbBundle;
072: import org.openide.windows.TopComponent;
073:
074: /**
075: * Opens files when requested. Main functionality.
076: *
077: * @author Jaroslav Tulach, Jesse Glick, Marian Petras, David Konecny
078: */
079: public class DefaultOpenFileImpl implements OpenFileImpl, Runnable {
080:
081: /** extenstion for .java files (including the dot) */
082: static final String JAVA_EXT = ".JAVA"; //NOI18N
083: /** extension for .txt files (including the dot) */
084: static final String TXT_EXT = ".TXT"; //NOI18N
085: /**
086: * if opening file using non-observable <code>EditorCookie</code>,
087: * how long should we wait (in milliseconds) between tries?
088: *
089: * @see #openDocAtLine
090: */
091: private static final int OPEN_EDITOR_WAIT_PERIOD_MS = 100;
092: /**
093: * if opening file using non-observable <code>EditorCookie</code>,
094: * how long should we wait (in milliseconds) in total before giving up?
095: *
096: * @see #openDocAtLine
097: */
098: private static final int OPEN_EDITOR_TOTAL_TIMEOUT_MS = 1000;
099: /**
100: * parameter of this <code>Runnable</code>
101: * - file to open
102: */
103: private final FileObject fileObject;
104: /**
105: * parameter of this <code>Runnable</code>
106: * - line number to open the {@link #fileObject file} at, or <code>-1</code>
107: * to ignore
108: */
109: private final int line;
110:
111: /**
112: * Creates an instance of this class.
113: * It is used only as an instance of <code>Runnable</code>
114: * used for rescheduling to the AWT thread.
115: * The arguments are stored to local variables and when the
116: * <code>run()</code> method gets executed (in the AWT thread),
117: * they are passed to the <code>open(...)</code> method.
118: *
119: * @param file file to open (must exist)
120: * @param line line number to try to open to (starting at zero),
121: * or <code>-1</code> to ignore
122: * @param waiter double-callback or <code>null</code>
123: */
124: private DefaultOpenFileImpl(FileObject fileObject, int line) {
125: this .fileObject = fileObject;
126: this .line = line;
127: }
128:
129: /** Creates a new instance of OpenFileImpl */
130: public DefaultOpenFileImpl() {
131:
132: /* These fields are not used in the default instance. */
133: this .fileObject = null;
134: this .line = -1;
135: }
136:
137: /**
138: * Sets the specified text into the status line.
139: *
140: * @param text text to be displayed
141: */
142: protected final void setStatusLine(String text) {
143: StatusDisplayer.getDefault().setStatusText(text);
144: }
145:
146: /**
147: * Displays a dialog that the file cannot be open.
148: * This method is to be used in cases that the file was open via
149: * the Open File Server. The message also informs that
150: * the launcher will be notified as if the file
151: * was closed immediately.
152: *
153: * @param fileName name of file that could not be opened
154: */
155: protected void notifyCannotOpen(String fileName) {
156: assert EventQueue.isDispatchThread();
157:
158: DialogDisplayer.getDefault().notify(
159: new NotifyDescriptor.Message(NbBundle.getMessage(
160: DefaultOpenFileImpl.class,
161: "MSG_cannotOpenWillClose", //NOI18N
162: fileName)));
163: }
164:
165: /**
166: * Opens an editor using <code>EditorCookie</code>.
167: * If non-negative line number is passed, it also places cursor at the given
168: * line.
169: *
170: * @param cookie cookie to use for opening an editor
171: * @param observable whether the cookie is
172: * <code>EditorCookie.Observable</code>
173: * @param line line number to place cursor to (starting at <code>0</code>)
174: * @return <code>true</code> if the cookie was successfully activated,
175: * <code>false</code> if some error occured
176: */
177: private boolean openEditor(final EditorCookie editorCookie,
178: final int line) {
179: assert EventQueue.isDispatchThread();
180:
181: /* if the editor is already open, just set the cursor and activate it */
182: JEditorPane[] openPanes = editorCookie.getOpenedPanes();
183: if (openPanes != null) {
184: if (line >= 0) {
185: int cursorOffset = getCursorOffset(editorCookie
186: .getDocument(), line);
187: openPanes[0].setCaretPosition(cursorOffset);
188: }
189: Container c = SwingUtilities.getAncestorOfClass(
190: TopComponent.class, openPanes[0]);
191: assert c != null;
192:
193: final TopComponent tc = (TopComponent) c;
194: EventQueue.invokeLater(new Runnable() {
195: public void run() {
196: tc.requestActive();
197: }
198: });
199: return true;
200: }
201:
202: /* get the document: */
203: final StyledDocument doc;
204: try {
205: doc = editorCookie.openDocument();
206: } catch (IOException ex) {
207: String msg = NbBundle.getMessage(DefaultOpenFileImpl.class,
208: "MSG_cannotOpenWillClose"); //NOI18N
209: ErrorManager.getDefault().notify(ErrorManager.EXCEPTION,
210: ErrorManager.getDefault().annotate(ex, msg));
211: return false;
212: }
213:
214: if (line < 0) {
215: editorCookie.open();
216:
217: /*
218: * editorCookie.open() may return before the editor is actually
219: * open. But since the document was successfully open,
220: * the editor should be opened quite quickly and no problem
221: * should occur.
222: */
223: } else {
224: openDocAtLine(editorCookie, doc, line);
225: }
226: return true;
227: }
228:
229: /**
230: * Opens a document in the editor at a given line.
231: * This method is used in the case that the editor is not opened yet
232: * (<code>EditorCookie.getOpenedPanes()</code> returned <code>null</code>)
233: * and is to be opened at a specific line.
234: *
235: * @param editorCookie editor cookie to use for opening the document
236: * @param doc document already loaded using the editor cookie
237: * @param line line to open the document at (first line = <code>0</code>);
238: * must be non-negative
239: */
240: private void openDocAtLine(final EditorCookie editorCookie,
241: final StyledDocument doc, final int line) {
242: assert EventQueue.isDispatchThread();
243: assert line >= 0;
244: assert editorCookie.getDocument() == doc;
245:
246: /* offset must be computed here so that it is available to the task: */
247: final int offset = getCursorOffset(doc, line);
248:
249: class SetCursorTask implements Runnable {
250: private boolean completed = false;
251: private PropertyChangeListener listenerToUnregister;
252:
253: private boolean perform() {
254: if (EventQueue.isDispatchThread()) {
255: run();
256: } else {
257: try {
258: EventQueue.invokeAndWait(this );
259: } catch (Exception ex) {
260: ErrorManager.getDefault().notify(ex);
261:
262: completed = true; //so that only one exception is thrown
263: }
264: }
265: return completed;
266: }
267:
268: public void run() {
269: assert EventQueue.isDispatchThread();
270:
271: if (completed) {
272: return;
273: }
274:
275: JEditorPane[] panes = editorCookie.getOpenedPanes();
276: if (panes != null) {
277: panes[0].setCaretPosition(offset);
278: if (listenerToUnregister != null) {
279: ((EditorCookie.Observable) editorCookie)
280: .removePropertyChangeListener(listenerToUnregister);
281: }
282: completed = true;
283: }
284: }
285:
286: private void setListenerToUnregister(
287: PropertyChangeListener l) {
288: listenerToUnregister = l;
289: }
290: }
291:
292: final SetCursorTask setCursorTask = new SetCursorTask();
293:
294: editorCookie.open();
295: if (setCursorTask.perform()) {
296: return;
297: }
298: if (editorCookie instanceof EditorCookie.Observable) {
299: if (!setCursorTask.perform()) {
300: PropertyChangeListener openPanesListener = new PropertyChangeListener() {
301: public void propertyChange(PropertyChangeEvent e) {
302: if (EditorCookie.Observable.PROP_OPENED_PANES
303: .equals(e.getPropertyName())) {
304: setCursorTask.perform();
305: }
306: }
307: };
308: setCursorTask
309: .setListenerToUnregister(openPanesListener);
310: ((EditorCookie.Observable) editorCookie)
311: .addPropertyChangeListener(openPanesListener);
312: setCursorTask.perform();
313: }
314: } else {
315: final int numberOfTries = OPEN_EDITOR_TOTAL_TIMEOUT_MS
316: / OPEN_EDITOR_WAIT_PERIOD_MS;
317: for (int i = 0; i < numberOfTries; i++) {
318: try {
319: Thread.currentThread().sleep(
320: OPEN_EDITOR_WAIT_PERIOD_MS);
321: } catch (InterruptedException ex) {
322: ErrorManager.getDefault().notify(
323: ErrorManager.EXCEPTION, ex);
324: }
325: if (setCursorTask.perform()) {
326: break;
327: }
328: }
329: if (!setCursorTask.completed) {
330: StatusDisplayer.getDefault().setStatusText(
331: NbBundle.getMessage(DefaultOpenFileImpl.class,
332: "MSG_couldNotOpenAt")); //NOI18N
333: }
334: }
335: }
336:
337: /**
338: * Computes cursor offset of a given line of a document.
339: * The line number must be non-negative.
340: * If the line number is greater than number of the last line,
341: * the returned offset corresponds to the last line of the document.
342: *
343: * @param doc document to computer offset for
344: * @param line line number (first line = <code>0</code>)
345: * @return cursor offset of the beginning of the given line
346: */
347: private int getCursorOffset(StyledDocument doc, int line) {
348: assert EventQueue.isDispatchThread();
349: assert line >= 0;
350:
351: try {
352: return NbDocument.findLineOffset(doc, line);
353: } catch (IndexOutOfBoundsException ex) {
354: /* probably line number out of bounds */
355:
356: Element lineRootElement = NbDocument
357: .findLineRootElement(doc);
358: int lineCount = lineRootElement.getElementCount();
359: if (line >= lineCount) {
360: return NbDocument.findLineOffset(doc, lineCount - 1);
361: } else {
362: throw ex;
363: }
364: }
365: }
366:
367: /**
368: * Activates the specified cookie, thus opening a file.
369: * The file is specified by the cookie, because the cookie was obtained
370: * from it. The cookie must be one of <code>EditorCookie</code>
371: * <code>OpenCookie</code>, <code>EditCookie</code>,
372: * <code>ViewCookie</code>.
373: *
374: * @param cookie cookie to activate
375: * @param cookieClass type of the cookie - specifies action to activate
376: * @param line used only by <code>EditorCookie</code>s -
377: * specifies initial line to open the file at
378: * @return <code>true</code> if the cookie was successfully activated,
379: * <code>false</code> if some error occured
380: * @exception java.lang.IllegalArgumentException
381: * if <code>cookieClass</code> is not any of
382: * <code>EditorCookie</code>, <code>OpenCookie</code>,
383: * <code>ViewCookie</code>
384: * @exception java.lang.ClassCastException
385: * if the <code>cookie</code> is not an instance
386: * of the specified cookie class
387: */
388: protected boolean openByCookie(Node.Cookie cookie,
389: Class cookieClass, final int line) {
390: assert EventQueue.isDispatchThread();
391:
392: if ((cookieClass == EditorCookie.Observable.class)
393: || (cookieClass == EditorCookie.Observable.class)) {
394: return openEditor((EditorCookie) cookie, line);
395: } else if (cookieClass == OpenCookie.class) {
396: ((OpenCookie) cookie).open();
397: } else if (cookieClass == EditCookie.class) {
398: ((EditCookie) cookie).edit();
399: } else if (cookieClass == ViewCookie.class) {
400: ((ViewCookie) cookie).view();
401: } else {
402: throw new IllegalArgumentException();
403: }
404: return true;
405: }
406:
407: /**
408: * Tries to open the specified file, using one of <code>EditorCookie</code>,
409: * <code>OpenCookie</code>, <code>EditCookie</code>, <code>ViewCookie</code>
410: * (in the same order).
411: * If the client of the open file server wants, waits until the file is
412: * closed and notifies the client.
413: *
414: * @param dataObject <code>DataObject</code> representing the file
415: * @param line if <code>EditorCookie</code> is used,
416: * specifies initial line to open the file at
417: * @return <code>true</code> if the file was successfully open,
418: * <code>false</code> otherwise
419: */
420: private final boolean openDataObjectByCookie(DataObject dataObject,
421: int line) {
422:
423: Class<? extends Node.Cookie> cookieClass;
424: Node.Cookie cookie;
425: if ((cookie = dataObject
426: .getCookie(cookieClass = OpenCookie.class)) != null
427: || (cookie = dataObject
428: .getCookie(cookieClass = EditCookie.class)) != null
429: || (cookie = dataObject
430: .getCookie(cookieClass = ViewCookie.class)) != null) {
431: return openByCookie(cookie, cookieClass, line);
432: }
433: return false;
434: }
435:
436: /**
437: * This method is called when it is rescheduled to the AWT thread.
438: * (from a different thread). It is always run in the AWT thread.
439: */
440: public void run() {
441: assert EventQueue.isDispatchThread();
442:
443: open(fileObject, line);
444: }
445:
446: /**
447: * Opens the <code>FileObject</code> either by calling {@link EditorCookie}
448: * (or {@link OpenCookie} or {@link ViewCookie}),
449: * or by showing it in the Explorer.
450: */
451: public boolean open(final FileObject fileObject, int line) {
452: if (!EventQueue.isDispatchThread()) {
453: EventQueue.invokeLater(new DefaultOpenFileImpl(fileObject,
454: line));
455: return true;
456: }
457:
458: assert EventQueue.isDispatchThread();
459:
460: String fileName = fileObject.getNameExt();
461:
462: /* Find a DataObject for the FileObject: */
463: final DataObject dataObject;
464: try {
465: dataObject = DataObject.find(fileObject);
466: } catch (DataObjectNotFoundException ex) {
467: ErrorManager.getDefault().notify(ex);
468: return false;
469: }
470:
471: Class<? extends Node.Cookie> cookieClass;
472: Node.Cookie cookie;
473:
474: if ((line != -1 && ((cookie = dataObject
475: .getCookie(cookieClass = EditorCookie.Observable.class)) != null || (cookie = dataObject
476: .getCookie(cookieClass = EditorCookie.class)) != null))) {
477: boolean ret = openByCookie(cookie, cookieClass, line);
478: return ret;
479: }
480:
481: /* try to open the object using the default action */
482: final Node dataNode = dataObject.getNodeDelegate();
483: final Action action = dataNode.getPreferredAction();
484: if (action != null && !(action instanceof FileSystemAction)
485: && !(action instanceof ToolsAction)) {
486: EventQueue.invokeLater(new Runnable() {
487: public void run() {
488: action.actionPerformed(new ActionEvent(dataNode, 0,
489: null));
490: }
491: });
492: return true;
493: }
494:
495: /* Try to grab an editor/open/edit/view cookie and open the object: */
496: StatusDisplayer.getDefault().setStatusText(
497: NbBundle.getMessage(DefaultOpenFileImpl.class,
498: "MSG_opening", fileName));
499: boolean success = openDataObjectByCookie(dataObject, line);
500: if (success) {
501: return true;
502: }
503: if (fileObject.isFolder() || FileUtil.isArchiveFile(fileObject)) {
504: // select it in explorer:
505: Node node = dataObject.getNodeDelegate();
506: if (node != null) {
507: NodeOperation.getDefault().explore(node);
508: return true;
509: } else {
510: return false;
511: }
512: }
513:
514: return false;
515: }
516:
517: }
|