You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

MultiSelectionModelImpl.java 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. /*
  2. * Copyright 2000-2018 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.ui.components.grid;
  17. import java.util.ArrayList;
  18. import java.util.Arrays;
  19. import java.util.Collection;
  20. import java.util.Collections;
  21. import java.util.LinkedHashSet;
  22. import java.util.List;
  23. import java.util.Objects;
  24. import java.util.Optional;
  25. import java.util.Set;
  26. import java.util.function.Consumer;
  27. import java.util.stream.Collectors;
  28. import java.util.stream.Stream;
  29. import com.vaadin.data.provider.DataCommunicator;
  30. import com.vaadin.data.provider.DataProvider;
  31. import com.vaadin.data.provider.HierarchicalDataProvider;
  32. import com.vaadin.data.provider.HierarchicalQuery;
  33. import com.vaadin.data.provider.Query;
  34. import com.vaadin.event.selection.MultiSelectionEvent;
  35. import com.vaadin.event.selection.MultiSelectionListener;
  36. import com.vaadin.shared.Registration;
  37. import com.vaadin.shared.data.selection.GridMultiSelectServerRpc;
  38. import com.vaadin.shared.ui.grid.MultiSelectionModelState;
  39. import com.vaadin.ui.MultiSelect;
  40. /**
  41. * Multiselection model for grid.
  42. * <p>
  43. * Shows a column of checkboxes as the first column of grid. Each checkbox
  44. * triggers the selection for that row.
  45. * <p>
  46. * Implementation detail: The Grid selection is updated immediately after user
  47. * selection on client side, without waiting for the server response.
  48. *
  49. * @author Vaadin Ltd.
  50. * @since 8.0
  51. *
  52. * @param <T>
  53. * the type of the selected item in grid.
  54. */
  55. public class MultiSelectionModelImpl<T> extends AbstractSelectionModel<T>
  56. implements MultiSelectionModel<T> {
  57. private class GridMultiSelectServerRpcImpl
  58. implements GridMultiSelectServerRpc {
  59. @Override
  60. public void select(String key) {
  61. MultiSelectionModelImpl.this.updateSelection(
  62. new LinkedHashSet<>(Arrays.asList(getData(key))),
  63. Collections.emptySet(), true);
  64. }
  65. @Override
  66. public void deselect(String key) {
  67. if (getState(false).allSelected) {
  68. // updated right away on client side
  69. getState(false).allSelected = false;
  70. getUI().getConnectorTracker()
  71. .getDiffState(MultiSelectionModelImpl.this)
  72. .put("allSelected", false);
  73. }
  74. MultiSelectionModelImpl.this.updateSelection(Collections.emptySet(),
  75. new LinkedHashSet<>(Arrays.asList(getData(key))), true);
  76. }
  77. @Override
  78. public void selectAll() {
  79. onSelectAll(true);
  80. }
  81. @Override
  82. public void deselectAll() {
  83. onDeselectAll(true);
  84. }
  85. }
  86. private List<T> selection = new ArrayList<>();
  87. private SelectAllCheckBoxVisibility selectAllCheckBoxVisibility = SelectAllCheckBoxVisibility.DEFAULT;
  88. @Override
  89. protected void init() {
  90. registerRpc(new GridMultiSelectServerRpcImpl());
  91. }
  92. @Override
  93. protected MultiSelectionModelState getState() {
  94. return (MultiSelectionModelState) super.getState();
  95. }
  96. @Override
  97. protected MultiSelectionModelState getState(boolean markAsDirty) {
  98. return (MultiSelectionModelState) super.getState(markAsDirty);
  99. }
  100. @Override
  101. public void setSelectAllCheckBoxVisibility(
  102. SelectAllCheckBoxVisibility selectAllCheckBoxVisibility) {
  103. if (this.selectAllCheckBoxVisibility != selectAllCheckBoxVisibility) {
  104. this.selectAllCheckBoxVisibility = selectAllCheckBoxVisibility;
  105. markAsDirty();
  106. }
  107. }
  108. @Override
  109. public SelectAllCheckBoxVisibility getSelectAllCheckBoxVisibility() {
  110. return selectAllCheckBoxVisibility;
  111. }
  112. @Override
  113. public boolean isSelectAllCheckBoxVisible() {
  114. updateCanSelectAll();
  115. return getState(false).selectAllCheckBoxVisible;
  116. }
  117. /**
  118. * Returns whether all items are selected or not.
  119. * <p>
  120. * This is only {@code true} if user has selected all rows with the select
  121. * all checkbox on client side, or if {@link #selectAll()} has been used
  122. * from server side.
  123. *
  124. * @return {@code true} if all selected, {@code false} if not
  125. */
  126. public boolean isAllSelected() {
  127. return getState(false).allSelected;
  128. }
  129. @Override
  130. public boolean isSelected(T item) {
  131. return selectionContainsId(getGrid().getDataProvider().getId(item));
  132. }
  133. /**
  134. * Returns if the given id belongs to one of the selected items.
  135. *
  136. * @param id
  137. * the id to check for
  138. * @return {@code true} if id is selected, {@code false} if not
  139. */
  140. protected boolean selectionContainsId(Object id) {
  141. DataProvider<T, ?> dataProvider = getGrid().getDataProvider();
  142. return selection.stream().map(dataProvider::getId)
  143. .anyMatch(i -> id.equals(i));
  144. }
  145. @Override
  146. public void beforeClientResponse(boolean initial) {
  147. super.beforeClientResponse(initial);
  148. updateCanSelectAll();
  149. }
  150. /**
  151. * Controls whether the select all checkbox is visible in the grid default
  152. * header, or not.
  153. * <p>
  154. * This is updated as a part of {@link #beforeClientResponse(boolean)},
  155. * since the data provider for grid can be changed on the fly.
  156. *
  157. * @see SelectAllCheckBoxVisibility
  158. */
  159. protected void updateCanSelectAll() {
  160. switch (selectAllCheckBoxVisibility) {
  161. case VISIBLE:
  162. getState(false).selectAllCheckBoxVisible = true;
  163. break;
  164. case HIDDEN:
  165. getState(false).selectAllCheckBoxVisible = false;
  166. break;
  167. case DEFAULT:
  168. getState(false).selectAllCheckBoxVisible = getGrid()
  169. .getDataProvider().isInMemory();
  170. break;
  171. default:
  172. break;
  173. }
  174. }
  175. @Override
  176. public Registration addMultiSelectionListener(
  177. MultiSelectionListener<T> listener) {
  178. return addListener(MultiSelectionEvent.class, listener,
  179. MultiSelectionListener.SELECTION_CHANGE_METHOD);
  180. }
  181. @Override
  182. public Set<T> getSelectedItems() {
  183. return Collections.unmodifiableSet(new LinkedHashSet<>(selection));
  184. }
  185. @Override
  186. public void updateSelection(Set<T> addedItems, Set<T> removedItems) {
  187. updateSelection(addedItems, removedItems, false);
  188. }
  189. @Override
  190. public void selectAll() {
  191. onSelectAll(false);
  192. }
  193. @Override
  194. public void deselectAll() {
  195. onDeselectAll(false);
  196. }
  197. /**
  198. * Gets a wrapper for using this grid as a multiselect in a binder.
  199. *
  200. * @return a multiselect wrapper for grid
  201. */
  202. @Override
  203. public MultiSelect<T> asMultiSelect() {
  204. return new MultiSelect<T>() {
  205. @Override
  206. public void setValue(Set<T> value) {
  207. Objects.requireNonNull(value);
  208. Set<T> copy = value.stream().map(Objects::requireNonNull)
  209. .collect(Collectors.toCollection(LinkedHashSet::new));
  210. updateSelection(copy, new LinkedHashSet<>(getSelectedItems()));
  211. }
  212. @Override
  213. public Set<T> getValue() {
  214. return getSelectedItems();
  215. }
  216. @Override
  217. public Registration addValueChangeListener(
  218. com.vaadin.data.HasValue.ValueChangeListener<Set<T>> listener) {
  219. return addSelectionListener(
  220. event -> listener.valueChange(event));
  221. }
  222. @Override
  223. public void setRequiredIndicatorVisible(
  224. boolean requiredIndicatorVisible) {
  225. // TODO support required indicator for grid ?
  226. throw new UnsupportedOperationException(
  227. "Required indicator is not supported in grid.");
  228. }
  229. @Override
  230. public boolean isRequiredIndicatorVisible() {
  231. // TODO support required indicator for grid ?
  232. throw new UnsupportedOperationException(
  233. "Required indicator is not supported in grid.");
  234. }
  235. @Override
  236. public void setReadOnly(boolean readOnly) {
  237. setUserSelectionAllowed(!readOnly);
  238. }
  239. @Override
  240. public boolean isReadOnly() {
  241. return !isUserSelectionAllowed();
  242. }
  243. @Override
  244. public void updateSelection(Set<T> addedItems,
  245. Set<T> removedItems) {
  246. MultiSelectionModelImpl.this.updateSelection(addedItems,
  247. removedItems);
  248. }
  249. @Override
  250. public Set<T> getSelectedItems() {
  251. return MultiSelectionModelImpl.this.getSelectedItems();
  252. }
  253. @Override
  254. public Registration addSelectionListener(
  255. MultiSelectionListener<T> listener) {
  256. return MultiSelectionModelImpl.this
  257. .addMultiSelectionListener(listener);
  258. }
  259. };
  260. }
  261. /**
  262. * Triggered when the user checks the select all checkbox.
  263. *
  264. * @param userOriginated
  265. * {@code true} if originated from client side by user
  266. */
  267. protected void onSelectAll(boolean userOriginated) {
  268. if (userOriginated) {
  269. verifyUserCanSelectAll();
  270. // all selected state has been updated in client side already
  271. getState(false).allSelected = true;
  272. getUI().getConnectorTracker().getDiffState(this).put("allSelected",
  273. true);
  274. } else {
  275. getState().allSelected = true;
  276. }
  277. Stream<T> allItemsStream;
  278. DataProvider<T, ?> dataProvider = getGrid().getDataProvider();
  279. // this will fetch everything from backend
  280. if (dataProvider instanceof HierarchicalDataProvider) {
  281. allItemsStream = fetchAllHierarchical(
  282. (HierarchicalDataProvider<T, ?>) dataProvider);
  283. } else {
  284. allItemsStream = fetchAll(dataProvider);
  285. }
  286. LinkedHashSet<T> allItems = new LinkedHashSet<>();
  287. allItemsStream.forEach(allItems::add);
  288. updateSelection(allItems, Collections.emptySet(), userOriginated);
  289. }
  290. /**
  291. * Fetch all items from the given hierarchical data provider.
  292. *
  293. * @since 8.1
  294. * @param dataProvider
  295. * the data provider to fetch from
  296. * @return all items in the data provider
  297. */
  298. private Stream<T> fetchAllHierarchical(
  299. HierarchicalDataProvider<T, ?> dataProvider) {
  300. return fetchAllDescendants(null, dataProvider);
  301. }
  302. /**
  303. * Fetch all the descendants of the given parent item from the given data
  304. * provider.
  305. *
  306. * @since 8.1
  307. * @param parent
  308. * the parent item to fetch descendants for
  309. * @param dataProvider
  310. * the data provider to fetch from
  311. * @return the stream of all descendant items
  312. */
  313. private Stream<T> fetchAllDescendants(T parent,
  314. HierarchicalDataProvider<T, ?> dataProvider) {
  315. List<T> children = dataProvider
  316. .fetchChildren(new HierarchicalQuery<>(null, parent))
  317. .collect(Collectors.toList());
  318. if (children.isEmpty()) {
  319. return Stream.empty();
  320. }
  321. return children.stream()
  322. .flatMap(child -> Stream.concat(Stream.of(child),
  323. fetchAllDescendants(child, dataProvider)));
  324. }
  325. /**
  326. * Fetch all items from the given data provider.
  327. *
  328. * @since 8.1
  329. * @param dataProvider
  330. * the data provider to fetch from
  331. * @return all items in this data provider
  332. */
  333. private Stream<T> fetchAll(DataProvider<T, ?> dataProvider) {
  334. return dataProvider.fetch(new Query<>());
  335. }
  336. /**
  337. * Triggered when the user unchecks the select all checkbox.
  338. *
  339. * @param userOriginated
  340. * {@code true} if originated from client side by user
  341. */
  342. protected void onDeselectAll(boolean userOriginated) {
  343. if (userOriginated) {
  344. verifyUserCanSelectAll();
  345. // all selected state has been update in client side already
  346. getState(false).allSelected = false;
  347. getUI().getConnectorTracker().getDiffState(this).put("allSelected",
  348. false);
  349. } else {
  350. getState().allSelected = false;
  351. }
  352. updateSelection(Collections.emptySet(), new LinkedHashSet<>(selection),
  353. userOriginated);
  354. }
  355. private void verifyUserCanSelectAll() {
  356. if (!getState(false).selectAllCheckBoxVisible) {
  357. throw new IllegalStateException(
  358. "Cannot select all from client since select all checkbox should not be visible");
  359. }
  360. }
  361. /**
  362. * Updates the selection by adding and removing the given items.
  363. * <p>
  364. * All selection updates should go through this method, since it handles
  365. * incorrect parameters, removing duplicates, notifying data communicator
  366. * and and firing events.
  367. *
  368. * @param addedItems
  369. * the items added to selection, not {@code} null
  370. * @param removedItems
  371. * the items removed from selection, not {@code} null
  372. * @param userOriginated
  373. * {@code true} if this was used originated, {@code false} if not
  374. */
  375. protected void updateSelection(Set<T> addedItems, Set<T> removedItems,
  376. boolean userOriginated) {
  377. Objects.requireNonNull(addedItems);
  378. Objects.requireNonNull(removedItems);
  379. if (userOriginated && !isUserSelectionAllowed()) {
  380. throw new IllegalStateException("Client tried to update selection"
  381. + " although user selection is disallowed");
  382. }
  383. DataProvider<T, ?> dataProvider = getGrid().getDataProvider();
  384. addedItems.removeIf(item -> {
  385. Object id = dataProvider.getId(item);
  386. Optional<T> toRemove = removedItems.stream()
  387. .filter(i -> dataProvider.getId(i).equals(id)).findFirst();
  388. toRemove.ifPresent(i -> removedItems.remove(i));
  389. return toRemove.isPresent();
  390. });
  391. if (addedItems.stream().map(dataProvider::getId)
  392. .allMatch(this::selectionContainsId)
  393. && removedItems.stream().map(dataProvider::getId)
  394. .noneMatch(this::selectionContainsId)) {
  395. return;
  396. }
  397. // update allSelected for server side selection updates
  398. if (getState(false).allSelected && !removedItems.isEmpty()
  399. && !userOriginated) {
  400. getState().allSelected = false;
  401. }
  402. doUpdateSelection(set -> {
  403. // order of add / remove does not matter since no duplicates
  404. Set<Object> removedItemIds = removedItems.stream()
  405. .map(dataProvider::getId).collect(Collectors.toSet());
  406. set.removeIf(
  407. item -> removedItemIds.contains(dataProvider.getId(item)));
  408. addedItems.stream().filter(
  409. item -> !selectionContainsId(dataProvider.getId(item)))
  410. .forEach(set::add);
  411. // refresh method is NOOP for items that are not present client side
  412. DataCommunicator<T> dataCommunicator = getGrid()
  413. .getDataCommunicator();
  414. removedItems.forEach(dataCommunicator::refresh);
  415. addedItems.forEach(dataCommunicator::refresh);
  416. }, userOriginated);
  417. }
  418. private void doUpdateSelection(Consumer<Collection<T>> handler,
  419. boolean userOriginated) {
  420. if (getParent() == null) {
  421. throw new IllegalStateException(
  422. "Trying to update selection for grid selection model that has been detached from the grid.");
  423. }
  424. LinkedHashSet<T> oldSelection = new LinkedHashSet<>(selection);
  425. handler.accept(selection);
  426. fireEvent(new MultiSelectionEvent<>(getGrid(), asMultiSelect(),
  427. oldSelection, userOriginated));
  428. }
  429. @Override
  430. public void refreshData(T item) {
  431. DataProvider<T, ?> dataProvider = getGrid().getDataProvider();
  432. Object refreshId = dataProvider.getId(item);
  433. for (int i = 0; i < selection.size(); ++i) {
  434. if (dataProvider.getId(selection.get(i)).equals(refreshId)) {
  435. selection.set(i, item);
  436. return;
  437. }
  438. }
  439. }
  440. }