org.fife.ui.rtextarea.ConfigurableCaret Maven / Gradle / Ivy
/*
* 12/21/2004
*
* ConfigurableCaret.java - The caret used by RTextArea.
*
* This library is distributed under a modified BSD license. See the included
* LICENSE file for details.
*/
package org.fife.ui.rtextarea;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.HeadlessException;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;
import javax.swing.UIManager;
import javax.swing.plaf.TextUI;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultCaret;
import javax.swing.text.Highlighter;
import javax.swing.text.JTextComponent;
import javax.swing.text.NavigationFilter;
import javax.swing.text.Position;
import javax.swing.text.Segment;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.folding.FoldManager;
/**
* The caret used by {@link RTextArea}. This caret has all of the properties
* that javax.swing.text.DefaultCaret
does, as well as adding the
* following niceties:
*
*
* - This caret can render itself many different ways; see the
* {@link #setStyle(CaretStyle)} method and {@link CaretStyle} for
* more information.
* - On Microsoft Windows and other operating systems that do not
* support system selection (i.e., selecting text, then pasting
* via the middle mouse button), clicking the middle mouse button
* will cause a regular paste operation to occur. On systems
* that support system selection (i.e., all UNIX variants),
* the middle mouse button will behave normally.
*
*
* @author Robert Futrell
* @version 0.6
*/
public class ConfigurableCaret extends DefaultCaret {
/**
* Action used to select a word on a double click.
*/
private static transient Action selectWord = null;
/**
* Action used to select a line on a triple click.
*/
private static transient Action selectLine = null;
/**
* holds last MouseEvent which caused the word selection.
*/
private transient MouseEvent selectedWordEvent = null;
/**
* Used for fastest-possible retrieval of the character at the
* caret's position in the document.
*/
private transient Segment seg;
/**
* Whether the caret is a vertical line, a horizontal line, or a block.
*/
private CaretStyle style;
/**
* The selection painter. By default this paints selections with the
* text area's selection color.
*/
private ChangeableHighlightPainter selectionPainter;
private boolean alwaysVisible;
/**
* Whether this caret will try to paste into the editor (assuming it is
* editable) on middle-mouse clicks.
*/
private boolean pasteOnMiddleMouseClick;
/**
* Creates the caret using {@link CaretStyle#THICK_VERTICAL_LINE_STYLE}.
*/
public ConfigurableCaret() {
this(CaretStyle.THICK_VERTICAL_LINE_STYLE);
}
/**
* Constructs a new ConfigurableCaret
.
*
* @param style The style to use when painting the caret. If this is
* invalid, then {@link CaretStyle#THICK_VERTICAL_LINE_STYLE} is
* used.
*/
public ConfigurableCaret(CaretStyle style) {
seg = new Segment();
setStyle(style);
selectionPainter = new ChangeableHighlightPainter();
pasteOnMiddleMouseClick = true;
}
/**
* Adjusts the caret location based on the MouseEvent.
*/
private void adjustCaret(MouseEvent e) {
if ((e.getModifiers()&ActionEvent.SHIFT_MASK)!=0 && getDot()!=-1) {
moveCaret(e);
}
else {
positionCaret(e);
}
}
/**
* Adjusts the focus, if necessary.
*
* @param inWindow if true indicates requestFocusInWindow should be used
*/
private void adjustFocus(boolean inWindow) {
RTextArea textArea = getTextArea();
if ((textArea != null) && textArea.isEnabled() &&
textArea.isRequestFocusEnabled()) {
if (inWindow) {
textArea.requestFocusInWindow();
}
else {
textArea.requestFocus();
}
}
}
/**
* Overridden to damage the correct width of the caret, since this caret
* can be different sizes.
*
* @param r The current location of the caret.
*/
@Override
protected synchronized void damage(Rectangle r) {
if (r != null) {
validateWidth(r); // Check for "0" or "1" caret width
x = r.x - 1;
y = r.y;
width = r.width + 4;
height = r.height;
repaint();
}
}
/**
* Called when the UI is being removed from the
* interface of a JTextComponent. This is used to
* unregister any listeners that were attached.
*
* @param c The text component. If this is not an
* RTextArea
, an Exception
* will be thrown.
*/
@Override
public void deinstall(JTextComponent c) {
if (!(c instanceof RTextArea)) {
throw new IllegalArgumentException(
"c must be instance of RTextArea");
}
super.deinstall(c);
c.setNavigationFilter(null);
}
/**
* Returns whether this caret will paste the contents of the clipboard into
* the editor (assuming it is editable) on middle-mouse-button clicks.
*
* @return Whether a paste operation will be performed.
* @see #setPasteOnMiddleMouseClick(boolean)
*/
public boolean getPasteOnMiddleMouseClick() {
return pasteOnMiddleMouseClick;
}
/**
* Gets the text editor component that this caret is bound to.
*
* @return The RTextArea
.
*/
protected RTextArea getTextArea() {
return (RTextArea)getComponent();
}
/**
* Returns whether this caret's selection uses rounded edges.
*
* @return Whether this caret's edges are rounded.
* @see #setRoundedSelectionEdges
*/
public boolean getRoundedSelectionEdges() {
return ((ChangeableHighlightPainter)getSelectionPainter()).
getRoundedEdges();
}
/**
* Gets the painter for the Highlighter. This is overridden to return
* our custom selection painter.
*
* @return The painter.
*/
@Override
protected Highlighter.HighlightPainter getSelectionPainter() {
return selectionPainter;
}
/**
* Gets the current style of this caret.
*
* @return The caret's style.
* @see #setStyle(CaretStyle)
*/
public CaretStyle getStyle() {
return style;
}
/**
* Installs this caret on a text component.
*
* @param c The text component. If this is not an {@link RTextArea},
* an Exception
will be thrown.
*/
@Override
public void install(JTextComponent c) {
if (!(c instanceof RTextArea)) {
throw new IllegalArgumentException(
"c must be instance of RTextArea");
}
super.install(c);
c.setNavigationFilter(new FoldAwareNavigationFilter());
}
/**
* Returns whether this caret is always visible (as opposed to
* blinking, or not visible when the editor's window is not focused).
* This can be used by popup windows that want the caret's location
* to still be visible for contextual purposes while they are displayed.
*
* @return Whether this caret is always visible.
* @see #setAlwaysVisible(boolean)
*/
public boolean isAlwaysVisible() {
return alwaysVisible;
}
/**
* Called when the mouse is clicked. If the click was generated from
* button1, a double click selects a word, and a triple click the
* current line.
*
* @param e the mouse event
*/
@Override
public void mouseClicked(MouseEvent e) {
if (! e.isConsumed()) {
RTextArea textArea = getTextArea();
int nclicks = e.getClickCount();
if (SwingUtilities.isLeftMouseButton(e)) {
if (nclicks>2) {
nclicks %= 2; // Alternate selecting word/line.
switch (nclicks) {
case 0:
selectWord(e);
selectedWordEvent = null;
break;
case 1:
Action a = null;
ActionMap map = textArea.getActionMap();
if (map != null) {
a = map.get(RTextAreaEditorKit.selectLineAction);
}
if (a == null) {
if (selectLine == null) {
selectLine = new RTextAreaEditorKit.SelectLineAction();
}
a = selectLine;
}
a.actionPerformed(new ActionEvent(textArea,
ActionEvent.ACTION_PERFORMED,
null, e.getWhen(), e.getModifiers()));
}
}
}
else if (SwingUtilities.isMiddleMouseButton(e) &&
getPasteOnMiddleMouseClick()) {
if (nclicks == 1 && textArea.isEditable() && textArea.isEnabled()) {
// Paste the system selection, if it exists (e.g., on UNIX
// platforms, the user can select text, the middle-mouse click
// to paste it; this doesn't work on Windows). If the system
// doesn't support system selection, just do a normal paste.
JTextComponent c = (JTextComponent) e.getSource();
if (c != null) {
try {
Toolkit tk = c.getToolkit();
Clipboard buffer = tk.getSystemSelection();
// If the system supports system selections, (e.g. UNIX),
// try to do it.
if (buffer != null) {
adjustCaret(e);
TransferHandler th = c.getTransferHandler();
if (th != null) {
Transferable trans = buffer.getContents(null);
if (trans != null) {
th.importData(c, trans);
}
}
adjustFocus(true);
}
// If the system doesn't support system selections
// (e.g. Windows), just do a normal paste.
else {
textArea.paste();
}
} catch (HeadlessException he) {
// do nothing... there is no system clipboard
}
} // if (c!=null)
} // if (nclicks == 1 && component.isEditable() && component.isEnabled())
} // else if (SwingUtilities.isMiddleMouseButton(e))
} // if (!c.isConsumed())
}
/**
* Overridden to also focus the text component on right mouse clicks.
*
* @param e The mouse event.
*/
@Override
public void mousePressed(MouseEvent e) {
super.mousePressed(e);
if (!e.isConsumed() && SwingUtilities.isRightMouseButton(e)) {
JTextComponent c = getComponent();
if (c!=null && c.isEnabled() && c.isRequestFocusEnabled()) {
c.requestFocusInWindow();
}
}
}
/**
* Paints the cursor.
*
* @param g The graphics context in which to paint.
*/
@Override
public void paint(Graphics g) {
// If the cursor is currently visible...
if (isVisible() || alwaysVisible) {
try {
RTextArea textArea = getTextArea();
g.setColor(textArea.getCaretColor());
TextUI mapper = textArea.getUI();
Rectangle r = mapper.modelToView(textArea, getDot());
// "Correct" the value of rect.width (takes into
// account caret being at EOL (and thus rect.width==1),
// etc.
// We do this even for LINE_STYLE because
// if they change from that caret to block/underline,
// the first time they do so width==1, so it will take
// one caret flash to paint correctly (wider). If we
// do this every time, then it's painted correctly the
// first blink.
validateWidth(r);
// This condition is most commonly hit when code folding is
// enabled and the user collapses a fold above the caret
// position. If our cached x/y/w/h aren't updated, this caret
// appears to stop blinking because the wrong line range gets
// damaged. This check keeps us in sync.
if (width>0 && height>0 &&
!contains(r.x, r.y, r.width, r.height)) {
Rectangle clip = g.getClipBounds();
if (clip != null && !clip.contains(this)) {
// Clip doesn't contain the old location, force it
// to be repainted lest we leave a caret around.
repaint();
}
// This will potentially cause a repaint of something
// we're already repainting, but without changing the
// semantics of damage we can't really get around this.
damage(r);
}
// Need to subtract 2 from height, otherwise
// the caret will expand too far vertically.
r.height -= 2;
switch (style) {
// Draw a big rectangle, and xor the foreground color.
case BLOCK_STYLE:
Color textAreaBg = textArea.getBackground();
if (textAreaBg==null) {
textAreaBg = Color.white;
}
g.setXORMode(textAreaBg);
// fills x==r.x to x==(r.x+(r.width)-1), inclusive.
g.fillRect(r.x,r.y, r.width,r.height);
break;
// Draw a rectangular border.
case BLOCK_BORDER_STYLE:
// fills x==r.x to x==(r.x+(r.width-1)), inclusive.
g.drawRect(r.x,r.y, r.width-1,r.height);
break;
// Draw an "underline" below the current position.
case UNDERLINE_STYLE:
textAreaBg = textArea.getBackground();
if (textAreaBg==null) {
textAreaBg = Color.white;
}
g.setXORMode(textAreaBg);
int y = r.y + r.height;
g.drawLine(r.x,y, r.x+r.width-1,y);
break;
// Draw a vertical line.
default:
case VERTICAL_LINE_STYLE:
g.drawLine(r.x,r.y, r.x,r.y+r.height);
break;
// A thicker vertical line.
case THICK_VERTICAL_LINE_STYLE:
g.drawLine(r.x,r.y, r.x,r.y+r.height);
r.x++;
g.drawLine(r.x,r.y, r.x,r.y+r.height);
break;
} // End of switch (style).
} catch (BadLocationException ble) {
ble.printStackTrace();
}
} // End of if (isVisible()).
}
/**
* Selects word based on a mouse event.
*/
private void selectWord(MouseEvent e) {
if (selectedWordEvent != null &&
selectedWordEvent.getX() == e.getX() &&
selectedWordEvent.getY() == e.getY()) {
// We've already the done selection for this.
return;
}
Action a = null;
RTextArea textArea = getTextArea();
ActionMap map = textArea.getActionMap();
if (map != null) {
a = map.get(RTextAreaEditorKit.selectWordAction);
}
if (a == null) {
if (selectWord == null) {
selectWord = new RTextAreaEditorKit.SelectWordAction();
}
a = selectWord;
}
a.actionPerformed(new ActionEvent(textArea,
ActionEvent.ACTION_PERFORMED,
null, e.getWhen(), e.getModifiers()));
selectedWordEvent = e;
}
/**
* Toggles whether this caret should always be visible (as opposed to
* blinking, or not visible when the editor's window is not focused).
* This can be used by popup windows that want the caret's location
* to still be visible for contextual purposes while they are displayed.
*
* @param alwaysVisible Whether this caret should always be visible.
* @see #isAlwaysVisible()
*/
public void setAlwaysVisible(boolean alwaysVisible) {
if (alwaysVisible != this.alwaysVisible) {
this.alwaysVisible = alwaysVisible;
if (!isVisible()) {
// Force painting of caret since super class's "flasher" timer
// won't fire when the window doesn't have focus
repaint();
}
}
}
/**
* Sets whether this caret will paste the contents of the clipboard into
* the editor (assuming it is editable) on middle-mouse-button clicks.
*
* @param paste Whether a paste operation will be performed.
* @see #getPasteOnMiddleMouseClick()
*/
public void setPasteOnMiddleMouseClick(boolean paste) {
pasteOnMiddleMouseClick = paste;
}
/**
* Sets whether this caret's selection should have rounded edges.
*
* @param rounded Whether it should have rounded edges.
* @see #getRoundedSelectionEdges()
*/
public void setRoundedSelectionEdges(boolean rounded) {
((ChangeableHighlightPainter)getSelectionPainter()).
setRoundedEdges(rounded);
}
/**
* Overridden to always render the selection, even when the text component
* loses focus.
*
* @param visible Whether the selection should be visible. This parameter
* is ignored.
*/
@Override
public void setSelectionVisible(boolean visible) {
super.setSelectionVisible(true);
}
/**
* Sets the style used when painting the caret.
*
* @param style The style to use. This should not be null
.
* @see #getStyle()
*/
public void setStyle(CaretStyle style) {
if (style==null) {
style = CaretStyle.THICK_VERTICAL_LINE_STYLE;
}
if (style!=this.style) {
this.style = style;
repaint();
}
}
/**
* Helper function used by the block and underline carets to ensure the
* width of the painted caret is valid. This is done for the following
* reasons:
*
*
* - The
View
classes in the javax.swing.text package
* always return a width of "1" when modelToView
is
* called. We'll be needing the actual width.
* - Even in smart views, such as
RSyntaxTextArea
's
* SyntaxView
and WrappedSyntaxView
that
* return the width of the current character, if the caret is at the
* end of a line for example, the width returned from
* modelToView
will be 0 (as the width of unprintable
* characters such as '\n' is calculated as 0). In this case, we'll
* use a default width value.
*
*
* @param rect The rectangle returned by the current
* View
's modelToView
* method for the caret position.
*/
private void validateWidth(Rectangle rect) {
// If the width value > 1, we assume the View is
// a "smart" view that returned the proper width.
// So only worry about this stuff if width <= 1.
if (rect!=null && rect.width<=1) {
// The width is either 1 (most likely, we're using a "dumb" view
// like those in javax.swing.text) or 0 (most likely, we're using
// a "smart" view like org.fife.ui.rsyntaxtextarea.SyntaxView,
// we're at the end of a line, and the width of '\n' is being
// computed as 0).
try {
// Try to get a width for the character at the caret
// position. We use the text area's font instead of g's
// because g's may vary in an RSyntaxTextArea.
RTextArea textArea = getTextArea();
textArea.getDocument().getText(getDot(),1, seg);
Font font = textArea.getFont();
FontMetrics fm = textArea.getFontMetrics(font);
rect.width = fm.charWidth(seg.array[seg.offset]);
// This width being returned 0 likely means that it is an
// unprintable character (which is almost 100% to be a
// newline char, i.e., we're at the end of a line). So,
// just use the width of a space.
if (rect.width==0) {
rect.width = fm.charWidth(' ');
}
} catch (BadLocationException ble) {
// This shouldn't ever happen.
ble.printStackTrace();
rect.width = 8;
}
} // End of if (rect!=null && rect.width<=1).
}
/**
* Keeps the caret out of folded regions in edge cases where it doesn't
* happen automatically, for example, when the caret moves automatically in
* response to Document.insert() and Document.remove() calls. Most keyboard
* shortcuts already take folding into account, as do viewToModel() and
* modelToView(), so this filter usually does not do anything.
*
* Common cases: backspacing to visible line of collapsed region.
*/
private class FoldAwareNavigationFilter extends NavigationFilter {
@Override
public void setDot(FilterBypass fb, int dot, Position.Bias bias) {
RTextArea textArea = getTextArea();
if (textArea instanceof RSyntaxTextArea) {
RSyntaxTextArea rsta = (RSyntaxTextArea)getTextArea();
if (rsta.isCodeFoldingEnabled()) {
int lastDot = getDot();
FoldManager fm = rsta.getFoldManager();
int line = 0;
try {
line = textArea.getLineOfOffset(dot);
} catch (Exception e) {
e.printStackTrace();
}
if (fm.isLineHidden(line)) {
//System.out.println("filterBypass: avoiding hidden line");
try {
if (dot>lastDot) { // Moving to further line
int lineCount = textArea.getLineCount();
while (++line=0 && fm.isLineHidden(line));
if (line>=0) {
dot = textArea.getLineEndOffset(line) - 1;
}
}
} catch (Exception e) {
e.printStackTrace();
return;
}
}
}
}
super.setDot(fb, dot, bias);
}
@Override
public void moveDot(FilterBypass fb, int dot, Position.Bias bias) {
super.moveDot(fb, dot, bias);
}
}
}