代码之家  ›  专栏  ›  技术社区  ›  MiguelMunoz

如何在Java Swing中获得标准的Caret行为?

  •  0
  • MiguelMunoz  · 技术社区  · 1 年前

    Swing Text组件,所有这些组件都扩展 JTextComponent ,当用户选择某些文本时 JTextComponent 类将处理所选文本的工作委派给的实例 Caret 接口,调用 DefaultCaret 。此界面不仅显示闪烁的插入符号,还跟踪用户选择的任何文本,以及对更改选择范围的鼠标和键盘事件的响应。

    The Swing 默认Caret 具有标准插入符号的大部分行为,但我的一些高端用户指出了它的作用。以下是他们的问题:

    (注意:这些例子在Microsoft Edge中有问题,因为当你选择文本时,它会显示一个“…”菜单。在这些例子中,如果你使用Edge,你需要键入转义键来去掉该菜单,然后再进入下一步。)

    1. 如果我双击一个单词,它应该会选择整个单词。Java Swing的Caret做到了这一点。但是,在双击一个单词后,如果我试图通过移位单击第二个单词来扩展选择,则标准插入符号会扩展选择以包括整个第二个词。为了说明这一点,在下面的示例文本中,如果我在时钟中的o之后双击,它会选择单词clock,这是应该的。但是,如果我按住shift键并在伤口中的o之后单击,它应该会将选择一直扩展到d。它在这个页面上这样做,但在Java Swing中不是这样。在Swing中,它仍然扩展选择,但仅扩展到鼠标单击的位置。

      钟绕得太紧了。

    2. 如果我试图通过完全单击然后拖动来选择一个文本块,当我拖动文本时,它应该会一次扩展整个单词的选择范围。(我所说的“完全点击,然后拖动”,是指在同一地点快速完成以下事件:mouseDown、mouseUp、mouseDown和mouseMove。这就像双击时没有最后一个鼠标向上事件。)你可以在这个页面上尝试它,它会起作用,但在Java Swing中不起作用。在Swing中,它仍然会扩展选择,但只扩展到鼠标的位置。

    3. 如果我三次点击某些文本,它会选择整个段落。(这在Microsoft Edge中不起作用,但在大多数浏览器和编辑器中都起作用)这在Swing中不起。

    4. 如果在三次点击选择一个段落后,我在另一个段落上进行移位点击,它应该扩展选择范围,以包括整个新段落。

    5. 与第2项中的完全点击和拖动示例一样,如果进行完全双击和拖动,则应首先选择整个段落,然后一次将所选内容扩展到一个段落。(同样,这在Edge中不起作用。)这种行为不如其他行为标准,但仍然很常见。

    我的一些高级用户希望能够在我维护的Java Swing应用程序中使用这些功能,我想将其提供给他们。我的应用程序的全部目的是加快对其数据的处理,这些更改将对此有所帮助。我该怎么做?

    0 回复  |  直到 1 年前
        1
  •  1
  •   MiguelMunoz    1 年前

    这是我为解决这些问题而写的一堂课。它解决了问题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;
      }
    }