Table example: shift- and ctrl-selection

Hi,

There have been several questions about how to make a Table behave more like a desktop application, with shift- and ctrl-selection. I was convinced to put an example here.

The trick is to turn off regular selection in the Table, and use the ItemClickListener to select things programmatically (this keeps selection code in one place, and generally avoids trouble).

Of course I found a bug while doing this; here’s the example, complete with workaround for
#3500
:


public class TableSelectionExample extends com.vaadin.Application {

    Table table;
    Object mostSelected; // last item clicked, essentially

    @Override
    public void init() {

        final Window main = new Window("Hello window");
        setMainWindow(main);

        // create table & configure
        table = new Table();
        main.addComponent(table);

        // fillerup
        table.addContainerProperty("Thing", String.class, null);
        for (int i = 0; i < 50; i++) {
            Item item = table.addItem("Number " + i);
            item.getItemProperty("Thing").setValue("Number " + i);
        }

        // this is the main magic + selectRange() and toggleSelected()
        table.addListener(new ItemClickListener() {

            public void itemClick(ItemClickEvent event) {
                Object id = event.getItemId();
                if (event.isCtrlKey()) {
                    if (toggleSelected(id)) {
                        mostSelected = id;
                    }
                } else if (event.isShiftKey()) {
                    if (mostSelected == null) {
                        mostSelected = id;
                    }
                    selectRange(mostSelected, id);
                    mostSelected = id;
                } else {
                    mostSelected = id;
                    HashSet<Object> selected = new HashSet<Object>();
                    selected.add(id);
                    table.setValue(selected);
                }
            }

        });

        // configure
        table.setImmediate(true);
        table.setMultiSelect(true);
        table.setSelectable(false);
        table.setNullSelectionAllowed(true);

        // Sorry, there is a bug, adding an ActionHandler as workaround
        // see http://dev.vaadin.com/ticket/3500
        table.addActionHandler(new Action.Handler() {
            public Action[] getActions(Object target, Object sender) {
                return null;
            }

            public void handleAction(Action action, Object sender, Object target) {
            }
        });

    }

    private void selectRange(Object startId, Object endId) {

        Container.Indexed container = (Container.Indexed) table
                .getContainerDataSource();
        HashSet<Object> range = new HashSet<Object>();

        int idx = container.indexOfId(startId);
        int end = container.indexOfId(endId);
        if (idx > end) {
            int t = idx;
            idx = end;
            end = t;
            range.add(endId);
        } else {
            range.add(startId);
        }

        while (idx < end) {
            Object id = container.getIdByIndex(++idx);
            range.add(id);
        }

        Set<Object> selected = (Set<Object>) table.getValue();
        if (selected != null && selected.containsAll(range)) {
            // unselect
            HashSet<Object> newSelection = new HashSet<Object>();
            newSelection.addAll(selected);
            newSelection.removeAll(range);
            table.setValue(newSelection);
        } else {
            table.setValue(range);
        }
    }

    private boolean toggleSelected(Object id) {

        HashSet<Object> select = new HashSet<Object>();
        Set<Object> selected = (Set<Object>) table.getValue();
        if (selected != null) {
            for (Object i : selected) {
                if (i != id) {
                    select.add(i);
                }
            }
        }
        boolean retval = false;
        if (!table.isSelected(id)) {
            select.add(id);
            retval = true;
        }
        table.setValue(select);
        return retval;
    }

}

I’ll try to remember to update this once the workaround is no longer needed :-
(this could probably be simplified a little as well, as it was made in a hurry - might update at some point)

Hint: You might want to wrap this up by extending Table and moving the functionality there.

Best Regards,
Marc

Very nice, thanks!

Unfortunately, if I’m correct, browsers doesn’t support partial disabling of text-selection… that would be useful here. But it still works nice!

In fact, modern browser do support that through CSS:

-webkit-user-select: none;
-moz-user-select: none;

That will then in effect also prevent users from copying any text from the Table, which is probably a bad idea.

Your statement implies that IE is not a modern web browser :smiley:
Thanks for that, at least I can disable it in firefox now :slight_smile: Well, poor user, but I think (and hope:P) the text in there isn’t very important.
Btw. in real select fields, they cannot copy either.

I still don’t get the table working in a wrapper class… :frowning:

[code removed, see new one below]

I cut&pasted your code into my application, and it works…

What problems are you seeing?

Best Regards,
Marc

I won’t tell you.

It’s very embarrassing.

