/*******************************************************************************
* Copyright (c) 2000, 2018 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 java.util.concurrent.atomic.AtomicBoolean;
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.JFaceTextUtil;
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.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
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.events.MouseWheelListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.events.VerifyListener;
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 TextConsole
.
*
* Clients may subclass this class. *
* * @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 boolean consoleAutoScrollLock = true; private IPropertyChangeListener propertyChangeListener; private IScrollLockStateProvider scrollLockStateProvider; private IDocumentListener documentListener = new IDocumentListener() { @Override public void documentAboutToBeChanged(DocumentEvent event) { } @Override public void documentChanged(DocumentEvent event) { updateLinks(event.fOffset); } }; // event listener used to send event to hyperlink for IHyperlink2 private Listener mouseUpListener = new Listener() { @Override 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(); } } } } } }; // to store to user scroll lock action private AtomicBoolean userHoldsScrollLock = new AtomicBoolean(false); WorkbenchJob revealJob = new WorkbenchJob("Reveal End of Document") {//$NON-NLS-1$ @Override public IStatus runInUIThread(IProgressMonitor monitor) { scrollToEndOfDocument(); return Status.OK_STATUS; } }; // reveal the end of the document private void scrollToEndOfDocument() { StyledText textWidget = getTextWidget(); if (textWidget != null && !textWidget.isDisposed()) { int lineCount = textWidget.getLineCount(); textWidget.setTopIndex(lineCount > 0 ? lineCount - 1 : 0); } } // set the scroll Lock setting for Console Viewer and Console View private void setScrollLock(boolean lock) { userHoldsScrollLock.set(lock); if (scrollLockStateProvider != null && scrollLockStateProvider.getAutoScrollLock() != lock) { scrollLockStateProvider.setAutoScrollLock(lock); } } /* * Checks if at the end of document */ private boolean checkEndOfDocument() { StyledText textWidget = getTextWidget(); if (textWidget != null && !textWidget.isDisposed()) { int partialBottomIndex = JFaceTextUtil.getPartialBottomIndex(textWidget); int lineCount = textWidget.getLineCount(); int delta = textWidget.getVerticalBar().getIncrement(); return lineCount - partialBottomIndex < delta; } return false; } /* * Check if user preference is enabled for auto scroll lock and the document is empty or the line count is smaller than each * vertical scroll */ private boolean isAutoScrollLockNotApplicable() { if (!consoleAutoScrollLock) { return true; } StyledText textWidget = getTextWidget(); if (textWidget != null && !textWidget.isDisposed()) { return (textWidget.getLineCount() <= textWidget.getVerticalBar().getIncrement()); } return false; } /* * Checks if at the start of document */ private boolean checkStartOfDocument() { StyledText textWidget = getTextWidget(); if (textWidget != null && !textWidget.isDisposed()) { int partialTopIndex = JFaceTextUtil.getPartialTopIndex(textWidget); int lineCount = textWidget.getLineCount(); int delta = textWidget.getVerticalBar().getIncrement(); return lineCount - partialTopIndex < delta; } return false; } private IPositionUpdater positionUpdater = new IPositionUpdater() { @Override 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 the containing composite * @param console the text console * @param scrollLockStateProvider the scroll lock state provider * @since 3.6 */ public TextConsoleViewer(Composite parent, TextConsole console, IScrollLockStateProvider scrollLockStateProvider) { this(parent, console); this.scrollLockStateProvider = scrollLockStateProvider; } /** * 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; this.consoleAutoScrollLock = console.isConsoleAutoScrollLock(); IDocument document = console.getDocument(); setDocument(document); StyledText styledText = getTextWidget(); styledText.setDoubleClickEnabled(true); styledText.addLineStyleListener(this); styledText.addLineBackgroundListener(this); styledText.setEditable(true); styledText.setBackground(console.getBackground()); setFont(console.getFont()); styledText.addMouseTrackListener(this); styledText.addListener(SWT.MouseUp, mouseUpListener); // event listener used to send event to vertical scroll bar styledText.getVerticalBar().addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (isAutoScrollLockNotApplicable()) { return; } // scroll lock if vertical scroll bar dragged, OR selection on // vertical bar used if (e.detail == SWT.TOP || e.detail == SWT.HOME) { // selecting TOP or HOME should lock setScrollLock(true); } if (e.detail == SWT.ARROW_UP || e.detail == SWT.PAGE_UP) { setScrollLock(true); } else if (e.detail == SWT.END || e.detail == SWT.BOTTOM) { // selecting BOTTOM or END from vertical scroll makes it // reveal the end setScrollLock(false); } else if (e.detail == SWT.DRAG) { if (checkEndOfDocument()) { setScrollLock(false); } else { setScrollLock(true); } } else if ((e.detail == SWT.PAGE_DOWN || e.detail == SWT.ARROW_DOWN) && checkEndOfDocument()) { // unlock if Down at the end of document setScrollLock(false); } } }); styledText.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (isAutoScrollLockNotApplicable()) { return; } // lock the scroll if PAGE_UP ,HOME or TOP selected if (e.keyCode == SWT.HOME || e.keyCode == SWT.TOP) { setScrollLock(true); } else if ((e.keyCode == SWT.PAGE_UP || e.keyCode == SWT.ARROW_UP) && !checkStartOfDocument()) { setScrollLock(true); } else if (e.keyCode == SWT.END || e.keyCode == SWT.BOTTOM) { setScrollLock(false);// selecting END makes it reveal the // end } else if ((e.keyCode == SWT.PAGE_DOWN || e.keyCode == SWT.ARROW_DOWN) && checkEndOfDocument()) { // unlock if Down at the end of document setScrollLock(false); } } }); styledText.addMouseWheelListener(new MouseWheelListener() { @Override public void mouseScrolled(MouseEvent e) { if (isAutoScrollLockNotApplicable()) { return; } if (e.count < 0) { // Mouse dragged down if (checkEndOfDocument()) { setScrollLock(false); } } else if (!userHoldsScrollLock.get()) { setScrollLock(true); } } }); styledText.addVerifyListener(new VerifyListener() { @Override public void verifyText(VerifyEvent e) { // unlock the auto lock if user starts typing only if it was not manual lock if (scrollLockStateProvider != null && !scrollLockStateProvider.getScrollLock()) { setScrollLock(false); } } }); 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) */ @Override public void lineGetStyle(LineStyleEvent event) { IDocument document = getDocument(); if (document != null && document.getLength() > 0) { ArrayListnull
*/
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;
}
}
Listnull
if none.
*
* @return the currently active hyperlink or null
if none
*/
public IHyperlink getHyperlink() {
return hyperlink;
}
/**
* Returns the hyperlink at the specified offset, or null
if
* none.
*
* @param offset
* offset at which a hyperlink has been requested
* @return hyperlink at the specified offset, or null
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)
*/
@Override
public void mouseDoubleClick(MouseEvent e) {
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.events.MouseListener#mouseDown(org.eclipse.swt.events.MouseEvent)
*/
@Override
public void mouseDown(MouseEvent e) {
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.events.MouseListener#mouseUp(org.eclipse.swt.events.MouseEvent)
*/
@Override
public void mouseUp(MouseEvent e) {
}
/*
* (non-Javadoc)
*
* @see org.eclipse.jface.text.TextViewer#createDocumentAdapter()
*/
@Override
protected IDocumentAdapter createDocumentAdapter() {
if (documentAdapter == null) {
documentAdapter = new ConsoleDocumentAdapter(consoleWidth = -1);
}
return documentAdapter;
}
/**
* Sets the user preference for console auto scroll lock.
*
* @param autoScrollLock user preference for console auto scroll lock
* @since 3.8
*/
public void setConsoleAutoScrollLock(boolean autoScrollLock) {
if (consoleAutoScrollLock != autoScrollLock) {
consoleAutoScrollLock = autoScrollLock;
}
}
/**
* Sets the console to have a fixed character width. Use -1 to indicate that
* a fixed width should not be used.
*
* @param width
* fixed character width of the console, or -1
*/
public void setConsoleWidth(int width) {
if (consoleWidth != width) {
consoleWidth = width;
ConsolePlugin.getStandardDisplay().asyncExec(new Runnable() {
@Override
public void run() {
if (documentAdapter != null) {
documentAdapter.setWidth(consoleWidth);
}
}
});
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.jface.text.TextViewer#handleDispose()
*/
@Override
protected void 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);
if(handCursor != null) {
handCursor.dispose();
}
handCursor = null;
if(textCursor != null) {
textCursor.dispose();
}
textCursor = null;
hyperlink = null;
console = null;
ColorRegistry colorRegistry = JFaceResources.getColorRegistry();
colorRegistry.removeListener(propertyChangeListener);
super.handleDispose();
}
class HyperlinkColorChangeListener implements IPropertyChangeListener {
@Override
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
*/
@Override
protected void updateTextListeners(WidgetCommand cmd) {
super.updateTextListeners(cmd);
cmd.preservedText = null;
cmd.event = null;
cmd.text = null;
}
@Override
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
@SuppressWarnings("deprecation")
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));
}
}
}
}