blob: f985028ee0c9193294fc03ffa77828e072683d84 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2005 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.ui.console;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.resource.ColorRegistry;
import org.eclipse.jface.resource.JFaceColors;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentAdapter;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.LineBackgroundEvent;
import org.eclipse.swt.custom.LineBackgroundListener;
import org.eclipse.swt.custom.LineStyleEvent;
import org.eclipse.swt.custom.LineStyleListener;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.ui.internal.console.ConsoleDocumentAdapter;
import org.eclipse.ui.internal.console.ConsoleHyperlinkPosition;
import org.eclipse.ui.progress.WorkbenchJob;
/**
* Default viewer used to display a <code>TextConsole</code>.
* <p>
* Clients may subclass this class.
* </p>
*
* @since 3.1
*/
public class TextConsoleViewer extends SourceViewer implements LineStyleListener, LineBackgroundListener, MouseTrackListener, MouseMoveListener, MouseListener {
/**
* Adapts document to the text widget.
*/
private ConsoleDocumentAdapter documentAdapter;
private IHyperlink hyperlink;
private Cursor handCursor;
private Cursor textCursor;
private int consoleWidth = -1;
private TextConsole console;
private IPropertyChangeListener propertyChangeListener;
private IDocumentListener documentListener = new IDocumentListener() {
public void documentAboutToBeChanged(DocumentEvent event) {
}
public void documentChanged(DocumentEvent event) {
updateLinks(event.fOffset);
}
};
// event listener used to send event to hyperlink for IHyperlink2
private Listener mouseUpListener = new Listener() {
public void handleEvent(Event event) {
if (hyperlink != null) {
String selection = getTextWidget().getSelectionText();
if (selection.length() <= 0) {
if (event.button == 1) {
if (hyperlink instanceof IHyperlink2) {
((IHyperlink2) hyperlink).linkActivated(event);
} else {
hyperlink.linkActivated();
}
}
}
}
}
};
WorkbenchJob revealJob = new WorkbenchJob("Reveal End of Document") {//$NON-NLS-1$
public IStatus runInUIThread(IProgressMonitor monitor) {
StyledText textWidget = getTextWidget();
if (textWidget != null) {
int lineCount = textWidget.getLineCount();
textWidget.setTopIndex(lineCount - 1);
}
return Status.OK_STATUS;
}
};
private IPositionUpdater positionUpdater = new IPositionUpdater() {
public void update(DocumentEvent event) {
try {
IDocument document = getDocument();
if (document != null) {
Position[] positions = document.getPositions(ConsoleHyperlinkPosition.HYPER_LINK_CATEGORY);
for (int i = 0; i < positions.length; i++) {
Position position = positions[i];
if (position.offset == event.fOffset && position.length<=event.fLength) {
position.delete();
}
if (position.isDeleted) {
document.removePosition(ConsoleHyperlinkPosition.HYPER_LINK_CATEGORY, position);
}
}
}
} catch (BadPositionCategoryException e) {
}
}
};
/**
* Constructs a new viewer in the given parent for the specified console.
*
* @param parent
* containing widget
* @param console
* text console
*/
public TextConsoleViewer(Composite parent, TextConsole console) {
super(parent, null, SWT.V_SCROLL | SWT.H_SCROLL);
this.console = console;
IDocument document = console.getDocument();
setDocument(document);
StyledText styledText = getTextWidget();
styledText.setDoubleClickEnabled(true);
styledText.addLineStyleListener(this);
styledText.addLineBackgroundListener(this);
styledText.setEditable(true);
setFont(console.getFont());
styledText.addMouseTrackListener(this);
styledText.addListener(SWT.MouseUp, mouseUpListener);
ColorRegistry colorRegistry = JFaceResources.getColorRegistry();
propertyChangeListener = new HyperlinkColorChangeListener();
colorRegistry.addListener(propertyChangeListener);
revealJob.setSystem(true);
document.addDocumentListener(documentListener);
document.addPositionUpdater(positionUpdater);
}
/**
* Sets the tab width used by this viewer.
*
* @param tabWidth
* the tab width used by this viewer
*/
public void setTabWidth(int tabWidth) {
StyledText styledText = getTextWidget();
int oldWidth = styledText.getTabs();
if (tabWidth != oldWidth) {
styledText.setTabs(tabWidth);
}
}
/**
* Sets the font used by this viewer.
*
* @param font
* the font used by this viewer
*/
public void setFont(Font font) {
StyledText styledText = getTextWidget();
Font oldFont = styledText.getFont();
if (oldFont == font) {
return;
}
if (font == null || !(font.equals(oldFont))) {
styledText.setFont(font);
}
}
/**
* Positions the cursor at the end of the document.
*/
protected void revealEndOfDocument() {
revealJob.schedule(50);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.custom.LineStyleListener#lineGetStyle(org.eclipse.swt.custom.LineStyleEvent)
*/
public void lineGetStyle(LineStyleEvent event) {
IDocument document = getDocument();
if (document != null && document.getLength() > 0) {
ArrayList ranges = new ArrayList();
int offset = event.lineOffset;
int length = event.lineText.length();
StyleRange[] partitionerStyles = ((IConsoleDocumentPartitioner) document.getDocumentPartitioner()).getStyleRanges(event.lineOffset, event.lineText.length());
if (partitionerStyles != null) {
for (int i = 0; i < partitionerStyles.length; i++) {
ranges.add(partitionerStyles[i]);
}
} else {
ranges.add(new StyleRange(offset, length, null, null));
}
try {
Position[] positions = getDocument().getPositions(ConsoleHyperlinkPosition.HYPER_LINK_CATEGORY);
Position[] overlap = findPosition(offset, length, positions);
Color color = JFaceColors.getHyperlinkText(Display.getCurrent());
if (overlap != null) {
for (int i = 0; i < overlap.length; i++) {
Position position = overlap[i];
StyleRange linkRange = new StyleRange(position.offset, position.length, color, null);
linkRange.underline = true;
override(ranges, linkRange);
}
}
} catch (BadPositionCategoryException e) {
}
if (ranges.size() > 0) {
event.styles = (StyleRange[]) ranges.toArray(new StyleRange[ranges.size()]);
}
}
}
private void override(List ranges, StyleRange newRange) {
if (ranges.isEmpty()) {
ranges.add(newRange);
return;
}
int start = newRange.start;
int end = start + newRange.length;
for (int i = 0; i < ranges.size(); i++) {
StyleRange existingRange = (StyleRange) ranges.get(i);
int rEnd = existingRange.start + existingRange.length;
if (end <= existingRange.start || start >= rEnd) {
continue;
}
if (start < existingRange.start && end > existingRange.start) {
start = existingRange.start;
}
if (start >= existingRange.start && end <= rEnd) {
existingRange.length = start - existingRange.start;
ranges.add(++i, newRange);
if (end != rEnd) {
ranges.add(++i, new StyleRange(end, rEnd - end - 1, existingRange.foreground, existingRange.background));
}
return;
} else if (start >= existingRange.start && start < rEnd) {
existingRange.length = start - existingRange.start;
ranges.add(++i, newRange);
} else if (end >= rEnd) {
ranges.remove(i);
} else {
ranges.add(++i, new StyleRange(end + 1, rEnd - end + 1, existingRange.foreground, existingRange.background));
}
}
}
/**
* Binary search for the positions overlapping the given range
*
* @param offset
* the offset of the range
* @param length
* the length of the range
* @param positions
* the positions to search
* @return the positions overlapping the given range, or <code>null</code>
*/
private Position[] findPosition(int offset, int length, Position[] positions) {
if (positions.length == 0)
return null;
int rangeEnd = offset + length;
int left = 0;
int right = positions.length - 1;
int mid = 0;
Position position = null;
while (left < right) {
mid = (left + right) / 2;
position = positions[mid];
if (rangeEnd < position.getOffset()) {
if (left == mid)
right = left;
else
right = mid - 1;
} else if (offset > (position.getOffset() + position.getLength() - 1)) {
if (right == mid)
left = right;
else
left = mid + 1;
} else {
left = right = mid;
}
}
List list = new ArrayList();
int index = left - 1;
if (index >= 0) {
position = positions[index];
while (index >= 0 && (position.getOffset() + position.getLength()) > offset) {
index--;
if (index > 0) {
position = positions[index];
}
}
}
index++;
position = positions[index];
while (index < positions.length && (position.getOffset() < rangeEnd)) {
list.add(position);
index++;
if (index < positions.length) {
position = positions[index];
}
}
if (list.isEmpty()) {
return null;
}
return (Position[]) list.toArray(new Position[list.size()]);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.custom.LineBackgroundListener#lineGetBackground(org.eclipse.swt.custom.LineBackgroundEvent)
*/
public void lineGetBackground(LineBackgroundEvent event) {
event.lineBackground = null;
}
/**
* Returns the hand cursor.
*
* @return the hand cursor
*/
protected Cursor getHandCursor() {
if (handCursor == null) {
handCursor = new Cursor(ConsolePlugin.getStandardDisplay(), SWT.CURSOR_HAND);
}
return handCursor;
}
/**
* Returns the text cursor.
*
* @return the text cursor
*/
protected Cursor getTextCursor() {
if (textCursor == null) {
textCursor = new Cursor(ConsolePlugin.getStandardDisplay(), SWT.CURSOR_IBEAM);
}
return textCursor;
}
/**
* Notification a hyperlink has been entered.
*
* @param link
* the link that was entered
*/
protected void linkEntered(IHyperlink link) {
Control control = getTextWidget();
if (hyperlink != null) {
linkExited(hyperlink);
}
hyperlink = link;
hyperlink.linkEntered();
control.setCursor(getHandCursor());
control.redraw();
control.addMouseListener(this);
}
/**
* Notification a link was exited.
*
* @param link
* the link that was exited
*/
protected void linkExited(IHyperlink link) {
link.linkExited();
hyperlink = null;
Control control = getTextWidget();
control.setCursor(getTextCursor());
control.redraw();
control.removeMouseListener(this);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.events.MouseTrackListener#mouseEnter(org.eclipse.swt.events.MouseEvent)
*/
public void mouseEnter(MouseEvent e) {
getTextWidget().addMouseMoveListener(this);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.events.MouseTrackListener#mouseExit(org.eclipse.swt.events.MouseEvent)
*/
public void mouseExit(MouseEvent e) {
getTextWidget().removeMouseMoveListener(this);
if (hyperlink != null) {
linkExited(hyperlink);
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.events.MouseTrackListener#mouseHover(org.eclipse.swt.events.MouseEvent)
*/
public void mouseHover(MouseEvent e) {
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.events.MouseMoveListener#mouseMove(org.eclipse.swt.events.MouseEvent)
*/
public void mouseMove(MouseEvent e) {
int offset = -1;
try {
Point p = new Point(e.x, e.y);
offset = getTextWidget().getOffsetAtLocation(p);
} catch (IllegalArgumentException ex) {
// out of the document range
}
updateLinks(offset);
}
/**
* The cursor has just be moved to the given offset, the mouse has hovered
* over the given offset. Update link rendering.
*
* @param offset
*/
protected void updateLinks(int offset) {
if (offset >= 0) {
IHyperlink link = getHyperlink(offset);
if (link != null) {
if (link.equals(hyperlink)) {
return;
}
linkEntered(link);
return;
}
}
if (hyperlink != null) {
linkExited(hyperlink);
}
}
/**
* Returns the currently active hyperlink or <code>null</code> if none.
*
* @return the currently active hyperlink or <code>null</code> if none
*/
public IHyperlink getHyperlink() {
return hyperlink;
}
/**
* Returns the hyperlink at the specified offset, or <code>null</code> if
* none.
*
* @param offset
* offset at which a hyperlink has been requested
* @return hyperlink at the specified offset, or <code>null</code> if none
*/
public IHyperlink getHyperlink(int offset) {
if (offset >= 0 && console != null) {
return console.getHyperlink(offset);
}
return null;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.events.MouseListener#mouseDoubleClick(org.eclipse.swt.events.MouseEvent)
*/
public void mouseDoubleClick(MouseEvent e) {
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.events.MouseListener#mouseDown(org.eclipse.swt.events.MouseEvent)
*/
public void mouseDown(MouseEvent e) {
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.events.MouseListener#mouseUp(org.eclipse.swt.events.MouseEvent)
*/
public void mouseUp(MouseEvent e) {
}
/*
* (non-Javadoc)
*
* @see org.eclipse.jface.text.TextViewer#createDocumentAdapter()
*/
protected IDocumentAdapter createDocumentAdapter() {
if (documentAdapter == null) {
documentAdapter = new ConsoleDocumentAdapter(consoleWidth = -1);
}
return documentAdapter;
}
/**
* Sets the console to have a fixed character width. Use -1 to indicate that
* a fixed width should not be used.
*
* @param width
* fixed characater width of the console, or -1
*/
public void setConsoleWidth(int width) {
if (consoleWidth != width) {
consoleWidth = width;
ConsolePlugin.getStandardDisplay().asyncExec(new Runnable() {
public void run() {
if (documentAdapter != null) {
documentAdapter.setWidth(consoleWidth);
}
}
});
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.jface.text.TextViewer#handleDispose()
*/
protected void handleDispose() {
super.handleDispose();
IDocument document = getDocument();
if (document != null) {
document.removeDocumentListener(documentListener);
document.removePositionUpdater(positionUpdater);
}
StyledText styledText = getTextWidget();
styledText.removeLineStyleListener(this);
styledText.removeLineBackgroundListener(this);
styledText.removeMouseTrackListener(this);
handCursor = null;
textCursor = null;
hyperlink = null;
console = null;
ColorRegistry colorRegistry = JFaceResources.getColorRegistry();
colorRegistry.removeListener(propertyChangeListener);
}
class HyperlinkColorChangeListener implements IPropertyChangeListener {
public void propertyChange(PropertyChangeEvent event) {
if (event.getProperty().equals(JFacePreferences.ACTIVE_HYPERLINK_COLOR) || event.getProperty().equals(JFacePreferences.HYPERLINK_COLOR)) {
getTextWidget().redraw();
}
}
}
/*
* work around to memory leak in TextViewer$WidgetCommand
*/
protected void updateTextListeners(WidgetCommand cmd) {
super.updateTextListeners(cmd);
cmd.preservedText = null;
cmd.event = null;
cmd.text = null;
}
protected void internalRevealRange(int start, int end) {
StyledText textWidget = getTextWidget();
int startLine = documentAdapter.getLineAtOffset(start);
int endLine = documentAdapter.getLineAtOffset(end);
int top = textWidget.getTopIndex();
if (top > -1) {
// scroll vertically
int lines = getVisibleLinesInViewport();
int bottom = top + lines;
// two lines at the top and the bottom should always be left
// if window is smaller than 5 lines, always center position is
// chosen
int bufferZone = 2;
if (startLine >= top + bufferZone && startLine <= bottom - bufferZone && endLine >= top + bufferZone && endLine <= bottom - bufferZone) {
// do not scroll at all as it is already visible
} else {
int delta = Math.max(0, lines - (endLine - startLine));
textWidget.setTopIndex(startLine - delta / 3);
updateViewportListeners(INTERNAL);
}
// scroll horizontally
if (endLine < startLine) {
endLine += startLine;
startLine = endLine - startLine;
endLine -= startLine;
}
int startPixel = -1;
int endPixel = -1;
if (endLine > startLine) {
// reveal the beginning of the range in the start line
IRegion extent = getExtent(start, start);
startPixel = extent.getOffset() + textWidget.getHorizontalPixel();
endPixel = startPixel;
} else {
IRegion extent = getExtent(start, end);
startPixel = extent.getOffset() + textWidget.getHorizontalPixel();
endPixel = startPixel + extent.getLength();
}
int visibleStart = textWidget.getHorizontalPixel();
int visibleEnd = visibleStart + textWidget.getClientArea().width;
// scroll only if not yet visible
if (startPixel < visibleStart || visibleEnd < endPixel) {
// set buffer zone to 10 pixels
bufferZone = 10;
int newOffset = visibleStart;
int visibleWidth = visibleEnd - visibleStart;
int selectionPixelWidth = endPixel - startPixel;
if (startPixel < visibleStart)
newOffset = startPixel;
else if (selectionPixelWidth + bufferZone < visibleWidth)
newOffset = endPixel + bufferZone - visibleWidth;
else
newOffset = startPixel;
float index = ((float) newOffset) / ((float) getAverageCharWidth());
textWidget.setHorizontalIndex(Math.round(index));
}
}
}
}