/*
* 01/06/2009
*
* MarkOccurrencesSupport.java - Handles marking all occurrences of the
* currently selected identifier in a text area.
* Copyright (C) 2009 Robert Futrell
* robert_futrell at users.sourceforge.net
* http://fifesoft.com/rsyntaxtextarea
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
*/
package org.fife.ui.rsyntaxtextarea;
import java.awt.Color;
import java.awt.Paint;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Timer;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
/**
* Marks all occurrences of the token at the current caret position, if it is an identifier.
*
* @author Robert Futrell
* @version 1.0
*/
class MarkOccurrencesSupport implements CaretListener, ActionListener {
private RSyntaxTextArea textArea;
private Timer timer;
private MarkOccurrencesHighlightPainter p;
private List tags;
/**
* The default delay.
*/
private static final int DEFAULT_DELAY_MS = 1000;
/**
* Constructor. Creates a listener with a 1 second delay.
*/
public MarkOccurrencesSupport() {
this(DEFAULT_DELAY_MS);
}
/**
* Constructor.
*
* @param delay
* The delay between when the caret last moves and when the text should be scanned for matching
* occurrences. This should be in milliseconds.
*/
public MarkOccurrencesSupport(int delay) {
this(delay, new Color(224, 224, 224));
}
/**
* Constructor.
*
* @param delay
* The delay between when the caret last moves and when the text should be scanned for matching
* occurrences. This should be in milliseconds.
* @param color
* The color to use to mark the occurrences. This cannot be <code>null</code>.
*/
public MarkOccurrencesSupport(int delay, Color color) {
timer = new Timer(delay, this);
timer.setRepeats(false);
p = new MarkOccurrencesHighlightPainter();
setColor(color);
tags = new ArrayList();
}
/**
* Called after the caret has been moved and a fixed time delay has elapsed. This locates and highlights all
* occurrences of the identifier at the caret position, if any.
*
* @param e
* The event.
*/
public void actionPerformed(ActionEvent e) {
// Don't do anything if they are selecting text.
Caret c = textArea.getCaret();
if (c.getDot() != c.getMark()) {
return;
}
RSyntaxDocument doc = (RSyntaxDocument) textArea.getDocument();
// long time = System.currentTimeMillis();
doc.readLock();
try {
// Remove old highlights
removeHighlights();
// Get the token at the caret position.
int line = textArea.getCaretLineNumber();
Token tokenList = textArea.getTokenListForLine(line);
int dot = c.getDot();
Token t = RSyntaxUtilities.getTokenAtOffset(tokenList, dot);
if (t == null /* EOL */|| !isValidType(t) || isNonWordChar(t)) {
// Try to the "left" of the caret.
dot--;
try {
if (dot >= textArea.getLineStartOffset(line)) {
t = RSyntaxUtilities.getTokenAtOffset(tokenList, dot);
}
} catch (BadLocationException ble) {
ble.printStackTrace(); // Never happens
}
}
// Add new highlights if an identifier is selected.
if (t != null && isValidType(t) && !isNonWordChar(t)) {
RSyntaxTextAreaHighlighter h = (RSyntaxTextAreaHighlighter) textArea.
getHighlighter();
String lexeme = t.getLexeme();
int type = t.type;
for (int i = 0; i < textArea.getLineCount(); i++) {
Token temp = textArea.getTokenListForLine(i);
while (temp != null && temp.isPaintable()) {
if (temp.is(type, lexeme)) {
try {
int end = temp.offset + temp.textCount;
Object tag = h.addMarkedOccurrenceHighlight(temp.offset, end, p);
// end--; // HACK to prevent typed chars from being added
// Object tag = h.addHighlight(temp.offset, end,p);
tags.add(tag);
// // HACK again, to ensure repaint of last char rendered.
// textArea.getUI().damageRange(textArea, end+1, end+1);
} catch (BadLocationException ble) {
ble.printStackTrace(); // Never happens
}
}
temp = temp.getNextToken();
}
}
}
} finally {
doc.readUnlock();
// time = System.currentTimeMillis() - time;
// System.out.println("Took: " + time + " ms");
}
textArea.fireMarkedOccurrencesChanged();
}
/**
* Called when the caret moves in the text area.
*
* @param e
* The event.
*/
public void caretUpdate(CaretEvent e) {
timer.restart();
}
/**
* Returns the color being used to mark occurrences.
*
* @return The color being used.
* @see #setColor(Paint)
*/
public Color getColor() {
return p.getColor();
}
/**
* Returns the delay, in milliseconds.
*
* @return The delay.
* @see #setDelay(int)
*/
public int getDelay() {
return timer.getDelay();
}
/**
* Installs this listener on a text area. If it is already installed on another text area, it is uninstalled first.
*
* @param textArea
* The text area to install on.
*/
public void install(RSyntaxTextArea textArea) {
if (this.textArea != null) {
uninstall();
}
this.textArea = textArea;
textArea.addCaretListener(this);
}
/**
* Returns whether the specified token is a single non-word char (e.g. not in <tt>[A-Za-z]</tt>. This is a HACK to
* work around the fact that many standard token makers return things like semicolons and periods as
* {@link Token#IDENTIFIER}s just to make the syntax highlighting coloring look a little better.
*
* @param t
* The token to check. This cannot be <tt>null</tt>.
* @return Whether the token is a single non-word char.
*/
private static final boolean isNonWordChar(Token t) {
return t.textCount == 1 &&
!RSyntaxUtilities.isLetter(t.text[t.textOffset]);
}
/**
* Returns whether the specified token is a type that we can do a "mark occurrences" on.
*
* @param t
* The token.
* @return Whether we should mark all occurrences of this token.
*/
private boolean isValidType(Token t) {
return textArea.getMarkOccurrencesOfTokenType(t.type);
}
/**
* Removes all highlights added to the text area by this listener.
*/
private void removeHighlights() {
if (textArea != null) {
RSyntaxTextAreaHighlighter h = (RSyntaxTextAreaHighlighter)
textArea.getHighlighter();
for (int i = 0; i < tags.size(); i++) {
h.removeMarkOccurrencesHighlight(tags.get(i));
}
}
tags.clear();
}
/**
* Sets the color to use when marking occurrences.
*
* @param color
* The color to use.
* @see #getColor()
*/
public void setColor(Color color) {
p.setColor(color);
if (textArea != null) {
removeHighlights();
caretUpdate(null); // Force a highlight repaint.
}
}
/**
* Sets the delay between the last caret position change and when the text is scanned for matching identifiers. A
* delay is needed to prevent repeated scanning while the user is typing.
*
* @param delay
* The new delay.
* @see #getDelay()
*/
public void setDelay(int delay) {
timer.setDelay(delay);
}
/**
* Uninstalls this listener from the current text area. Does nothing if it not currently installed on any text area.
*
* @see #install(RSyntaxTextArea)
*/
public void uninstall() {
if (textArea != null) {
removeHighlights();
textArea.removeCaretListener(this);
}
}
}