/*
* 11/14/2003
*
* RTextArea.java - An extension of JTextArea that adds many features.
* Copyright (C) 2003 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.Color;
import java.awt.ComponentOrientation;
import java.awt.Graphics;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Reader;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.InputMap;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.CaretEvent;
import javax.swing.plaf.TextUI;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.Highlighter;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import org.fife.print.RPrintUtilities;
import org.fife.ui.rtextarea.Macro.MacroRecord;
/**
* An extension of <code>JTextArea</code> that adds the following features:
* <ul>
* <li>Insert/Overwrite modes (can be toggled via the Insert key)
* <li>A right-click popup menu with standard editing options
* <li>Macro support
* <li>"Mark all" functionality.
* <li>A way to change the background to an image (gif/png/jpg)
* <li>Highlight the current line (can be toggled)
* <li>An easy way to print its text (implements Printable)
* <li>Hard/soft (emulated with spaces) tabs
* <li>Fixes a bug with setTabSize
* <li>Other handy new methods
* </ul>
* NOTE: If the background for an <code>RTextArea</code> is set to a color, its opaque property is set to
* <code>true</code> for performance reasons. If the background is set to an image, then the opaque property is set to
* <code>false</code>. This slows things down a little, but if it didn't happen then we would see garbage on-screen when
* the user scrolled through a document using the arrow keys (not the page-up/down keys though). You should never have
* to set the opaque property yourself; it is always done for you.
*
* @author Robert Futrell
* @version 1.0
*/
public class RTextArea extends RTextAreaBase
implements Printable, Serializable {
/**
* Constant representing insert mode.
*
* @see #setCaretStyle(int, int)
*/
public static final int INSERT_MODE = 0;
/**
* Constant representing overwrite mode.
*
* @see #setCaretStyle(int, int)
*/
public static final int OVERWRITE_MODE = 1;
/**
* The property fired when the "mark all" color changes.
*/
public static final String MARK_ALL_COLOR_PROPERTY = "RTA.markAllColor";
/*
* Constants for all actions.
*/
private static final int MIN_ACTION_CONSTANT = 0;
public static final int COPY_ACTION = 0;
public static final int CUT_ACTION = 1;
public static final int DELETE_ACTION = 2;
public static final int PASTE_ACTION = 3;
public static final int REDO_ACTION = 4;
public static final int SELECT_ALL_ACTION = 5;
public static final int UNDO_ACTION = 6;
private static final int MAX_ACTION_CONSTANT = 6;
private static final Color DEFAULT_MARK_ALL_COLOR = Color.ORANGE;
/**
* The current text mode ({@link #INSERT_MODE} or {@link #OVERWRITE_MODE}).
*/
private int textMode;
// All macros are shared across all RTextAreas.
private static boolean recordingMacro; // Whether we're recording a macro.
private static Macro currentMacro;
private int previousLineSelected = -1;
/**
* This text area's popup menu.
*/
private JPopupMenu popupMenu;
/**
* Whether the popup menu has been created.
*/
private boolean popupMenuCreated;
/**
* Can return tool tips for this text area. Subclasses can install a supplier as a means of adding custom tool tips
* without subclassing <tt>RTextArea</tt>. {@link #getToolTipText()} checks this supplier before calling the super
* class's version.
*/
private ToolTipSupplier toolTipSupplier;
private static RecordableTextAction cutAction;
private static RecordableTextAction copyAction;
private static RecordableTextAction pasteAction;
private static RecordableTextAction deleteAction;
private static RecordableTextAction undoAction;
private static RecordableTextAction redoAction;
private static RecordableTextAction selectAllAction;
private static IconGroup iconGroup; // Info on icons for actions.
private transient RUndoManager undoManager;
private transient LineHighlightManager lineHighlightManager;
private ArrayList markAllHighlights; // Highlights from "mark all".
private String markedWord; // Expression marked in "mark all."
private ChangeableHighlightPainter markAllHighlightPainter;
private int[] carets; // Index 0=>insert caret, 1=>overwrite.
private static final String MSG = "org.fife.ui.rtextarea.RTextArea";
/**
* Constructor.
*/
public RTextArea() {
init(INSERT_MODE);
}
/**
* Constructor.
*
* @param doc
* The document for the editor.
*/
public RTextArea(AbstractDocument doc) {
super(doc);
init(INSERT_MODE);
}
/**
* Constructor.
*
* @param text
* The initial text to display.
*/
public RTextArea(String text) {
super(text);
init(INSERT_MODE);
}
public void setCurrentLine(int line) {
try {
int lineStartOffset = getLineStartOffset(line);
setCaretPosition(lineStartOffset);
} catch (BadLocationException e) {
e.printStackTrace();
}
}
/**
* @return the previousLineSelected
*/
public int getPreviousLineSelected() {
return previousLineSelected;
}
/**
* @param previousLineSelected
* the previousLineSelected to set
*/
public void setPreviousLineSelected(int previousLineSelected) {
this.previousLineSelected = previousLineSelected;
}
/**
* Constructor.
*
* @param rows
* The number of rows to display.
* @param cols
* The number of columns to display.
* @throws IllegalArgumentException
* If either <code>rows</code> or <code>cols</code> is negative.
*/
public RTextArea(int rows, int cols) {
super(rows, cols);
init(INSERT_MODE);
}
/**
* Constructor.
*
* @param text
* The initial text to display.
* @param rows
* The number of rows to display.
* @param cols
* The number of columns to display.
* @throws IllegalArgumentException
* If either <code>rows</code> or <code>cols</code> is negative.
*/
public RTextArea(String text, int rows, int cols) {
super(text, rows, cols);
init(INSERT_MODE);
}
/**
* Constructor.
*
* @param doc
* The document for the editor.
* @param text
* The initial text to display.
* @param rows
* The number of rows to display.
* @param cols
* The number of columns to display.
* @throws IllegalArgumentException
* If either <code>rows</code> or <code>cols</code> is negative.
*/
public RTextArea(AbstractDocument doc, String text, int rows, int cols) {
super(doc, text, rows, cols);
init(INSERT_MODE);
}
/**
* Creates a new <code>RTextArea</code>.
*
* @param textMode
* Either <code>INSERT_MODE</code> or <code>OVERWRITE_MODE</code>.
*/
public RTextArea(int textMode) {
init(textMode);
}
/**
* Adds an action event to the current macro. This shouldn't be called directly, as it is called by the actions
* themselves.
*
* @param id
* The ID of the recordable text action.
* @param actionCommand
* The "command" of the action event passed to it.
*/
static synchronized void addToCurrentMacro(String id,
String actionCommand) {
currentMacro.addMacroRecord(new Macro.MacroRecord(id, actionCommand));
}
/**
* Adds a line highlight.
*
* @param line
* The line to highlight. This is zero-based.
* @param color
* The color to highlight the line with.
* @throws BadLocationException
* If <code>line</code> is an invalid line number.
* @see #removeLineHighlight(Object)
* @see #removeAllLineHighlights()
*/
public Object addLineHighlight(int line, Color color)
throws BadLocationException {
if (lineHighlightManager == null) {
lineHighlightManager = new LineHighlightManager(this);
}
return lineHighlightManager.addLineHighlight(line, color);
}
/**
* Begins an "atomic edit." All text editing operations between this call and the next call to
* <tt>endAtomicEdit()</tt> will be treated as a single operation by the undo manager.
* <p>
*
* Using this method should be done with great care. You should probably wrap the call to <tt>endAtomicEdit()</tt>
* in a <tt>finally</tt> block:
*
* <pre>
* textArea.beginAtomicEdit();
* try {
* // Do editing
* } finally {
* textArea.endAtomicEdit();
* }
* </pre>
*
* @see #endAtomicEdit()
*/
public void beginAtomicEdit() {
undoManager.beginInternalAtomicEdit();
}
/**
* Begins recording a macro. After this method is called, all input/caret events, etc. are recorded until
* <code>endMacroRecording</code> is called. If this method is called but the text component is already recording a
* macro, nothing happens (but the macro keeps recording).
*
* @see #isRecordingMacro()
* @see #endRecordingMacro()
*/
public static synchronized void beginRecordingMacro() {
if (isRecordingMacro()) {
// System.err.println("Macro already being recorded!");
return;
}
// JOptionPane.showMessageDialog(this, "Now recording a macro");
if (currentMacro != null)
currentMacro = null; // May help gc?
currentMacro = new Macro();
recordingMacro = true;
}
/**
* Tells whether an undo is possible
*
* @see #canRedo()
* @see #undoLastAction()
*/
public boolean canUndo() {
return undoManager.canUndo();
}
/**
* Tells whether a redo is possible
*
* @see #canUndo()
* @see #redoLastAction()
*/
public boolean canRedo() {
return undoManager.canRedo();
}
/**
* Clears any "mark all" highlights, if any.
*
* @see #markAll
* @see #getMarkAllHighlightColor
* @see #setMarkAllHighlightColor
*/
public void clearMarkAllHighlights() {
Highlighter h = getHighlighter();
if (h != null && markAllHighlights != null) {
int count = markAllHighlights.size();
for (int i = 0; i < count; i++)
h.removeHighlight(markAllHighlights.get(i));
markAllHighlights.clear();
}
markedWord = null;
repaint();
}
/**
* Configures the popup menu for this text area. This method is called right before it is displayed, so a hosting
* application can do any custom configuration (configuring actions, adding/removing items, etc.).
* <p>
*
* The default implementation does nothing.
*
* @param popupMenu
* The popup menu. This will never be <code>null</code>.
* @see #createPopupMenu()
* @see #setPopupMenu(JPopupMenu)
*/
protected void configurePopupMenu(JPopupMenu popupMenu) {
}
/**
* Returns the caret event/mouse listener for <code>RTextArea</code>s.
*
* @return The caret event/mouse listener.
*/
protected RTAMouseListener createMouseListener() {
return new RTextAreaMutableCaretEvent(this);
}
/**
* Creates the right-click popup menu. Subclasses can override this method to replace or augment the popup menu
* returned.
*
* @return The popup menu.
* @see #setPopupMenu(JPopupMenu)
* @see #configurePopupMenu(JPopupMenu)
*/
protected JPopupMenu createPopupMenu() {
JPopupMenu menu = new JPopupMenu();
JMenuItem menuItem;
menuItem = new JMenuItem(undoAction);
menuItem.setAccelerator(null);
menuItem.setToolTipText(null);
menu.add(menuItem);
menuItem = new JMenuItem(redoAction);
menuItem.setAccelerator(null);
menuItem.setToolTipText(null);
menu.add(menuItem);
menu.addSeparator();
menuItem = new JMenuItem(cutAction);
menuItem.setAccelerator(null);
menuItem.setToolTipText(null);
menu.add(menuItem);
menuItem = new JMenuItem(copyAction);
menuItem.setAccelerator(null);
menuItem.setToolTipText(null);
menu.add(menuItem);
menuItem = new JMenuItem(pasteAction);
menuItem.setAccelerator(null);
menuItem.setToolTipText(null);
menu.add(menuItem);
menuItem = new JMenuItem(deleteAction);
menuItem.setAccelerator(null);
menuItem.setToolTipText(null);
menu.add(menuItem);
menu.addSeparator();
menuItem = new JMenuItem(selectAllAction);
menuItem.setAccelerator(null);
menuItem.setToolTipText(null);
menu.add(menuItem);
return menu;
}
/**
* Creates the actions used in the popup menu and retrievable by {@link #getAction(int)}.
*/
private static void createPopupMenuActions() {
ResourceBundle bundle = ResourceBundle.getBundle(MSG);
// Create actions for right-click popup menu.
// 1.5.2004/pwy: Replaced the CTRL_MASK with the cross-platform version...
int mod = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
String name = bundle.getString("CutName");
char mnemonic = bundle.getString("CutMnemonic").charAt(0);
String desc = bundle.getString("CutDesc");
cutAction = new RTextAreaEditorKit.CutAction(name, null, desc,
new Integer(mnemonic), KeyStroke.getKeyStroke(KeyEvent.VK_X, mod));
name = bundle.getString("CopyName");
mnemonic = bundle.getString("CopyMnemonic").charAt(0);
desc = bundle.getString("CopyDesc");
copyAction = new RTextAreaEditorKit.CopyAction(name, null, desc,
new Integer(mnemonic), KeyStroke.getKeyStroke(KeyEvent.VK_C, mod));
name = bundle.getString("PasteName");
mnemonic = bundle.getString("PasteMnemonic").charAt(0);
desc = bundle.getString("PasteDesc");
pasteAction = new RTextAreaEditorKit.PasteAction(name, null, desc,
new Integer(mnemonic), KeyStroke.getKeyStroke(KeyEvent.VK_V, mod));
name = bundle.getString("DeleteName");
mnemonic = bundle.getString("DeleteMnemonic").charAt(0);
desc = bundle.getString("DeleteDesc");
deleteAction = new RTextAreaEditorKit.DeleteNextCharAction(name, null, desc,
new Integer(mnemonic), KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0));
name = bundle.getString("CantUndoName");
mnemonic = bundle.getString("UndoMnemonic").charAt(0);
desc = bundle.getString("UndoDesc");
undoAction = new RTextAreaEditorKit.UndoAction(name, null, desc,
new Integer(mnemonic), KeyStroke.getKeyStroke(KeyEvent.VK_Z, mod));
name = bundle.getString("CantRedoName");
mnemonic = bundle.getString("RedoMnemonic").charAt(0);
desc = bundle.getString("RedoDesc");
redoAction = new RTextAreaEditorKit.RedoAction(name, null, desc,
new Integer(mnemonic), KeyStroke.getKeyStroke(KeyEvent.VK_Y, mod));
name = bundle.getString("SAName");
mnemonic = bundle.getString("SAMnemonic").charAt(0);
desc = bundle.getString("SelectAllDesc");
selectAllAction = new RTextAreaEditorKit.SelectAllAction(name, null, desc,
new Integer(mnemonic), KeyStroke.getKeyStroke(KeyEvent.VK_A, mod));
}
/**
* Returns the a real UI to install on this text area.
*
* @return The UI.
*/
protected RTextAreaUI createRTextAreaUI() {
return new RTextAreaUI(this);
}
/**
* Removes all undoable edits from this document's undo manager. This method also makes the undo/redo actions
* disabled.
*/
/*
* NOTE: For some reason, it appears I have to create an entirely new <code>undoManager</code> for undo/redo to
* continue functioning properly; if I don't, it only ever lets you do one undo. Not too sure why this is...
*/
public void discardAllEdits() {
undoManager.discardAllEdits();
getDocument().removeUndoableEditListener(undoManager);
undoManager = new RUndoManager(this);
getDocument().addUndoableEditListener(undoManager);
undoManager.updateActions();
}
/**
* Completes an "atomic" edit.
*
* @see #beginAtomicEdit()
*/
public void endAtomicEdit() {
undoManager.endInternalAtomicEdit();
}
/**
* Ends recording a macro. If this method is called but the text component is not recording a macro, nothing
* happens.
*
* @see #isRecordingMacro()
* @see #beginRecordingMacro()
*/
/*
* FIXME: This should throw an exception if we're not recording a macro.
*/
public static synchronized void endRecordingMacro() {
if (!isRecordingMacro()) {
// System.err.println("Not recording a macro!");
return;
}
recordingMacro = false;
}
/**
* Notifies all listeners that a caret change has occurred.
*
* @param e
* The caret event.
*/
protected void fireCaretUpdate(CaretEvent e) {
// Decide whether we need to repaint the current line background.
possiblyUpdateCurrentLineHighlightLocation();
// Now, if there is a highlighted region of text, allow them to cut
// and copy.
if (e != null && e.getDot() != e.getMark()) {// && !cutAction.isEnabled()) {
cutAction.setEnabled(true);
copyAction.setEnabled(true);
}
// Otherwise, if there is no highlighted region, don't let them cut
// or copy. The condition here should speed things up, because this
// way, we will only enable the actions the first time the selection
// becomes nothing.
else if (cutAction.isEnabled()) {
cutAction.setEnabled(false);
copyAction.setEnabled(false);
}
super.fireCaretUpdate(e);
}
/**
* Removes the "Ctrl+H <=> Backspace" behavior that Java shows, for some odd reason...
*/
private void fixCtrlH() {
InputMap inputMap = getInputMap();
KeyStroke char010 = KeyStroke.getKeyStroke("typed \010");
InputMap parent = inputMap;
while (parent != null) {
parent.remove(char010);
parent = parent.getParent();
}
KeyStroke backspace = KeyStroke.getKeyStroke("BACK_SPACE");
inputMap.put(backspace, DefaultEditorKit.deletePrevCharAction);
}
/**
* Provides a way to gain access to the editor actions on the right-click popup menu. This way you can make
* toolbar/menu bar items use the actual actions used by all <code>RTextArea</code>s, so that icons stay
* synchronized and you don't have to worry about enabling/disabling them yourself.
* <p>
* Keep in mind that these actions are shared across all instances of <code>RTextArea</code>, so a change to any
* action returned by this method is global across all <code>RTextArea</code> editors in your application.
*
* @param action
* The action to retrieve, such as {@link #CUT_ACTION}. If the action name is invalid, <code>null</code>
* is returned.
* @return The action, or <code>null</code> if an invalid action is requested.
*/
public static RecordableTextAction getAction(int action) {
if (action < MIN_ACTION_CONSTANT || action > MAX_ACTION_CONSTANT)
return null;
switch (action) {
case COPY_ACTION:
return copyAction;
case CUT_ACTION:
return cutAction;
case DELETE_ACTION:
return deleteAction;
case PASTE_ACTION:
return pasteAction;
case REDO_ACTION:
return redoAction;
case SELECT_ALL_ACTION:
return selectAllAction;
case UNDO_ACTION:
return undoAction;
}
return null;
}
/**
* Returns the macro currently stored in this <code>RTextArea</code>. Since macros are shared, all
* <code>RTextArea</code>s in the currently- running application are using this macro.
*
* @return The current macro, or <code>null</code> if no macro has been recorded/loaded.
* @see #loadMacro(Macro)
*/
public static synchronized Macro getCurrentMacro() {
return currentMacro;
}
/**
* Returns the default color used for "mark all."
*
* @return The color.
* @see #getMarkAllHighlightColor()
* @see #setMarkAllHighlightColor(Color)
*/
public static final Color getDefaultMarkAllHighlightColor() {
return DEFAULT_MARK_ALL_COLOR;
}
/**
* Returns the icon group being used for the actions of this text area.
*
* @return The icon group.
* @see #setIconGroup(IconGroup)
*/
public static IconGroup getIconGroup() {
return iconGroup;
}
/**
* Returns the line highlight manager.
*
* @return The line highlight manager. This may be <code>null</code>.
*/
LineHighlightManager getLineHighlightManager() {
return lineHighlightManager;
}
/**
* Returns the color used in "mark all."
*
* @return The color.
* @see #setMarkAllHighlightColor(Color)
*/
public Color getMarkAllHighlightColor() {
return (Color) markAllHighlightPainter.getPaint();
}
/**
* Returns the maximum ascent of all fonts used in this text area. In the case of a standard <code>RTextArea</code>,
* this is simply the ascent of the current font.
* <p>
*
* This value could be useful, for example, to implement a line-numbering scheme.
*
* @return The ascent of the current font.
*/
public int getMaxAscent() {
return getFontMetrics(getFont()).getAscent();
}
/**
* Returns the popup menu for this component, lazily creating it if necessary.
*
* @return The popup menu.
* @see #createPopupMenu()
* @see #setPopupMenu(JPopupMenu)
*/
public JPopupMenu getPopupMenu() {
if (!popupMenuCreated) {
popupMenu = createPopupMenu();
if (popupMenu != null) {
ComponentOrientation orientation = ComponentOrientation.
getOrientation(Locale.getDefault());
popupMenu.applyComponentOrientation(orientation);
}
popupMenuCreated = true;
}
return popupMenu;
}
/**
* Returns the text mode this editor pane is currently in.
*
* @return Either {@link #INSERT_MODE} or {@link #OVERWRITE_MODE}.
* @see #setTextMode(int)
*/
public final int getTextMode() {
return textMode;
}
/**
* Returns the tool tip supplier.
*
* @return The tool tip supplier, or <code>null</code> if one isn't installed.
* @see #setToolTipSupplier(ToolTipSupplier)
*/
public ToolTipSupplier getToolTipSupplier() {
return toolTipSupplier;
}
/**
* Returns the tooltip to display for a mouse event at the given location. This method is overridden to check for a
* {@link ToolTipSupplier}; if there is one installed, it is queried for tool tip text before using the super
* class's implementation of this method.
*
* @param e
* The mouse event.
* @return The tool tip text, or <code>null</code> if none.
* @see #getToolTipSupplier()
* @see #setToolTipSupplier(ToolTipSupplier)
*/
public String getToolTipText(MouseEvent e) {
String tip = null;
if (getToolTipSupplier() != null) {
tip = getToolTipSupplier().getToolTipText(this, e);
}
return tip != null ? tip : super.getToolTipText();
}
/**
* Does the actual dirty-work of replacing the selected text in this text area (i.e., in its document). This method
* provides a hook for subclasses to handle this in a different way.
*
* @param content
* The content to add.
*/
protected void handleReplaceSelection(String content) {
// Call into super to handle composed text (1.5+ only though).
super.replaceSelection(content);
}
/**
* Initializes this text area.
*
* @param textMode
* The text mode.
*/
private void init(int textMode) {
// NOTE: Our actions are created here instead of in a static block
// so they are only created when the first RTextArea is instantiated,
// not before. There have been reports of users calling static getters
// (e.g. RSyntaxTextArea.getDefaultBracketMatchBGColor()) which would
// cause these actions to be created and (possibly) incorrectly
// localized, if they were in a static block.
if (cutAction == null) {
createPopupMenuActions();
}
// Install the undo manager.
undoManager = new RUndoManager(this);
getDocument().addUndoableEditListener(undoManager);
// Set the defaults for various stuff.
Color markAllHighlightColor = getDefaultMarkAllHighlightColor();
markAllHighlightPainter = new ChangeableHighlightPainter(
markAllHighlightColor);
setMarkAllHighlightColor(markAllHighlightColor);
carets = new int[2];
setCaretStyle(INSERT_MODE, ConfigurableCaret.THICK_VERTICAL_LINE_STYLE);
setCaretStyle(OVERWRITE_MODE, ConfigurableCaret.BLOCK_STYLE);
setDragEnabled(true); // Enable drag-and-drop.
// Set values for stuff the user passed in.
setTextMode(textMode); // carets array must be initialized first!
// Fix the odd "Ctrl+H <=> Backspace" Java behavior.
fixCtrlH();
}
/**
* Returns whether or not a macro is being recorded.
*
* @return Whether or not a macro is being recorded.
* @see #beginRecordingMacro()
* @see #endRecordingMacro()
*/
public static synchronized boolean isRecordingMacro() {
return recordingMacro;
}
/**
* Loads a macro to be used by all <code>RTextArea</code>s in the current application.
*
* @param macro
* The macro to load.
* @see #getCurrentMacro()
*/
public static synchronized void loadMacro(Macro macro) {
currentMacro = macro;
}
/**
* Marks all instances of the specified text in this text area.
*
* @param toMark
* The text to mark.
* @param matchCase
* Whether the match should be case-sensitive.
* @param wholeWord
* Whether the matches should be surrounded by spaces or tabs.
* @param regex
* Whether <code>toMark</code> is a Java regular expression.
* @return The number of matches marked.
* @see #clearMarkAllHighlights
* @see #getMarkAllHighlightColor
* @see #setMarkAllHighlightColor
*/
public int markAll(String toMark, boolean matchCase, boolean wholeWord,
boolean regex) {
Highlighter h = getHighlighter();
int numMarked = 0;
if (toMark != null && !toMark.equals(markedWord) && h != null) {
if (markAllHighlights != null)
clearMarkAllHighlights();
else
markAllHighlights = new ArrayList(10);
int caretPos = getCaretPosition();
markedWord = toMark;
setCaretPosition(0);
boolean found = SearchEngine.find(this, toMark, true, matchCase,
wholeWord, regex);
while (found) {
int start = getSelectionStart();
int end = getSelectionEnd();
try {
markAllHighlights.add(h.addHighlight(start, end,
markAllHighlightPainter));
} catch (BadLocationException ble) {
ble.printStackTrace();
}
numMarked++;
found = SearchEngine.find(this, toMark, true,
matchCase, wholeWord, regex);
}
setCaretPosition(caretPos);
repaint();
}
return numMarked;
}
/**
* {@inheritDoc}
*/
public void paste() {
// Treat paste operations as atomic, otherwise the removal and
// insertion are treated as two separate undo-able operations.
beginAtomicEdit();
try {
super.paste();
} finally {
endAtomicEdit();
}
}
/**
* "Plays back" the last recorded macro in this text area.
*/
public synchronized void playbackLastMacro() {
if (currentMacro != null) {
Action[] actions = getActions();
int numActions = actions.length;
List macroRecords = currentMacro.getMacroRecords();
int num = macroRecords.size();
if (num > 0) {
undoManager.beginInternalAtomicEdit();
try {
for (int i = 0; i < num; i++) {
MacroRecord record = (MacroRecord) macroRecords.get(i);
for (int j = 0; j < numActions; j++) {
if ((actions[j] instanceof RecordableTextAction) &&
record.id.equals(
((RecordableTextAction) actions[j]).getMacroID())) {
actions[j].actionPerformed(
new ActionEvent(this,
ActionEvent.ACTION_PERFORMED,
record.actionCommand));
break;
}
}
}
} finally {
undoManager.endInternalAtomicEdit();
}
}
}
}
/**
* Method called when it's time to print this badboy (the old-school, AWT way).
*
* @param g
* The context into which the page is drawn.
* @param pageFormat
* The size and orientation of the page being drawn.
* @param pageIndex
* The zero based index of the page to be drawn.
*/
public int print(Graphics g, PageFormat pageFormat, int pageIndex) {
return RPrintUtilities.printDocumentWordWrap(g, this, getFont(), pageIndex, pageFormat, getTabSize());
}
/**
* We override this method because the super version gives us an entirely new <code>Document</code>, thus requiring
* us to re-attach our Undo manager. With this version we just replace the text.
*/
public void read(Reader in, Object desc) throws IOException {
RTextAreaEditorKit kit = (RTextAreaEditorKit) getUI().getEditorKit(this);
setText(null);
Document doc = getDocument();
if (desc != null)
doc.putProperty(Document.StreamDescriptionProperty, desc);
try {
// NOTE: Resets the "line separator" property.
kit.read(in, doc, 0);
} catch (BadLocationException e) {
throw new IOException(e.getMessage());
}
}
/**
* De-serializes a text area.
*
* @param s
* The stream to read from.
* @throws ClassNotFoundException
* @throws IOException
*/
private void readObject(ObjectInputStream s)
throws ClassNotFoundException, IOException {
s.defaultReadObject();
// UndoManagers cannot be serialized without Exceptions. See
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4275892
undoManager = new RUndoManager(this);
getDocument().addUndoableEditListener(undoManager);
lineHighlightManager = null; // Keep FindBugs happy.
}
/**
* Attempt to redo the last action.
*
* @see #undoLastAction()
*/
public void redoLastAction() {
// NOTE: The try/catch block shouldn't be necessary...
try {
if (undoManager.canRedo())
undoManager.redo();
} catch (CannotRedoException cre) {
cre.printStackTrace();
}
}
/**
* Removes all line highlights.
*
* @see #removeLineHighlight(Object)
*/
public void removeAllLineHighlights() {
if (lineHighlightManager != null) {
lineHighlightManager.removeAllLineHighlights();
}
}
/**
* Removes a line highlight.
*
* @param tag
* The tag of the line highlight to remove.
* @see #removeAllLineHighlights()
* @see #addLineHighlight(int, Color)
*/
public void removeLineHighlight(Object tag) {
if (lineHighlightManager != null) {
lineHighlightManager.removeLineHighlight(tag);
}
}
/**
* Replaces text from the indicated start to end position with the new text specified. Does nothing if the model is
* null. Simply does a delete if the new string is null or empty.
* <p>
* This method is thread safe, although most Swing methods are not.
* <p>
* This method is overridden so that our Undo manager remembers it as a single operation (it has trouble with this,
* especially for <code>RSyntaxTextArea</code> and the "auto-indent" feature).
*
* @param str
* the text to use as the replacement
* @param start
* the start position >= 0
* @param end
* the end position >= start
* @exception IllegalArgumentException
* if part of the range is an invalid position in the model
* @see #insert(String, int)
* @see #replaceRange(String, int, int)
*/
public void replaceRange(String str, int start, int end) {
if (end < start)
throw new IllegalArgumentException("end before start");
Document doc = getDocument();
if (doc != null) {
try {
// Without this, in some cases we'll have to do two undos
// for one logical operation (for example, try editing a
// Java source file in an RSyntaxTextArea, and moving a line
// with text already on it down via Enter. Without this
// line, doing a single "undo" moves all later text up,
// but the first line moved down isn't there! Doing a
// second undo puts it back.
undoManager.beginInternalAtomicEdit();
((AbstractDocument) doc).replace(start, end - start,
str, null);
} catch (BadLocationException e) {
throw new IllegalArgumentException(e.getMessage());
} finally {
undoManager.endInternalAtomicEdit();
}
}
}
/**
* This method overrides <code>JTextComponent</code>'s <code>replaceSelection</code>, so that if
* <code>textMode</code> is {@link #OVERWRITE_MODE}, it actually overwrites.
*
* @param text
* The content to replace the selection with.
*/
public void replaceSelection(String text) {
// It's legal for null to be used here...
if (text == null) {
handleReplaceSelection(text);
return;
}
if (getTabsEmulated() && text.indexOf('\t') > -1) {
text = replaceTabsWithSpaces(text);
}
// If the user wants to overwrite text...
if (textMode == OVERWRITE_MODE && !"\n".equals(text)) {
Caret caret = getCaret();
int caretPos = caret.getDot();
Document doc = getDocument();
Element map = doc.getDefaultRootElement();
int curLine = map.getElementIndex(caretPos);
int lastLine = map.getElementCount() - 1;
try {
// If we're not at the end of a line, select the characters
// that will be overwritten (otherwise JTextArea will simply
// insert in front of them).
int curLineEnd = getLineEndOffset(curLine);
if (caretPos == caret.getMark() && caretPos != curLineEnd) {
if (curLine == lastLine)
caretPos = Math.min(caretPos + text.length(), curLineEnd);
else
caretPos = Math.min(caretPos + text.length(), curLineEnd - 1);
caret.moveDot(caretPos);// moveCaretPosition(caretPos);
}
} catch (BadLocationException ble) { // Never happens
UIManager.getLookAndFeel().provideErrorFeedback(this);
ble.printStackTrace();
}
} // End of if (textMode==OVERWRITE_MODE).
// Now, actually do the inserting/replacing. Our undoManager will
// take care of remembering the remove/insert as atomic if we are in
// overwrite mode.
handleReplaceSelection(text);
}
private StringBuffer repTabsSB;
/**
* Replaces all instances of the tab character in <code>text</code> with the number of spaces equivalent to a tab in
* this text area.
* <p>
*
* This method should only be called from thread-safe methods, such as {@link replaceSelection(String)}.
*
* @param text
* The <code>java.lang.String</code> in which to replace tabs with spaces. This has already been verified
* to have at least one tab character in it.
* @return A <code>java.lang.String</code> just like <code>text</code>, but with spaces instead of tabs.
*/
private final String replaceTabsWithSpaces(final String text) {
String tabText = "";
int temp = getTabSize();
for (int i = 0; i < temp; i++) {
tabText += ' ';
}
// Common case: User's entering a single tab (pressed the tab key).
if (text.length() == 1) {
return tabText;
}
// Otherwise, there may be more than one tab. Manually search for
// tabs for performance, as opposed to using String#replaceAll().
// This method is called for each character inserted when "replace
// tabs with spaces" is enabled, so we need to be quick.
// return text.replaceAll("\t", tabText);
if (repTabsSB == null) {
repTabsSB = new StringBuffer();
}
repTabsSB.setLength(0);
char[] array = text.toCharArray(); // Wouldn't be needed in 1.5!
int oldPos = 0;
int pos = 0;
while ((pos = text.indexOf('\t', oldPos)) > -1) {
// repTabsSB.append(text, oldPos, pos); // Added in Java 1.5
if (pos > oldPos) {
repTabsSB.append(array, oldPos, pos - oldPos);
}
repTabsSB.append(tabText);
oldPos = pos + 1;
}
if (oldPos < array.length) {
repTabsSB.append(array, oldPos, array.length - oldPos);
}
return repTabsSB.toString();
}
/**
* Sets the properties of one of the actions this text area owns.
*
* @param action
* The action to modify; for example, {@link #CUT_ACTION}.
* @param name
* The new name for the action.
* @param mnemonic
* The new mnemonic for the action.
* @param accelerator
* The new accelerator key for the action.
*/
public static void setActionProperties(int action, String name,
char mnemonic, KeyStroke accelerator) {
setActionProperties(action, name, new Integer(mnemonic), accelerator);
}
/**
* Sets the properties of one of the actions this text area owns.
*
* @param action
* The action to modify; for example, {@link #CUT_ACTION}.
* @param name
* The new name for the action.
* @param mnemonic
* The new mnemonic for the action.
* @param accelerator
* The new accelerator key for the action.
*/
public static void setActionProperties(int action, String name,
Integer mnemonic, KeyStroke accelerator) {
Action tempAction = null;
switch (action) {
case CUT_ACTION:
tempAction = cutAction;
break;
case COPY_ACTION:
tempAction = copyAction;
break;
case PASTE_ACTION:
tempAction = pasteAction;
break;
case DELETE_ACTION:
tempAction = deleteAction;
break;
case SELECT_ALL_ACTION:
tempAction = selectAllAction;
break;
case UNDO_ACTION:
case REDO_ACTION:
default:
return;
}
tempAction.putValue(Action.NAME, name);
tempAction.putValue(Action.SHORT_DESCRIPTION, name);
tempAction.putValue(Action.ACCELERATOR_KEY, accelerator);
tempAction.putValue(Action.MNEMONIC_KEY, mnemonic);
}
/**
* This method is overridden to make sure that instances of <code>RTextArea</code> only use
* {@link ConfigurableCaret}s. To set the style of caret (vertical line, block, etc.) used for insert or overwrite
* mode, use {@link #setCaretStyle(int, int)}.
*
* @param caret
* The caret to use. If this is not an instance of <code>ConfigurableCaret</code>, an exception is
* thrown.
* @throws IllegalArgumentException
* If the specified caret is not an <code>ConfigurableCaret</code>.
* @see #setCaretStyle(int, int)
*/
public void setCaret(Caret caret) {
if (!(caret instanceof ConfigurableCaret)) {
throw new IllegalArgumentException(
"RTextArea needs ConfigurableCaret");
}
super.setCaret(caret);
if (carets != null) { // Called by setUI() before carets is initialized
((ConfigurableCaret) caret).setStyle(carets[getTextMode()]);
}
}
/**
* Sets the style of caret used when in insert or overwrite mode.
*
* @param mode
* Either {@link #INSERT_MODE} or {@link #OVERWRITE_MODE}.
* @param style
* The style for the caret (such as {@link ConfigurableCaret#VERTICAL_LINE_STYLE}).
* @see org.fife.ui.rtextarea.ConfigurableCaret
*/
public void setCaretStyle(int mode, int style) {
style = (style >= ConfigurableCaret.MIN_STYLE &&
style <= ConfigurableCaret.MAX_STYLE ?
style : ConfigurableCaret.THICK_VERTICAL_LINE_STYLE);
carets[mode] = style;
if (mode == getTextMode()) {
// Will repaint the caret if necessary.
((ConfigurableCaret) getCaret()).setStyle(style);
}
}
/**
* Sets the document used by this text area.
*
* @param document
* The new document to use.
* @throws IllegalArgumentException
* If the document is not an instance of {@link AbstractDocument}.
*/
public void setDocument(Document document) {
if (!(document instanceof AbstractDocument)) {
throw new IllegalArgumentException("RTextArea requires " +
"instances of AbstractDocument for its document");
}
if (undoManager != null) { // First time through, undoManager==null
Document old = getDocument();
if (old != null) {
old.removeUndoableEditListener(undoManager);
}
}
super.setDocument(document);
if (undoManager != null) {
document.addUndoableEditListener(undoManager);
discardAllEdits();
}
}
/**
* Sets the path in which to find images to associate with the editor's actions. The path MUST contain the following
* images (with the appropriate extension as defined by the icon group):<br>
* <ul>
* <li>cut</li>
* <li>copy</li>
* <li>paste</li>
* <li>delete</li>
* <li>undo</li>
* <li>redo</li>
* <li>selectall</li>
* </ul>
* If any of the above images don't exist, the corresponding action will not have an icon.
*
* @param group
* The icon group to load.
* @see #getIconGroup()
*/
public static synchronized void setIconGroup(IconGroup group) {
Icon icon = group.getIcon("cut");
cutAction.putValue(Action.SMALL_ICON, icon);
icon = group.getIcon("copy");
copyAction.putValue(Action.SMALL_ICON, icon);
icon = group.getIcon("paste");
pasteAction.putValue(Action.SMALL_ICON, icon);
icon = group.getIcon("delete");
deleteAction.putValue(Action.SMALL_ICON, icon);
icon = group.getIcon("undo");
undoAction.putValue(Action.SMALL_ICON, icon);
icon = group.getIcon("redo");
redoAction.putValue(Action.SMALL_ICON, icon);
icon = group.getIcon("selectall");
selectAllAction.putValue(Action.SMALL_ICON, icon);
iconGroup = group;
}
/**
* Sets the color used for "mark all." This fires a property change of type {@link #MARK_ALL_COLOR_PROPERTY}.
*
* @param color
* The color to use for "mark all."
* @see #getMarkAllHighlightColor()
*/
public void setMarkAllHighlightColor(Color color) {
Color old = (Color) markAllHighlightPainter.getPaint();
if (old != null && !old.equals(color)) {
markAllHighlightPainter.setPaint(color);
if (markedWord != null)
repaint(); // Repaint if words are highlighted.
firePropertyChange(MARK_ALL_COLOR_PROPERTY, old, color);
}
}
/**
* Sets the popup menu used by this text area.
*
* @param popupMenu
* The popup menu. If this is <code>null</code>, no popup menu will be displayed.
* @see #getPopupMenu()
*/
public void setPopupMenu(JPopupMenu popupMenu) {
this.popupMenu = popupMenu;
popupMenuCreated = true;
}
/**
* {@inheritDoc}
*/
public void setRoundedSelectionEdges(boolean rounded) {
if (getRoundedSelectionEdges() != rounded) {
markAllHighlightPainter.setRoundedEdges(rounded);
super.setRoundedSelectionEdges(rounded); // Fires event.
}
}
/**
* Sets the text mode for this editor pane.
*
* @param mode
* Either {@link #INSERT_MODE} or {@link #OVERWRITE_MODE}.
*/
public void setTextMode(int mode) {
if (mode != INSERT_MODE && mode != OVERWRITE_MODE)
mode = INSERT_MODE;
if (textMode != mode) {
ConfigurableCaret cc = (ConfigurableCaret) getCaret();
cc.setStyle(carets[mode]);
textMode = mode;
}
}
/**
* Sets the tool tip supplier.
*
* @param supplier
* The new tool tip supplier, or <code>null</code> if there is to be no supplier.
* @see #getToolTipSupplier()
*/
public void setToolTipSupplier(ToolTipSupplier supplier) {
this.toolTipSupplier = supplier;
}
/**
* Sets the UI used by this text area. This is overridden so only the right-click popup menu's UI is updated. The
* look and feel of an <code>RTextArea</code> is independent of the Java Look and Feel, and so this method does not
* change the text area itself. Subclasses (such as <code>RSyntaxTextArea</code> can call
* <code>setRTextAreaUI</code> if they wish to install a new UI.
*
* @param ui
* This parameter is ignored.
*/
public final void setUI(TextUI ui) {
// Update the popup menu's ui.
if (popupMenu != null) {
SwingUtilities.updateComponentTreeUI(popupMenu);
}
// Set things like selection color, selected text color, etc. to
// laf defaults (if values are null or UIResource instances).
RTextAreaUI rtaui = (RTextAreaUI) getUI();
if (rtaui != null) {
rtaui.installDefaults();
}
}
/**
* Attempt to undo an "action" done in this text area.
*
* @see #redoLastAction()
*/
public void undoLastAction() {
// NOTE: that the try/catch block shouldn't be necessary...
try {
if (undoManager.canUndo())
undoManager.undo();
} catch (CannotUndoException cre) {
cre.printStackTrace();
}
}
/**
* Serializes this text area.
*
* @param s
* The stream to write to.
* @throws IOException
* If an IO error occurs.
*/
private void writeObject(ObjectOutputStream s) throws IOException {
// UndoManagers cannot be serialized without Exceptions. See
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4275892
getDocument().removeUndoableEditListener(undoManager);
s.defaultWriteObject();
getDocument().addUndoableEditListener(undoManager);
}
/**
* Modified from <code>MutableCaretEvent</code> in <code>JTextComponent</code> so that mouse events get fired when
* the user is selecting text with the mouse as well. This class also displays the popup menu when the user
* right-clicks in the text area.
*/
protected class RTextAreaMutableCaretEvent extends RTAMouseListener {
protected RTextAreaMutableCaretEvent(RTextArea textArea) {
super(textArea);
}
public void focusGained(FocusEvent e) {
Caret c = getCaret();
boolean enabled = c.getDot() != c.getMark();
cutAction.setEnabled(enabled);
copyAction.setEnabled(enabled);
undoManager.updateActions(); // To reflect this text area.
}
public void focusLost(FocusEvent e) {
}
public void mouseDragged(MouseEvent e) {
if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {
Caret caret = getCaret();
dot = caret.getDot();
mark = caret.getMark();
fireCaretUpdate(this);
}
}
public void mousePressed(MouseEvent e) {
// WORKAROUND: Since JTextComponent only updates the caret
// location on mouse clicked and released, we'll do it on dragged
// events when the left mouse button is clicked.
if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {
Caret caret = getCaret();
dot = caret.getDot();
mark = caret.getMark();
fireCaretUpdate(this);
}
}
public void mouseReleased(MouseEvent e) {
if ((e.getModifiers() & MouseEvent.BUTTON3_MASK) != 0)
showPopup(e);
}
/**
* Shows a popup menu with cut, copy, paste, etc. options if the user clicked the right button.
*
* @param e
* The mouse event that caused this method to be called.
*/
private void showPopup(MouseEvent e) {
JPopupMenu popupMenu = getPopupMenu();
if (popupMenu != null) {
configurePopupMenu(popupMenu);
popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
}
}