I copied the files from a test-project to the original one. Afterwards, I changed the code. I didn’t realize that I was still editing the file from the test-project :smiley: :*)

It works like a charm now, and it’s perfect with the disabled text selection :slight_smile:
thanks

Edit:
I think this features should be implemented on client side. Users don’t want to click 10 times to select 10 item. Therefore I created
a ticket
.

Hehe - been there, done that, will do it again :wink:

Yeah, I agree. It used to be unusual in web applications, and the user would not realize how it worked, but times change, and it’s much more usable for ‘power users’. And you could still use the old one-checkbox-per-row -trick if needed.

// Marc

There were two small bugs in your code which caused “the selection always been done” (bad english i think) from the last to the second last clicked item (which is incorrect if you want to extend or reduce your selection with the shift modifier).

Here comes the updated code:

package com.example;

import java.util.HashSet;
import java.util.Set;

import com.vaadin.data.Container;
import com.vaadin.data.Item;
import com.vaadin.event.Action;
import com.vaadin.event.ItemClickEvent;
import com.vaadin.event.ItemClickEvent.ItemClickListener;
import com.vaadin.ui.Table;

public class SelectableTable extends Table {

  /**
   * 
   */
  private static final long serialVersionUID = -6232921468057007629L;

  Object startItem; // last item clicked, essentially

  public SelectableTable() {

    // fillerup
    addContainerProperty( "Thing", String.class, null );
    for ( int i = 0; i < 50; i++ ) {
      Item item = addItem( "Number " + i );
      item.getItemProperty( "Thing" ).setValue( "Number " + i );
    }

    // this is the main magic + selectRange() and toggleSelected()
    addListener( new ItemClickListener() {

      public void itemClick( ItemClickEvent event ) {
        Object id = event.getItemId();
        if ( event.isCtrlKey() ) {
          if ( toggleSelected( id ) ) {
            startItem = id;
          }
        } else if ( event.isShiftKey() ) {
          if ( startItem == null ) {
            startItem = id;
          }
          selectRange( startItem, id );
        } else {
          startItem = id;
          HashSet<Object> selected = new HashSet<Object>();
          selected.add( id );
          setValue( selected );
        }
      }
    } );

    // configure
    setImmediate( true );
    setMultiSelect( true );
    setSelectable( false );
    setNullSelectionAllowed( true );

    // Sorry, there is a bug, adding an ActionHandler as workaround
    // see http://dev.vaadin.com/ticket/3500
    addActionHandler( new Action.Handler() {
      public Action[] getActions( Object target, Object sender ) {
        return null;
      }

      public void handleAction( Action action, Object sender, Object target ) {
      }
    } );

  }

  private void selectRange( Object startId, Object endId ) {

    Container.Indexed container = ( Container.Indexed ) getContainerDataSource();
    HashSet<Object> range = new HashSet<Object>();

    int idx = container.indexOfId( startId );
    int end = container.indexOfId( endId );
    if ( idx > end ) {
      int t = idx;
      idx = end;
      end = t;
      range.add( endId );
    } else {
      range.add( startId );
    }

    while ( idx < end ) {
      Object id = container.getIdByIndex( ++idx );
      range.add( id );
    }

    Set<Object> selected = ( Set<Object> ) getValue();
    if ( ( selected != null ) && selected.containsAll( range ) ) {
      // unselect
      HashSet<Object> newSelection = new HashSet<Object>();
      newSelection.removeAll( selected );
      newSelection.addAll( range );
      setValue( newSelection );
    } else {
      setValue( range );
    }
  }

  private boolean toggleSelected( Object id ) {

    HashSet<Object> select = new HashSet<Object>();
    Set<Object> selected = ( Set<Object> ) getValue();
    if ( selected != null ) {
      for ( Object i : selected ) {
        if ( i != id ) {
          select.add( i );
        }
      }
    }
    boolean retval = false;
    if ( !isSelected( id ) ) {
      select.add( id );
      retval = true;
    }
    setValue( select );
    return retval;
  }

}

Hi Marc & al,

Just wanted to add my 2 cents to make all Mac users happy. To get cmd-click to work like ctrl-click in windows with this extension to the table, you should make the following change:

if (event.isCtrlKey()) {
    if (toggleSelected(id)) {
        mostSelected = id;
    }
} else if ...

should be changed into:

if (event.isCtrlKey() || event.isMetaKey()) {
    if (toggleSelected(id)) {
        mostSelected = id;
    }
} else if ...

This makes cmd-click work as expected by mac users.

HTH,
/Jonatan