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:
042: package org.netbeans.modules.search;
043:
044: import java.awt.BorderLayout;
045: import java.awt.CardLayout;
046: import java.awt.Dimension;
047: import java.awt.EventQueue;
048: import java.awt.Rectangle;
049: import javax.swing.BorderFactory;
050: import javax.swing.Box;
051: import javax.swing.JEditorPane;
052: import javax.swing.JLabel;
053: import javax.swing.JPanel;
054: import javax.swing.JScrollBar;
055: import javax.swing.JScrollPane;
056: import javax.swing.JTree;
057: import javax.swing.SwingConstants;
058: import javax.swing.UIManager;
059: import javax.swing.border.Border;
060: import javax.swing.event.TreeSelectionEvent;
061: import javax.swing.event.TreeSelectionListener;
062: import javax.swing.text.BadLocationException;
063: import javax.swing.text.Document;
064: import javax.swing.text.Element;
065: import javax.swing.text.StyledDocument;
066: import javax.swing.tree.TreePath;
067: import org.openide.ErrorManager;
068: import org.openide.text.NbDocument;
069: import org.openide.util.NbBundle;
070: import org.openide.util.RequestProcessor;
071: import static java.lang.Thread.NORM_PRIORITY;
072:
073: /**
074: * Panel for displaying context of a matching string within a file.
075: * When a node representing a matching string is selected in the tree
076: * of results, this panel displays a part of the file surrounding the selected
077: * matching string, with the matching string highlighted.
078: * When a node representing the whole file is selected, the beginning
079: * of the file is displayed.
080: *
081: * @author Tim Boudreau
082: * @author Marian Petras
083: */
084: final class ContextView extends JPanel implements TreeSelectionListener {
085:
086: /** */
087: private static final String FILE_VIEW = "file view"; //NOI18N
088: /** */
089: private static final String MESSAGE_VIEW = "message view"; //NOI18N
090:
091: /** */
092: private final CardLayout cardLayout;
093: /** editor pane actually displaying (part of) the file */
094: private final JEditorPane editorPane = new JEditorPane();
095: /** scroll pane containing the editor pane */
096: private final JScrollPane editorScroll;
097: /** displays location of the file above the editor pane */
098: private final JLabel lblPath = new JLabel();
099: /** displays message if no file is displayed */
100: private final JLabel lblMessage = new JLabel();
101: /**
102: * displays content of file after it has been asynchronously loaded
103: * by the {@link #requestProcessor}
104: */
105: private final Displayer displayer = new Displayer();
106: /** used for asynchronous loading of files' contents */
107: private final RequestProcessor requestProcessor = new RequestProcessor(
108: "TextView", NORM_PRIORITY, true); //NOI18N
109:
110: /** */
111: private ResultModel resultModel;
112: /** */
113: private RequestProcessor.Task task = null;
114: /** */
115: private TextFetcher textFetcher = null;
116: /** */
117: private String displayedCard = null;
118: /** */
119: private String msgNoFileSelected = null;
120: /** */
121: private String msgMultipleFilesSelected = null;
122: /** the current MIME-type set for the {@link #editorPane} */
123: private String editorMimeType = null;
124:
125: /**
126: *
127: * @author Tim Boudreau
128: * @author Marian Petras
129: */
130: public ContextView(ResultModel resultModel) {
131: Border b = BorderFactory.createCompoundBorder(
132: BorderFactory
133: .createMatteBorder( //outside border
134: 0, 0, 1, 0, UIManager
135: .getColor("controlShadow")), //NOI18N
136: BorderFactory.createEmptyBorder( //inside border
137: 5, 5, 1, 5));
138: lblPath.setBorder(b);
139:
140: editorPane.setEditable(false);
141: editorPane.getCaret().setBlinkRate(0);
142:
143: editorScroll = new JScrollPane(editorPane);
144: editorScroll.setViewportBorder(BorderFactory
145: .createEmptyBorder());
146: editorScroll.setBorder(BorderFactory.createEmptyBorder());
147:
148: JPanel fileViewPanel = new JPanel();
149: fileViewPanel.setLayout(new BorderLayout());
150: fileViewPanel.add(lblPath, BorderLayout.NORTH);
151: fileViewPanel.add(editorScroll, BorderLayout.CENTER);
152:
153: Box messagePanel = Box.createVerticalBox();
154: messagePanel.add(Box.createVerticalGlue());
155: messagePanel.add(lblMessage);
156: messagePanel.add(Box.createVerticalGlue());
157: lblMessage.setAlignmentX(0.5f);
158: lblMessage.setHorizontalAlignment(SwingConstants.CENTER);
159: lblMessage.setEnabled(false);
160:
161: setLayout(cardLayout = new CardLayout());
162: add(fileViewPanel, FILE_VIEW);
163: add(messagePanel, MESSAGE_VIEW);
164:
165: setResultModel(resultModel);
166: }
167:
168: @Override
169: public Dimension getMinimumSize() {
170: /*
171: * Without this, the minimum width would be equal to the width
172: * of the {@linkplain #lblPath file path label}.
173: */
174: Dimension minSize = super .getMinimumSize();
175: minSize.width = 0;
176: return minSize;
177: }
178:
179: /**
180: */
181: void setResultModel(ResultModel resultModel) {
182: if (resultModel == this .resultModel) {
183: return;
184: }
185:
186: synchronized (this ) { //PENDING - review synchronization
187: if (textFetcher != null) {
188: textFetcher.cancel();
189: textFetcher = null;
190: }
191: }
192: this .resultModel = resultModel;
193: }
194:
195: /**
196: */
197: void bindToTreeSelection(final JTree tree) {
198: assert EventQueue.isDispatchThread();
199:
200: displaySelectedFiles(tree);
201: tree.addTreeSelectionListener(this );
202: }
203:
204: /**
205: */
206: void unbindFromTreeSelection(final JTree tree) {
207: assert EventQueue.isDispatchThread();
208:
209: tree.removeTreeSelectionListener(this );
210:
211: synchronized (this ) { //PENDING - review synchronization
212: if (textFetcher != null) {
213: textFetcher.cancel();
214: textFetcher = null;
215: }
216: }
217: }
218:
219: /**
220: * Called when selection of nodes in the result tree changes.
221: */
222: public void valueChanged(TreeSelectionEvent e) {
223: displaySelectedFiles((JTree) e.getSource());
224: }
225:
226: /**
227: * Displays file(s) selected in the given tree.
228: *
229: * @author Marian Petras
230: */
231: private void displaySelectedFiles(final JTree tree) {
232: final TreePath[] selectedPaths = tree.getSelectionPaths();
233: if ((selectedPaths == null) || (selectedPaths.length == 0)) {
234: displayNoFileSelected();
235: } else if (selectedPaths.length > 1) {
236: displayMultipleItemsSelected();
237: } else {
238: assert selectedPaths.length == 1;
239:
240: final TreePath path = selectedPaths[0];
241: int pathCount = path.getPathCount();
242: if (pathCount == 1) { //root node selected
243: displayNoFileSelected();
244: } else {
245: assert pathCount == 2 || pathCount == 3;
246: MatchingObject matchingObj;
247: int matchIndex;
248: if (pathCount == 2) { //file node selected
249: matchingObj = (MatchingObject) path
250: .getLastPathComponent();
251: matchIndex = -1;
252: } else { //match node selected
253: TreePath matchingObjPath = path.getParentPath();
254: matchingObj = (MatchingObject) matchingObjPath
255: .getLastPathComponent();
256: int matchingObjRow = tree
257: .getRowForPath(matchingObjPath);
258: int matchRow = tree.getRowForPath(path);
259: matchIndex = matchRow - matchingObjRow - 1;
260: }
261: displayFile(matchingObj, matchIndex);
262: }
263: }
264: }
265:
266: /**
267: */
268: private void displayNoFileSelected() {
269: if (msgNoFileSelected == null) {
270: msgNoFileSelected = NbBundle.getMessage(getClass(),
271: "MsgNoFileSelected"); //NOI18N
272: }
273: displayMessage(msgNoFileSelected);
274: }
275:
276: /**
277: */
278: private void displayMultipleItemsSelected() {
279: if (msgMultipleFilesSelected == null) {
280: msgMultipleFilesSelected = NbBundle.getMessage(getClass(),
281: "MsgMultipleFilesSelected");//NOI18N
282: }
283: displayMessage(msgMultipleFilesSelected);
284: }
285:
286: /**
287: */
288: private void displayMessage(String message) {
289: lblMessage.setText(message);
290: if (displayedCard != MESSAGE_VIEW) {
291: cardLayout.show(this , displayedCard = MESSAGE_VIEW);
292: }
293: }
294:
295: /**
296: * @author Tim Boudreau
297: * @author Marian Petras
298: */
299: private void displayFile(final MatchingObject matchingObj,
300: final int partIndex) {
301: assert EventQueue.isDispatchThread();
302:
303: synchronized (displayer) { //PENDING - review synchronization
304: if (task != null) {
305: task.cancel();
306: task = null;
307: }
308:
309: final Item item = new Item(resultModel, matchingObj,
310: partIndex);
311:
312: MatchingObject.InvalidityStatus invalidityStatus = matchingObj
313: .checkValidity();
314: if (invalidityStatus != null) {
315: displayMessage(invalidityStatus
316: .getDescription(matchingObj.getFile().getPath()));
317: return;
318: }
319:
320: requestText(item, displayer);
321: String description = matchingObj.getDescription();
322: lblPath.setText(description);
323: lblPath.setToolTipText(description); //in case it doesn't fit
324: }
325: }
326:
327: /**
328: * Fetch the text of an {@code Item}. Since the text is retrieved
329: * asynchronously, this method is passed a {@code TextDisplayer},
330: * which will get its {@code setText()} method called on the event thread
331: * after it has been loaded on a background thread.
332: *
333: * @param item item to be displayed by the text displayer
334: * @param textDisplayer displayer that should display the item
335: *
336: * @author Tim Boudreau
337: */
338: private void requestText(Item item, TextDisplayer textDisplayer) {
339: assert EventQueue.isDispatchThread();
340:
341: synchronized (this ) { //PENDING - review synchronization
342: if (textFetcher != null) {
343: if (textFetcher.replaceLocation(item, textDisplayer)) {
344: return;
345: } else {
346: textFetcher.cancel();
347: textFetcher = null;
348: }
349: }
350: if (textFetcher == null) {
351: textFetcher = new TextFetcher(item, textDisplayer,
352: requestProcessor);
353: }
354: }
355: }
356:
357: /**
358: * Implementation of {@code TextDisplayer} which is passed to get the text
359: * of an item. The text is fetched from the file asynchronously, and then
360: * passed to {@link #setText()} to set the text, select the text the item
361: * represents and scroll it into view.
362: *
363: * @see TextReceiver
364: * @author Tim Boudreau
365: * @author Marian Petras
366: */
367: private class Displayer implements TextDisplayer, Runnable {
368:
369: private TextDetail location;
370:
371: /**
372: * @author Tim Boudreau
373: */
374: public void setText(final String text, String mimeType,
375: final TextDetail location) {
376: assert EventQueue.isDispatchThread();
377:
378: if ("content/unknown".equals(mimeType)) { //NOI18N
379: mimeType = "text/plain"; //Good idea? Bad? Hmm... //NOI18N
380: }
381:
382: /*
383: * Changing content type clears the text - so the content type
384: * (in this case, MIME-type only) must be set _before_ the text
385: * is set.
386: */
387: if ((editorMimeType == null)
388: || !editorMimeType.equals(mimeType)) {
389: editorPane.setContentType(mimeType);
390: editorMimeType = mimeType;
391: }
392: editorPane.setText(text);
393:
394: if (displayedCard != FILE_VIEW) {
395: cardLayout.show(ContextView.this ,
396: displayedCard = FILE_VIEW);
397: }
398:
399: if (location != null) {
400: //Let the L&F do anything it needs to do before we try to fiddle
401: //with it - get out of its way. Some Swing View classes don't
402: //have accurate position data until they've painted once.
403: this .location = location;
404: EventQueue.invokeLater(this );
405: } else {
406: scrollToTop();
407: }
408: }
409:
410: /**
411: *
412: * @author Tim Boudreau
413: * @author Marian Petras
414: */
415: public void run() {
416: assert EventQueue.isDispatchThread();
417:
418: boolean scrolled = false;
419: try {
420: if (!editorPane.isShowing()) {
421: return;
422: }
423:
424: if (location != null) {
425: final Document document = editorPane.getDocument();
426: if (document instanceof StyledDocument) {
427: StyledDocument styledDocument = (StyledDocument) document;
428: int cursorOffset = getCursorOffset(
429: (StyledDocument) document, location
430: .getLine() - 1);
431: int startOff = cursorOffset
432: + location.getColumn() - 1;
433: int endOff = startOff
434: + location.getMarkLength();
435: editorPane.setSelectionStart(startOff);
436: editorPane.setSelectionEnd(endOff);
437: Rectangle r = editorPane.modelToView(startOff);
438: if (r != null) {
439: //Editor kit not yet updated, what to do
440: editorPane.scrollRectToVisible(r);
441: scrolled = true;
442: }
443: }
444: editorPane.getCaret().setBlinkRate(0);
445: editorPane.repaint();
446: }
447: } catch (BadLocationException e) {
448: //Maybe not even notify this - not all editors
449: //will have a 1:1 correspondence to file positions -
450: //it's perfectly reasonable for this to be thrown
451: ErrorManager.getDefault().notify( //PENDING - ErrorManager?
452: ErrorManager.INFORMATIONAL, e);
453: }
454: if (!scrolled) {
455: scrollToTop();
456: }
457: }
458:
459: /**
460: * Computes cursor offset of a given line of a document.
461: * The line number must be non-negative.
462: * If the line number is greater than number of the last line,
463: * the returned offset corresponds to the last line of the document.
464: *
465: * @param doc document to computer offset for
466: * @param line line number (first line = <code>0</code>)
467: * @return cursor offset of the beginning of the given line
468: *
469: * @author Marian Petras
470: */
471: private int getCursorOffset(StyledDocument doc, int line) {
472: assert EventQueue.isDispatchThread();
473: assert line >= 0;
474:
475: try {
476: return NbDocument.findLineOffset(doc, line);
477: } catch (IndexOutOfBoundsException ex) {
478: /* probably line number out of bounds */
479:
480: Element lineRootElement = NbDocument
481: .findLineRootElement(doc);
482: int lineCount = lineRootElement.getElementCount();
483: if (line >= lineCount) {
484: return NbDocument
485: .findLineOffset(doc, lineCount - 1);
486: } else {
487: throw ex;
488: }
489: }
490: }
491:
492: /**
493: */
494: private void scrollToTop() {
495: JScrollBar scrollBar;
496:
497: scrollBar = editorScroll.getHorizontalScrollBar();
498: scrollBar.setValue(scrollBar.getMinimum());
499:
500: scrollBar = editorScroll.getVerticalScrollBar();
501: scrollBar.setValue(scrollBar.getMinimum());
502: }
503:
504: }
505:
506: }
|