All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.ui.components.grid.MultiSelectionModelImpl Maven / Gradle / Ivy

There is a newer version: 8.27.3
Show newest version
/*
 * Copyright (C) 2000-2024 Vaadin Ltd
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See  for the full
 * license.
 */
package com.vaadin.ui.components.grid;

import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.vaadin.data.provider.DataCommunicator;
import com.vaadin.data.provider.DataProvider;
import com.vaadin.data.provider.HierarchicalDataProvider;
import com.vaadin.data.provider.HierarchicalQuery;
import com.vaadin.data.provider.Query;
import com.vaadin.event.selection.MultiSelectionEvent;
import com.vaadin.event.selection.MultiSelectionListener;
import com.vaadin.shared.Registration;
import com.vaadin.shared.data.selection.GridMultiSelectServerRpc;
import com.vaadin.shared.ui.grid.MultiSelectionModelState;
import com.vaadin.ui.MultiSelect;

/**
 * Multiselection model for grid.
 * 

* Shows a column of checkboxes as the first column of grid. Each checkbox * triggers the selection for that row. *

* Implementation detail: The Grid selection is updated immediately after user * selection on client side, without waiting for the server response. * * @author Vaadin Ltd. * @since 8.0 * * @param * the type of the selected item in grid. */ public class MultiSelectionModelImpl extends AbstractSelectionModel implements MultiSelectionModel { private class GridMultiSelectServerRpcImpl implements GridMultiSelectServerRpc { @Override public void select(String key) { MultiSelectionModelImpl.this.updateSelection( new LinkedHashSet<>(Arrays.asList(getData(key))), Collections.emptySet(), true); } @Override public void deselect(String key) { if (getState(false).allSelected) { // updated right away on client side getState(false).allSelected = false; getUI().getConnectorTracker() .getDiffState(MultiSelectionModelImpl.this) .put("allSelected", false); } MultiSelectionModelImpl.this.updateSelection(Collections.emptySet(), new LinkedHashSet<>(Arrays.asList(getData(key))), true); } @Override public void selectAll() { onSelectAll(true); } @Override public void deselectAll() { onDeselectAll(true); } } private Map selection = new LinkedHashMap<>(); private SelectAllCheckBoxVisibility selectAllCheckBoxVisibility = SelectAllCheckBoxVisibility.DEFAULT; @Override protected void init() { registerRpc(new GridMultiSelectServerRpcImpl()); } @Override protected MultiSelectionModelState getState() { return (MultiSelectionModelState) super.getState(); } @Override protected MultiSelectionModelState getState(boolean markAsDirty) { return (MultiSelectionModelState) super.getState(markAsDirty); } @Override public void setSelectAllCheckBoxVisibility( SelectAllCheckBoxVisibility selectAllCheckBoxVisibility) { if (this.selectAllCheckBoxVisibility != selectAllCheckBoxVisibility) { this.selectAllCheckBoxVisibility = selectAllCheckBoxVisibility; markAsDirty(); } } @Override public SelectAllCheckBoxVisibility getSelectAllCheckBoxVisibility() { return selectAllCheckBoxVisibility; } @Override public boolean isSelectAllCheckBoxVisible() { updateCanSelectAll(); return getState(false).selectAllCheckBoxVisible; } @Override public boolean isAllSelected() { return getState(false).allSelected; } @Override public boolean isSelected(T item) { DataProvider dataProvider = getGrid().getDataProvider(); return selectionContainsId(dataProvider.getId(item)); } /** * Returns if the given id belongs to one of the selected items. * * @param id * the id to check for * @return {@code true} if id is selected, {@code false} if not */ protected boolean selectionContainsId(Object id) { return selection.containsKey(id); } @Override public void beforeClientResponse(boolean initial) { super.beforeClientResponse(initial); updateCanSelectAll(); } /** * Controls whether the select all checkbox is visible in the grid default * header, or not. *

* This is updated as a part of {@link #beforeClientResponse(boolean)}, * since the data provider for grid can be changed on the fly. * * @see SelectAllCheckBoxVisibility */ protected void updateCanSelectAll() { switch (selectAllCheckBoxVisibility) { case VISIBLE: getState(false).selectAllCheckBoxVisible = true; break; case HIDDEN: getState(false).selectAllCheckBoxVisible = false; break; case DEFAULT: getState(false).selectAllCheckBoxVisible = getGrid() .getDataProvider().isInMemory(); break; default: break; } } @Override public Registration addMultiSelectionListener( MultiSelectionListener listener) { return addListener(MultiSelectionEvent.class, listener, MultiSelectionListener.SELECTION_CHANGE_METHOD); } @Override public Set getSelectedItems() { return Collections .unmodifiableSet(new LinkedHashSet<>(selection.values())); } @Override public void updateSelection(Set addedItems, Set removedItems) { updateSelection(addedItems, removedItems, false); } @Override public void selectAll() { onSelectAll(false); } @Override public void deselectAll() { onDeselectAll(false); } /** * Gets a wrapper for using this grid as a multiselect in a binder. * * @return a multiselect wrapper for grid */ @Override public MultiSelect asMultiSelect() { return new MultiSelect() { @Override public void setValue(Set value) { Objects.requireNonNull(value); Set copy = value.stream().map(Objects::requireNonNull) .collect(Collectors.toCollection(LinkedHashSet::new)); updateSelection(copy, new LinkedHashSet<>(getSelectedItems())); } @Override public Set getValue() { return getSelectedItems(); } @Override public Registration addValueChangeListener( com.vaadin.data.HasValue.ValueChangeListener> listener) { return addSelectionListener( event -> listener.valueChange(event)); } @Override public void setRequiredIndicatorVisible( boolean requiredIndicatorVisible) { // TODO support required indicator for grid ? throw new UnsupportedOperationException( "Required indicator is not supported in grid."); } @Override public boolean isRequiredIndicatorVisible() { // TODO support required indicator for grid ? throw new UnsupportedOperationException( "Required indicator is not supported in grid."); } @Override public void setReadOnly(boolean readOnly) { setUserSelectionAllowed(!readOnly); } @Override public boolean isReadOnly() { return !isUserSelectionAllowed(); } @Override public void updateSelection(Set addedItems, Set removedItems) { MultiSelectionModelImpl.this.updateSelection(addedItems, removedItems); } @Override public Set getSelectedItems() { return MultiSelectionModelImpl.this.getSelectedItems(); } @Override public Registration addSelectionListener( MultiSelectionListener listener) { return MultiSelectionModelImpl.this .addMultiSelectionListener(listener); } }; } /** * Triggered when the user checks the select all checkbox. * * @param userOriginated * {@code true} if originated from client side by user */ protected void onSelectAll(boolean userOriginated) { if (userOriginated) { verifyUserCanSelectAll(); // all selected state has been updated in client side already getState(false).allSelected = true; getUI().getConnectorTracker().getDiffState(this).put("allSelected", true); } else { getState().allSelected = true; } Stream allItemsStream; DataProvider dataProvider = getGrid().getDataProvider(); // this will fetch everything from backend if (dataProvider instanceof HierarchicalDataProvider) { allItemsStream = fetchAllHierarchical( (HierarchicalDataProvider) dataProvider); } else { allItemsStream = fetchAll(dataProvider); } LinkedHashSet allItems = new LinkedHashSet<>(); // ensure speedy closing in case the stream is connected to IO channels try (Stream stream = allItemsStream) { stream.forEach(allItems::add); } updateSelection(allItems, Collections.emptySet(), userOriginated); } /** * Fetch all items from the given hierarchical data provider. * * @since 8.1 * @param dataProvider * the data provider to fetch from * @return all items in the data provider */ private Stream fetchAllHierarchical( HierarchicalDataProvider dataProvider) { return fetchAllDescendants(null, dataProvider); } /** * Fetch all the descendants of the given parent item from the given data * provider. * * @since 8.1 * @param parent * the parent item to fetch descendants for * @param dataProvider * the data provider to fetch from * @return the stream of all descendant items */ private Stream fetchAllDescendants(T parent, HierarchicalDataProvider dataProvider) { List children; try (Stream stream = dataProvider .fetchChildren(new HierarchicalQuery<>(null, parent))) { children = stream.collect(Collectors.toList()); } if (children.isEmpty()) { return Stream.empty(); } return children.stream() .flatMap(child -> Stream.concat(Stream.of(child), fetchAllDescendants(child, dataProvider))); } /** * Fetch all items from the given data provider. * * @since 8.1 * @param dataProvider * the data provider to fetch from * @return all items in this data provider */ private Stream fetchAll(DataProvider dataProvider) { return dataProvider.fetch(new Query<>()); } /** * Triggered when the user unchecks the select all checkbox. * * @param userOriginated * {@code true} if originated from client side by user */ protected void onDeselectAll(boolean userOriginated) { if (userOriginated) { verifyUserCanSelectAll(); // all selected state has been update in client side already getState(false).allSelected = false; getUI().getConnectorTracker().getDiffState(this).put("allSelected", false); } else { getState().allSelected = false; } updateSelection(Collections.emptySet(), new LinkedHashSet<>(selection.values()), userOriginated); } private void verifyUserCanSelectAll() { if (!getState(false).selectAllCheckBoxVisible) { throw new IllegalStateException( "Cannot select all from client since select all checkbox should not be visible"); } } /** * Updates the selection by adding and removing the given items. *

* All selection updates should go through this method, since it handles * incorrect parameters, removing duplicates, notifying data communicator * and and firing events. * * @param addedItems * the items added to selection, not {@code} null * @param removedItems * the items removed from selection, not {@code} null * @param userOriginated * {@code true} if this was used originated, {@code false} if not */ protected void updateSelection(Set addedItems, Set removedItems, boolean userOriginated) { Objects.requireNonNull(addedItems); Objects.requireNonNull(removedItems); if (userOriginated && !isUserSelectionAllowed()) { throw new IllegalStateException("Client tried to update selection" + " although user selection is disallowed"); } DataProvider dataProvider = getGrid().getDataProvider(); addedItems.removeIf(item -> { Object id = dataProvider.getId(item); Optional toRemove = removedItems.stream() .filter(i -> dataProvider.getId(i).equals(id)).findFirst(); toRemove.ifPresent(i -> removedItems.remove(i)); return toRemove.isPresent(); }); if (addedItems.stream().map(dataProvider::getId) .allMatch(this::selectionContainsId) && removedItems.stream().map(dataProvider::getId) .noneMatch(this::selectionContainsId)) { return; } // update allSelected for server side selection updates if (getState(false).allSelected && !removedItems.isEmpty() && !userOriginated) { getState().allSelected = false; } doUpdateSelection(map -> { // order of add / remove does not matter since no duplicates Set removedItemIds = removedItems.stream() .map(dataProvider::getId).collect(Collectors.toSet()); removedItemIds.forEach(itemId -> map.remove(itemId)); addedItems.forEach(item -> map.put(dataProvider.getId(item), item)); // refresh method is NOOP for items that are not present client side DataCommunicator dataCommunicator = getGrid() .getDataCommunicator(); removedItems.forEach(dataCommunicator::refresh); addedItems.forEach(dataCommunicator::refresh); }, userOriginated); } private void doUpdateSelection(Consumer> handler, boolean userOriginated) { if (getParent() == null) { throw new IllegalStateException( "Trying to update selection for grid selection model that has been detached from the grid."); } LinkedHashSet oldSelection = new LinkedHashSet<>(selection.values()); handler.accept(selection); fireEvent(new MultiSelectionEvent<>(getGrid(), asMultiSelect(), oldSelection, userOriginated)); } @Override public void refreshData(T item) { DataProvider dataProvider = getGrid().getDataProvider(); Object refreshId = dataProvider.getId(item); if (selection.containsKey(refreshId)) { selection.put(refreshId, item); } } }