Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

MultiSelectionModelImpl.java 16KB

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