0

I have an external device that is sending me data 1 character at a time. I'm writing this to a StyledDocument on a JTextPane. This data is sent to me on a thread that is not the AWT thread so I need to create AWTEvents and push them to the EventQueue so AWT handles the writing so that I do not get an Exception.

I have a funny issue now...

My text is printing to the document backwards.

This is obviously because I am pushing the characters to the Event queue 1 at a time as i receive them. A queue is obviously last pushed is first popped. I'm trying to thing of a way that I can fire the event before I add a new one or something similar so that I can get the events to fire in order.

http://www.kauss.org/Stephan/swing/index.html is the example I used to create the events.

private class RUMAddTextEvent extends AWTEvent {

    public static final int EVENT_ID = AWTEvent.RESERVED_ID_MAX + 1;
    private int index;
    private String str;
    private AttributeSet as;

    public RUMAddTextEvent(Component target, int index, String str, AttributeSet as) {
        super(target, EVENT_ID);
        this.index = index;
        this.str = str;
        this.as = as;
    }

    public int getIndex() {
        return index;
    }

    public String getStr() {
        return str;
    }

    public AttributeSet getAs() {
        return as;
    }
}

private class RUMRemoveTextEvent extends AWTEvent {

    public static final int EVENT_ID = AWTEvent.RESERVED_ID_MAX + 1;
    int index;
    int size;

    RUMRemoveTextEvent(Component target, int index, int size) {
        super(target, EVENT_ID);
        this.index = index;
        this.size = size;
    }

    public int getIndex() {
        return index;
    }

    public int getSize() {
        return size;
    }
}

/**
 * Prints a character at a time to the RUMComm window.
 *
 * @param c
 */
public void simpleOut(Character c) {
    cursor.x++;
    if (lines.isEmpty()) {
        this.lines.add(c.toString());
    } else {
        this.lines.add(cursor.y, this.lines.get(cursor.y).concat(c.toString()));
        this.lines.remove(cursor.y + 1);

    }

    try {
        //doc.insertString(doc.getLength(), c.toString(), as);

        eventQueue = Toolkit.getDefaultToolkit().getSystemEventQueue();
        eventQueue.postEvent(new RUMAddTextEvent(this, doc.getLength(), c.toString(), as));
        getCaret().setDot(doc.getLength());
    } catch (Exception ex) {
        //Exceptions.printStackTrace(ex);
    }
}

/**
 * Creates a new line
 */
public void newLine() {
    cursor.y++;
    cursor.x = 0;
    this.lines.add("");

    //doc.insertString(doc.getLength(), "\n", null);
    eventQueue = Toolkit.getDefaultToolkit().getSystemEventQueue();
    eventQueue.postEvent(new RUMAddTextEvent(this, doc.getLength(), "\n", null));
    getCaret().setDot(doc.getLength());

}

/**
 * Backspace implementation.
 *
 */
public void deleteLast() {
    int endPos = doc.getLength();
    //doc.remove(endPos - 1, 1);
    eventQueue = Toolkit.getDefaultToolkit().getSystemEventQueue();
    eventQueue.postEvent(new RUMRemoveTextEvent(this, endPos - 1, 1));
    cursor.x--;

}

 @Override
protected void processEvent(AWTEvent awte) {
    //super.processEvent(awte);
    if (awte instanceof RUMAddTextEvent) {
        RUMAddTextEvent ev = (RUMAddTextEvent) awte;
        try {
            doc.insertString(ev.getIndex(), ev.getStr(), ev.getAs());
        } catch (BadLocationException ex) {
            Exceptions.printStackTrace(ex);
        }
    } else if (awte instanceof RUMRemoveTextEvent) {
        RUMRemoveTextEvent ev = (RUMRemoveTextEvent) awte;
        try {
            doc.remove(ev.getIndex(), ev.getSize());
        } catch (BadLocationException ex) {
            Exceptions.printStackTrace(ex);
        }

    } else {
        super.processEvent(awte);
    }
}
merjr
  • 149
  • 1
  • 13
  • 1
    Do you have to push each character to the event queue? Couldn't you simply update the document/field directly (via something like `SwingUtilities.invokeLater/invokeAndWait`)? Also, how are you pushing the events? – MadProgrammer Oct 01 '12 at 20:21
  • The event that writes the characters is not coming from an AWT thread so I cannot write to the document directly with out an exception being thrown. I need to push the events to the AWT thread to execute and catch them in my components processEvent() method. Ill post some code. I attempted SwingUtility invoke later but it was a nightmare and the output was not readable. – merjr Oct 01 '12 at 20:25
  • I'm a little concerned with some of your code. You appear to make changes to the UI (`getCaret().setDot(doc.getLength())` for example) out side the EDT anyway (it's difficult to be sure as I don't have the whole code) – MadProgrammer Oct 01 '12 at 20:45
  • FYI _A queue is obviously last pushed is first popped_ Hum, a queue is precisely the opposite: first in, first out. Last in, last out, is a Stack. – Guillaume Polet Oct 01 '12 at 22:13
  • That is correct :) Brain fart – merjr Oct 01 '12 at 22:46

4 Answers4

2

I can only encourage people who try to take care of the Swing threading rules. However, despite your efforts to create events and push them onto the EventQueue you still access the Swing component on the background Thread with the calls for the length of the document and the altering of the caret position.

