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 16KB

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