Package org.fife.ui.rtextarea

Source Code of org.fife.ui.rtextarea.RTextArea$RTextAreaMutableCaretEvent

/*
* 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.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.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;

  /**
   * 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);
  }


  /**
   * 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;
  }


  /**
   * 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());
      }
    }

  }


}
TOP

Related Classes of org.fife.ui.rtextarea.RTextArea$RTextAreaMutableCaretEvent

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.