Java Change the Document in Documentlistener

java change the document in DocumentListener

DocumentListener is really only good for notification of changes and should never be used to modify a text field/document.

Instead, use a DocumentFilter

Check here for examples

FYI

The root course of your problem is that the DocumentListener is notified WHILE the document is been updated. Attempts to modify the document (apart from risking a infinite loop) put the document into a invalid state, hence the exception

Updated with an example

This is VERY basic example...It doesn't handle insert or remove, but my testing had remove working without doing anything anyway...

Sample Image

public class TestHighlight {

public static void main(String[] args) {
new TestHighlight();
}

public TestHighlight() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}

JTextPane textPane = new JTextPane(new DefaultStyledDocument());
((AbstractDocument) textPane.getDocument()).setDocumentFilter(new HighlightDocumentFilter(textPane));
JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new JScrollPane(textPane));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}

});
}

public class HighlightDocumentFilter extends DocumentFilter {

private DefaultHighlightPainter highlightPainter = new DefaultHighlightPainter(Color.YELLOW);
private JTextPane textPane;
private SimpleAttributeSet background;

public HighlightDocumentFilter(JTextPane textPane) {
this.textPane = textPane;
background = new SimpleAttributeSet();
StyleConstants.setBackground(background, Color.RED);
}

@Override
public void insertString(FilterBypass fb, int offset, String text, AttributeSet attr) throws BadLocationException {
System.out.println("insert");
super.insertString(fb, offset, text, attr);
}

@Override
public void remove(FilterBypass fb, int offset, int length) throws BadLocationException {
System.out.println("remove");
super.remove(fb, offset, length);
}

@Override
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {

String match = "test";

super.replace(fb, offset, length, text, attrs);

int startIndex = offset - match.length();
if (startIndex >= 0) {

String last = fb.getDocument().getText(startIndex, match.length()).trim();
System.out.println(last);
if (last.equalsIgnoreCase(match)) {

textPane.getHighlighter().addHighlight(startIndex, startIndex + match.length(), highlightPainter);

}

}
}

}

}

Getting Changed Text from DocumentListener

As TimeLineTextClass extends JFormattedField, you'll find there actually is a getText() method.

    @Override
public void insertUpdate(DocumentEvent e) {
fieldList.put(field,getText());
}

@Override
public void removeUpdate(DocumentEvent e) {
fieldList.put(field,getText());
}

The above code should work fine.

Java Swing - Detecting a change in the document

How does your this even compile

