Browse Source

Oh lawd he documenting

Sam Jaffe 6 years ago
parent
commit
b097b8037d

+ 55 - 0
README.md

@@ -0,0 +1,55 @@
+# Observable - Disjoint UI updates for complex objects
+This library exists to combine together two objectives in updating UI objects:
+1) Be able to 'remotely' notify UI objects about updates without having to iterate through the entire component tree.
+2) Allow editting of a shared resource from multiple potential call sites without needing a controller object that holds references to every single interested view.
+
+This is a useful construct in the situation where you have a large number of fixed GUI components in a program (for example, rendering the various statistics of a Dungeons and Dragons (c) character), because the connections are made anonymously.
+## Requirements
+- Java 7
+- org.leumasjaffe.event
+- Google Guava
+## Installation
+### Steps
+1) Download or clone this repository.
+2) mvn install
+3) Add the below block to your pom.xml
+### Maven Artifact
+``` xml
+<dependency>
+  <groupId>org.leumasjaffe</groupId>
+  <artifactId>observable</artifactId>
+</dependency>
+```
+## Example Code Snippets
+``` java
+class SpellBook extends Observable {
+  int spellSlotsRemaining, maxSpellSlots;
+  List<List<Spell>> spellsAtLevel;
+}
+
+class SpellBookUI extends JPanel {
+  ObservableListener<JTextField, SpellBook> listener;
+  SpellBookUI(SpellBook model) {
+    JTextField spellsRemaining = new JTextField();
+    add(spellsRemaining);
+    
+    // Set up a listener to anonymously update when model changes
+    listener = new ObservableListener<>(spellsRemaining, (text, book) -> {
+      text.setText(book.getSpellSlotsRemaining());
+    });
+    
+    // Automatically invokes callback, setting the text field
+    listener.setObserved(model);
+  }
+}
+
+class CastSpellCommand {
+  SpellBook book;
+  void cast(Spell spell) {
+    // ...
+    --book.spellSlotsRemaining;
+    // Will automatically invoke the code in the ObservableListener above
+    ObserverDispatch.notifySubscribers(book);
+  }
+}
+```

+ 22 - 2
src/main/lombok/org/leumasjaffe/observer/IndirectObservableListener.java

