001: /*******************************************************************************
002: * Copyright (c) 2000, 2007 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.ui.console;
011:
012: import java.util.ArrayList;
013: import java.util.List;
014:
015: import org.eclipse.core.runtime.IProgressMonitor;
016: import org.eclipse.core.runtime.IStatus;
017: import org.eclipse.core.runtime.Status;
018: import org.eclipse.jface.preference.JFacePreferences;
019: import org.eclipse.jface.resource.ColorRegistry;
020: import org.eclipse.jface.resource.JFaceColors;
021: import org.eclipse.jface.resource.JFaceResources;
022: import org.eclipse.jface.text.BadPositionCategoryException;
023: import org.eclipse.jface.text.DocumentEvent;
024: import org.eclipse.jface.text.IDocument;
025: import org.eclipse.jface.text.IDocumentAdapter;
026: import org.eclipse.jface.text.IDocumentListener;
027: import org.eclipse.jface.text.IPositionUpdater;
028: import org.eclipse.jface.text.IRegion;
029: import org.eclipse.jface.text.Position;
030: import org.eclipse.jface.text.source.SourceViewer;
031: import org.eclipse.jface.util.IPropertyChangeListener;
032: import org.eclipse.jface.util.PropertyChangeEvent;
033: import org.eclipse.swt.SWT;
034: import org.eclipse.swt.custom.LineBackgroundEvent;
035: import org.eclipse.swt.custom.LineBackgroundListener;
036: import org.eclipse.swt.custom.LineStyleEvent;
037: import org.eclipse.swt.custom.LineStyleListener;
038: import org.eclipse.swt.custom.StyleRange;
039: import org.eclipse.swt.custom.StyledText;
040: import org.eclipse.swt.events.MouseEvent;
041: import org.eclipse.swt.events.MouseListener;
042: import org.eclipse.swt.events.MouseMoveListener;
043: import org.eclipse.swt.events.MouseTrackListener;
044: import org.eclipse.swt.graphics.Color;
045: import org.eclipse.swt.graphics.Cursor;
046: import org.eclipse.swt.graphics.Font;
047: import org.eclipse.swt.graphics.Point;
048: import org.eclipse.swt.widgets.Composite;
049: import org.eclipse.swt.widgets.Control;
050: import org.eclipse.swt.widgets.Display;
051: import org.eclipse.swt.widgets.Event;
052: import org.eclipse.swt.widgets.Listener;
053: import org.eclipse.ui.internal.console.ConsoleDocumentAdapter;
054: import org.eclipse.ui.internal.console.ConsoleHyperlinkPosition;
055: import org.eclipse.ui.progress.WorkbenchJob;
056:
057: /**
058: * Default viewer used to display a <code>TextConsole</code>.
059: * <p>
060: * Clients may subclass this class.
061: * </p>
062: *
063: * @since 3.1
064: */
065: public class TextConsoleViewer extends SourceViewer implements
066: LineStyleListener, LineBackgroundListener, MouseTrackListener,
067: MouseMoveListener, MouseListener {
068: /**
069: * Adapts document to the text widget.
070: */
071: private ConsoleDocumentAdapter documentAdapter;
072:
073: private IHyperlink hyperlink;
074:
075: private Cursor handCursor;
076:
077: private Cursor textCursor;
078:
079: private int consoleWidth = -1;
080:
081: private TextConsole console;
082:
083: private IPropertyChangeListener propertyChangeListener;
084:
085: private IDocumentListener documentListener = new IDocumentListener() {
086: public void documentAboutToBeChanged(DocumentEvent event) {
087: }
088:
089: public void documentChanged(DocumentEvent event) {
090: updateLinks(event.fOffset);
091: }
092: };
093: // event listener used to send event to hyperlink for IHyperlink2
094: private Listener mouseUpListener = new Listener() {
095: public void handleEvent(Event event) {
096: if (hyperlink != null) {
097: String selection = getTextWidget().getSelectionText();
098: if (selection.length() <= 0) {
099: if (event.button == 1) {
100: if (hyperlink instanceof IHyperlink2) {
101: ((IHyperlink2) hyperlink)
102: .linkActivated(event);
103: } else {
104: hyperlink.linkActivated();
105: }
106: }
107: }
108: }
109: }
110: };
111:
112: WorkbenchJob revealJob = new WorkbenchJob("Reveal End of Document") {//$NON-NLS-1$
113: public IStatus runInUIThread(IProgressMonitor monitor) {
114: StyledText textWidget = getTextWidget();
115: if (textWidget != null && !textWidget.isDisposed()) {
116: int lineCount = textWidget.getLineCount();
117: textWidget.setTopIndex(lineCount - 1);
118: }
119: return Status.OK_STATUS;
120: }
121: };
122:
123: private IPositionUpdater positionUpdater = new IPositionUpdater() {
124: public void update(DocumentEvent event) {
125: try {
126: IDocument document = getDocument();
127: if (document != null) {
128: Position[] positions = document
129: .getPositions(ConsoleHyperlinkPosition.HYPER_LINK_CATEGORY);
130: for (int i = 0; i < positions.length; i++) {
131: Position position = positions[i];
132: if (position.offset == event.fOffset
133: && position.length <= event.fLength) {
134: position.delete();
135: }
136: if (position.isDeleted) {
137: document
138: .removePosition(
139: ConsoleHyperlinkPosition.HYPER_LINK_CATEGORY,
140: position);
141: }
142: }
143: }
144: } catch (BadPositionCategoryException e) {
145: }
146: }
147: };
148:
149: /**
150: * Constructs a new viewer in the given parent for the specified console.
151: *
152: * @param parent
153: * containing widget
154: * @param console
155: * text console
156: */
157: public TextConsoleViewer(Composite parent, TextConsole console) {
158: super (parent, null, SWT.V_SCROLL | SWT.H_SCROLL);
159: this .console = console;
160:
161: IDocument document = console.getDocument();
162: setDocument(document);
163:
164: StyledText styledText = getTextWidget();
165: styledText.setDoubleClickEnabled(true);
166: styledText.addLineStyleListener(this );
167: styledText.addLineBackgroundListener(this );
168: styledText.setEditable(true);
169: setFont(console.getFont());
170: styledText.addMouseTrackListener(this );
171: styledText.addListener(SWT.MouseUp, mouseUpListener);
172:
173: ColorRegistry colorRegistry = JFaceResources.getColorRegistry();
174: propertyChangeListener = new HyperlinkColorChangeListener();
175: colorRegistry.addListener(propertyChangeListener);
176:
177: revealJob.setSystem(true);
178: document.addDocumentListener(documentListener);
179: document.addPositionUpdater(positionUpdater);
180: }
181:
182: /**
183: * Sets the tab width used by this viewer.
184: *
185: * @param tabWidth
186: * the tab width used by this viewer
187: */
188: public void setTabWidth(int tabWidth) {
189: StyledText styledText = getTextWidget();
190: int oldWidth = styledText.getTabs();
191: if (tabWidth != oldWidth) {
192: styledText.setTabs(tabWidth);
193: }
194: }
195:
196: /**
197: * Sets the font used by this viewer.
198: *
199: * @param font
200: * the font used by this viewer
201: */
202: public void setFont(Font font) {
203: StyledText styledText = getTextWidget();
204: Font oldFont = styledText.getFont();
205: if (oldFont == font) {
206: return;
207: }
208: if (font == null || !(font.equals(oldFont))) {
209: styledText.setFont(font);
210: }
211: }
212:
213: /**
214: * Positions the cursor at the end of the document.
215: */
216: protected void revealEndOfDocument() {
217: revealJob.schedule(50);
218: }
219:
220: /*
221: * (non-Javadoc)
222: *
223: * @see org.eclipse.swt.custom.LineStyleListener#lineGetStyle(org.eclipse.swt.custom.LineStyleEvent)
224: */
225: public void lineGetStyle(LineStyleEvent event) {
226: IDocument document = getDocument();
227: if (document != null && document.getLength() > 0) {
228: ArrayList ranges = new ArrayList();
229: int offset = event.lineOffset;
230: int length = event.lineText.length();
231:
232: StyleRange[] partitionerStyles = ((IConsoleDocumentPartitioner) document
233: .getDocumentPartitioner()).getStyleRanges(
234: event.lineOffset, event.lineText.length());
235: if (partitionerStyles != null) {
236: for (int i = 0; i < partitionerStyles.length; i++) {
237: ranges.add(partitionerStyles[i]);
238: }
239: } else {
240: ranges.add(new StyleRange(offset, length, null, null));
241: }
242:
243: try {
244: Position[] positions = getDocument().getPositions(
245: ConsoleHyperlinkPosition.HYPER_LINK_CATEGORY);
246: Position[] overlap = findPosition(offset, length,
247: positions);
248: Color color = JFaceColors.getHyperlinkText(Display
249: .getCurrent());
250: if (overlap != null) {
251: for (int i = 0; i < overlap.length; i++) {
252: Position position = overlap[i];
253: StyleRange linkRange = new StyleRange(
254: position.offset, position.length,
255: color, null);
256: linkRange.underline = true;
257: override(ranges, linkRange);
258: }
259: }
260: } catch (BadPositionCategoryException e) {
261: }
262:
263: if (ranges.size() > 0) {
264: event.styles = (StyleRange[]) ranges
265: .toArray(new StyleRange[ranges.size()]);
266: }
267: }
268: }
269:
270: private void override(List ranges, StyleRange newRange) {
271: if (ranges.isEmpty()) {
272: ranges.add(newRange);
273: return;
274: }
275:
276: int start = newRange.start;
277: int end = start + newRange.length;
278: for (int i = 0; i < ranges.size(); i++) {
279: StyleRange existingRange = (StyleRange) ranges.get(i);
280: int rEnd = existingRange.start + existingRange.length;
281: if (end <= existingRange.start || start >= rEnd) {
282: continue;
283: }
284:
285: if (start < existingRange.start
286: && end > existingRange.start) {
287: start = existingRange.start;
288: }
289:
290: if (start >= existingRange.start && end <= rEnd) {
291: existingRange.length = start - existingRange.start;
292: ranges.add(++i, newRange);
293: if (end != rEnd) {
294: ranges.add(++i, new StyleRange(end, rEnd - end - 1,
295: existingRange.foreground,
296: existingRange.background));
297: }
298: return;
299: } else if (start >= existingRange.start && start < rEnd) {
300: existingRange.length = start - existingRange.start;
301: ranges.add(++i, newRange);
302: } else if (end >= rEnd) {
303: ranges.remove(i);
304: } else {
305: ranges.add(++i, new StyleRange(end + 1, rEnd - end + 1,
306: existingRange.foreground,
307: existingRange.background));
308: }
309: }
310: }
311:
312: /**
313: * Binary search for the positions overlapping the given range
314: *
315: * @param offset
316: * the offset of the range
317: * @param length
318: * the length of the range
319: * @param positions
320: * the positions to search
321: * @return the positions overlapping the given range, or <code>null</code>
322: */
323: private Position[] findPosition(int offset, int length,
324: Position[] positions) {
325:
326: if (positions.length == 0)
327: return null;
328:
329: int rangeEnd = offset + length;
330: int left = 0;
331: int right = positions.length - 1;
332: int mid = 0;
333: Position position = null;
334:
335: while (left < right) {
336:
337: mid = (left + right) / 2;
338:
339: position = positions[mid];
340: if (rangeEnd < position.getOffset()) {
341: if (left == mid)
342: right = left;
343: else
344: right = mid - 1;
345: } else if (offset > (position.getOffset()
346: + position.getLength() - 1)) {
347: if (right == mid)
348: left = right;
349: else
350: left = mid + 1;
351: } else {
352: left = right = mid;
353: }
354: }
355:
356: List list = new ArrayList();
357: int index = left - 1;
358: if (index >= 0) {
359: position = positions[index];
360: while (index >= 0
361: && (position.getOffset() + position.getLength()) > offset) {
362: index--;
363: if (index > 0) {
364: position = positions[index];
365: }
366: }
367: }
368: index++;
369: position = positions[index];
370: while (index < positions.length
371: && (position.getOffset() < rangeEnd)) {
372: list.add(position);
373: index++;
374: if (index < positions.length) {
375: position = positions[index];
376: }
377: }
378:
379: if (list.isEmpty()) {
380: return null;
381: }
382: return (Position[]) list.toArray(new Position[list.size()]);
383: }
384:
385: /*
386: * (non-Javadoc)
387: *
388: * @see org.eclipse.swt.custom.LineBackgroundListener#lineGetBackground(org.eclipse.swt.custom.LineBackgroundEvent)
389: */
390: public void lineGetBackground(LineBackgroundEvent event) {
391: event.lineBackground = null;
392: }
393:
394: /**
395: * Returns the hand cursor.
396: *
397: * @return the hand cursor
398: */
399: protected Cursor getHandCursor() {
400: if (handCursor == null) {
401: handCursor = new Cursor(ConsolePlugin.getStandardDisplay(),
402: SWT.CURSOR_HAND);
403: }
404: return handCursor;
405: }
406:
407: /**
408: * Returns the text cursor.
409: *
410: * @return the text cursor
411: */
412: protected Cursor getTextCursor() {
413: if (textCursor == null) {
414: textCursor = new Cursor(ConsolePlugin.getStandardDisplay(),
415: SWT.CURSOR_IBEAM);
416: }
417: return textCursor;
418: }
419:
420: /**
421: * Notification a hyperlink has been entered.
422: *
423: * @param link
424: * the link that was entered
425: */
426: protected void linkEntered(IHyperlink link) {
427: Control control = getTextWidget();
428: if (hyperlink != null) {
429: linkExited(hyperlink);
430: }
431: hyperlink = link;
432: hyperlink.linkEntered();
433: control.setCursor(getHandCursor());
434: control.redraw();
435: control.addMouseListener(this );
436: }
437:
438: /**
439: * Notification a link was exited.
440: *
441: * @param link
442: * the link that was exited
443: */
444: protected void linkExited(IHyperlink link) {
445: link.linkExited();
446: hyperlink = null;
447: Control control = getTextWidget();
448: control.setCursor(getTextCursor());
449: control.redraw();
450: control.removeMouseListener(this );
451: }
452:
453: /*
454: * (non-Javadoc)
455: *
456: * @see org.eclipse.swt.events.MouseTrackListener#mouseEnter(org.eclipse.swt.events.MouseEvent)
457: */
458: public void mouseEnter(MouseEvent e) {
459: getTextWidget().addMouseMoveListener(this );
460: }
461:
462: /*
463: * (non-Javadoc)
464: *
465: * @see org.eclipse.swt.events.MouseTrackListener#mouseExit(org.eclipse.swt.events.MouseEvent)
466: */
467: public void mouseExit(MouseEvent e) {
468: getTextWidget().removeMouseMoveListener(this );
469: if (hyperlink != null) {
470: linkExited(hyperlink);
471: }
472: }
473:
474: /*
475: * (non-Javadoc)
476: *
477: * @see org.eclipse.swt.events.MouseTrackListener#mouseHover(org.eclipse.swt.events.MouseEvent)
478: */
479: public void mouseHover(MouseEvent e) {
480: }
481:
482: /*
483: * (non-Javadoc)
484: *
485: * @see org.eclipse.swt.events.MouseMoveListener#mouseMove(org.eclipse.swt.events.MouseEvent)
486: */
487: public void mouseMove(MouseEvent e) {
488: int offset = -1;
489: try {
490: Point p = new Point(e.x, e.y);
491: offset = getTextWidget().getOffsetAtLocation(p);
492: } catch (IllegalArgumentException ex) {
493: // out of the document range
494: }
495: updateLinks(offset);
496: }
497:
498: /**
499: * The cursor has just be moved to the given offset, the mouse has hovered
500: * over the given offset. Update link rendering.
501: *
502: * @param offset
503: */
504: protected void updateLinks(int offset) {
505: if (offset >= 0) {
506: IHyperlink link = getHyperlink(offset);
507: if (link != null) {
508: if (link.equals(hyperlink)) {
509: return;
510: }
511: linkEntered(link);
512: return;
513: }
514: }
515: if (hyperlink != null) {
516: linkExited(hyperlink);
517: }
518: }
519:
520: /**
521: * Returns the currently active hyperlink or <code>null</code> if none.
522: *
523: * @return the currently active hyperlink or <code>null</code> if none
524: */
525: public IHyperlink getHyperlink() {
526: return hyperlink;
527: }
528:
529: /**
530: * Returns the hyperlink at the specified offset, or <code>null</code> if
531: * none.
532: *
533: * @param offset
534: * offset at which a hyperlink has been requested
535: * @return hyperlink at the specified offset, or <code>null</code> if none
536: */
537: public IHyperlink getHyperlink(int offset) {
538: if (offset >= 0 && console != null) {
539: return console.getHyperlink(offset);
540: }
541: return null;
542: }
543:
544: /*
545: * (non-Javadoc)
546: *
547: * @see org.eclipse.swt.events.MouseListener#mouseDoubleClick(org.eclipse.swt.events.MouseEvent)
548: */
549: public void mouseDoubleClick(MouseEvent e) {
550: }
551:
552: /*
553: * (non-Javadoc)
554: *
555: * @see org.eclipse.swt.events.MouseListener#mouseDown(org.eclipse.swt.events.MouseEvent)
556: */
557: public void mouseDown(MouseEvent e) {
558: }
559:
560: /*
561: * (non-Javadoc)
562: *
563: * @see org.eclipse.swt.events.MouseListener#mouseUp(org.eclipse.swt.events.MouseEvent)
564: */
565: public void mouseUp(MouseEvent e) {
566: }
567:
568: /*
569: * (non-Javadoc)
570: *
571: * @see org.eclipse.jface.text.TextViewer#createDocumentAdapter()
572: */
573: protected IDocumentAdapter createDocumentAdapter() {
574: if (documentAdapter == null) {
575: documentAdapter = new ConsoleDocumentAdapter(
576: consoleWidth = -1);
577: }
578: return documentAdapter;
579: }
580:
581: /**
582: * Sets the console to have a fixed character width. Use -1 to indicate that
583: * a fixed width should not be used.
584: *
585: * @param width
586: * fixed character width of the console, or -1
587: */
588: public void setConsoleWidth(int width) {
589: if (consoleWidth != width) {
590: consoleWidth = width;
591: ConsolePlugin.getStandardDisplay().asyncExec(
592: new Runnable() {
593: public void run() {
594: if (documentAdapter != null) {
595: documentAdapter.setWidth(consoleWidth);
596: }
597: }
598: });
599: }
600: }
601:
602: /*
603: * (non-Javadoc)
604: *
605: * @see org.eclipse.jface.text.TextViewer#handleDispose()
606: */
607: protected void handleDispose() {
608: IDocument document = getDocument();
609: if (document != null) {
610: document.removeDocumentListener(documentListener);
611: document.removePositionUpdater(positionUpdater);
612: }
613:
614: StyledText styledText = getTextWidget();
615: styledText.removeLineStyleListener(this );
616: styledText.removeLineBackgroundListener(this );
617: styledText.removeMouseTrackListener(this );
618:
619: handCursor = null;
620: textCursor = null;
621: hyperlink = null;
622: console = null;
623:
624: ColorRegistry colorRegistry = JFaceResources.getColorRegistry();
625: colorRegistry.removeListener(propertyChangeListener);
626:
627: super .handleDispose();
628: }
629:
630: class HyperlinkColorChangeListener implements
631: IPropertyChangeListener {
632: public void propertyChange(PropertyChangeEvent event) {
633: if (event.getProperty().equals(
634: JFacePreferences.ACTIVE_HYPERLINK_COLOR)
635: || event.getProperty().equals(
636: JFacePreferences.HYPERLINK_COLOR)) {
637: getTextWidget().redraw();
638: }
639: }
640:
641: }
642:
643: /*
644: * work around to memory leak in TextViewer$WidgetCommand
645: */
646: protected void updateTextListeners(WidgetCommand cmd) {
647: super .updateTextListeners(cmd);
648: cmd.preservedText = null;
649: cmd.event = null;
650: cmd.text = null;
651: }
652:
653: protected void internalRevealRange(int start, int end) {
654: StyledText textWidget = getTextWidget();
655: int startLine = documentAdapter.getLineAtOffset(start);
656: int endLine = documentAdapter.getLineAtOffset(end);
657:
658: int top = textWidget.getTopIndex();
659: if (top > -1) {
660: // scroll vertically
661: int lines = getVisibleLinesInViewport();
662: int bottom = top + lines;
663:
664: // two lines at the top and the bottom should always be left
665: // if window is smaller than 5 lines, always center position is
666: // chosen
667: int bufferZone = 2;
668: if (startLine >= top + bufferZone
669: && startLine <= bottom - bufferZone
670: && endLine >= top + bufferZone
671: && endLine <= bottom - bufferZone) {
672:
673: // do not scroll at all as it is already visible
674: } else {
675: int delta = Math.max(0, lines - (endLine - startLine));
676: textWidget.setTopIndex(startLine - delta / 3);
677: updateViewportListeners(INTERNAL);
678: }
679:
680: // scroll horizontally
681: if (endLine < startLine) {
682: endLine += startLine;
683: startLine = endLine - startLine;
684: endLine -= startLine;
685: }
686:
687: int startPixel = -1;
688: int endPixel = -1;
689:
690: if (endLine > startLine) {
691: // reveal the beginning of the range in the start line
692: IRegion extent = getExtent(start, start);
693: startPixel = extent.getOffset()
694: + textWidget.getHorizontalPixel();
695: endPixel = startPixel;
696: } else {
697: IRegion extent = getExtent(start, end);
698: startPixel = extent.getOffset()
699: + textWidget.getHorizontalPixel();
700: endPixel = startPixel + extent.getLength();
701: }
702:
703: int visibleStart = textWidget.getHorizontalPixel();
704: int visibleEnd = visibleStart
705: + textWidget.getClientArea().width;
706:
707: // scroll only if not yet visible
708: if (startPixel < visibleStart || visibleEnd < endPixel) {
709: // set buffer zone to 10 pixels
710: bufferZone = 10;
711: int newOffset = visibleStart;
712: int visibleWidth = visibleEnd - visibleStart;
713: int selectionPixelWidth = endPixel - startPixel;
714:
715: if (startPixel < visibleStart)
716: newOffset = startPixel;
717: else if (selectionPixelWidth + bufferZone < visibleWidth)
718: newOffset = endPixel + bufferZone - visibleWidth;
719: else
720: newOffset = startPixel;
721:
722: float index = ((float) newOffset)
723: / ((float) getAverageCharWidth());
724:
725: textWidget.setHorizontalIndex(Math.round(index));
726: }
727:
728: }
729: }
730:
731: }
|