blob: e8157155b5f2bd26e8f9993d859786329368f839 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2014 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.jface.text.source;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IPaintPositionManager;
import org.eclipse.jface.text.IPainter;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextListener;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextEvent;
/**
* Highlights the peer character matching the character near the caret position, or a pair of peer
* characters enclosing the caret position. This painter can be configured with an
* {@link org.eclipse.jface.text.source.ICharacterPairMatcher} or an
* {@link org.eclipse.jface.text.source.ICharacterPairMatcherExtension}.
* <p>
* Clients instantiate and configure an object of this class.
* </p>
*
* @since 2.1
*/
public final class MatchingCharacterPainter implements IPainter, PaintListener {
/** Indicates whether this painter is active */
private boolean fIsActive= false;
/** The source viewer this painter is associated with */
private ISourceViewer fSourceViewer;
/** The viewer's widget */
private StyledText fTextWidget;
/** The color in which to highlight the peer character */
private Color fColor;
/** The paint position manager */
private IPaintPositionManager fPaintPositionManager;
/** The strategy for finding matching characters */
private ICharacterPairMatcher fMatcher;
/** The position tracking the matching characters */
private Position fPairPosition= new Position(0, 0);
/** The anchor indicating whether the character is left or right of the caret */
private int fAnchor;
/**
* Whether to highlight enclosing peer characters or not.
*
* @since 3.8
*/
private boolean fHighlightEnclosingPeerCharacters;
/**
* Whether to highlight the character at caret location or not.
*
* @since 3.8
*/
private boolean fHighlightCharacterAtCaretLocation;
/**
* Whether a character is present at caret location or not.
*
* @since 3.8
*/
private boolean fCharacterPresentAtCaretLocation;
/**
* The previous selection, used to determine the need for computing enclosing brackets.
*
* @since 3.8
*/
private IRegion fPreviousSelection;
/**
* Previous length of the document this painter is associated with.
*
* @since 3.8
*/
private int fPreviousLengthOfDocument;
/**
* The text viewer change listener.
*
* @since 3.8
*/
private TextListener fTextListener;
/**
* Creates a new MatchingCharacterPainter for the given source viewer using the given character
* pair matcher. The character matcher is not adopted by this painter. Thus, it is not disposed.
* However, this painter requires exclusive access to the given pair matcher.
*
* @param sourceViewer the source viewer
* @param matcher the character pair matcher
*/
public MatchingCharacterPainter(ISourceViewer sourceViewer, ICharacterPairMatcher matcher) {
fSourceViewer= sourceViewer;
fMatcher= matcher;
fTextWidget= sourceViewer.getTextWidget();
}
/**
* Sets whether to highlight the character at caret location or not.
*
* @param highlightCharacterAtCaretLocation whether to highlight the character at caret location
* or not
* @since 3.8
*/
public void setHighlightCharacterAtCaretLocation(boolean highlightCharacterAtCaretLocation) {
handleDrawRequest(null); // see https://bugs.eclipse.org/372515
fHighlightCharacterAtCaretLocation= highlightCharacterAtCaretLocation;
}
/**
* Sets whether to highlight enclosing peer characters or not.
*
* @param highlightEnclosingPeerCharcters whether to highlight enclosing peer characters or not
* @since 3.8
*/
public void setHighlightEnclosingPeerCharacters(boolean highlightEnclosingPeerCharcters) {
fHighlightEnclosingPeerCharacters= highlightEnclosingPeerCharcters;
installUninstallTextListener(highlightEnclosingPeerCharcters);
}
/**
* Sets the color in which to highlight the match character.
*
* @param color the color
*/
public void setColor(Color color) {
fColor= color;
}
/*
* @see org.eclipse.jface.text.IPainter#dispose()
*/
public void dispose() {
if (fMatcher != null) {
if (fMatcher instanceof ICharacterPairMatcherExtension && fTextListener != null) {
installUninstallTextListener(false);
}
fMatcher.clear();
fMatcher= null;
}
fColor= null;
fTextWidget= null;
}
/*
* @see org.eclipse.jface.text.IPainter#deactivate(boolean)
*/
public void deactivate(boolean redraw) {
if (fIsActive) {
fIsActive= false;
fTextWidget.removePaintListener(this);
if (fPaintPositionManager != null)
fPaintPositionManager.unmanagePosition(fPairPosition);
if (redraw)
handleDrawRequest(null);
}
fPreviousSelection= null;
}
/*
* @see org.eclipse.swt.events.PaintListener#paintControl(org.eclipse.swt.events.PaintEvent)
*/
public void paintControl(PaintEvent event) {
if (fTextWidget != null)
handleDrawRequest(event.gc);
}
/**
* Handles a redraw request.
*
* @param gc the GC to draw into or <code>null</code> to send a redraw request if necessary
*/
private void handleDrawRequest(GC gc) {
if (fPairPosition.isDeleted)
return;
int offset= fPairPosition.getOffset();
int length= fPairPosition.getLength();
if (length < 1)
return;
if (fSourceViewer instanceof ITextViewerExtension5) {
ITextViewerExtension5 extension= (ITextViewerExtension5) fSourceViewer;
IRegion widgetRange= extension.modelRange2WidgetRange(new Region(offset, length));
if (widgetRange == null)
return;
try {
// don't draw if the pair position is really hidden and widgetRange just
// marks the coverage around it.
IDocument doc= fSourceViewer.getDocument();
int startLine= doc.getLineOfOffset(offset);
int endLine= doc.getLineOfOffset(offset + length);
if (extension.modelLine2WidgetLine(startLine) == -1 || extension.modelLine2WidgetLine(endLine) == -1)
return;
} catch (BadLocationException e) {
return;
}
offset= widgetRange.getOffset();
length= widgetRange.getLength();
} else {
IRegion region= fSourceViewer.getVisibleRegion();
if (region.getOffset() > offset || region.getOffset() + region.getLength() < offset + length)
return;
offset -= region.getOffset();
}
if (fHighlightCharacterAtCaretLocation || (fHighlightEnclosingPeerCharacters && !fCharacterPresentAtCaretLocation)) {
draw(gc, offset, 1);
draw(gc, offset + length - 1, 1);
} else {
if (ICharacterPairMatcher.RIGHT == fAnchor)
draw(gc, offset, 1);
else
draw(gc, offset + length - 1, 1);
}
}
/**
* Highlights the given widget region.
*
* @param gc the GC to draw into or <code>null</code> to send a redraw request
* @param offset the offset of the widget region
* @param length the length of the widget region
*/
private void draw(GC gc, int offset, int length) {
if (gc != null) {
gc.setForeground(fColor);
Rectangle bounds;
if (length > 0)
bounds= fTextWidget.getTextBounds(offset, offset + length - 1);
else {
Point loc= fTextWidget.getLocationAtOffset(offset);
bounds= new Rectangle(loc.x, loc.y, 1, fTextWidget.getLineHeight(offset));
}
// draw box around line segment
gc.drawRectangle(bounds.x, bounds.y, bounds.width - 1, bounds.height - 1);
// draw box around character area
// int widgetBaseline= fTextWidget.getBaseline();
// FontMetrics fm= gc.getFontMetrics();
// int fontBaseline= fm.getAscent() + fm.getLeading();
// int fontBias= widgetBaseline - fontBaseline;
// gc.drawRectangle(left.x, left.y + fontBias, right.x - left.x - 1, fm.getHeight() - 1);
} else {
fTextWidget.redrawRange(offset, length, true);
}
}
/**
* Returns the signed current selection. The length will be negative if the resulting selection
* is right-to-left.
* <p>
* The selection offset is model based.
* </p>
*
* @param sourceViewer the source viewer
* @return a region denoting the current signed selection, for a resulting RtoL selections
* length is < 0
* @since 3.8
*/
private static final IRegion getSignedSelection(ISourceViewer sourceViewer) {
Point viewerSelection= sourceViewer.getSelectedRange();
StyledText text= sourceViewer.getTextWidget();
Point selection= text.getSelectionRange();
if (text.getCaretOffset() == selection.x) {
viewerSelection.x= viewerSelection.x + viewerSelection.y;
viewerSelection.y= -viewerSelection.y;
}
return new Region(viewerSelection.x, viewerSelection.y);
}
/*
* @see org.eclipse.jface.text.IPainter#paint(int)
*/
public void paint(int reason) {
IDocument document= fSourceViewer.getDocument();
if (document == null) {
deactivate(false);
return;
}
IRegion selection= getSignedSelection(fSourceViewer);
IRegion pair;
boolean characterPresentAtCaretLocation;
if (fMatcher instanceof ICharacterPairMatcherExtension) {
ICharacterPairMatcherExtension matcher= (ICharacterPairMatcherExtension)fMatcher;
pair= matcher.match(document, selection.getOffset(), selection.getLength());
characterPresentAtCaretLocation= (pair != null);
if (pair == null && fHighlightEnclosingPeerCharacters) {
int length= document.getLength();
boolean lengthChanged= length != fPreviousLengthOfDocument;
fPreviousLengthOfDocument= length;
if (reason != IPainter.CONFIGURATION && fSourceViewer.getDocument() == document && !lengthChanged && (selection.equals(fPreviousSelection) || fPreviousSelection == null)) {
return;
}
if (reason == IPainter.TEXT_CHANGE) {
fPreviousSelection= selection;
return;
}
if (reason != IPainter.CONFIGURATION && !lengthChanged && fPreviousSelection != null && reason != IPainter.INTERNAL) {
if (!matcher.isRecomputationOfEnclosingPairRequired(document, selection, fPreviousSelection)) {
if (fCharacterPresentAtCaretLocation && !fHighlightCharacterAtCaretLocation) {
fCharacterPresentAtCaretLocation= false;
handleDrawRequest(null);
}
fPreviousSelection= selection;
return;
}
}
pair= matcher.findEnclosingPeerCharacters(document, selection.getOffset(), selection.getLength());
}
} else {
if (Math.abs(selection.getLength()) > 0) {
deactivate(true);
return;
}
pair= fMatcher.match(document, selection.getOffset());
characterPresentAtCaretLocation= (pair != null);
}
fPreviousSelection= selection;
if (pair == null) {
deactivate(true);
return;
}
if (fIsActive) {
if (IPainter.CONFIGURATION == reason) {
// redraw current highlighting
handleDrawRequest(null);
} else if (pair.getOffset() != fPairPosition.getOffset() ||
pair.getLength() != fPairPosition.getLength() ||
fMatcher.getAnchor() != fAnchor ||
characterPresentAtCaretLocation != fCharacterPresentAtCaretLocation) {
// otherwise only do something if position is different
// remove old highlighting
handleDrawRequest(null);
// update position
fPairPosition.isDeleted= false;
fPairPosition.offset= pair.getOffset();
fPairPosition.length= pair.getLength();
fAnchor= fMatcher.getAnchor();
fCharacterPresentAtCaretLocation= characterPresentAtCaretLocation;
// apply new highlighting
handleDrawRequest(null);
}
} else {
fIsActive= true;
fPairPosition.isDeleted= false;
fPairPosition.offset= pair.getOffset();
fPairPosition.length= pair.getLength();
fAnchor= fMatcher.getAnchor();
fCharacterPresentAtCaretLocation= characterPresentAtCaretLocation;
fTextWidget.addPaintListener(this);
fPaintPositionManager.managePosition(fPairPosition);
handleDrawRequest(null);
}
}
/*
* @see org.eclipse.jface.text.IPainter#setPositionManager(org.eclipse.jface.text.IPaintPositionManager)
*/
public void setPositionManager(IPaintPositionManager manager) {
fPaintPositionManager= manager;
}
/**
* Installs or uninstalls the text listener depending on the boolean parameter.
*
* @param install <code>true</code> to install the text listener, <code>false</code> to uninstall
*
* @since 3.8
*/
private void installUninstallTextListener(boolean install) {
if (!(fMatcher instanceof ICharacterPairMatcherExtension))
return;
if (install) {
fTextListener= new TextListener();
fSourceViewer.addTextListener(fTextListener);
} else {
if (fTextListener != null) {
fSourceViewer.removeTextListener(fTextListener);
fTextListener= null;
}
}
}
/**
* Listens to document changes and if required by those document changes causes a re-computation
* of matching characters.
*
* @since 3.8
*/
private class TextListener implements ITextListener {
/**
* @see org.eclipse.jface.text.ITextListener#textChanged(org.eclipse.jface.text.TextEvent)
*/
public void textChanged(TextEvent event) {
if (!fHighlightEnclosingPeerCharacters || !(fMatcher instanceof ICharacterPairMatcherExtension))
return;
if (!event.getViewerRedrawState())
return;
String text= event.getText();
String replacedText= event.getReplacedText();
ICharacterPairMatcherExtension matcher= (ICharacterPairMatcherExtension)fMatcher;
if (searchForCharacters(text, matcher) || searchForCharacters(replacedText, matcher))
paint(IPainter.INTERNAL);
}
/**
* Searches for matched characters in the given string.
*
* @param text the string to search
* @param matcher the pair matcher
* @return <code>true</code> if a matched character is found, <code>false</code> otherwise
*/
private boolean searchForCharacters(String text, ICharacterPairMatcherExtension matcher) {
if (text == null)
return false;
for (int i= 0; i < text.length(); i++) {
if (matcher.isMatchedChar(text.charAt(i))) {
return true;
}
}
return false;
}
}
}