@@ -13,16 +13,32 @@ import lombok.RequiredArgsConstructor;
 @Getter(value=AccessLevel.PACKAGE)
 @RequiredArgsConstructor
 public class IndirectObservableListener<C, T> {
+	/** Some component, typically a GUI object like a JTextField or JComboBox */
 	C component;
+	/** A callback function to apply to component and model */
 	BiConsumer<? super C, ? super T> update;
-
+	
+	/** A model that our parent GUI is interested in, usually Observable, or an aggregation */
 	@NonFinal T model = null;
 	
+	/**
+	 * @param obs Sets the object that we are observing, this object specifically will then be
+	 * passed on to our callback function whenever {@see notifySubscribers} is invoked.
+	 * @param extra Any number of {@see org.leumasjaffe.observer.Observable} objects. If any
+	 * one of these objects is modified (and by convention, we call
+	 * {@see ObserverDispatch#notifySubscribers}), then we fire off
+	 * the update on the component and model.
+	 */
 	public void setObserved( T obs, Observable... extra ) {
 		Objects.requireNonNull( obs );
 		if ( obs == model ) return;
+		// Make sure that we aren't listening to any of the previous things.
+		// This means that we can re-use objects instead of destroying and re-making a
+		// bunch of high-level GUI objects any time the model changes.
 		ObserverDispatch.unsubscribeAll( this );
 		model = obs;
+		// Invoke the update callback - our parent does not need to update the
+		// {@see component} manually.
 		updateComponent( );
 		for ( int i = 0; i < extra.length; ++i ) {
 			ObserverDispatch.subscribe( extra[i], this, this::updateComponent );
@@ -33,7 +49,11 @@ public class IndirectObservableListener<C, T> {
 		update.accept(component, model);
 	}
 
+	/**
+	 * Notify everyone except me about a change in the model. Used by {@see ObservableController}
+	 * {@see org.leumasjaffe.observer.ObserverDispatch#notifySubscribers}
+	 */
 	public void notifySubscribers(Observable obs) {
 		ObserverDispatch.notifySubscribers(obs, this);
 	}
-}
+}

+ 24 - 0
src/main/lombok/org/leumasjaffe/observer/ObservableController.java

@@ -12,10 +12,25 @@ import org.leumasjaffe.event.AnyActionDocumentListener;
 import lombok.experimental.FieldDefaults;
 import lombok.AccessLevel;
 
+/**
+ * An ObservableListener object that also provides an update link in the other direction.
+ * The normal Listener updates some (UI) component when the model object updates. This
+ * object adds the feature to update the model from an update to UI component.
+ *
+ * This allows you to have a text input field attached to your model (like with a JavaBean),
+ * that also fires off notifications to other interested listeners when you edit the text.
+ */
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class ObservableController<S extends JTextComponent, T extends Observable> extends ObservableListener<S, T> {
 	BiFunction<String, T, Boolean> func;
 
+	/**
+	 * Construct an ObservableController that modifies its model whenever the linked
+	 * JTextComponent is updated. Ignores changes when the text document is empty.
+	 * @param comp A GUI object that is to be coupled with the model
+	 * @param func An update function to modify the model from text content
+	 * @param update The standard callback to set the component when the model changes elsewhere
+	 */
 	public ObservableController(final S comp, final BiFunction<String, T, Boolean> func,
 			final BiConsumer<? super S, ? super T> update) {
 		super(comp, update);
@@ -23,6 +38,15 @@ public class ObservableController<S extends JTextComponent, T extends Observable
 		AnyActionDocumentListener.skipEmpty(comp, evt -> accept( ) );
 	}
 	
+	/**
+	 * Construct an ObservableController that modifies its model whenever the linked
+	 * JTextComponent is updated, including deleting all of the text.
+	 * @param comp A GUI object that is to be coupled with the model
+	 * @param func An update function to modify the model from text content
+	 * @param update The standard callback to set the component when the model changes elsewhere
+	 * @param onEmpty A special handler to set model value when we delete the content of the
+	 * component.
+	 */
 	public ObservableController(final S comp, final BiFunction<String, T, Boolean> func,
 			final BiConsumer<? super S, ? super T> update, final Consumer<T> onEmpty) {
 		super(comp, update);

+ 18 - 1
src/main/lombok/org/leumasjaffe/observer/ObservableListener.java

@@ -5,6 +5,14 @@ import java.util.function.BiConsumer;
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
 
+/**
+ * A refinement of the {@see IndirectObservableListener} root object that carries the following
+ * conditions on the model object:
+ * 1) The model must implement {@see Observable}
+ * 2) We only subscribe to updates from the model
+ *
+ * Uses composition to prevent certain functions from being mode available at the compiler level
+ */
 @FieldDefaults(level=AccessLevel.PROTECTED, makeFinal=true)
 public class ObservableListener<C, T extends Observable> {
 	IndirectObservableListener<C, T> impl;
@@ -13,6 +21,10 @@ public class ObservableListener<C, T extends Observable> {
 		impl = new IndirectObservableListener<>(comp, func);
 	}
 	
+	/** 
+	 * @param obs The model of this object, as well as the item being observed for updates.
+	 * {@see IndirectObservableListener#setObserved}
+	 */
 	public void setObserved( T obs ) {
 		impl.setObserved(obs, obs);
 	}
@@ -21,6 +33,11 @@ public class ObservableListener<C, T extends Observable> {
 		this.impl.notifySubscribers(obs);
 	}
 	
+	/**
+	 * Create a Listener where the component being 'updated' is another Observable object.
+	 * The behavior of this listener is to cascade a notice between two objects that are
+	 * related in a context, but not in the datamodel.
+	 */
 	public static <C extends Observable, T extends Observable> ObservableListener<C, T> cascade(T from, C to) {
 		ObservableListener<C, T> lis = new ObservableListener<C, T>(to, ObservableListener::cascadeImpl);
 		lis.setObserved(from);
@@ -30,4 +47,4 @@ public class ObservableListener<C, T extends Observable> {
 	private static void cascadeImpl(Observable to, Observable from) {
 		ObserverDispatch.notifySubscribers(to);
 	}
-}
+}

+ 30 - 0
src/main/lombok/org/leumasjaffe/observer/ObserverDispatch.java

@@ -11,6 +11,12 @@ import lombok.AllArgsConstructor;
 import lombok.experimental.FieldDefaults;
 import lombok.experimental.UtilityClass;
 
+/**
+ * The master object that associates observale objects with listeners and invocations
+ * TODO: May crash if updating an object where a Listener was destroyed without unsub
+ * TODO: Potentially leaks memory if Listeners go out of scope without unsub
+ * TODO: No way to drop Observable objects as they go out of scope
+ */
 @UtilityClass
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class ObserverDispatch {
@@ -23,20 +29,44 @@ public class ObserverDispatch {
 	
 	Multimap<UUID, Pair> observers = LinkedListMultimap.create();
 	
+	/**
+	 * Register a listener callback
+	 * @param target The object we're observing. Make sure that it's an observable and not just
+	 * some random UUID someone came up with to harass me.
+	 * @param src The Listener object that we are using, used for invalidation in case we
+	 * forget to invoke {@see unsubscribeAll} when destroying an object.
+	 * @param sub A callback function with a signature of void() that will be triggered when
+	 * we request an update.
+	 */
 	public void subscribe(Observable target, Object src, Subscriber sub) {
 		observers.put(target.observableId(), new Pair(new WeakReference<>(src), sub));
 	}
 	
+	/**
+	 * Remove every callback that this listener is associated with
+	 * @param src A Listener object that was generally provided in a previous {@see subscribe}
+	 * call.
+	 */
 	public void unsubscribeAll(Object src) {
 		if (src == null) return;
 		observers.entries().removeIf( e -> e.getValue().obj.get() == src );
 	}
 	
+	/**
+	 * Dispatch notices of an update to an observable object that comes from an owning
+	 * {@see IndirectObservableListener}.
+	 * @param target The Observable that was updated by our caller
+	 * @param src An owning Listener that does not need to be updated.
+	 */
 	void notifySubscribers(Observable target, IndirectObservableListener<?, ?> src) {
 		observers.get(target.observableId()).stream().filter(
 				p -> p.obj.get() != src).forEach( p -> p.sub.notifyUpdate() );
 	}
 
+	/**
+	 * Dispatch notices of an update to an observable object.
+	 * @param target The Observable that was updated by our caller
+	 */
 	public void notifySubscribers(Observable target) {
 		observers.get(target.observableId()).stream().forEach( p -> p.sub.notifyUpdate() );
 	}