notepad = new JTextArea();
notepad.addTextListener(new TextListener() {
// ....
}

since TextListeners are not defined to work with JTextAreas but rather with TextAreas, a completely different beast.

You should add a DocumentListener to your JTextArea's Document.

notepad.getDocument().addDocumentListener(new DocumentListener() {
void insertUpdate(DocumentEvent e) {
documentChanged = true;
}

void removeUpdate(DocumentEvent e) {
documentChanged = true;
}

void changedUpdate(DocumentEvent e) {
documentChanged = true;
}

});

Regarding

My question is, is there any way to use both TextListeners and ActionListeners (or any other listener) simultaneously in the same class?

Use of a DocumentListener has nothing to do with ActionListeners used elsewhere in your program since their domains are orthogonal to each other, i.e., the one has absolutely nothing to do with the other.

Swing JTextField text changed listener DocumentListener infinity loop

There are two approaches to properly solve this:

Fire a property change event only if actually something changed

There's no point in notifying if the property value is the same as before (not changed at all). Avoiding the unnecessary event will effectively break your loop:

public void setHost(String host) {
// check if property actually changed
if (Objects.equals(this.host, host) return;
String oldHost = this.host;
this.host = host;
this.firePropertyChange("Host", oldHost, this.host);
}

or (fancy compact form):

public void setHost(String host) {
if (!Objects.equals(this.host, host)) {
firePropertyChange("Host", this.host, this.host=host);
}
}

Do one-way synchronization to avoid cascades

Changing a propery in the model can change a property in the view can change a property in the model can change... - this can quickly run in circles.

To break these cascades, do a one-way synchronization:

while the model notifies the view about changes, ignore any cascading updates.

To do so, you need a flag on the controller (in your case, the view holding the microcontrollers a.k.a. Swing listeners):

MyView.java:

boolean updating;

@Override public void modelPropertyChange(final PropertyChangeEvent event)
{
if (updating) {
// cascading update, ignore
return;
}
updating=true;
try {
if(event.getPropertyName().equals("Username")) {
{
String username = (String) event.getNewValue();
this.nameField.setText(username);
}
...
}
finally {
updating=false;
}
}

The first approach is pretty straightforward (but can get complicated when dealing with complex objects and collections).
The second approach is easy and more forgiving by design - view always represents model (no changes missed), and cascading updates are blocked.

How to remove content of JTextField while DocumentListener is running?

In general, you don't -- you don't change the state of the Document while listening to it when using a DocumentListener. The two possible solutions that I know of:

  • From within your Listener, put the code that makes the changes that you wish to make within a Runnable and queue the Runnable onto the Swing event thread by calling SwingUtilities.invokeLater(yourRunnable). This is a shameless kludge
  • Much better: Don't use a DocumentListener but rather a DocumentFilter since this type of listener was geared towards making changes to the Document before the text is visualized within the component.

Unrelated side issue: your code shows a worrisome degree of coupling in that you try to change the text in a specific text component from within your listener. DocumentListeners should be fully agnostic of the text component whose document that they listen to, and in fact may be added to more than one Document.


A DocumentFilter has 3 methods that need to be overridden and do what you expect them to do: what you would expect them to do:

  • insertString: insert a String into the document
  • remove: removes text from the document
  • replace: replaces text in the document

What is more, these methods do their actions before the text component renders the changes to the document.

So within my method overrides, I extract the current document's text, and use the parameters to create what the new text will look like, for example for the replace method I did:

@Override
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs)
throws BadLocationException {
String currentText = fb.getDocument().getText(0, fb.getDocument().getLength());
StringBuilder sb = new StringBuilder(currentText);

String newText = sb.replace(offset, offset + length, text).toString();

I then do a boolean test on the newText to see if it is "good", and if so, call the super's method, here replace(...), passing in all the parameters. If not, if the newText fails the test, then I remove all the text from the document and show a JOptionPane.

So in this example, I use this as my test method:

private boolean isTextOk(String text) {
return !BAD_TEXTS.contains(text);
}

which tests to see if the text is any of the disallowed Strings, here "...", " ", "oops", "OOPS", but it could be any Strings that you desire. Again, if the text passes the text, call the super's method, else remove the text:

if (isTextOk(newText)) {
super.replace(fb, offset, length, text, attrs);
} else {
badText(fb);
}

Where badText(fb) does:

private void badText(FilterBypass fb) throws BadLocationException {
remove(fb, 0, fb.getDocument().getLength());
JOptionPane.showMessageDialog(null, "Don't do this!", "Bad Text Entered",
JOptionPane.WARNING_MESSAGE);
}

The whole example is:

import java.util.Arrays;
import java.util.List;
import javax.swing.*;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DocumentFilter;
import javax.swing.text.PlainDocument;

@SuppressWarnings("serial")
public class ClearThreeDots extends JPanel {
private JTextField textField = new JTextField(40);

public ClearThreeDots() {
((PlainDocument) textField.getDocument()).setDocumentFilter(new MyDocFilter());
add(textField);
}

private static void createAndShowGui() {
ClearThreeDots mainPanel = new ClearThreeDots();

JFrame frame = new JFrame("Clear Three Dots");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(mainPanel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}

public static void main(String[] args) {
SwingUtilities.invokeLater(() -> createAndShowGui());
}
}


class MyDocFilter extends DocumentFilter {
private static final List<String> BAD_TEXTS = Arrays.asList("...", " ", "oops", "OOPS");

@Override
public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr)
throws BadLocationException {
String currentText = fb.getDocument().getText(0, fb.getDocument().getLength());
StringBuilder sb = new StringBuilder(currentText);

String newText = sb.insert(offset, string).toString();

if (isTextOk(newText)) {
super.insertString(fb, offset, string, attr);
} else {
badText(fb);
}
}

@Override
public void remove(FilterBypass fb, int offset, int length) throws BadLocationException {
String currentText = fb.getDocument().getText(0, fb.getDocument().getLength());
StringBuilder sb = new StringBuilder(currentText);

String newText = sb.replace(offset, offset + length, "").toString();

if (isTextOk(newText)) {
super.remove(fb, offset, length);
} else {
badText(fb);
}

}

@Override
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs)
throws BadLocationException {
String currentText = fb.getDocument().getText(0, fb.getDocument().getLength());
StringBuilder sb = new StringBuilder(currentText);

String newText = sb.replace(offset, offset + length, text).toString();

if (isTextOk(newText)) {
super.replace(fb, offset, length, text, attrs);
} else {
badText(fb);
}

}

private boolean isTextOk(String text) {
return !BAD_TEXTS.contains(text);
}

private void badText(FilterBypass fb) throws BadLocationException {
remove(fb, 0, fb.getDocument().getLength());
JOptionPane.showMessageDialog(null, "Don't do this!", "Bad Text Entered",
JOptionPane.WARNING_MESSAGE);
}

}

Get changed content in DocumentEvent

DocumentEvent has getOffset() and getLength() methods. Normally images are leaves so you can use htmlDocument.getCharacterElement() from the range of change and check whether they are images.

UPDATE: as result of discusing in commetns

You can add a DocumentFilter overriding remove() and use it as listener to get deleted fragment (in fact fragment to-be-deleted).

Document listener for multiple text fields

Well, you could assign the DocumentListener to a variable

DocumentListener docListener = new DocumentListener() {
...
};

And the use it for all your textfields

filterR.getDocument().addDocumentListener(docListener);
filterR1.getDocument().addDocumentListener(docListener);
filterR2.getDocument().addDocumentListener(docListener);

...



Related Topics



Leave a reply



Submit