Friday, February 13, 2015

Bringing XMPP Chat States to JavaFX

Chat States. Everybody knows them from Instant Messengers or Facebook Chat. Those tiny notifications, which tell you, if your chat partner is currently composing a message, is (in)active or has paused typing. E.g. "XY is typing..."

XMPP defines these states in XEP-0085: Chat State Notifications as:
  • active
  • inactive
  • gone
  • composing
  • paused

Each of them - except "gone" - are applicable to a "message input interface". So let's translate them to a JavaFX TextArea!

First we define, that whenever the TextArea receives focus, we want to change the state from 'inactive' (which is the initial default state) to 'active' (if there's no text) or to 'paused' (if there's already text):

Secondly we have to change to 'composing', whenever the text changes. Easy.

The slightly tricky part is to change to the 'paused' state. To achieve this, we can set up a javafx.animation.PauseTransition and restart it everytime the text or focus has changed. Eventually, when the transition has finished (e.g. after 3 seconds), it will automatically change the state to 'paused':

Lastly, we change to 'inactive' when focus is lost:

And here's my take on a simple implementation. Enjoy!

import javafx.animation.PauseTransition;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.TextArea;
import javafx.util.Duration;
import rocks.xmpp.extensions.chatstates.model.ChatState;

public class ChatStateTextArea extends TextArea {

    private final PauseTransition pauseTransition = new PauseTransition(Duration.seconds(3));

    private final ReadOnlyObjectWrapper<ChatState> chatState = new ReadOnlyObjectWrapper<>();

    public ChatStateTextArea() {
        // This is the initial state.
        chatState.set(ChatState.INACTIVE);

        focusedProperty().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observableValue, Boolean aBoolean, Boolean
                    aBoolean2) {
                if (aBoolean2) {
                    if (getText().isEmpty()) {
                        // If we have received focus in an empty text field, immediately transition to "active".
                        chatState.set(ChatState.ACTIVE);
                        pauseTransition.stop();
                    } else {
                        // If we have received focus in an non-empty text field, transition to "paused".
                        chatState.set(ChatState.PAUSED);
                        // Start the timer, which will automatically transition to the next state.
                        pauseTransition.playFromStart();
                    }
                } else {
                    pauseTransition.playFromStart();
                }
            }
        });

        textProperty().addListener(new ChangeListener<String>() {
            @Override
            public void changed(ObservableValue<? extends String> observableValue, String s, String s2) {
                // We are in "composing" state.
                chatState.set(ChatState.COMPOSING);
                // Restart the timer.
                pauseTransition.playFromStart();
            }
        });

        pauseTransition.setOnFinished(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent actionEvent) {
                // When the time is up, switch to "paused", if there's any text, otherwise to active.
                if (isFocused()) {
                    if (getText() != null && !getText().isEmpty()) {
                        chatState.set(ChatState.PAUSED);
                    } else {
                        chatState.set(ChatState.ACTIVE);
                    }
                } else {
                    chatState.set(ChatState.INACTIVE);
                }
            }
        });
    }

    // Ommitted getters and setters for clearness.
}

4 comments:

  1. A great example how animation can be used beyond visual animation. If you replace Transition by Timeline, you can get rid of the event handler and the "if": The key frame can directly modify a property, and the "When" expression does the rest. :-)

    ReplyDelete
    Replies
    1. Thanks for your proposal, but what is the "When" expression? I can use a Timeline/Keyframe, but the onFinished EventHandler would still be there.

      Delete
    2. Ah I now know what you mean. I've ended up with something like the following, but it doesn't even work as expected and imo is hard to maintain/debug/read. The rest of the code isn't much cleaner either. Do you meant it to be like that?

      chatState.bind(new When(timeIsUp)
      .then(new When(focusedProperty())
      .then(new When(textProperty().isEmpty())
      .then(ChatState.ACTIVE)
      .otherwise(ChatState.PAUSED))
      .otherwise(ChatState.INACTIVE))
      .otherwise(new When(focusedProperty())
      .then(new When(textProperty().isEmpty())
      .then(ChatState.ACTIVE)
      .otherwise(ChatState.COMPOSING))
      .otherwise(ChatState.INACTIVE)));

      Delete
    3. Christian,

      no need for the on finished handler with key frame: The key frame can set a boolean property to which you can bind a "When". Also, possibly it makes sense to bind more properties to one KeyValue instead of simply "timeUp" (have not deeper checked that).

      If it doesn't work as you expected it, either your expectation or your code was simply wrong. It actually works like a charm in my apps. ;-)

      Actually I would use fluent API (static Bindings.when) and static imports, as it is simple to read (like English):

      chatState.bind(when(focused).then(when(isEmpty()).then(ACTIVE).otherwise.(when(timeUp).then(PAUSED).otherwise(COMPOSING)).otherwise(INACTIVE)));

      (BTW, the fact that this is hard to read in Java is why I asked Oracle to finish FXML expression. There it would read something like ...text="${focused ? isEmpty ? ACTIVE : timeUp ? PAUSED : COMPOSITING : INACTIVE}")

      The trick with the binding (instead of listener) is (a) it is reactive, so the invariant is guaranteed to be true always without to make up a mind about the when-then implied by listeners, and the invariant keeps the complete definition of the state engine in one single place (even in one single Java statement) and does not scatter it all over the code, (b) it's shorter than old-school Java, (c) once Oracle fixes FXML expressions, you can omit Java at all and let the designer do the bindings. :-)

      Regards
      -Markus

      Delete