Extending a text component in order to set text on it looks like overkill to me, and certainly not the best way to approach this problem. Personally I would let the background Thread fill a buffer and flush that buffer to the text document once in a while (e.g. at each new line, twice a second using a Timer, each time I reach 1000 characters, ... ). For the update, I would simply use SwingUtilities.invokeLater.

Some example code to illustrate this, retrieved from an old project of mine. It won't compile but it illustrates my point. The class appends Strings to an ISwingLogger which should be accessed on the EDT. Note the usage of the javax.swing.Timer to have periodical updates on the EDT.

import javax.swing.Timer;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class SwingOutputStream extends ByteArrayOutputStream{
  private final ISwingLogger fSwingLogger;

  private Timer fTimer;
  private final StringBuilder fBuffer = new StringBuilder( 1000 );

  public SwingOutputStream( ISwingLogger aSwingLogger ) {
    fSwingLogger = aSwingLogger;

    fTimer = new Timer( 200, new ActionListener() {
      public void actionPerformed( ActionEvent aActionEvent ) {
        flushBuffer();
      }
    } );
    fTimer.setRepeats( false );
  }

  @Override
  public void flush() throws IOException {
    synchronized( fBuffer ){
      fBuffer.append( toString( "UTF-8") );
    }
    if ( fTimer.isRunning() ){
      fTimer.restart();
    } else {
      fTimer.start();
    }

    super.flush();
    reset();
  }

  private void flushBuffer(){
    synchronized ( fBuffer ){
      final String output = fBuffer.toString();
      fSwingLogger.appendString( output );
      fBuffer.setLength( 0 );
    }
  }
}
Robin
  • 36,233
  • 5
  • 47
  • 99
  • Swing Timer isnot proper, see my comment to Mad – mKorbel Oct 01 '12 at 21:39
  • This is the best answer for the information i provided. I did not solve my issue this way. Instead I made an event much further up in the code. When the Byte codes are being parsed it created an event to be executed on the AWT thread if it an AWT event. – merjr Oct 01 '12 at 21:50
1

Your actual issue isn't that the events are processed out of order, it's that you're creating the events with soon-to-be-out-of-date information:

new RUMAddTextEvent(this, doc.getLength(), ...

When the event is submitted, the doc may have length 0, but that might not be true by the time the event is processed. You could resolve that by having an AppendTextEvent instead of specifying the index, though if you have position-based insertions, too, you'll have to account for those.

An alternative option for the whole lot is to use an invalidate model:

// in data thread
void receiveData(String newData) {
    updateModelText(newData);
    invalidateUI();
    invokeLater(validateUI);
}

// called in UI thread
void validateUI() {
    if (!valid) {
        ui.text = model.text;
    }
    valid = true;
}
Michael Brewer-Davis
  • 14,018
  • 5
  • 37
  • 49
1

IMHO, you've taken something which should have been simple and made it more complicated then it needs to be (I'm a real master of this by the way ;))

Without knowing you exact problem, it's difficult to be 100%, but this small example demonstrates how I might approach the same problem using SwingUtilities.invokeLater/invokeAndWait

public class EventTest {

    public static void main(String[] args) {

        JFrame frame = new JFrame();
        frame.setLayout(new BorderLayout());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(400, 400);
        JTextArea area = new JTextArea();
        frame.add(new JScrollPane(area));
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);

        Thread thread = new Thread(new Dispatcher(area));
        thread.setDaemon(true);
        thread.start();

    }

    public static class Dispatcher implements Runnable {

        private String message = "Hello from the other side\n\nCan you read this\n\nAll done now";
        private JTextArea area;

        public Dispatcher(JTextArea area) {
            this.area = area;
        }

        @Override
        public void run() {

            int index = 0;
            while (index < message.length()) {

                try {
                    Thread.sleep(250);
                } catch (InterruptedException ex) {
                }

                send(message.charAt(index));
                index++;

            }

        }

        public void send(final char text) {

            System.out.println("Hello from out side the EDT - " + EventQueue.isDispatchThread());

            try {
                SwingUtilities.invokeAndWait(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("Hello from the EDT - " + EventQueue.isDispatchThread());
                        area.append(new String(new char[]{text}));
                    }
                });
            } catch (Exception exp) {
                exp.printStackTrace();
            }

        }

    }

}

In you case, I would probably make a series of runnables that were capable of handling each update you want to make.

For example, I might create a InsertRunnable and DeleteRunnable (from you sample code) that could, insert a string at the current location or remove a string/characters from the current caret location.

To these I would pass the required information and let them take care of the rest.

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • INvokeLater seems to work okay but it takes time to print all the characters it receives from the device. A buffer may work in combination with this, though. – merjr Oct 01 '12 at 21:10
  • Your right, you don't want to swamp the EDT if you can help it – MadProgrammer Oct 01 '12 at 21:13
  • sorry util.Timer with output to the Document, wrapped in invokeLater, everything else or different complicating..., accurate Timer and EDT notifier, no reason for invokeAndWait – mKorbel Oct 01 '12 at 21:38
  • If you use a `javax.swing.Timer` the event should be fired in the EDT anyway – MadProgrammer Oct 01 '12 at 22:46
0

...as i receive them. A queue is obviously last pushed is first popped. I'm trying to...

You have some conceptual mistake here. A queue is FIFO, means first pushed in is first popped out. A stack, will be LIFO, means last pushed in is first popped out. I believe this is where your problem truly was.

Z_K
  • 151
  • 1
  • 5