/* * 12/21/2004 * * ConfigurableCaret.java - The caret used by RTextArea. * * This library is distributed under a modified BSD license. See the included * RSyntaxTextArea.License.txt file for details. */ package org.fife.ui.rtextarea; import java.awt.*; import java.awt.event.*; import java.awt.datatransfer.*; import java.awt.event.ActionEvent; import javax.swing.*; import javax.swing.plaf.*; import javax.swing.text.*; 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 <code>javax.swing.text.DefaultCaret</code> does, as well as adding the * following niceties: * * <ul> * <li>This caret can render itself many different ways; see the * {@link #setStyle(CaretStyle)} method and {@link CaretStyle} for * more information.</li> * <li>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.</li> * </ul> * * @author Robert Futrell * @version 0.6 */ public class ConfigurableCaret extends DefaultCaret { /** * Action used to select a word on a double click. */ static private transient Action selectWord = null; /** * Action used to select a line on a triple click. */ static private 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; /** * Creates the caret using {@link CaretStyle#THICK_VERTICAL_LINE_STYLE}. */ public ConfigurableCaret() { this(CaretStyle.THICK_VERTICAL_LINE_STYLE); } /** * Constructs a new <code>ConfigurableCaret</code>. * * @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(); } /** * 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 * <code>RTextArea</code>, an <code>Exception</code> * will be thrown. * @see Caret#deinstall */ @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); } /** * Gets the text editor component that this caret is bound to. * * @return The <code>RTextArea</code>. */ 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 <code>Exception</code> will be thrown. * @see Caret#install */ @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 * @see MouseListener#mouseClicked */ @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)) { 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 the MouseEvent */ 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'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 <code>null</code>. * @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: * * <ul> * <li>The <code>View</code> classes in the javax.swing.text package * always return a width of "1" when <code>modelToView</code> is * called. We'll be needing the actual width.</li> * <li>Even in smart views, such as <code>RSyntaxTextArea</code>'s * <code>SyntaxView</code> and <code>WrappedSyntaxView</code> that * return the width of the current character, if the caret is at the * end of a line for example, the width returned from * <code>modelToView</code> 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.</li> * </ul> * * @param rect The rectangle returned by the current * <code>View</code>'s <code>modelToView</code> * 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.<p> * * 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<lineCount && fm.isLineHidden(line)); if (line<lineCount) { dot = textArea.getLineStartOffset(line); } else { // No lower lines visible UIManager.getLookAndFeel(). provideErrorFeedback(textArea); return; } } else if (dot<lastDot) { // Moving to earlier line 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); } } }