1. Core classes

Three common classes used throughout the framework are Event, State and Value and their respective observers Observer and ObservableState.

1.1. Event

event diagram

The Event class is a synchronous event implementation used throughout the framework. Classes typically expose observers for their events via public accessors. Events are triggered by calling the run method in case no data is associated with the event or accept in case data should be propogated to consumers.

The associated Observer instance can not trigger the event and can be safely passed around.

Event listeners must implement either Runnable or Consumer, depending on whether they are interested in the data associated with the event.

Note
Both listeners and consumer are notified each time the event is triggered, regardless of whether run or accept is used, listeners and consumers are notified in the order they were added.

Events are instantiated via factory methods in the Event class.

// specify an event propagating
// a String as the event data
Event<String> event = Event.event();

// an observer manages the listeners
// for an Event but can not trigger it
Observer<String> observer = event.observer();

// add a listener if you're not
// interested in the event data
observer.addListener(() -> System.out.println("Event occurred"));

event.run();//output: 'Event occurred'

// or a consumer if you're
// interested in the event data
observer.addConsumer(data -> System.out.println("Event: " + data));

event.accept("info");//output: 'Event: info'

// Event implements Observer so
// listeneres can be added directly without
// referring to the Observer
event.addConsumer(System.out::println);

1.2. Value

value diagram

A Value wraps a value and provides a change observer.

Values are instantiated via factory methods in the Value class.

Values can be linked so that changes in one are reflected in the other.

// a nullable value with 2 as the initial value
Value<Integer> value =
        Value.nullable(2);

value.set(4);

// a non-null value using 0 as null substitute
Value<Integer> otherValue =
        Value.nonNull(0);

// linked to the value above
value.link(otherValue);

System.out.println(otherValue.get());// output: 4

otherValue.set(3);

System.out.println(value.get());// output: 3

value.addConsumer(System.out::println);

otherValue.addListener(() ->
        System.out.println("Value changed: " + otherValue.get()));

Values can be non-nullable if a nullValue is specified when the value is initialized. Null is then translated to the nullValue when set.

Integer initialValue = 42;
Integer nullValue = 0;

Value<Integer> value =
        Value.builder()
                .nonNull(nullValue)
                .value(initialValue)
                .build();

System.out.println(value.isNullable());//output: false

System.out.println(value.get());// output: 42

value.set(null); //or value.clear();

System.out.println(value.get());//output: 0

1.3. State

state diagram

The State class encapsulates a boolean state and provides read only access and a change observer via ObservableState. A State implements Value<Boolean> and is non-nullable with null translating to false.

States are instantiated via factory methods in the State class.

// a boolean state, false by default
State state = State.state();

// an observable manages the listeners for a State but can not modify it
ObservableState observable = state.observable();
// a not observable is always available, which is
// always the reverse of the original state
ObservableState not = state.not();

// add a listener notified each time the state changes
observable.addListener(() -> System.out.println("State changed"));

state.set(true);//output: 'State changed'

observable.addConsumer(value -> System.out.println("State: " + value));

state.set(null);//output: 'State: false'

// State extends ObservableState so listeners can be added
// directly without referring to the ObservableState
state.addListener(() -> System.out.println("State changed"));
private static final class IntegerValue {

  private final State negative = State.state(false);
  private final Value<Integer> integer = Value.builder()
          .nonNull(0)
          .consumer(value -> negative.set(value < 0))
          .build();

  /**
   * Increment the value by one
   */
  public void increment() {
    integer.map(value -> value + 1);
  }

  /**
   * Decrement the value by one
   */
  public void decrement() {
    integer.map(value -> value - 1);
  }

  /**
   * @return an observer notified each time the value changes
   */
  public Observer<Integer> changed() {
    return integer.observable();
  }

  /**
   * @return a state observer indicating whether the value is negative
   */
  public ObservableState negative() {
    return negative.observable();
  }
}

Any Action object can be linked to a State instance via the Utilities.linkToEnabledState method, where the action’s enabled status is updated according to the state.

State state = State.state();

Action action = new AbstractAction("action") {
  @Override
  public void actionPerformed(ActionEvent e) {
    System.out.println("Hello Action");
  }
};

Utilities.enableActions(state, action);

System.out.println(action.isEnabled());// output: false

state.set(true);

System.out.println(action.isEnabled());// output: true

Controls can also be linked to a State instance.

State state = State.state();

CommandControl control = Control.builder()
        .command(() -> System.out.println("Hello Control"))
        .enabled(state)
        .build();

System.out.println(control.isEnabled());// output: false

state.set(true);

System.out.println(control.isEnabled());// output: true
Note
When a State or Event is linked to a Swing component, for example its enabled state, all state changes must happen on the Event Dispatch Thread.