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.Toolkit;
045: import java.io.CharConversionException;
046: import javax.swing.Action;
047: import javax.swing.JEditorPane;
048: import javax.swing.SwingUtilities;
049: import javax.swing.text.Caret;
050: import org.openide.cookies.EditorCookie;
051: import org.openide.cookies.LineCookie;
052: import org.openide.loaders.DataObject;
053: import org.openide.nodes.AbstractNode;
054: import org.openide.nodes.Children;
055: import org.openide.nodes.Node;
056: import org.openide.text.Line;
057: import org.openide.util.HelpCtx;
058: import org.openide.util.NbBundle;
059: import org.openide.util.actions.NodeAction;
060: import org.openide.util.actions.SystemAction;
061: import org.openide.windows.OutputEvent;
062: import org.openide.windows.OutputListener;
063: import org.openide.xml.XMLUtil;
064: import org.openidex.search.SearchHistory;
065: import org.openidex.search.SearchPattern;
066:
067: /**
068: * Holds details about one search hit in the text document.
069: *
070: * @author Tomas Pavek
071: * @author Marian Petras
072: */
073: final class TextDetail {
074:
075: /** Property name which indicates this detail to show. */
076: static final int DH_SHOW = 1;
077: /** Property name which indicates this detail to go to. */
078: static final int DH_GOTO = 2;
079: /** Property name which indicates this detail to hide. */
080: static final int DH_HIDE = 3;
081:
082: /** Data object. */
083: private DataObject dobj;
084: /** Line number where search result occures.*/
085: private int line;
086: /** Text of the line. */
087: private String lineText;
088: /** Column where search result starts. */
089: private int column;
090: /** Length of search result which to mark. */
091: private int markLength;
092: /** Line. */
093: private Line lineObj;
094: /** SearchPattern used to create the hit of this DetailNode */
095: private SearchPattern searchPattern;
096:
097: /** Constructor using data object.
098: * @param pattern SearchPattern used to create the hit of this DetailNode
099: */
100: TextDetail(DataObject dobj, SearchPattern pattern) {
101: this .dobj = dobj;
102: this .searchPattern = pattern;
103: }
104:
105: /**
106: * Shows the search detail on the DataObject.
107: * The document is opened in the editor, the caret is positioned on the right line and column
108: * and searched string is marked.
109: *
110: * @param how indicates how to show detail.
111: * @see #DH_GOTO
112: * @see #DH_SHOW
113: * @see #DH_HIDE */
114: void showDetail(int how) {
115: if ((dobj == null) || !dobj.isValid()) {
116: Toolkit.getDefaultToolkit().beep();
117: return;
118: }
119: if (lineObj == null) { // try to get Line from DataObject
120: LineCookie lineCookie = dobj.getCookie(LineCookie.class);
121: if (lineCookie != null) {
122: Line.Set lineSet = lineCookie.getLineSet();
123: try {
124: lineObj = lineSet.getOriginal(line - 1);
125: } catch (IndexOutOfBoundsException ioobex) {
126: // The line doesn't exist - go to the last line
127: lineObj = lineSet.getOriginal(findMaxLine(lineSet));
128: column = markLength = 0;
129: }
130: }
131: if (lineObj == null) {
132: Toolkit.getDefaultToolkit().beep();
133: return;
134: }
135: }
136:
137: if (how == DH_HIDE) {
138: return;
139: }
140: EditorCookie edCookie = dobj.getCookie(EditorCookie.class);
141: if (edCookie != null) {
142: edCookie.open();
143: }
144: if (how == DH_SHOW) {
145: lineObj.show(Line.SHOW_TRY_SHOW, column - 1);
146: } else if (how == DH_GOTO) {
147: lineObj.show(Line.SHOW_GOTO, column - 1);
148: }
149: if ((markLength > 0) && (edCookie != null)) {
150: final JEditorPane[] panes = edCookie.getOpenedPanes();
151: if (panes != null && panes.length > 0) {
152: // Necessary since above lineObj.show leads to invoke later as well.
153: SwingUtilities.invokeLater(new Runnable() {
154: public void run() {
155: Caret caret = panes[0].getCaret(); // http://www.netbeans.org/issues/show_bug.cgi?id=23626
156: caret.moveDot(caret.getDot() + markLength);
157: }
158: });
159: }
160: }
161: SearchHistory.getDefault().setLastSelected(searchPattern);
162: }
163:
164: /** Getter for <code>lineText</code> property. */
165: String getLineText() {
166: return lineText;
167: }
168:
169: /** Setter for <code>lineText</code> property. */
170: void setLineText(String text) {
171: lineText = text;
172: }
173:
174: /**
175: * Gets the <code>DataObject</code> where the searched text was found.
176: *
177: * @return data object or <code>null</code> if no data object is available
178: */
179: DataObject getDataObject() {
180: return dobj;
181: }
182:
183: /** Gets the line position of the text. */
184: int getLine() {
185: return line;
186: }
187:
188: /** Sets the line position of the text. */
189: void setLine(int line) {
190: this .line = line;
191: }
192:
193: /** Gets the column position of the text or 0 (1 based). */
194: int getColumn() {
195: return column;
196: }
197:
198: /** Sets the column position of the text. */
199: void setColumn(int col) {
200: column = col;
201: }
202:
203: /** Gets the length of the text that should be marked when the detail is shown. */
204: void setMarkLength(int len) {
205: markLength = len;
206: }
207:
208: /** @return length or 0 */
209: int getMarkLength() {
210: return markLength;
211: }
212:
213: /**
214: * Returns the maximum line in the <code>set</code>.
215: * Used to display the end of file when the corresponding
216: * line no longer exists. (Copied from org.openide.text)
217: *
218: * @param set the set we want to search.
219: * @return maximum line in the <code>set</code>.
220: */
221: private static int findMaxLine(Line.Set set) {
222: int from = 0;
223: int to = 32000;
224:
225: for (;;) {
226: try {
227: set.getOriginal(to);
228: // if the line exists, double the max number, but keep
229: // for reference that it exists
230: from = to;
231: to *= 2;
232: } catch (IndexOutOfBoundsException ex) {
233: break;
234: }
235: }
236:
237: while (from < to) {
238: int middle = (from + to + 1) / 2;
239:
240: try {
241: set.getOriginal(middle);
242: // line exists
243: from = middle;
244: } catch (IndexOutOfBoundsException ex) {
245: // line does not exists, we have to search lower
246: to = middle - 1;
247: }
248: }
249:
250: return from;
251: }
252:
253: /**
254: * Node that represents information about one occurence of a matching
255: * string.
256: *
257: * @see TextDetail
258: */
259: static final class DetailNode extends AbstractNode implements
260: OutputListener {
261:
262: /** Detail to represent. */
263: private TextDetail txtDetail;
264:
265: /**
266: * Constructs a node representing the specified information about
267: * a matching string.
268: *
269: * @param txtDetail information to be represented by this node
270: */
271: public DetailNode(TextDetail txtDetail) {
272: super (Children.LEAF);
273:
274: this .txtDetail = txtDetail;
275:
276: setShortDescription(DetailNode.getShortDesc(txtDetail));
277: setValue(SearchDisplayer.ATTR_OUTPUT_LINE, DetailNode
278: .getFullDesc(txtDetail));
279: }
280:
281: @Override
282: public Action[] getActions(boolean context) {
283: if (!context) {
284: return new Action[] { getPreferredAction() };
285: } else {
286: return new Action[0];
287: }
288: }
289:
290: @Override
291: public Action getPreferredAction() {
292: return SystemAction.get(GotoDetailAction.class);
293: }
294:
295: @Override
296: public boolean equals(Object anotherObj) {
297: return (anotherObj != null)
298: && (anotherObj.getClass() == DetailNode.class)
299: && (((DetailNode) anotherObj).txtDetail
300: .equals(this .txtDetail));
301: }
302:
303: @Override
304: public int hashCode() {
305: return txtDetail.hashCode() + 1;
306: }
307:
308: /** */
309: @Override
310: public String getName() {
311: return txtDetail.getLineText() + " ["
312: + DetailNode.getName(txtDetail) + "]"; // NOI18N
313: }
314:
315: @Override
316: public String getHtmlDisplayName() {
317: String colored;
318: if (txtDetail.getMarkLength() > 0
319: && txtDetail.getColumn() > 0) {
320: try {
321: StringBuffer bold = new StringBuffer();
322: String plain = txtDetail.getLineText();
323: int col0 = txtDetail.getColumn() - 1; // base 0
324:
325: bold.append(XMLUtil.toElementContent(plain
326: .substring(0, col0))); // NOI18N
327: bold.append("<b>"); // NOi18N
328: int end = col0 + txtDetail.getMarkLength();
329: bold.append(XMLUtil.toElementContent(plain
330: .substring(col0, end)));
331: bold.append("</b>"); // NOi18N
332: if (txtDetail.getLineText().length() > end) {
333: bold.append(XMLUtil.toElementContent(plain
334: .substring(end)));
335: }
336: colored = bold.toString();
337: } catch (CharConversionException ex) {
338: return null;
339: }
340: } else {
341: try {
342: colored = XMLUtil.toElementContent(txtDetail
343: .getLineText());
344: } catch (CharConversionException e) {
345: return null;
346: }
347: }
348:
349: try {
350: return colored
351: + " <font color='!controlShadow'>["
352: + XMLUtil.toElementContent(DetailNode
353: .getName(txtDetail)) + "]"; // NOI18N
354: } catch (CharConversionException e) {
355: return null;
356: }
357: }
358:
359: /** Displays the matching string in a text editor. */
360: private void gotoDetail() {
361: txtDetail.showDetail(TextDetail.DH_GOTO);
362: }
363:
364: /** Show the text occurence. */
365: private void showDetail() {
366: txtDetail.showDetail(TextDetail.DH_SHOW);
367: }
368:
369: /** Implements <code>OutputListener</code> interface method. */
370: public void outputLineSelected(OutputEvent evt) {
371: txtDetail.showDetail(TextDetail.DH_SHOW);
372: }
373:
374: /** Implements <code>OutputListener</code> interface method. */
375: public void outputLineAction(OutputEvent evt) {
376: txtDetail.showDetail(TextDetail.DH_GOTO);
377: }
378:
379: /** Implements <code>OutputListener</code> interface method. */
380: public void outputLineCleared(OutputEvent evt) {
381: txtDetail.showDetail(TextDetail.DH_HIDE);
382: }
383:
384: /**
385: * Returns name of a node representing a <code>TextDetail</code>.
386: *
387: * @param det detailed information about location of a matching string
388: * @return name for the node
389: */
390: private static String getName(TextDetail det) {
391: int line = det.getLine();
392: int col = det.getColumn();
393:
394: if (col > 0) {
395:
396: /* position <line>:<col> */
397: return NbBundle.getMessage(DetailNode.class,
398: "TEXT_DETAIL_FMT_NAME1", //NOI18N
399: Integer.toString(line), Integer.toString(col));
400: } else {
401:
402: /* position <line> */
403: return NbBundle.getMessage(DetailNode.class,
404: "TEXT_DETAIL_FMT_NAME2", //NOI18N
405: Integer.toString(line));
406: }
407: }
408:
409: /**
410: * Returns short description of a visual representation of
411: * a <code>TextDetail</code>. The description may be used e.g.
412: * for a tooltip text of a node.
413: *
414: * @param det detailed information about location of a matching string
415: * @return short description of a visual representation
416: */
417: private static String getShortDesc(TextDetail det) {
418: int line = det.getLine();
419: int col = det.getColumn();
420:
421: if (col > 0) {
422:
423: /* line <line>, column <col> */
424: return NbBundle.getMessage(DetailNode.class,
425: "TEXT_DETAIL_FMT_SHORT1", //NOI18N
426: new Object[] { Integer.toString(line),
427: Integer.toString(col) });
428: } else {
429:
430: /* line <line> */
431: return NbBundle.getMessage(DetailNode.class,
432: "TEXT_DETAIL_FMT_SHORT2", //NOI18N
433: Integer.toString(line));
434: }
435: }
436:
437: /**
438: * Returns full description of a visual representation of
439: * a <code>TextDetail</code>. The description may be printed e.g. to
440: * an OutputWindow.
441: *
442: * @param det detailed information about location of a matching string
443: * @return full description of a visual representation
444: */
445: private static String getFullDesc(TextDetail det) {
446: String filename = det.getDataObject().getPrimaryFile()
447: .getNameExt();
448: String lineText = det.getLineText();
449: int line = det.getLine();
450: int col = det.getColumn();
451:
452: if (col > 0) {
453:
454: /* [<filename> at line <line>, column <col>] <text> */
455: return NbBundle.getMessage(DetailNode.class,
456: "TEXT_DETAIL_FMT_FULL1", //NOI18N
457: new Object[] { lineText, filename,
458: Integer.toString(line),
459: Integer.toString(col) });
460: } else {
461:
462: /* [<filename> line <line>] <text> */
463: return NbBundle.getMessage(DetailNode.class,
464: "TEXT_DETAIL_FMT_FULL2", //NOI18N
465: new Object[] { lineText, filename,
466: Integer.toString(line) });
467: }
468: }
469:
470: } // End of DetailNode class.
471:
472: /**
473: * This action displays the matching string in a text editor.
474: * This action is to be used in the window/dialog displaying a list of
475: * found occurences of strings matching a search pattern.
476: */
477: private static class GotoDetailAction extends NodeAction {
478:
479: /** */
480: public String getName() {
481: return NbBundle.getBundle(GotoDetailAction.class)
482: .getString("LBL_GotoDetailAction");
483: }
484:
485: /** */
486: public HelpCtx getHelpCtx() {
487: return new HelpCtx(GotoDetailAction.class);
488: }
489:
490: /**
491: * @return <code>true</code> if at least one node is activated and
492: * the first node is an instance of <code>DetailNode</code>
493: * (or its subclass), <code>false</code> otherwise
494: */
495: protected boolean enable(Node[] activatedNodes) {
496: return activatedNodes != null && activatedNodes.length != 0
497: && activatedNodes[0] instanceof DetailNode;
498: }
499:
500: /**
501: * Displays the matching string in a text editor.
502: * Works only if condition specified in method {@link #enable} is met,
503: * otherwise does nothing.
504: */
505: protected void performAction(Node[] activatedNodes) {
506: if (enable(activatedNodes)) {
507: ((DetailNode) activatedNodes[0]).gotoDetail();
508: }
509: }
510:
511: /**
512: */
513: @Override
514: protected boolean asynchronous() {
515: return false;
516: }
517:
518: } // End of GotoDetailAction class.
519:
520: }
|