Dépôt officiel du code source de l'ERP OpenConcerto
Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
package org.jedit;
/*
* JEditTextArea.java - jEdit's text component Copyright (C) 1999 Slava Pestov
*
* You may use and modify this package for any purpose. Redistribution is permitted, in both source
* and binary form, provided that this notice remains intact in all source distributions of this
* package.
*/
import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.Enumeration;
import java.util.Vector;
import javax.swing.Action;
import javax.swing.JPopupMenu;
import javax.swing.JScrollBar;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.EventListenerList;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import javax.swing.text.Segment;
import javax.swing.text.TextAction;
import javax.swing.text.Utilities;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoableEdit;
/**
* jEdit's text area component. It is more suited for editing program source code than JEditorPane,
* because it drops the unnecessary features (images, variable-width lines, and so on) and adds a
* whole bunch of useful goodies such as:
* <ul>
* <li>More flexible key binding scheme
* <li>Supports macro recorders
* <li>Rectangular selection
* <li>Bracket highlighting
* <li>Syntax highlighting
* <li>Command repetition
* <li>Block caret can be enabled
* </ul>
* It is also faster and doesn't have as many problems. It can be used in other applications; the
* only other part of jEdit it depends on is the syntax package.
* <p>
*
* To use it in your app, treat it like any other component, for example:
*
* <pre>
* JEditTextArea ta = new JEditTextArea();
* ta.setTokenMarker(new JavaTokenMarker());
* ta.setText("public class Test {\n" + " public static void main(String[] args) {\n" + " System.out.println(\"Hello World\");\n" + " }\n" + "}");
* </pre>
*
* @author Slava Pestov
* @version $Id: JEditTextArea.java,v 1.36 1999/12/13 03:40:30 sp Exp $
*/
public class JEditTextArea extends JTextComponent {
/**
* Adding components with this name to the text area will place them left of the horizontal
* scroll bar. In jEdit, the status bar is added this way.
*/
public static String LEFT_OF_SCROLLBAR = "los";
/**
* Creates a new JEditTextArea with the default settings.
*/
public JEditTextArea() {
this(new TextAreaDefaults().getDefaults());
}
/**
* Creates a new JEditTextArea with the specified settings.
*
* @param defaults The default settings
*/
public JEditTextArea(TextAreaDefaults defaults) {
// Enable the necessary events
enableEvents(AWTEvent.KEY_EVENT_MASK);
// Initialize some misc. stuff
this.painter = new TextAreaPainter(this, defaults);
this.documentHandler = new DocumentHandler();
this.listenerList = new EventListenerList();
this.caretEvent = new MutableCaretEvent();
this.lineSegment = new Segment();
this.bracketLine = this.bracketPosition = -1;
this.blink = true;
// Initialize the GUI
setLayout(new ScrollLayout());
add(CENTER, this.painter);
add(RIGHT, this.vertical = new JScrollBar(JScrollBar.VERTICAL));
add(BOTTOM, this.horizontal = new JScrollBar(JScrollBar.HORIZONTAL));
// Add some event listeners
this.vertical.addAdjustmentListener(new AdjustHandler());
this.horizontal.addAdjustmentListener(new AdjustHandler());
this.painter.addComponentListener(new ComponentHandler());
this.painter.addMouseListener(new MouseHandler());
this.painter.addMouseMotionListener(new DragHandler());
addFocusListener(new FocusHandler());
// Load the defaults
setInputHandler(defaults.inputHandler);
setDocument(defaults.document);
this.editable = defaults.editable;
this.caretVisible = defaults.caretVisible;
this.caretBlinks = defaults.caretBlinks;
this.electricScroll = defaults.electricScroll;
this.popup = defaults.popup;
// We don't seem to get the initial focus event?
focusedComponent = this;
}
/**
* Returns if this component can be traversed by pressing the Tab key. This returns false.
*/
public final boolean isManagingFocus() {
return true;
}
/**
* Returns the object responsible for painting this text area.
*/
public final TextAreaPainter getPainter() {
return this.painter;
}
/**
* Returns the input handler.
*/
public final InputHandler getInputHandler() {
return this.inputHandler;
}
/**
* Sets the input handler.
*
* @param inputHandler The new input handler
*/
public void setInputHandler(InputHandler inputHandler) {
this.inputHandler = inputHandler;
}
/**
* Returns true if the caret is blinking, false otherwise.
*/
public final boolean isCaretBlinkEnabled() {
return this.caretBlinks;
}
/**
* Toggles caret blinking.
*
* @param caretBlinks True if the caret should blink, false otherwise
*/
public void setCaretBlinkEnabled(boolean caretBlinks) {
this.caretBlinks = caretBlinks;
if (!caretBlinks)
this.blink = false;
this.painter.invalidateSelectedLines();
}
/**
* Returns true if the caret is visible, false otherwise.
*/
public final boolean isCaretVisible() {
return (!this.caretBlinks || this.blink) && this.caretVisible;
}
/**
* Sets if the caret should be visible.
*
* @param caretVisible True if the caret should be visible, false otherwise
*/
public void setCaretVisible(boolean caretVisible) {
this.caretVisible = caretVisible;
this.blink = true;
this.painter.invalidateSelectedLines();
}
/**
* Blinks the caret.
*/
public final void blinkCaret() {
if (this.caretBlinks) {
this.blink = !this.blink;
this.painter.invalidateSelectedLines();
} else
this.blink = true;
}
/**
* Returns the number of lines from the top and button of the text area that are always visible.
*/
public final int getElectricScroll() {
return this.electricScroll;
}
/**
* Sets the number of lines from the top and bottom of the text area that are always visible
*
* @param electricScroll The number of lines always visible from the top or bottom
*/
public final void setElectricScroll(int electricScroll) {
this.electricScroll = electricScroll;
}
/**
* Updates the state of the scroll bars. This should be called if the number of lines in the
* document changes, or when the size of the text are changes.
*/
public void updateScrollBars() {
if (this.vertical != null && this.visibleLines != 0) {
this.vertical.setValues(this.firstLine, this.visibleLines, 0, getLineCount());
this.vertical.setUnitIncrement(2);
this.vertical.setBlockIncrement(this.visibleLines);
}
int width = this.painter.getWidth();
if (this.horizontal != null && width != 0) {
this.horizontal.setValues(-this.horizontalOffset, width, 0, width * 5);
this.horizontal.setUnitIncrement(this.painter.getFontMetrics().charWidth('w'));
this.horizontal.setBlockIncrement(width / 2);
}
}
/**
* Returns the line displayed at the text area's origin.
*/
public final int getFirstLine() {
return this.firstLine;
}
/**
* Sets the line displayed at the text area's origin without updating the scroll bars.
*/
public void setFirstLine(int firstLine) {
if (firstLine == this.firstLine)
return;
this.firstLine = firstLine;
if (firstLine != this.vertical.getValue())
updateScrollBars();
this.painter.repaint();
}
/**
* Returns the number of lines visible in this text area.
*/
public final int getVisibleLines() {
return this.visibleLines;
}
/**
* Recalculates the number of visible lines. This should not be called directly.
*/
public final void recalculateVisibleLines() {
if (this.painter == null)
return;
int height = this.painter.getHeight();
int lineHeight = this.painter.getFontMetrics().getHeight();
this.visibleLines = height / lineHeight;
updateScrollBars();
}
/**
* Returns the horizontal offset of drawn lines.
*/
public final int getHorizontalOffset() {
return this.horizontalOffset;
}
/**
* Sets the horizontal offset of drawn lines. This can be used to implement horizontal
* scrolling.
*
* @param horizontalOffset offset The new horizontal offset
*/
public void setHorizontalOffset(int horizontalOffset) {
if (horizontalOffset == this.horizontalOffset)
return;
this.horizontalOffset = horizontalOffset;
if (horizontalOffset != this.horizontal.getValue())
updateScrollBars();
this.painter.repaint();
}
/**
* A fast way of changing both the first line and horizontal offset.
*
* @param firstLine The new first line
* @param horizontalOffset The new horizontal offset
* @return True if any of the values were changed, false otherwise
*/
public boolean setOrigin(int firstLine, int horizontalOffset) {
boolean changed = false;
if (horizontalOffset != this.horizontalOffset) {
this.horizontalOffset = horizontalOffset;
changed = true;
}
if (firstLine != this.firstLine) {
this.firstLine = firstLine;
changed = true;
}
if (changed) {
updateScrollBars();
this.painter.repaint();
}
return changed;
}
/**
* Ensures that the caret is visible by scrolling the text area if necessary.
*
* @return True if scrolling was actually performed, false if the caret was already visible
*/
public boolean scrollToCaret() {
int line = getCaretLine();
int lineStart = getLineStartOffset(line);
int offset = Math.max(0, Math.min(getLineLength(line) - 1, getCaretPosition() - lineStart));
return scrollTo(line, offset);
}
/**
* Ensures that the specified line and offset is visible by scrolling the text area if
* necessary.
*
* @param line The line to scroll to
* @param offset The offset in the line to scroll to
* @return True if scrolling was actually performed, false if the line and offset was already
* visible
*/
public boolean scrollTo(int line, int offset) {
// visibleLines == 0 before the component is realized
// we can't do any proper scrolling then, so we have
// this hack...
if (this.visibleLines == 0) {
setFirstLine(Math.max(0, line - this.electricScroll));
return true;
}
int newFirstLine = this.firstLine;
int newHorizontalOffset = this.horizontalOffset;
if (line < this.firstLine + this.electricScroll) {
newFirstLine = Math.max(0, line - this.electricScroll);
} else if (line + this.electricScroll >= this.firstLine + this.visibleLines) {
newFirstLine = (line - this.visibleLines) + this.electricScroll + 1;
if (newFirstLine + this.visibleLines >= getLineCount())
newFirstLine = getLineCount() - this.visibleLines;
if (newFirstLine < 0)
newFirstLine = 0;
}
int x = _offsetToX(line, offset);
int width = this.painter.getFontMetrics().charWidth('w');
if (x < 0) {
newHorizontalOffset = Math.min(0, this.horizontalOffset - x + width + 5);
} else if (x + width >= this.painter.getWidth()) {
newHorizontalOffset = this.horizontalOffset + (this.painter.getWidth() - x) - width - 5;
}
return setOrigin(newFirstLine, newHorizontalOffset);
}
/**
* Converts a line index to a y co-ordinate.
*
* @param line The line
*/
public int lineToY(int line) {
FontMetrics fm = this.painter.getFontMetrics();
return (line - this.firstLine) * fm.getHeight() - (fm.getLeading() + fm.getMaxDescent());
}
/**
* Converts a y co-ordinate to a line index.
*
* @param y The y co-ordinate
*/
public int yToLine(int y) {
FontMetrics fm = this.painter.getFontMetrics();
int height = fm.getHeight();
return Math.max(0, Math.min(getLineCount() - 1, y / height + this.firstLine));
}
/**
* Converts an offset in a line into an x co-ordinate. This is a slow version that can be used
* any time.
*
* @param line The line
* @param offset The offset, from the start of the line
*/
public final int offsetToX(int line, int offset) {
// don't use cached tokens
this.painter.currentLineTokens = null;
return _offsetToX(line, offset);
}
/**
* Converts an offset in a line into an x co-ordinate. This is a fast version that should only
* be used if no changes were made to the text since the last repaint.
*
* @param line The line
* @param offset The offset, from the start of the line
*/
public int _offsetToX(int line, int offset) {
TokenMarker tokenMarker = getTokenMarker();
/* Use painter's cached info for speed */
FontMetrics fm = this.painter.getFontMetrics();
getLineText(line, this.lineSegment);
int segmentOffset = this.lineSegment.offset;
int x = this.horizontalOffset;
/* If syntax coloring is disabled, do simple translation */
if (tokenMarker == null) {
this.lineSegment.count = offset;
return x + Utilities.getTabbedTextWidth(this.lineSegment, fm, x, this.painter, 0);
}
Token tokens;
if (this.painter.currentLineIndex == line && this.painter.currentLineTokens != null)
tokens = this.painter.currentLineTokens;
else {
this.painter.currentLineIndex = line;
tokens = this.painter.currentLineTokens = tokenMarker.markTokens(this.lineSegment, line);
}
// Toolkit toolkit = this.painter.getToolkit();
Font defaultFont = this.painter.getFont();
SyntaxStyle[] styles = this.painter.getStyles();
for (;;) {
byte id = tokens.id;
if (id == Token.END) {
return x;
}
if (id == Token.NULL)
fm = this.painter.getFontMetrics();
else
fm = styles[id].getFontMetrics(defaultFont);
int length = tokens.length;
if (offset + segmentOffset < this.lineSegment.offset + length) {
this.lineSegment.count = offset - (this.lineSegment.offset - segmentOffset);
return x + Utilities.getTabbedTextWidth(this.lineSegment, fm, x, this.painter, 0);
}
this.lineSegment.count = length;
x += Utilities.getTabbedTextWidth(this.lineSegment, fm, x, this.painter, 0);
this.lineSegment.offset += length;
tokens = tokens.next;
}
}
/**
* Converts an x co-ordinate to an offset within a line.
*
* @param line The line
* @param x The x co-ordinate
*/
public int xToOffset(int line, int x) {
TokenMarker tokenMarker = getTokenMarker();
/* Use painter's cached info for speed */
FontMetrics fm = this.painter.getFontMetrics();
getLineText(line, this.lineSegment);
char[] segmentArray = this.lineSegment.array;
int segmentOffset = this.lineSegment.offset;
int segmentCount = this.lineSegment.count;
int width = this.horizontalOffset;
if (tokenMarker == null) {
for (int i = 0; i < segmentCount; i++) {
char c = segmentArray[i + segmentOffset];
int charWidth;
if (c == '\t')
charWidth = (int) this.painter.nextTabStop(width, i) - width;
else
charWidth = fm.charWidth(c);
if (this.painter.isBlockCaretEnabled()) {
if (x - charWidth <= width)
return i;
} else {
if (x - charWidth / 2 <= width)
return i;
}
width += charWidth;
}
return segmentCount;
}
Token tokens;
if (this.painter.currentLineIndex == line && this.painter.currentLineTokens != null)
tokens = this.painter.currentLineTokens;
else {
this.painter.currentLineIndex = line;
tokens = this.painter.currentLineTokens = tokenMarker.markTokens(this.lineSegment, line);
}
int offset = 0;
// Toolkit toolkit = this.painter.getToolkit();
Font defaultFont = this.painter.getFont();
SyntaxStyle[] styles = this.painter.getStyles();
for (;;) {
byte id = tokens.id;
if (id == Token.END)
return offset;
if (id == Token.NULL)
fm = this.painter.getFontMetrics();
else
fm = styles[id].getFontMetrics(defaultFont);
int length = tokens.length;
for (int i = 0; i < length; i++) {
char c = segmentArray[segmentOffset + offset + i];
int charWidth;
if (c == '\t')
charWidth = (int) this.painter.nextTabStop(width, offset + i) - width;
else
charWidth = fm.charWidth(c);
if (this.painter.isBlockCaretEnabled()) {
if (x - charWidth <= width)
return offset + i;
} else {
if (x - charWidth / 2 <= width)
return offset + i;
}
width += charWidth;
}
offset += length;
tokens = tokens.next;
}
}
/**
* Converts a point to an offset, from the start of the text.
*
* @param x The x co-ordinate of the point
* @param y The y co-ordinate of the point
*/
public int xyToOffset(int x, int y) {
int line = yToLine(y);
int start = getLineStartOffset(line);
return start + xToOffset(line, x);
}
/**
* Returns the document this text area is editing.
*/
public final SyntaxDocument getDocument() {
return this.document;
}
/**
* Sets the document this text area is editing.
*
* @param document The document
*/
public void setDocument(SyntaxDocument document) {
if (this.document == document)
return;
if (this.document != null)
this.document.removeDocumentListener(this.documentHandler);
this.document = document;
document.addDocumentListener(this.documentHandler);
select(0, 0);
updateScrollBars();
this.painter.repaint();
}
/**
* Returns the document's token marker. Equivalent to calling
* <code>getDocument().getTokenMarker()</code>.
*/
public final TokenMarker getTokenMarker() {
return this.document.getTokenMarker();
}
/**
* Sets the document's token marker. Equivalent to caling
* <code>getDocument().setTokenMarker()</code>.
*
* @param tokenMarker The token marker
*/
public final void setTokenMarker(TokenMarker tokenMarker) {
this.document.setTokenMarker(tokenMarker);
}
/**
* Returns the length of the document. Equivalent to calling
* <code>getDocument().getLength()</code>.
*/
public final int getDocumentLength() {
return this.document.getLength();
}
/**
* Returns the number of lines in the document.
*/
public final int getLineCount() {
return this.document.getDefaultRootElement().getElementCount();
}
/**
* Returns the line containing the specified offset.
*
* @param offset The offset
*/
public final int getLineOfOffset(int offset) {
return this.document.getDefaultRootElement().getElementIndex(offset);
}
/**
* Returns the start offset of the specified line.
*
* @param line The line
* @return The start offset of the specified line, or -1 if the line is invalid
*/
public int getLineStartOffset(int line) {
Element lineElement = this.document.getDefaultRootElement().getElement(line);
if (lineElement == null)
return -1;
return lineElement.getStartOffset();
}
/**
* Returns the end offset of the specified line.
*
* @param line The line
* @return The end offset of the specified line, or -1 if the line is invalid.
*/
public int getLineEndOffset(int line) {
Element lineElement = this.document.getDefaultRootElement().getElement(line);
if (lineElement == null)
return -1;
return lineElement.getEndOffset();
}
/**
* Returns the length of the specified line.
*
* @param line The line
*/
public int getLineLength(int line) {
Element lineElement = this.document.getDefaultRootElement().getElement(line);
if (lineElement == null)
return -1;
return lineElement.getEndOffset() - lineElement.getStartOffset() - 1;
}
/**
* Returns the entire text of this text area.
*/
public String getText() {
try {
return this.document.getText(0, this.document.getLength());
} catch (BadLocationException bl) {
bl.printStackTrace();
return null;
}
}
/**
* Sets the entire text of this text area.
*/
public void setText(String text) {
try {
this.document.beginCompoundEdit();
this.document.remove(0, this.document.getLength());
this.document.insertString(0, text, null);
} catch (BadLocationException bl) {
bl.printStackTrace();
} finally {
this.document.endCompoundEdit();
}
}
/**
* Returns the specified substring of the document.
*
* @param start The start offset
* @param len The length of the substring
* @return The substring, or null if the offsets are invalid
*/
public final String getText(int start, int len) {
try {
return this.document.getText(start, len);
} catch (BadLocationException bl) {
bl.printStackTrace();
return null;
}
}
/**
* Copies the specified substring of the document into a segment. If the offsets are invalid,
* the segment will contain a null string.
*
* @param start The start offset
* @param len The length of the substring
* @param segment The segment
*/
public final void getText(int start, int len, Segment segment) {
try {
this.document.getText(start, len, segment);
} catch (BadLocationException bl) {
bl.printStackTrace();
segment.offset = segment.count = 0;
}
}
/**
* Returns the text on the specified line.
*
* @param lineIndex The line
* @return The text, or null if the line is invalid
*/
public final String getLineText(int lineIndex) {
int start = getLineStartOffset(lineIndex);
return getText(start, getLineEndOffset(lineIndex) - start - 1);
}
/**
* Copies the text on the specified line into a segment. If the line is invalid, the segment
* will contain a null string.
*
* @param lineIndex The line
*/
public final void getLineText(int lineIndex, Segment segment) {
int start = getLineStartOffset(lineIndex);
getText(start, getLineEndOffset(lineIndex) - start - 1, segment);
}
/**
* Returns the selection start offset.
*/
public final int getSelectionStart() {
return this.selectionStart;
}
/**
* Returns the offset where the selection starts on the specified line.
*/
public int getSelectionStart(int line) {
if (line == this.selectionStartLine)
return this.selectionStart;
else if (this.rectSelect) {
Element map = this.document.getDefaultRootElement();
int start = this.selectionStart - map.getElement(this.selectionStartLine).getStartOffset();
Element lineElement = map.getElement(line);
int lineStart = lineElement.getStartOffset();
int lineEnd = lineElement.getEndOffset() - 1;
return Math.min(lineEnd, lineStart + start);
} else
return getLineStartOffset(line);
}
/**
* Returns the selection start line.
*/
public final int getSelectionStartLine() {
return this.selectionStartLine;
}
/**
* Sets the selection start. The new selection will be the new selection start and the old
* selection end.
*
* @param selectionStart The selection start
* @see #select(int,int)
*/
public final void setSelectionStart(int selectionStart) {
select(selectionStart, this.selectionEnd);
}
/**
* Returns the selection end offset.
*/
public final int getSelectionEnd() {
return this.selectionEnd;
}
/**
* Returns the offset where the selection ends on the specified line.
*/
public int getSelectionEnd(int line) {
if (line == this.selectionEndLine)
return this.selectionEnd;
else if (this.rectSelect) {
Element map = this.document.getDefaultRootElement();
int end = this.selectionEnd - map.getElement(this.selectionEndLine).getStartOffset();
Element lineElement = map.getElement(line);
int lineStart = lineElement.getStartOffset();
int lineEnd = lineElement.getEndOffset() - 1;
return Math.min(lineEnd, lineStart + end);
} else
return getLineEndOffset(line) - 1;
}
/**
* Returns the selection end line.
*/
public final int getSelectionEndLine() {
return this.selectionEndLine;
}
/**
* Sets the selection end. The new selection will be the old selection start and the bew
* selection end.
*
* @param selectionEnd The selection end
* @see #select(int,int)
*/
public final void setSelectionEnd(int selectionEnd) {
select(this.selectionStart, selectionEnd);
}
/**
* Returns the caret position. This will either be the selection start or the selection end,
* depending on which direction the selection was made in.
*/
public final int getCaretPosition() {
return (this.biasLeft ? this.selectionStart : this.selectionEnd);
}
/**
* Returns the caret line.
*/
public final int getCaretLine() {
return (this.biasLeft ? this.selectionStartLine : this.selectionEndLine);
}
/**
* Returns the mark position. This will be the opposite selection bound to the caret position.
*
* @see #getCaretPosition()
*/
public final int getMarkPosition() {
return (this.biasLeft ? this.selectionEnd : this.selectionStart);
}
/**
* Returns the mark line.
*/
public final int getMarkLine() {
return (this.biasLeft ? this.selectionEndLine : this.selectionStartLine);
}
/**
* Sets the caret position. The new selection will consist of the caret position only (hence no
* text will be selected)
*
* @param caret The caret position
* @see #select(int,int)
*/
public final void setCaretPosition(int caret) {
select(caret, caret);
}
/**
* Selects all text in the document.
*/
public final void selectAll() {
select(0, getDocumentLength());
}
/**
* Moves the mark to the caret position.
*/
public final void selectNone() {
select(getCaretPosition(), getCaretPosition());
}
/**
* Selects from the start offset to the end offset. This is the general selection method used by
* all other selecting methods. The caret position will be start if start < end, and end if
* end > start.
*
* @param start The start offset
* @param end The end offset
*/
public void select(int start, int end) {
// System.err.println("JEditTextArea:select:" + start + " ," + end);
int newStart, newEnd;
boolean newBias;
if (start <= end) {
newStart = start;
newEnd = end;
newBias = false;
} else {
newStart = end;
newEnd = start;
newBias = true;
}
if (newStart < 0 || newEnd > getDocumentLength()) {
throw new IllegalArgumentException("Bounds out of" + " range: " + newStart + "," + newEnd);
}
// If the new position is the same as the old, we don't
// do all this crap, however we still do the stuff at
// the end (clearing magic position, scrolling)
if (newStart != selectionStart || newEnd != selectionEnd || newBias != biasLeft) {
int newStartLine = getLineOfOffset(newStart);
int newEndLine = getLineOfOffset(newEnd);
if (painter.isBracketHighlightEnabled()) {
if (bracketLine != -1)
painter.invalidateLine(bracketLine);
updateBracketHighlight(end);
if (bracketLine != -1)
painter.invalidateLine(bracketLine);
}
painter.invalidateLineRange(selectionStartLine, selectionEndLine);
painter.invalidateLineRange(newStartLine, newEndLine);
document.addUndoableEdit(new CaretUndo(selectionStart, selectionEnd));
selectionStart = newStart;
selectionEnd = newEnd;
selectionStartLine = newStartLine;
selectionEndLine = newEndLine;
biasLeft = newBias;
fireCaretEvent();
}
// When the user is typing, etc, we don't want the caret
// to blink
blink = true;
caretTimer.restart();
// Disable rectangle select if selection start = selection end
if (selectionStart == selectionEnd)
rectSelect = false;
// Clear the `magic' caret position used by up/down
magicCaret = -1;
scrollToCaret();
}
/**
* Returns the selected text, or null if no selection is active.
*/
public final String getSelectedText() {
if (selectionStart == selectionEnd)
return null;
if (rectSelect) {
// Return each row of the selection on a new line
Element map = document.getDefaultRootElement();
int start = selectionStart - map.getElement(selectionStartLine).getStartOffset();
int end = selectionEnd - map.getElement(selectionEndLine).getStartOffset();
// Certain rectangles satisfy this condition...
if (end < start) {
int tmp = end;
end = start;
start = tmp;
}
StringBuffer buf = new StringBuffer();
Segment seg = new Segment();
for (int i = selectionStartLine; i <= selectionEndLine; i++) {
Element lineElement = map.getElement(i);
int lineStart = lineElement.getStartOffset();
int lineEnd = lineElement.getEndOffset() - 1;
int lineLen = lineEnd - lineStart;
lineStart = Math.min(lineStart + start, lineEnd);
lineLen = Math.min(end - start, lineEnd - lineStart);
getText(lineStart, lineLen, seg);
buf.append(seg.array, seg.offset, seg.count);
if (i != selectionEndLine)
buf.append('\n');
}
return buf.toString();
}
return getText(selectionStart, selectionEnd - selectionStart);
}
/**
* Replaces the selection with the specified text.
*
* @param selectedText The replacement text for the selection
*/
public void setSelectedText(String selectedText) {
if (!editable) {
throw new InternalError("Text component" + " read only");
}
document.beginCompoundEdit();
try {
if (rectSelect) {
Element map = document.getDefaultRootElement();
int start = selectionStart - map.getElement(selectionStartLine).getStartOffset();
int end = selectionEnd - map.getElement(selectionEndLine).getStartOffset();
// Certain rectangles satisfy this condition...
if (end < start) {
int tmp = end;
end = start;
start = tmp;
}
int lastNewline = 0;
int currNewline = 0;
for (int i = selectionStartLine; i <= selectionEndLine; i++) {
Element lineElement = map.getElement(i);
int lineStart = lineElement.getStartOffset();
int lineEnd = lineElement.getEndOffset() - 1;
int rectStart = Math.min(lineEnd, lineStart + start);
document.remove(rectStart, Math.min(lineEnd - rectStart, end - start));
if (selectedText == null)
continue;
currNewline = selectedText.indexOf('\n', lastNewline);
if (currNewline == -1)
currNewline = selectedText.length();
document.insertString(rectStart, selectedText.substring(lastNewline, currNewline), null);
lastNewline = Math.min(selectedText.length(), currNewline + 1);
}
if (selectedText != null && currNewline != selectedText.length()) {
int offset = map.getElement(selectionEndLine).getEndOffset() - 1;
document.insertString(offset, "\n", null);
document.insertString(offset + 1, selectedText.substring(currNewline + 1), null);
}
} else {
document.remove(selectionStart, selectionEnd - selectionStart);
if (selectedText != null) {
document.insertString(selectionStart, selectedText, null);
}
}
} catch (BadLocationException bl) {
bl.printStackTrace();
throw new InternalError("Cannot replace" + " selection");
}
// No matter what happends... stops us from leaving document
// in a bad state
finally {
document.endCompoundEdit();
}
setCaretPosition(selectionEnd);
}
/**
* Returns true if this text area is editable, false otherwise.
*/
public final boolean isEditable() {
return editable;
}
/**
* Sets if this component is editable.
*
* @param editable True if this text area should be editable, false otherwise
*/
public final void setEditable(boolean editable) {
this.editable = editable;
}
/**
* Returns the right click popup menu.
*/
public final JPopupMenu getRightClickPopup() {
return popup;
}
/**
* Sets the right click popup menu.
*
* @param popup The popup
*/
public final void setRightClickPopup(JPopupMenu popup) {
this.popup = popup;
}
/**
* Returns the `magic' caret position. This can be used to preserve the column position when
* moving up and down lines.
*/
public final int getMagicCaretPosition() {
return magicCaret;
}
/**
* Sets the `magic' caret position. This can be used to preserve the column position when moving
* up and down lines.
*
* @param magicCaret The magic caret position
*/
public final void setMagicCaretPosition(int magicCaret) {
this.magicCaret = magicCaret;
}
/**
* Similar to <code>setSelectedText()</code>, but overstrikes the appropriate number of
* characters if overwrite mode is enabled.
*
* @param str The string
* @see #setSelectedText(String)
* @see #isOverwriteEnabled()
*/
public void overwriteSetSelectedText(String str) {
// Don't overstrike if there is a selection
if (!overwrite || selectionStart != selectionEnd) {
setSelectedText(str);
return;
}
// Don't overstrike if we're on the end of
// the line
int caret = getCaretPosition();
int caretLineEnd = getLineEndOffset(getCaretLine());
if (caretLineEnd - caret <= str.length()) {
setSelectedText(str);
return;
}
document.beginCompoundEdit();
try {
document.remove(caret, str.length());
document.insertString(caret, str, null);
} catch (BadLocationException bl) {
bl.printStackTrace();
} finally {
document.endCompoundEdit();
}
}
/**
* Returns true if overwrite mode is enabled, false otherwise.
*/
public final boolean isOverwriteEnabled() {
return overwrite;
}
/**
* Sets if overwrite mode should be enabled.
*
* @param overwrite True if overwrite mode should be enabled, false otherwise.
*/
public final void setOverwriteEnabled(boolean overwrite) {
this.overwrite = overwrite;
painter.invalidateSelectedLines();
}
/**
* Returns true if the selection is rectangular, false otherwise.
*/
public final boolean isSelectionRectangular() {
return rectSelect;
}
/**
* Sets if the selection should be rectangular.
*
* @param overwrite True if the selection should be rectangular, false otherwise.
*/
public final void setSelectionRectangular(boolean rectSelect) {
this.rectSelect = rectSelect;
painter.invalidateSelectedLines();
}
/**
* Returns the position of the highlighted bracket (the bracket matching the one before the
* caret)
*/
public final int getBracketPosition() {
return bracketPosition;
}
/**
* Returns the line of the highlighted bracket (the bracket matching the one before the caret)
*/
public final int getBracketLine() {
return bracketLine;
}
/**
* Adds a caret change listener to this text area.
*
* @param listener The listener
*/
public final void addCaretListener(CaretListener listener) {
listenerList.add(CaretListener.class, listener);
}
/**
* Removes a caret change listener from this text area.
*
* @param listener The listener
*/
public final void removeCaretListener(CaretListener listener) {
listenerList.remove(CaretListener.class, listener);
}
/**
* Deletes the selected text from the text area and places it into the clipboard.
*/
public void cut() {
if (editable) {
copy();
setSelectedText("");
}
}
/**
* Places the selected text into the clipboard.
*/
public void copy() {
if (selectionStart != selectionEnd) {
Clipboard clipboard = getToolkit().getSystemClipboard();
String selection = getSelectedText();
int repeatCount = inputHandler.getRepeatCount();
StringBuffer buf = new StringBuffer();
for (int i = 0; i < repeatCount; i++)
buf.append(selection);
clipboard.setContents(new StringSelection(buf.toString()), null);
}
}
/**
* Inserts the clipboard contents into the text.
*/
public void paste() {
if (editable) {
Clipboard clipboard = getToolkit().getSystemClipboard();
try {
// The MacOS MRJ doesn't convert \r to \n,
// so do it here
String selection = ((String) clipboard.getContents(this).getTransferData(DataFlavor.stringFlavor)).replace('\r', '\n');
int repeatCount = inputHandler.getRepeatCount();
StringBuffer buf = new StringBuffer();
for (int i = 0; i < repeatCount; i++)
buf.append(selection);
selection = buf.toString();
setSelectedText(selection);
} catch (Exception e) {
getToolkit().beep();
System.err.println("Clipboard does not" + " contain a string");
}
}
}
/**
* Called by the AWT when this component is removed from it's parent. This stops clears the
* currently focused component.
*/
public void removeNotify() {
super.removeNotify();
if (focusedComponent == this)
focusedComponent = null;
}
/**
* Forwards key events directly to the input handler. This is slightly faster than using a
* KeyListener because some Swing overhead is avoided.
*/
protected void processComponentKeyEvent(KeyEvent evt) {
if (inputHandler == null)
return;
switch (evt.getID()) {
case KeyEvent.KEY_TYPED:
inputHandler.keyTyped(evt);
break;
case KeyEvent.KEY_PRESSED:
inputHandler.keyPressed(evt);
break;
case KeyEvent.KEY_RELEASED:
inputHandler.keyReleased(evt);
break;
}
}
// protected members
protected static final String CENTER = "center";
protected static final String RIGHT = "right";
protected static final String BOTTOM = "bottom";
static JEditTextArea focusedComponent;
protected static final Timer caretTimer;
protected TextAreaPainter painter;
protected JPopupMenu popup;
protected EventListenerList listenerList;
protected MutableCaretEvent caretEvent;
protected boolean caretBlinks;
protected boolean caretVisible;
protected boolean blink;
protected boolean editable;
protected int firstLine;
protected int visibleLines;
protected int electricScroll;
protected int horizontalOffset;
protected JScrollBar vertical;
protected JScrollBar horizontal;
protected boolean scrollBarsInitialized;
protected InputHandler inputHandler;
protected SyntaxDocument document;
protected DocumentHandler documentHandler;
protected Segment lineSegment;
protected int selectionStart;
protected int selectionStartLine;
protected int selectionEnd;
protected int selectionEndLine;
protected boolean biasLeft;
protected int bracketPosition;
protected int bracketLine;
protected int magicCaret;
protected boolean overwrite;
protected boolean rectSelect;
protected void fireCaretEvent() {
Object[] listeners = listenerList.getListenerList();
for (int i = listeners.length - 2; i >= 0; i--) {
if (listeners[i] == CaretListener.class) {
((CaretListener) listeners[i + 1]).caretUpdate(caretEvent);
}
}
}
protected void updateBracketHighlight(int newCaretPosition) {
if (newCaretPosition == 0) {
bracketPosition = bracketLine = -1;
return;
}
try {
int offset = TextUtilities.findMatchingBracket(document, newCaretPosition - 1);
if (offset != -1) {
bracketLine = getLineOfOffset(offset);
bracketPosition = offset - getLineStartOffset(bracketLine);
return;
}
} catch (BadLocationException bl) {
bl.printStackTrace();
}
bracketLine = bracketPosition = -1;
}
protected void documentChanged(DocumentEvent evt) {
DocumentEvent.ElementChange ch = evt.getChange(document.getDefaultRootElement());
int count;
if (ch == null)
count = 0;
else
count = ch.getChildrenAdded().length - ch.getChildrenRemoved().length;
int line = getLineOfOffset(evt.getOffset());
if (count == 0) {
painter.invalidateLine(line);
}
// do magic stuff
else if (line < firstLine) {
setFirstLine(firstLine + count);
}
// end of magic stuff
else {
painter.invalidateLineRange(line, firstLine + visibleLines);
updateScrollBars();
}
}
class ScrollLayout implements LayoutManager {
public void addLayoutComponent(String name, Component comp) {
if (name.equals(CENTER))
center = comp;
else if (name.equals(RIGHT))
right = comp;
else if (name.equals(BOTTOM))
bottom = comp;
else if (name.equals(LEFT_OF_SCROLLBAR))
leftOfScrollBar.addElement(comp);
}
public void removeLayoutComponent(Component comp) {
if (center == comp)
center = null;
if (right == comp)
right = null;
if (bottom == comp)
bottom = null;
else
leftOfScrollBar.removeElement(comp);
}
public Dimension preferredLayoutSize(Container parent) {
Dimension dim = new Dimension();
Insets insets = getInsets();
dim.width = insets.left + insets.right;
dim.height = insets.top + insets.bottom;
Dimension centerPref = center.getPreferredSize();
dim.width += centerPref.width;
dim.height += centerPref.height;
Dimension rightPref = right.getPreferredSize();
dim.width += rightPref.width;
Dimension bottomPref = bottom.getPreferredSize();
dim.height += bottomPref.height;
return dim;
}
public Dimension minimumLayoutSize(Container parent) {
Dimension dim = new Dimension();
Insets insets = getInsets();
dim.width = insets.left + insets.right;
dim.height = insets.top + insets.bottom;
Dimension centerPref = center.getMinimumSize();
dim.width += centerPref.width;
dim.height += centerPref.height;
Dimension rightPref = right.getMinimumSize();
dim.width += rightPref.width;
Dimension bottomPref = bottom.getMinimumSize();
dim.height += bottomPref.height;
return dim;
}
public void layoutContainer(Container parent) {
Dimension size = parent.getSize();
Insets insets = parent.getInsets();
int itop = insets.top;
int ileft = insets.left;
int ibottom = insets.bottom;
int iright = insets.right;
int rightWidth = right.getPreferredSize().width;
int bottomHeight = bottom.getPreferredSize().height;
int centerWidth = size.width - rightWidth - ileft - iright;
int centerHeight = size.height - bottomHeight - itop - ibottom;
center.setBounds(ileft, itop, centerWidth, centerHeight);
right.setBounds(ileft + centerWidth, itop, rightWidth, centerHeight);
// Lay out all status components, in order
Enumeration status = leftOfScrollBar.elements();
while (status.hasMoreElements()) {
Component comp = (Component) status.nextElement();
Dimension dim = comp.getPreferredSize();
comp.setBounds(ileft, itop + centerHeight, dim.width, bottomHeight);
ileft += dim.width;
}
bottom.setBounds(ileft, itop + centerHeight, size.width - rightWidth - ileft - iright, bottomHeight);
}
// private members
private Component center;
private Component right;
private Component bottom;
private Vector leftOfScrollBar = new Vector();
}
static class CaretBlinker implements ActionListener {
public void actionPerformed(ActionEvent evt) {
if (focusedComponent != null && focusedComponent.hasFocus())
focusedComponent.blinkCaret();
}
}
class MutableCaretEvent extends CaretEvent {
MutableCaretEvent() {
super(JEditTextArea.this);
}
public int getDot() {
return getCaretPosition();
}
public int getMark() {
return getMarkPosition();
}
}
class AdjustHandler implements AdjustmentListener {
public void adjustmentValueChanged(final AdjustmentEvent evt) {
if (!scrollBarsInitialized)
return;
// If this is not done, mousePressed events accumilate
// and the result is that scrolling doesn't stop after
// the mouse is released
SwingUtilities.invokeLater(new Runnable() {
public void run() {
if (evt.getAdjustable() == vertical)
setFirstLine(vertical.getValue());
else
setHorizontalOffset(-horizontal.getValue());
}
});
}
}
class ComponentHandler extends ComponentAdapter {
public void componentResized(ComponentEvent evt) {
recalculateVisibleLines();
scrollBarsInitialized = true;
}
}
class DocumentHandler implements DocumentListener {
public void insertUpdate(DocumentEvent evt) {
documentChanged(evt);
int offset = evt.getOffset();
int length = evt.getLength();
int newStart;
int newEnd;
if (selectionStart > offset || (selectionStart == selectionEnd && selectionStart == offset))
newStart = selectionStart + length;
else
newStart = selectionStart;
if (selectionEnd >= offset)
newEnd = selectionEnd + length;
else
newEnd = selectionEnd;
select(newStart, newEnd);
}
public void removeUpdate(DocumentEvent evt) {
documentChanged(evt);
int offset = evt.getOffset();
int length = evt.getLength();
int newStart;
int newEnd;
if (selectionStart > offset) {
if (selectionStart > offset + length)
newStart = selectionStart - length;
else
newStart = offset;
} else
newStart = selectionStart;
if (selectionEnd > offset) {
if (selectionEnd > offset + length)
newEnd = selectionEnd - length;
else
newEnd = offset;
} else
newEnd = selectionEnd;
select(newStart, newEnd);
}
public void changedUpdate(DocumentEvent evt) {
}
}
class DragHandler implements MouseMotionListener {
public void mouseDragged(MouseEvent evt) {
if (popup != null && popup.isVisible())
return;
setSelectionRectangular((evt.getModifiers() & InputEvent.CTRL_MASK) != 0);
select(getMarkPosition(), xyToOffset(evt.getX(), evt.getY()));
}
public void mouseMoved(MouseEvent evt) {
}
}
class FocusHandler implements FocusListener {
public void focusGained(FocusEvent evt) {
setCaretVisible(true);
focusedComponent = JEditTextArea.this;
}
public void focusLost(FocusEvent evt) {
setCaretVisible(false);
focusedComponent = null;
}
}
class MouseHandler extends MouseAdapter {
public void mousePressed(MouseEvent evt) {
requestFocus();
// Focus events not fired sometimes?
setCaretVisible(true);
focusedComponent = JEditTextArea.this;
if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) != 0 && popup != null) {
popup.show(painter, evt.getX(), evt.getY());
return;
}
int line = yToLine(evt.getY());
int offset = xToOffset(line, evt.getX());
int dot = getLineStartOffset(line) + offset;
switch (evt.getClickCount()) {
case 1:
doSingleClick(evt, line, offset, dot);
break;
case 2:
// It uses the bracket matching stuff, so
// it can throw a BLE
doDoubleClick(evt, line, offset, dot);
break;
case 3:
doTripleClick(evt, line, offset, dot);
break;
}
}
private void doSingleClick(MouseEvent evt, int line, int offset, int dot) {
if ((evt.getModifiers() & InputEvent.SHIFT_MASK) != 0) {
rectSelect = (evt.getModifiers() & InputEvent.CTRL_MASK) != 0;
select(getMarkPosition(), dot);
} else
setCaretPosition(dot);
}
private void doDoubleClick(MouseEvent evt, int line, int offset, int dot) {
// Ignore empty lines
if (getLineLength(line) == 0)
return;
try {
int bracket = TextUtilities.findMatchingBracket(document, Math.max(0, dot - 1));
if (bracket != -1) {
int mark = getMarkPosition();
// Hack
if (bracket > mark) {
bracket++;
mark--;
}
select(mark, bracket);
return;
}
} catch (BadLocationException bl) {
bl.printStackTrace();
}
// Ok, it's not a bracket... select the word
String lineText = getLineText(line);
char ch = lineText.charAt(Math.max(0, offset - 1));
String noWordSep = (String) document.getProperty("noWordSep");
if (noWordSep == null)
noWordSep = "";
// If the user clicked on a non-letter char,
// we select the surrounding non-letters
boolean selectNoLetter = (!Character.isLetterOrDigit(ch) && noWordSep.indexOf(ch) == -1);
int wordStart = 0;
for (int i = offset - 1; i >= 0; i--) {
ch = lineText.charAt(i);
if (selectNoLetter ^ (!Character.isLetterOrDigit(ch) && noWordSep.indexOf(ch) == -1)) {
wordStart = i + 1;
break;
}
}
int wordEnd = lineText.length();
for (int i = offset; i < lineText.length(); i++) {
ch = lineText.charAt(i);
if (selectNoLetter ^ (!Character.isLetterOrDigit(ch) && noWordSep.indexOf(ch) == -1)) {
wordEnd = i;
break;
}
}
int lineStart = getLineStartOffset(line);
select(lineStart + wordStart, lineStart + wordEnd);
/*
* String lineText = getLineText(line); String noWordSep =
* (String)document.getProperty("noWordSep"); int wordStart =
* TextUtilities.findWordStart(lineText,offset,noWordSep); int wordEnd =
* TextUtilities.findWordEnd(lineText,offset,noWordSep);
*
* int lineStart = getLineStartOffset(line); select(lineStart + wordStart,lineStart +
* wordEnd);
*/
}
private void doTripleClick(MouseEvent evt, int line, int offset, int dot) {
select(getLineStartOffset(line), getLineEndOffset(line) - 1);
}
}
class CaretUndo extends AbstractUndoableEdit {
private int start;
private int end;
CaretUndo(int start, int end) {
this.start = start;
this.end = end;
}
public boolean isSignificant() {
return false;
}
public String getPresentationName() {
return "caret move";
}
public void undo() throws CannotUndoException {
super.undo();
select(start, end);
}
public void redo() throws CannotRedoException {
super.redo();
select(start, end);
}
public boolean addEdit(UndoableEdit edit) {
if (edit instanceof CaretUndo) {
CaretUndo cedit = (CaretUndo) edit;
start = cedit.start;
end = cedit.end;
cedit.die();
return true;
}
return false;
}
}
static {
caretTimer = new Timer(500, new CaretBlinker());
caretTimer.setInitialDelay(500);
caretTimer.start();
}
public void updateUI() {
invalidate();
}
/**
* Fetches the command list for the editor. This is the list of commands supported by the
* plugged-in UI augmented by the collection of commands that the editor itself supports. These
* are useful for binding to events, such as in a keymap.
*
* @return the command list
*/
/*
* public Action[] getActions() { return TextAction.augmentList(super.getActions(),
* defaultActions); }
*/
}