/******************************************************************************* * 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) { 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 = 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 = 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 null */ 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 list.toArray(new Position[list.size()]); } /* * (non-Javadoc) * * @see org.eclipse.swt.custom.LineBackgroundListener#lineGetBackground(org.eclipse.swt.custom.LineBackgroundEvent) */ @Override 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) */ @Override public void mouseEnter(MouseEvent e) { getTextWidget().addMouseMoveListener(this); } /* * (non-Javadoc) * * @see org.eclipse.swt.events.MouseTrackListener#mouseExit(org.eclipse.swt.events.MouseEvent) */ @Override 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) */ @Override public void mouseHover(MouseEvent e) { } /* * (non-Javadoc) * * @see org.eclipse.swt.events.MouseMoveListener#mouseMove(org.eclipse.swt.events.MouseEvent) */ @Override public void mouseMove(MouseEvent e) { Point p = new Point(e.x, e.y); int offset = getTextWidget().getOffsetAtPoint(p); 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 null 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)); } } } }