/* * 11/14/2003 * * RTextArea.java - An extension of JTextArea that adds many features. * * 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.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.MouseEvent; 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.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.Segment; import javax.swing.undo.CannotRedoException; import javax.swing.undo.CannotUndoException; import org.fife.print.RPrintUtilities; import org.fife.ui.rsyntaxtextarea.DocumentRange; 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 { /** * Constant representing insert mode. * * @see #setCaretStyle(int, CaretStyle) */ public static final int INSERT_MODE = 0; /** * Constant representing overwrite mode. * * @see #setCaretStyle(int, CaretStyle) */ 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"; /** * The property fired when what ranges are labeled "mark all" changes. */ public static final String MARK_ALL_OCCURRENCES_CHANGED_PROPERTY = "RTA.markAllOccurrencesChanged"; /* * 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 = new Color(0xffc800); /** * 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; /** * This text area's popup menu. */ private JPopupMenu popupMenu; private JMenuItem undoMenuItem; private JMenuItem redoMenuItem; private JMenuItem cutMenuItem; private JMenuItem pasteMenuItem; private JMenuItem deleteMenuItem; /** * Whether the popup menu has been created. */ private boolean popupMenuCreated; /** * The text last searched for via Ctrl+K or Ctrl+Shift+K. */ private static String selectedOccurrenceText; /** * 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 SmartHighlightPainter markAllHighlightPainter; private CaretStyle[] carets; // Index 0=>insert caret, 1=>overwrite. private static final String MSG = "org.fife.ui.rtextarea.RTextArea"; /** * Constructor. */ public RTextArea() { } /** * Constructor. * * @param doc The document for the editor. */ public RTextArea(AbstractDocument doc) { super(doc); } /** * Constructor. * * @param text The initial text to display. */ public RTextArea(String text) { super(text); } /** * 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); } /** * 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); } /** * 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); } /** * Creates a new <code>RTextArea</code>. * * @param textMode Either <code>INSERT_MODE</code> or * <code>OVERWRITE_MODE</code>. */ public RTextArea(int textMode) { setTextMode(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(List) * @see #getMarkAllHighlightColor() * @see #setMarkAllHighlightColor(Color) */ void clearMarkAllHighlights() { ((RTextAreaHighlighter)getHighlighter()).clearMarkAllHighlights(); //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.<p> * * If you set the popup menu via {@link #setPopupMenu(JPopupMenu)}, you * will want to override this method, especially if you removed any of the * menu items in the default popup menu. * * @param popupMenu The popup menu. This will never be <code>null</code>. * @see #createPopupMenu() * @see #setPopupMenu(JPopupMenu) */ protected void configurePopupMenu(JPopupMenu popupMenu) { boolean canType = isEditable() && isEnabled(); // Since the user can customize the popup menu, these actions may not // have been created. if (undoMenuItem!=null) { undoMenuItem.setEnabled(undoAction.isEnabled() && canType); redoMenuItem.setEnabled(redoAction.isEnabled() && canType); cutMenuItem.setEnabled(cutAction.isEnabled() && canType); pasteMenuItem.setEnabled(pasteAction.isEnabled() && canType); deleteMenuItem.setEnabled(deleteAction.isEnabled() && canType); } } /** * Creates the default implementation of the model to be used at * construction if one isn't explicitly given. A new instance of RDocument * is returned. * * @return The default document. */ @Override protected Document createDefaultModel() { return new RDocument(); } /** * Returns the caret event/mouse listener for <code>RTextArea</code>s. * * @return The caret event/mouse listener. */ @Override 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) * @see #createPopupMenuItem(Action) */ protected JPopupMenu createPopupMenu() { JPopupMenu menu = new JPopupMenu(); menu.add(undoMenuItem = createPopupMenuItem(undoAction)); menu.add(redoMenuItem = createPopupMenuItem(redoAction)); menu.addSeparator(); menu.add(cutMenuItem = createPopupMenuItem(cutAction)); menu.add(createPopupMenuItem(copyAction)); menu.add(pasteMenuItem = createPopupMenuItem(pasteAction)); menu.add(deleteMenuItem = createPopupMenuItem(deleteAction)); menu.addSeparator(); menu.add(createPopupMenuItem(selectAllAction)); return menu; } /** * Creates the actions used in the popup menu and retrievable by * {@link #getAction(int)}. * TODO: Remove these horrible hacks and move localizing of actions into * the editor kits, where it should be! The context menu should contain * actions from the editor kits. */ private static void createPopupMenuActions() { // 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(); ResourceBundle msg = ResourceBundle.getBundle(MSG); cutAction = new RTextAreaEditorKit.CutAction(); cutAction.setProperties(msg, "Action.Cut"); cutAction.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X, mod)); copyAction = new RTextAreaEditorKit.CopyAction(); copyAction.setProperties(msg, "Action.Copy"); copyAction.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, mod)); pasteAction = new RTextAreaEditorKit.PasteAction(); pasteAction.setProperties(msg, "Action.Paste"); pasteAction.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_V, mod)); deleteAction = new RTextAreaEditorKit.DeleteNextCharAction(); deleteAction.setProperties(msg, "Action.Delete"); deleteAction.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0)); undoAction = new RTextAreaEditorKit.UndoAction(); undoAction.setProperties(msg, "Action.Undo"); undoAction.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, mod)); redoAction = new RTextAreaEditorKit.RedoAction(); redoAction.setProperties(msg, "Action.Redo"); redoAction.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, mod)); selectAllAction = new RTextAreaEditorKit.SelectAllAction(); selectAllAction.setProperties(msg, "Action.SelectAll"); selectAllAction.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, mod)); } /** * Creates and configures a menu item for used in the popup menu. * * @param a The action for the menu item. * @return The menu item. * @see #createPopupMenu() */ protected JMenuItem createPopupMenuItem(Action a) { JMenuItem item = new JMenuItem(a) { @Override public void setToolTipText(String text) { // Ignore! Actions (e.g. undo/redo) set this when changing // their text due to changing enabled state. } }; item.setAccelerator(null); return item; } /** * Returns the a real UI to install on this text area. * * @return The UI. */ @Override protected RTextAreaUI createRTextAreaUI() { return new RTextAreaUI(this); } /** * Creates a string of space characters of the specified size. * * @param size The number of spaces. * @return The string of spaces. */ private final String createSpacer(int size) { StringBuilder sb = new StringBuilder(); for (int i=0; i<size; i++) { sb.append(' '); } return sb.toString(); } /** * Creates an undo manager for use in this text area. * * @return The undo manager. */ protected RUndoManager createUndoManager() { return new RUndoManager(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 = createUndoManager(); 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. */ @Override 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 last selected and used in a Ctrl+K operation. * * @return The text, or <code>null</code> if none. * @see #setSelectedOccurrenceText(String) */ public static String getSelectedOccurrenceText() { return selectedOccurrenceText; } /** * 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) */ @Override 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. super.replaceSelection(content); } /** * {@inheritDoc} */ @Override protected void init() { super.init(); // 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 = createUndoManager(); getDocument().addUndoableEditListener(undoManager); // Set the defaults for various stuff. Color markAllHighlightColor = getDefaultMarkAllHighlightColor(); markAllHighlightPainter = new SmartHighlightPainter( markAllHighlightColor); setMarkAllHighlightColor(markAllHighlightColor); carets = new CaretStyle[2]; setCaretStyle(INSERT_MODE, CaretStyle.THICK_VERTICAL_LINE_STYLE); setCaretStyle(OVERWRITE_MODE, CaretStyle.BLOCK_STYLE); setDragEnabled(true); // Enable drag-and-drop. setTextMode(INSERT_MODE); // Carets array must be created 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 ranges specified with the "mark all" highlighter. Typically, * this method is called indirectly from {@link SearchEngine} when doing * a fine or replace operation.<p> * * This method fires a property change event of type * {@link #MARK_ALL_OCCURRENCES_CHANGED_PROPERTY}. * * @param ranges The ranges to mark. This should not be <code>null</code>. * @see SearchEngine * @see SearchContext#setMarkAll(boolean) * @see #clearMarkAllHighlights() * @see #getMarkAllHighlightColor() * @see #setMarkAllHighlightColor(Color) */ void markAll(List<DocumentRange> ranges) { RTextAreaHighlighter h = (RTextAreaHighlighter)getHighlighter(); if (/*toMark!=null && !toMark.equals(markedWord) && */h!=null) { //markedWord = toMark; if (ranges!=null) { for (DocumentRange range : ranges) { try { h.addMarkAllHighlight( range.getStartOffset(), range.getEndOffset(), markAllHighlightPainter); } catch (BadLocationException ble) { ble.printStackTrace(); } } } repaint(); firePropertyChange(MARK_ALL_OCCURRENCES_CHANGED_PROPERTY, null, ranges); } } /** * {@inheritDoc} */ @Override 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) { List<MacroRecord> macroRecords = currentMacro.getMacroRecords(); if (!macroRecords.isEmpty()) { Action[] actions = getActions(); undoManager.beginInternalAtomicEdit(); try { for (MacroRecord record : macroRecords) { for (int i=0; i<actions.length; i++) { if ((actions[i] instanceof RecordableTextAction) && record.id.equals( ((RecordableTextAction)actions[i]).getMacroID())) { actions[i].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. */ @Override 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 = createUndoManager(); 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) */ @Override 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. */ @Override public void replaceSelection(String text) { // It's legal for null to be used here... if (text==null) { handleReplaceSelection(text); return; } if (getTabsEmulated()) { int firstTab = text.indexOf('\t'); if (firstTab>-1) { int docOffs = getSelectionStart(); try { text = replaceTabsWithSpaces(text, docOffs, firstTab); } catch (BadLocationException ble) { // Never happens ble.printStackTrace(); } } } // 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 static StringBuilder repTabsSB; private static Segment repTabsSeg = new Segment(); /** * 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. * @param docOffs The offset in the document at which the text is being * inserted. * @param firstTab The offset into <code>text</code> of the first tab. Assumed * to be >= 0. * @return A <code>String</code> just like <code>text</code>, but with * spaces instead of tabs. */ private final String replaceTabsWithSpaces(String text, int docOffs, int firstTab) throws BadLocationException { int tabSize = getTabSize(); // Get how many chars into the current line we are Document doc = getDocument(); Element root = doc.getDefaultRootElement(); int lineIndex = root.getElementIndex(docOffs); Element line = root.getElement(lineIndex); int lineStart = line.getStartOffset(); int charCount = docOffs - lineStart; // Figure out how many chars into the "current tab" we are if (charCount>0) { doc.getText(lineStart, charCount, repTabsSeg); charCount = 0; for (int i=0; i<repTabsSeg.count; i++) { char ch = repTabsSeg.array[repTabsSeg.offset + i]; if (ch=='\t') { charCount = 0; } else { charCount = (charCount + 1) % tabSize; } } } // Common case: The user's entering a single tab (pressed the tab key). if (text.length()==1) { return createSpacer(tabSize - charCount); } // Otherwise, there may be more than one tab. if (repTabsSB==null) { repTabsSB = new StringBuilder(); } repTabsSB.setLength(0); char[] array = text.toCharArray(); int lastPos = 0; int offsInLine = charCount; // Accurate enough for our start for (int pos=firstTab; pos<array.length; pos++) { char ch = array[pos]; switch (ch) { case '\t': if (pos>lastPos) { repTabsSB.append(array, lastPos, pos-lastPos); } int thisTabSize = tabSize - (offsInLine%tabSize); repTabsSB.append(createSpacer(thisTabSize)); lastPos = pos + 1; offsInLine = 0; break; case '\n': offsInLine = 0; break; default: offsInLine++; break; } } if (lastPos<array.length) { repTabsSB.append(array, lastPos, array.length-lastPos); } 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, Integer.valueOf(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); } /** * Sets the caret to use in this text area. It is strongly encouraged to * use {@link ConfigurableCaret}s (which is used by default), or a * subclass, since they know how to render themselves differently when the * user toggles between insert and overwrite modes. * * @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, CaretStyle) */ @Override public void setCaret(Caret caret) { super.setCaret(caret); if (carets!=null && // Called by setUI() before carets is initialized caret instanceof ConfigurableCaret) { ((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. * @see ConfigurableCaret */ public void setCaretStyle(int mode, CaretStyle style) { if (style==null) { style = CaretStyle.THICK_VERTICAL_LINE_STYLE; } carets[mode] = style; if (mode==getTextMode() && getCaret() instanceof ConfigurableCaret) { // 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 RDocument}. */ @Override public void setDocument(Document document) { if (!(document instanceof RDocument)) { throw new IllegalArgumentException("RTextArea requires " + "instances of RDocument 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); RTextAreaHighlighter h = (RTextAreaHighlighter)getHighlighter(); if (h.getMarkAllHighlightCount()>0) { repaint(); // Repaint if words are highlighted. } firePropertyChange(MARK_ALL_COLOR_PROPERTY, old, color); } } /** * Sets the popup menu used by this text area.<p> * * If you set the popup menu with this method, you'll want to consider also * overriding {@link #configurePopupMenu(JPopupMenu)}, especially if you * removed any of the default menu items. * * @param popupMenu The popup menu. If this is <code>null</code>, no * popup menu will be displayed. * @see #getPopupMenu() * @see #configurePopupMenu(JPopupMenu) */ public void setPopupMenu(JPopupMenu popupMenu) { this.popupMenu = popupMenu; popupMenuCreated = true; } /** * {@inheritDoc} */ @Override public void setRoundedSelectionEdges(boolean rounded) { if (getRoundedSelectionEdges()!=rounded) { markAllHighlightPainter.setRoundedEdges(rounded); super.setRoundedSelectionEdges(rounded); // Fires event. } } /** * Sets the text last selected/Ctrl+K'd in an <code>RTextArea</code>. * This text will be searched for in subsequent Ctrl+K/Ctrl+Shift+K * actions (Cmd+K on OS X).<p> * * Since the selected occurrence actions are built into RTextArea, * applications usually do not have to call this method directly, but can * choose to do so if they wish (for example, if they wish to set this * value when the user does a search via a Find dialog). * * @param text The selected text. * @see #getSelectedOccurrenceText() */ public static void setSelectedOccurrenceText(String text) { selectedOccurrenceText = text; } /** * Sets the text mode for this editor pane. If the currently installed * caret is an instance of {@link ConfigurableCaret}, it will be * automatically updated to render itself appropriately for the new text * mode. * * @param mode Either {@link #INSERT_MODE} or {@link #OVERWRITE_MODE}. * @see #getTextMode() */ public void setTextMode(int mode) { if (mode!=INSERT_MODE && mode!=OVERWRITE_MODE) mode = INSERT_MODE; if (textMode != mode) { Caret caret = getCaret(); if (caret instanceof ConfigurableCaret) { ((ConfigurableCaret)caret).setStyle(carets[mode]); } textMode = mode; // Prevent the caret from blinking while e.g. holding down the // Insert key to toggle insert/overwrite modes caret.setVisible(false); caret.setVisible(true); } } /** * 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. */ @Override 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); } @Override 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. } @Override public void focusLost(FocusEvent e) { } @Override public void mouseDragged(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); } } @Override public void mousePressed(MouseEvent e) { if (e.isPopupTrigger()) { // OS X popup triggers are on pressed showPopup(e); } else if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) { Caret caret = getCaret(); dot = caret.getDot(); mark = caret.getMark(); fireCaretUpdate(this); } } @Override public void mouseReleased(MouseEvent e) { if (e.isPopupTrigger()) { 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()); e.consume(); } } } }