javajtextfielddocumentlistener

JTextFields and DocumentListeners


I want two textfields (from now A and B) sharing the same content as the user inputs on any of them. I can make one mirror the other (B mirrors A) or the opposite (A mirrors B). But when I keep both DocumentListeners the execution starts to throw Exceptions.

According to the Oracle's Docs I can't use a DocumentListener to mutate the content of a document from within the Listener itself. Which I find weird since I already did it in the first case (B mirrors A) or the opposite case. Anyway the code still "works" but with an Exception thrown every two events triggered.

KeyListeners are not reliable for this particular case and I refuse to use buttons because I like the real-time look DocumentListener gives.

Any suggestions?

Here's my code:

import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

public class Mirror {
    private JTextField oriText;
    private JTextField mirrorText;
    private static int debugCounter; //Counts the times an Event is Triggered.

    public static void main(String[] args) {
        Mirror gui = new Mirror();
        gui.build();
    }

    public void build(){

        JFrame frame = new JFrame();
        JPanel panel = new JPanel();
        panel.setLayout(new GridBagLayout());
        GridBagConstraints c = new GridBagConstraints();
        frame.getContentPane().add(panel);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        c.gridx = 0;
        c.gridy = 0;
        JLabel original = new JLabel("Original");
        panel.add(original, c);

        c.gridy = 1;
        oriText = new JTextField(10);
        panel.add(oriText,c);


        c.gridy = 2;
        JLabel mirror = new JLabel("Mirror");
        panel.add(mirror, c);

        c.gridy = 3;
        mirrorText = new JTextField(10);
        panel.add(mirrorText, c);           
        mirrorText.getDocument().addDocumentListener(new MyDocumentListenerII()); // Comment this line to see only the 1st Case (B mirrors A)
        oriText.getDocument().addDocumentListener(new MyDocumentListener()); // Comment this line to see only the 2nd Case (A mirrors B) 


        frame.pack();
        frame.setVisible(true);
    }

    class MyDocumentListener implements DocumentListener{

        @Override
        public void changedUpdate(DocumentEvent e) {
            //Does nothing.

        }

        @Override
        public void insertUpdate(DocumentEvent e) {
            mirror();
        }

        @Override
        public void removeUpdate(DocumentEvent e) {
            mirror();
        }


    }

    class MyDocumentListenerII implements DocumentListener{

        @Override
        public void changedUpdate(DocumentEvent e) {
            // Does nothing.
        }

        @Override
        public void insertUpdate(DocumentEvent e) {
            mirror1();
        }

        @Override
        public void removeUpdate(DocumentEvent e) {
            mirror1();
        }

    }

    public void mirror(){
        if (!oriText.getText().equals(mirrorText.getText())){ //Without this each Event trigger the other in some sort of Paradoxical cycle. 
            mirrorText.setText(oriText.getText());
            debugCounter++;
            System.out.println(debugCounter+" events triggered");
        }
    }

    public void mirror1(){
        if (!mirrorText.getText().equals(oriText.getText())){
            oriText.setText(mirrorText.getText());
            debugCounter++;
            System.out.println(debugCounter+" events triggered");
        }
    }


}

Solution

  • The problem you're having is that since both JTextFields need to be sync, each field's DocumentListener needs to update the other field. However, that update causes the other DocumentListener to attempt to update the first field, causing the thrown IllegalStateException, since something is attempting to modify the field while a DocumentListener for it is executing.

    The solution is to block calls to setText(String) when a DocumentListener is being executed for that field. This can be done with boolean variables. Below is the code for one of the DocumentListeners:

    textFieldA.getDocument().addDocumentListener(new DocumentListener() {
      @Override
      public void removeUpdate (DocumentEvent e) {
        blockA = true;
        if (!blockB) textFieldB.setText(textFieldA.getText());
        blockA = false;
      }
      @Override
      public void insertUpdate (DocumentEvent e) {
        blockA = true;
        if (!blockB) textFieldB.setText(textFieldA.getText());
        blockA = false;
      }
      @Override
      public void changedUpdate (DocumentEvent e) {
        blockA = true;
        if (!blockB) textFieldB.setText(textFieldA.getText());
        blockA = false;
      }
    });
    

    where blockA and blockB are boolean instance fields (or possibly final variables in the method). Now, when a method fires in textFieldA's DocumentListener, a flag is set to indicate not to use textFieldA.setText(String). We, also see how textFieldB.setText(String) is blocked when blockB is set. The DocumentListener for textFieldB looks about the same.

    Now, textFieldA won't be set during a call inside its DocumentListener, same for the other field. Such a call would be redundant anyway: the text of each field would be same at that point.