/* * 12/21/2004 * * ConfigurableCaret.java - The caret used by RTextArea. * Copyright (C) 2004 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.rtextarea; import java.awt.*; import java.awt.event.*; import java.awt.datatransfer.*; import java.awt.event.ActionEvent; import java.io.*; import javax.swing.*; import javax.swing.plaf.*; import javax.swing.text.*; /** * 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 paint itself several different ways: * <ol> * <li>As a vertical line (like <code>DefaultCaret</code>)</li> * <li>As a slightly thicker vertical line (like Eclipse)</li> * <li>As an underline</li> * <li>As a "block caret"</li> * <li>As a rectangle around the current character</li> * </ol> * </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 { /** * The minimum value of a caret style. */ public static final int MIN_STYLE = 0; /** * The vertical line style. */ public static final int VERTICAL_LINE_STYLE = 0; /** * The horizontal line style. */ public static final int UNDERLINE_STYLE = 1; /** * The block style. */ public static final int BLOCK_STYLE = 2; /** * The block border style. */ public static final int BLOCK_BORDER_STYLE = 3; /** * A thicker vertical line (2 pixels instead of 1). */ public static final int THICK_VERTICAL_LINE_STYLE = 4; /** * The maximum value of a caret style. */ public static final int MAX_STYLE = THICK_VERTICAL_LINE_STYLE; /** * 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 int style; /** * The selection painter. By default this paints selections with the text area's selection color. */ private ChangeableHighlightPainter selectionPainter; /** * Whether this is Java 1.4. */ /* TODO: Remove me when 1.4 support is removed. */ private static final boolean IS_JAVA_1_4 = "1.4".equals(System.getProperty("java.specification.version")); /** * Creates the caret using {@link #VERTICAL_LINE_STYLE}. */ public ConfigurableCaret() { this(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 #VERTICAL_LINE_STYLE} is * used. */ public ConfigurableCaret(int 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. */ 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 */ public void deinstall(JTextComponent c) { if (!(c instanceof RTextArea)) throw new IllegalArgumentException( "c must be instance of RTextArea"); super.deinstall(c); } /** * 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. */ protected Highlighter.HighlightPainter getSelectionPainter() { return selectionPainter; } /** * Gets the current style of this caret. * * @return The caret's style. * @see #setStyle(int) */ public int 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 */ public void install(JTextComponent c) { if (!(c instanceof RTextArea)) throw new IllegalArgumentException( "c must be instance of RTextArea"); super.install(c); } /** * 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 */ public void mouseClicked(MouseEvent e) { if (!e.isConsumed()) { RTextArea textArea = getTextArea(); int nclicks = e.getClickCount(); if (SwingUtilities.isLeftMouseButton(e)) { if (nclicks <= 2) { // Only handle these clicks for 1.4. In 1.5 the word // selection is (also?) handled in mousePressed, and if we // handle it here, our word selection gets doubled-up. if (IS_JAVA_1_4) { if (nclicks == 1) { selectedWordEvent = null; } else { // 2 selectWord(e); selectedWordEvent = null; } } } else { 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()) } /** * Paints the cursor. * * @param g * The graphics context in which to paint. */ public void paint(Graphics g) { // If the cursor is currently visible... if (isVisible()) { 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); // 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: g.setXORMode(Color.WHITE); // 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: g.setXORMode(Color.WHITE); 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()). } /** * Deserializes a caret. This is overridden to read the caret's style. * * @param s * The stream to read from. * @throws ClassNotFoundException * @throws IOException */ private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException { s.defaultReadObject(); setStyle(s.readInt()); seg = new Segment(); } /** * 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; } /** * 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. */ public void setSelectionVisible(boolean visible) { super.setSelectionVisible(true); } /** * Sets the style used when painting the caret. * * @param style * The style to use. If this isn't one of <code>VERTICAL_LINE_STYLE</code>, <code>UNDERLINE_STYLE</code>, * or <code>BLOCK_STYLE</code>, then <code>VERTICAL_LINE_STYLE</code> is used. * @see #getStyle() */ public void setStyle(int style) { if (style < MIN_STYLE || style > MAX_STYLE) style = VERTICAL_LINE_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). } /** * Serializes this caret. This is overridden to write the style of the caret. * * @param s * The stream to write to. * @throws IOException * If an IO error occurs. */ private void writeObject(ObjectOutputStream s) throws IOException { s.defaultWriteObject(); s.writeInt(getStyle()); } }