gabrielgiussi
gabrielgiussi

Reputation: 9585

How to get selected items from a SWT Table using RxJava?

I have a table and a button and I want to emit an event ItemsSelected with the selected items of the table when the button is clicked.

The button should not know the table and it should remain only as a stream of clicks. So this solution is discarded:

 final ETable table = ...
 PublishSubject<ItemSelected> selected = PublishSubject.create();
 button.addSelectionListener(new SelectionListener(){

      @Override
      public void widgetSelected(SelectionEvent e) {
        for (TableItem item : table.getSelection()) {
         selected.onNext(new ItemSelected(item)); 
        }
        
      }
      
    });

I would prefer a way to compose the click stream of the button with the item selection stream of the table in order to keep loose coupling between this two elements.

Because the table allows multiple selection I must first scan the items selected in order to emit an event with all the items. Something like:

public static class ItemsSelected<T> {

    final List<T> items = new ArrayList<T>();

  }

  public abstract static class ItemSelection<T> {

    public abstract void apply(ItemsSelected<T> selection);
  }

  public static class ItemUnselected<T> extends ItemSelection<T> {

    final T item;

    public ItemUnselected(T item) {
      this.item = item;
    }

    public void apply(ItemsSelected<T> selection) {
      selection.items.remove(item);
    }

  }

  public static class ItemSelected<T> extends ItemSelection<T> {

    final T item;

    public ItemSelected(T item) {
      this.item = item;
    }

    public void apply(ItemsSelected<T> selection) {
      selection.items.add(item);
    }

  }

  public static class ObservableTable<T> extends Table {

    private PublishSubject<ItemSelection<T>> clicks = PublishSubject.create();

    public Observable<ItemsSelected<T>> selection = clicks.scan(new ItemsSelected<T>(),
        new Func2<ItemsSelected<T>, ItemSelection<T>, ItemsSelected<T>>() {

          @Override
          public ItemsSelected<T> call(ItemsSelected<T> t1, ItemSelection<T> t2) {
            // breaking events immutability
            t2.apply(t1);
            return t1;
          }

        });

    public ObservableTable(Composite parent, int style) {
      super(parent, style);
      this.addSelectionListener(new SelectionListener() {

        @SuppressWarnings("unchecked")
        @Override
        public void widgetSelected(SelectionEvent e) {
          if (((TableItem) e.item).getChecked())
            clicks.onNext(new ItemSelected<T>((T) e.item.getData()));
          else
            clicks.onNext(new ItemUnselected<T>((T) e.item.getData()));
        }

        @Override
        public void widgetDefaultSelected(SelectionEvent e) {

        }
      });
    }

  }

Then, I must combine the table.selection stream with the button.clicks stream in a selectionForAction stream. The idea is that when a ButtonClick is emitted, an SelectionForAction will be emitted if and only if an ItemSelected was previously emitted.

-------S1--U1-----S2---S3--------- table.clicks
           (scan)
-------(1)--()---(2)---(2,3)------ table.selection
                           
----O----------O-------------O---- button.clicks
              (?)
-----------------------------(2,3) selectionForAction

So, which operation should I use?


table.selection.join(button.clicks, new Func1<ItemsSelected,Observable<Long>>() {

  @Override
  public Observable<Long> call(ItemsSelected t) {
    // it doesn't seem a good idea
    return Observable.timer(1, TimeUnit.DAYS);
  }

}, new Func1<ClickEvent, Observable<Long>>() {

  @Override
  public Observable<Long> call(ClickEvent t) {
    // this makes the ClickEvent be dropped if there is no previous ItemsSelected event emitted
    return Observable.timer(1, TimeUnit.MILLISECONDS);
  }

}, new Func2<ItemsSelected, ClickEvent, SelectionForAction>() {

  @Override
  public SelectionForActioncall(ItemsSelected t1, ClickEvent t2) {
    return new SelectionForAction(t1.items);
  }

});

Any idea?

Upvotes: 0

Views: 134

Answers (1)

gabrielgiussi
gabrielgiussi

Reputation: 9585

I've found the operator that I needed to achieve the join behaviour with a very large time unit (DAYS in the example) and a very small one (MILLISECONDS).

With a variant of sample that takes another Observable as the sampler I could emit an event A only after an event of B would be emitted. enter image description here

In my example the click acts as the sampler and the stream selection emits the events that I'm interested in. (This also requires to ignore the last event that is being emitted when the stream completes).

Another possible solution will be use the buffer(boundary): enter image description here

The clicks stream would act as the boundary and I could avoid the scan operator because the list of items selected is created by the buffer operator. However with this solution I would not be considering unselection.

So, with sample I've achieved my original goal, however, I'm not happy with the way I handle items unselection and the final list of items selected. In this case I need to maintain the state of the items selected in order to perform some operation on all of them when a ClickEvent occurs.

I could subscribe to the items selection/unselection and maintain a List of the items selected but then I'll have lost the possibility of compose the clicks observable with the selection observable.

With scan I maintain state and also keep the composability of observables, but representing the list of current selection as an event seems a little forced, in fact this represents a new issue: if I select x items and then click the button, an event with the selection is being emitted as expected, but if neither the items are unselected nor a new one is selected and then click again the button, nothing happens. So, it seems that selection doesn't fit as an event.

Upvotes: 0

Related Questions