如何在Java Swing中获得标准的插入符号行为?

gz5pxeao  于 2023-02-11  发布在  Java
关注(0)|答案(1)|浏览(97)

Swing Text组件都扩展了JTextComponent,当用户选择一些文本时,JTextComponent类将处理所选文本的工作委托给Caret接口的一个示例(称为DefaultCaret)。以及对改变选择范围的鼠标和键盘事件的响应。
Swing DefaultCaret具有标准插入符号的大部分行为,但我的一些高端用户指出了它的不足之处。
(Note:这些示例在Microsoft Edge中会遇到问题,因为当您选择文本时,它会显示......“”菜单。在这些示例中,如果您使用Edge,则需要键入escape键以摆脱该菜单,然后再继续下一步。)
1.如果我双击一个单词,它应该选择整个单词。Java Swing的脱字符可以做到这一点。但是,双击一个单词后,如果我试图通过按住Shift键并单击第二个单词来扩展选择范围,标准脱字符会扩展选择范围以包括整个第二个单词。为了说明这一点,在下面的示例文本中,如果我双击时钟中的o,它将选择单词时钟。但是如果我接着按住shift键并在wound中的o之后单击,它应该将选择一直扩展到d。它在这个页面上这样做,但在Java Swing中不是这样。在Swing中,它仍然扩展选择,但只扩展到鼠标单击的位置。
这钟上的发条太紧了。
1.如果我尝试通过单击鼠标左键来选择一个文本块,然后拖动,当我拖动文本时,它应该会一次扩展一个完整的单词(“单击鼠标左键,然后拖动",我的意思是在同一个位置快速完成以下事件:mouseDown,mouseUp,mouseDown,mouseMove.这就像是没有最后一个mouseup事件的双击.)你可以在这个页面上尝试一下,它会起作用,但是在Java Swing中不起作用.在Swing中它仍然会扩展选择,但是只会扩展到鼠标的位置.
1.如果我三次点击某个文本,它将选择整个段落。(这在Microsoft Edge中不起作用,但在大多数浏览器和编辑器中起作用)这在Swing中不起作用。
1.如果在三次单击选择一个段落后,我在按住Shift键的同时单击另一个段落,它应该会扩展选择范围以包括整个新段落。
1.就像第2项中的完全单击并拖动示例一样,如果您进行完全双击并拖动,它应该首先选择整个段落,然后一次扩展选择一个段落。(同样,这在Edge中不起作用。)这种行为没有其他行为那么标准,但仍然非常常见。
我的一些高级用户希望能够在我维护的Java Swing应用程序中使用这些功能,我想把这些功能提供给他们。我的应用程序的全部意义在于加快他们的数据处理速度,这些更改将有助于实现这一点。我该如何做到呢?

6l7fqoea

6l7fqoea1#

我写了一个类来解决这些问题。它解决了问题1和问题2。就三次点击而言,它不会改变选择行而不是选择段落的行为,但它解决了所选行上的问题4,与三次点击的行为一致。它没有解决问题5。
这个类还有两个工厂方法,可以将插入符号无故障地安装到文本组件中。

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeListener;
import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.DefaultCaret;
import javax.swing.text.JTextComponent;
import javax.swing.text.Position;
import javax.swing.text.Utilities;

/**
 * <p>Implements Standard rules for extending the selection, consistent with the standard
 * behavior for extending the selection in all word processors, browsers, and other text
 * editing tools, on all platforms. Without this, Swing's behavior on extending the
 * selection is inconsistent with all other text editing tools.
 * </p><p>
 * Swing components don't handle selectByWord the way most UI text components do. If you
 * double-click on a word, they will all select the entire word. But if you do a 
 * click-and-drag, most components will (a) select the entire clicked word, and
 * (b) extend the selection a word at a time as the user drags across the text. And if
 * you double- click on a word and follow that with a shift-click, most components will
 * also extend the selection a word at a time.  Swing components handle a double-clicked
 * word the standard way, but do not handle click-and-drag or shift-click correctly. This
 * caret, which replaces the standard DefaultCaret, fixes this.</p>
 * <p>Created by IntelliJ IDEA.</p>
 * <p>Date: 2/23/20</p>
 * <p>Time: 10:58 PM</p>
 *
 * @author Miguel Mu\u00f1oz
 */
public class StandardCaret extends DefaultCaret {
  // In the event of a double-click, these are the positions of the low end and high end
  // of the word that was clicked.
  private int highMark;
  private int lowMark;
  private boolean selectingByWord = false; // true when the last selection was done by word.
  private boolean selectingByRow = false; // true when the last selection was done by paragraph. 

  /**
   * Instantiate an EnhancedCaret.
   */
  public StandardCaret() {
    super();
  }

  /**
   * <p>Install this Caret into a JTextComponent. Carets may not be shared among multiple
   * components.</p>
   * @param component The component to use the EnhancedCaret.
   */
  public void installInto(JTextComponent component) {
    replaceCaret(component, this);
  }

  /**
   * Install a new StandardCaret into a JTextComponent, such as a JTextField or
   * JTextArea, and starts the Caret blinking using the same blink-rate as the
   * previous Caret.
   *
   * @param component The JTextComponent subclass
   */
  public static void installStandardCaret(JTextComponent component) {
    replaceCaret(component, new StandardCaret());
  }

  /**
   * Installs the specified Caret into the JTextComponent, and starts the Caret blinking
   * using the same blink-rate as the previous Caret. This works with any Caret
   *
   * @param component The text component to get the new Caret
   * @param caret     The new Caret to install
   */
  public static void replaceCaret(final JTextComponent component, final Caret caret) {
    final Caret priorCaret = component.getCaret();
    int blinkRate = priorCaret.getBlinkRate();
    if (priorCaret instanceof PropertyChangeListener) {
      // For example, com.apple.laf.AquaCaret, the troublemaker, installs this listener
      // which doesn't get removed when the Caret gets uninstalled.
      component.removePropertyChangeListener((PropertyChangeListener) priorCaret);
    }
    component.setCaret(caret);
    caret.setBlinkRate(blinkRate); // Starts the new caret blinking.
  }

  @Override
  public void mousePressed(final MouseEvent e) {
    // if user is doing a shift-click. Construct a new MouseEvent that happened at one
    // end of the word, and send that to super.mousePressed().
    boolean isExtended = isExtendSelection(e);
    if (selectingByWord && isExtended) {
      MouseEvent alternateEvent = getRevisedMouseEvent(e, Utilities::getWordStart, Utilities::getWordEnd);
      super.mousePressed(alternateEvent);
    } else if (selectingByRow && isExtended) {
      MouseEvent alternateEvent = getRevisedMouseEvent(e, Utilities::getRowStart, Utilities::getRowEnd);
      super.mousePressed(alternateEvent);
    } else  {
      if (!isExtended) {
        int clickCount = e.getClickCount();
        selectingByWord = clickCount == 2;
        selectingByRow = clickCount == 3;
      }
      super.mousePressed(e); // let the system select the clicked word
      // save the low end of the selected word.
      lowMark = getMark();
      if (selectingByWord || selectingByRow) {
        // User did a double- or triple-click...
        // They've selected the whole word. Record the high end.
        highMark = getDot();
      } else {
        // Not a double-click.
        highMark = lowMark;
      }
    }
  }

  @Override
  public void mouseClicked(final MouseEvent e) {
    super.mouseClicked(e);
    if (selectingByRow) {
      int mark = getMark();
      int dot = getDot();
      lowMark = Math.min(mark, dot);
      highMark = Math.max(mark, dot);
    }
  }

  private MouseEvent getRevisedMouseEvent(final MouseEvent e, final BiTextFunction getStart, final BiTextFunction getEnd) {
    int newPos;
    int pos = getPos(e);
    final JTextComponent textComponent = getComponent();
    try {
      if (pos > highMark) {
        newPos = getEnd.loc(textComponent, pos);
        setDot(lowMark);
      } else if (pos < lowMark) {
        newPos = getStart.loc(textComponent, pos);
        setDot(highMark);
      } else {
        if (getMark() == lowMark) {
          newPos = getEnd.loc(textComponent, pos);
        } else {
          newPos = getStart.loc(textComponent, pos);
        }
        pos = -1; // ensure we make a new event
      }
    } catch (BadLocationException ex) {
      throw new IllegalStateException(ex);
    }
    MouseEvent alternateEvent;
    if (newPos == pos) {
      alternateEvent = e;
    } else {
      alternateEvent = makeNewEvent(e, newPos);
    }
    return alternateEvent;
  }

  private boolean isExtendSelection(MouseEvent e) {
    // We extend the selection when the shift is down but control is not. Other modifiers don't matter.
    int modifiers = e.getModifiersEx();
    int shiftAndControlDownMask = MouseEvent.SHIFT_DOWN_MASK | MouseEvent.CTRL_DOWN_MASK;
    return (modifiers & shiftAndControlDownMask) == MouseEvent.SHIFT_DOWN_MASK;
  }

  @Override
  public void setDot(final int dot, final Position.Bias dotBias) {
    super.setDot(dot, dotBias);
  }

  @Override
  public void mouseDragged(final MouseEvent e) {
    if (!selectingByWord && !selectingByRow) {
      super.mouseDragged(e);
    } else {
      BiTextFunction getStart;
      BiTextFunction getEnd;
      if (selectingByWord) {
        getStart = Utilities::getWordStart;
        getEnd = Utilities::getWordEnd;
      } else {
        // selecting by paragraph
        getStart = Utilities::getRowStart;
        getEnd = Utilities::getRowEnd;
      }
      // super.mouseDragged just calls moveDot() after getting the position. We can do
      // the same thing...
      // There's no "setMark()" method. You can set the mark by calling setDot(). It sets
      // both the mark and the dot to the same place. Then you can call moveDot() to put
      // the dot somewhere else.
      if ((!e.isConsumed()) && SwingUtilities.isLeftMouseButton(e)) {
        int pos = getPos(e);
        JTextComponent component = getComponent();
        try {
          if (pos > highMark) {
            int wordEnd = getEnd.loc(component, pos);
            setDot(lowMark);
            moveDot(wordEnd);
          } else if (pos < lowMark) {
            int wordStart = getStart.loc(component, pos);
            setDot(wordStart); // Sets the mark, too
            moveDot(highMark);
          } else {
            setDot(lowMark);
            moveDot(highMark);
          }
        } catch (BadLocationException ex) {
          ex.printStackTrace();
        }
      }
    }
  }

  private int getPos(final MouseEvent e) {
    JTextComponent component = getComponent();
    Point pt = new Point(e.getX(), e.getY());
    Position.Bias[] biasRet = new Position.Bias[1];
    return component.getUI().viewToModel(component, pt, biasRet);
  }
  
  private MouseEvent makeNewEvent(MouseEvent e, int pos) {
    JTextComponent component = getComponent();
    try {
      Rectangle rect = component.getUI().modelToView(component, pos);
      return new MouseEvent(
          component,
          e.getID(),
          e.getWhen(),
          e.getModifiers(),
          rect.x,
          rect.y,
          e.getClickCount(),
          e.isPopupTrigger(),
          e.getButton()
      );
    } catch (BadLocationException ev) {
      ev.printStackTrace();
      throw new IllegalStateException(ev);
    }
  }

// For eventual use by a "select paragraph" feature:
//  private static final char NEW_LINE = '\n';
//  private static int getParagraphStart(JTextComponent component, int position) {
//    return component.getText().substring(0, position).lastIndexOf(NEW_LINE);
//  }
//  
//  private static int getParagraphEnd(JTextComponent component, int position) {
//    return component.getText().indexOf(NEW_LINE, position);
//  }

  /**
   * Don't use this. I should throw CloneNotSupportedException, but it won't compile if I
   * do. Changing the access to protected doesn't help. If you don't believe me, try it
   * yourself.
   * @return A bad clone of this.
   */      
  @SuppressWarnings({"CloneReturnsClassType", "UseOfClone"})
  @Override
  public Object clone() {
    return super.clone();
  }
  
  @FunctionalInterface
  private interface BiTextFunction {
    int loc(JTextComponent component, int position) throws BadLocationException;
  }
}

相关问题