123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487 |
- /*
- * Copyright 2000-2018 Vaadin Ltd.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
- package com.vaadin.ui.components.grid;
-
- import com.vaadin.data.provider.*;
- 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;
-
- import java.util.*;
- import java.util.function.Consumer;
- import java.util.stream.Collectors;
- import java.util.stream.Stream;
-
- /**
- * Multiselection model for grid.
- * <p>
- * Shows a column of checkboxes as the first column of grid. Each checkbox
- * triggers the selection for that row.
- * <p>
- * 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 <T>
- * the type of the selected item in grid.
- */
- public class MultiSelectionModelImpl<T> extends AbstractSelectionModel<T>
- implements MultiSelectionModel<T> {
-
- 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 List<T> selection = new ArrayList<>();
-
- 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;
- }
-
- /**
- * Returns whether all items are selected or not.
- * <p>
- * This is only {@code true} if user has selected all rows with the select
- * all checkbox on client side, or if {@link #selectAll()} has been used
- * from server side.
- *
- * @return {@code true} if all selected, {@code false} if not
- */
- public boolean isAllSelected() {
- return getState(false).allSelected;
- }
-
- @Override
- public boolean isSelected(T item) {
- return isAllSelected()
- || selectionContainsId(getGrid().getDataProvider().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) {
- DataProvider<T, ?> dataProvider = getGrid().getDataProvider();
- return selection.stream().map(dataProvider::getId)
- .anyMatch(i -> id.equals(i));
- }
-
- @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.
- * <p>
- * 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<T> listener) {
- return addListener(MultiSelectionEvent.class, listener,
- MultiSelectionListener.SELECTION_CHANGE_METHOD);
- }
-
- @Override
- public Set<T> getSelectedItems() {
- return Collections.unmodifiableSet(new LinkedHashSet<>(selection));
- }
-
- @Override
- public void updateSelection(Set<T> addedItems, Set<T> 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<T> asMultiSelect() {
- return new MultiSelect<T>() {
-
- @Override
- public void setValue(Set<T> value) {
- Objects.requireNonNull(value);
- Set<T> copy = value.stream().map(Objects::requireNonNull)
- .collect(Collectors.toCollection(LinkedHashSet::new));
-
- updateSelection(copy, new LinkedHashSet<>(getSelectedItems()));
- }
-
- @Override
- public Set<T> getValue() {
- return getSelectedItems();
- }
-
- @Override
- public Registration addValueChangeListener(
- com.vaadin.data.HasValue.ValueChangeListener<Set<T>> 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<T> addedItems,
- Set<T> removedItems) {
- MultiSelectionModelImpl.this.updateSelection(addedItems,
- removedItems);
- }
-
- @Override
- public Set<T> getSelectedItems() {
- return MultiSelectionModelImpl.this.getSelectedItems();
- }
-
- @Override
- public Registration addSelectionListener(
- MultiSelectionListener<T> 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<T> allItemsStream;
- DataProvider<T, ?> dataProvider = getGrid().getDataProvider();
- // this will fetch everything from backend
- if (dataProvider instanceof HierarchicalDataProvider) {
- allItemsStream = fetchAllHierarchical(
- (HierarchicalDataProvider<T, ?>) dataProvider);
- } else {
- allItemsStream = fetchAll(dataProvider);
- }
- LinkedHashSet<T> allItems = new LinkedHashSet<>();
- allItemsStream.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<T> fetchAllHierarchical(
- HierarchicalDataProvider<T, ?> 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<T> fetchAllDescendants(T parent,
- HierarchicalDataProvider<T, ?> dataProvider) {
- List<T> children = dataProvider
- .fetchChildren(new HierarchicalQuery<>(null, parent))
- .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<T> fetchAll(DataProvider<T, ?> 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),
- 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.
- * <p>
- * 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<T> addedItems, Set<T> 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<T, ?> dataProvider = getGrid().getDataProvider();
-
- addedItems.removeIf(item -> {
- Object id = dataProvider.getId(item);
- Optional<T> 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(set -> {
- // order of add / remove does not matter since no duplicates
- Set<Object> removedItemIds = removedItems.stream()
- .map(dataProvider::getId).collect(Collectors.toSet());
- set.removeIf(
- item -> removedItemIds.contains(dataProvider.getId(item)));
- addedItems.stream().filter(
- item -> !selectionContainsId(dataProvider.getId(item)))
- .forEach(set::add);
-
- // refresh method is NOOP for items that are not present client side
- DataCommunicator<T> dataCommunicator = getGrid()
- .getDataCommunicator();
- removedItems.forEach(dataCommunicator::refresh);
- addedItems.forEach(dataCommunicator::refresh);
- }, userOriginated);
- }
-
- private void doUpdateSelection(Consumer<Collection<T>> 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<T> oldSelection = new LinkedHashSet<>(selection);
- handler.accept(selection);
-
- fireEvent(new MultiSelectionEvent<>(getGrid(), asMultiSelect(),
- oldSelection, userOriginated));
- }
-
- @Override
- public void refreshData(T item) {
- DataProvider<T, ?> dataProvider = getGrid().getDataProvider();
- Object refreshId = dataProvider.getId(item);
- for (int i = 0; i < selection.size(); ++i) {
- if (dataProvider.getId(selection.get(i)).equals(refreshId)) {
- selection.set(i, item);
- return;
- }
- }
- }
- }
|