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.

GridRowDragger.java 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  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.io.Serializable;
  18. import java.util.Collection;
  19. import java.util.List;
  20. import java.util.Optional;
  21. import com.vaadin.data.provider.DataProvider;
  22. import com.vaadin.data.provider.ListDataProvider;
  23. import com.vaadin.shared.ui.dnd.DropEffect;
  24. import com.vaadin.shared.ui.grid.DropLocation;
  25. import com.vaadin.shared.ui.grid.DropMode;
  26. import com.vaadin.ui.Grid;
  27. import com.vaadin.ui.Grid.Column;
  28. /**
  29. * Allows dragging rows for reordering within a Grid and between two separate
  30. * Grids when the item type is the same.
  31. * <p>
  32. * When dragging a selected row, all the visible selected rows are dragged. Note
  33. * that ONLY currently visible rows are taken into account. The drop mode for
  34. * the target grid is by default {@link DropMode#BETWEEN}.
  35. * <p>
  36. * To customize the settings for either the source or the target grid, use
  37. * {@link #getGridDragSource()} and {@link #getGridDropTarget()}.The drop target
  38. * grid has been set to not allow drops for a target row when the grid has been
  39. * sorted, since the visual drop target location would not match where the item
  40. * would actually be dropped into. Additionally, a grid MUST NOT be the target
  41. * of more than one GridRowDragger.
  42. * <p>
  43. * <em>NOTE: this helper works only with {@link ListDataProvider} on both grids.
  44. * If you have another data provider, you should customize data provider
  45. * updating on drop with
  46. * {@link #setSourceDataProviderUpdater(SourceDataProviderUpdater)} &
  47. * {@link #setTargetDataProviderUpdater(TargetDataProviderUpdater)} and add a
  48. * custom drop index calculator with
  49. * {@link #setDropIndexCalculator(DropIndexCalculator)}.</em>
  50. * <p>
  51. * In case you are not using a {@link ListDataProvider} and don't have custom
  52. * handlers, {@link UnsupportedOperationException} is thrown on drop event.
  53. *
  54. * @param <T>
  55. * The Grid bean type.
  56. * @author Vaadin Ltd
  57. * @since 8.2
  58. */
  59. public class GridRowDragger<T> implements Serializable {
  60. private final GridDropTarget<T> gridDropTarget;
  61. private final GridDragSource<T> gridDragSource;
  62. private DropIndexCalculator<T> dropTargetIndexCalculator = null;
  63. private SourceDataProviderUpdater<T> sourceDataProviderUpdater = null;
  64. private TargetDataProviderUpdater<T> targetDataProviderUpdater = null;
  65. /**
  66. * Set of items currently being dragged.
  67. */
  68. private List<T> draggedItems;
  69. private int shiftedDropIndex;
  70. /**
  71. * Enables DnD reordering for the rows in the given grid.
  72. * <p>
  73. * {@link DropMode#BETWEEN} is used.
  74. * <p>
  75. * <em>NOTE:</em> this only works when the grid has a
  76. * {@link ListDataProvider}. Use the custom handlers
  77. * {@link #setSourceDataProviderUpdater(SourceDataProviderUpdater)} and
  78. * {@link #setTargetDataProviderUpdater(TargetDataProviderUpdater)} for
  79. * other data providers.
  80. * <p>
  81. * <em>NOTE:</em> When allowing the user to DnD reorder a grid's rows, you
  82. * should not allow the user to sort the grid since when the grid is sorted,
  83. * as the reordering doens't make any sense since the drop target cannot be
  84. * shown for the correct place due to the sorting. Sorting columns is
  85. * enabled by default for in-memory data provider grids. Sorting can be
  86. * disabled for columns with {@link Grid#getColumns()} and
  87. * {@link Column#setSortable(boolean)}.
  88. *
  89. * @param grid
  90. * Grid to be extended.
  91. */
  92. public GridRowDragger(Grid<T> grid) {
  93. this(grid, DropMode.BETWEEN);
  94. }
  95. /**
  96. * Enables DnD reordering the rows in the given grid with the given drop
  97. * mode.
  98. * <p>
  99. * <em>NOTE:</em> this only works when the grid has a
  100. * {@link ListDataProvider}. Use the custom handlers
  101. * {@link #setSourceDataProviderUpdater(SourceDataProviderUpdater)} and
  102. * {@link #setTargetDataProviderUpdater(TargetDataProviderUpdater)} for
  103. * other data providers.
  104. * <p>
  105. * <em>NOTE:</em> When allowing the user to DnD reorder a grid's rows, you
  106. * should not allow the user to sort the grid since when the grid is sorted,
  107. * as the reordering doens't make any sense since the drop target cannot be
  108. * shown for the correct place due to the sorting. Sorting columns is
  109. * enabled by default for in-memory data provider grids. Sorting can be
  110. * disabled for columns with {@link Grid#getColumns()} and
  111. * {@link Column#setSortable(boolean)}.
  112. *
  113. * @param grid
  114. * the grid to enable row DnD reordering on
  115. * @param dropMode
  116. * DropMode to be used.
  117. */
  118. public GridRowDragger(Grid<T> grid, DropMode dropMode) {
  119. this(grid, grid, dropMode);
  120. }
  121. /**
  122. * Enables DnD moving of rows from the source grid to the target grid.
  123. * <p>
  124. * {@link DropMode#BETWEEN} is used.
  125. * <p>
  126. * <em>NOTE: this only works when the grids have a
  127. * {@link ListDataProvider}.</em> Use the custom handlers
  128. * {@link #setSourceDataProviderUpdater(SourceDataProviderUpdater)} and
  129. * {@link #setTargetDataProviderUpdater(TargetDataProviderUpdater)} for
  130. * other data providers.
  131. *
  132. * @param source
  133. * the source grid dragged from.
  134. * @param target
  135. * the target grid dropped to.
  136. */
  137. public GridRowDragger(Grid<T> source, Grid<T> target) {
  138. this(source, target, DropMode.BETWEEN);
  139. }
  140. /**
  141. * Enables DnD moving of rows from the source grid to the target grid with
  142. * the custom data provider updaters.
  143. * <p>
  144. * {@link DropMode#BETWEEN} is used.
  145. *
  146. * @param source
  147. * grid dragged from
  148. * @param target
  149. * grid dragged to
  150. * @param targetDataProviderUpdater
  151. * handler for updating target grid data provider
  152. * @param sourceDataProviderUpdater
  153. * handler for updating source grid data provider
  154. */
  155. public GridRowDragger(Grid<T> source, Grid<T> target,
  156. TargetDataProviderUpdater<T> targetDataProviderUpdater,
  157. SourceDataProviderUpdater<T> sourceDataProviderUpdater) {
  158. this(source, target, DropMode.BETWEEN);
  159. this.targetDataProviderUpdater = targetDataProviderUpdater;
  160. this.sourceDataProviderUpdater = sourceDataProviderUpdater;
  161. }
  162. /**
  163. * Enables DnD moving of rows from the source grid to the target grid with
  164. * the given drop mode.
  165. * <p>
  166. * <em>NOTE: this only works when the grids have a
  167. * {@link ListDataProvider}.</em> Use the other constructors or custom
  168. * handlers {@link #setSourceDataProviderUpdater(SourceDataProviderUpdater)}
  169. * and {@link #setTargetDataProviderUpdater(TargetDataProviderUpdater)} for
  170. * other data providers.
  171. *
  172. * @param source
  173. * the drag source grid
  174. * @param target
  175. * the drop target grid
  176. * @param dropMode
  177. * the drop mode to use
  178. */
  179. public GridRowDragger(Grid<T> source, Grid<T> target, DropMode dropMode) {
  180. gridDragSource = new GridDragSource<>(source);
  181. gridDropTarget = new GridDropTarget<>(target, dropMode);
  182. gridDropTarget.setDropAllowedOnRowsWhenSorted(false);
  183. gridDragSource.addGridDragStartListener(event -> {
  184. draggedItems = event.getDraggedItems();
  185. });
  186. gridDropTarget.addGridDropListener(this::handleDrop);
  187. }
  188. /**
  189. * Sets the target data provider updater, which handles adding the dropped
  190. * items to the target grid.
  191. * <p>
  192. * By default, items are added to the index where they were dropped on for
  193. * any {@link ListDataProvider}. If another type of data provider is used,
  194. * this updater should be set to handle updating instead.
  195. *
  196. * @param targetDataProviderUpdater
  197. * the target drop handler to set, or {@code null} to remove
  198. */
  199. public void setTargetDataProviderUpdater(
  200. TargetDataProviderUpdater<T> targetDataProviderUpdater) {
  201. this.targetDataProviderUpdater = targetDataProviderUpdater;
  202. }
  203. /**
  204. * Returns the target grid data provider updater.
  205. *
  206. * @return target grid drop handler
  207. */
  208. public TargetDataProviderUpdater<T> getTargetDataProviderUpdater() {
  209. return targetDataProviderUpdater;
  210. }
  211. /**
  212. * Sets the source data provider updater, which handles removing items from
  213. * the drag source grid.
  214. * <p>
  215. * By default the items are removed from any {@link ListDataProvider}. If
  216. * another type of data provider is used, this updater should be set to
  217. * handle updating instead.
  218. * <p>
  219. * If you want to skip removing items from the source, you can use
  220. * {@link SourceDataProviderUpdater#NOOP}.
  221. *
  222. * @param sourceDataProviderUpdater
  223. * the drag source data provider updater to set, or {@code null}
  224. * to remove
  225. */
  226. public void setSourceDataProviderUpdater(
  227. SourceDataProviderUpdater<T> sourceDataProviderUpdater) {
  228. this.sourceDataProviderUpdater = sourceDataProviderUpdater;
  229. }
  230. /**
  231. * Returns the source grid data provider updater.
  232. * <p>
  233. * Default is {@code null} and the items are just removed from the source
  234. * grid, which only works for {@link ListDataProvider}.
  235. *
  236. * @return the source grid drop handler
  237. */
  238. public SourceDataProviderUpdater<T> getSourceDataProviderUpdater() {
  239. return sourceDataProviderUpdater;
  240. }
  241. /**
  242. * Sets the drop index calculator for the target grid. With this callback
  243. * you can have a custom drop location instead of the actual one.
  244. * <p>
  245. * By default, items are placed on the index they are dropped into in the
  246. * target grid.
  247. * <p>
  248. * If you want to always drop items to the end of the target grid, you can
  249. * use {@link DropIndexCalculator#ALWAYS_DROP_TO_END}.
  250. *
  251. * @param dropIndexCalculator
  252. * the drop index calculator
  253. */
  254. public void setDropIndexCalculator(
  255. DropIndexCalculator<T> dropIndexCalculator) {
  256. this.dropTargetIndexCalculator = dropIndexCalculator;
  257. }
  258. /**
  259. * Gets the drop index calculator.
  260. * <p>
  261. * Default is {@code null} and the dropped items are placed on the drop
  262. * location.
  263. *
  264. * @return the drop index calculator
  265. */
  266. public DropIndexCalculator<T> getDropIndexCalculator() {
  267. return dropTargetIndexCalculator;
  268. }
  269. /**
  270. * Returns the drop target grid to allow performing customizations such as
  271. * altering {@link DropEffect}.
  272. *
  273. * @return the drop target grid
  274. */
  275. public GridDropTarget<T> getGridDropTarget() {
  276. return gridDropTarget;
  277. }
  278. /**
  279. * Returns the drag source grid, exposing it for customizations.
  280. *
  281. * @return the drag source grid
  282. */
  283. public GridDragSource<T> getGridDragSource() {
  284. return gridDragSource;
  285. }
  286. /**
  287. * Returns the currently dragged items captured from the source grid no drag
  288. * start event, or {@code null} if no drag active.
  289. *
  290. * @return the currently dragged items or {@code null}
  291. */
  292. protected List<T> getDraggedItems() {
  293. return draggedItems;
  294. }
  295. /**
  296. * This method is triggered when there has been a drop on the target grid.
  297. * <p>
  298. * <em>This method is protected only for testing reasons, you should not
  299. * override this</em> but instead use
  300. * {@link #setSourceDataProviderUpdater(SourceDataProviderUpdater)},
  301. * {@link #setTargetDataProviderUpdater(TargetDataProviderUpdater)} and
  302. * {@link #setDropIndexCalculator(DropIndexCalculator)} to customize how to
  303. * handle the drops.
  304. *
  305. * @param event
  306. * the drop event on the target grid
  307. */
  308. protected void handleDrop(GridDropEvent<T> event) {
  309. // there is a case that the drop happened from some other grid than the
  310. // source one
  311. if (getDraggedItems() == null) {
  312. return;
  313. }
  314. // don't do anything if not supported data providers used without custom
  315. // handlers
  316. verifySupportedDataProviders();
  317. shiftedDropIndex = -1;
  318. handleSourceGridDrop(event, getDraggedItems());
  319. int index = calculateDropIndex(event);
  320. handleTargetGridDrop(event, index, getDraggedItems());
  321. draggedItems = null;
  322. }
  323. private void handleSourceGridDrop(GridDropEvent<T> event,
  324. final Collection<T> droppedItems) {
  325. Grid<T> source = getGridDragSource().getGrid();
  326. if (getSourceDataProviderUpdater() != null) {
  327. getSourceDataProviderUpdater().removeItems(event.getDropEffect(),
  328. source.getDataProvider(), droppedItems);
  329. return;
  330. }
  331. ListDataProvider<T> listDataProvider = (ListDataProvider<T>) source
  332. .getDataProvider();
  333. // use the existing data source to keep filters and sort orders etc. in
  334. // place.
  335. Collection<T> sourceItems = listDataProvider.getItems();
  336. // if reordering the same grid and dropping on top of one of the dragged
  337. // rows, need to calculate the new drop index before removing the items
  338. if (getGridDragSource().getGrid() == getGridDropTarget().getGrid()
  339. && event.getDropTargetRow().isPresent()
  340. && getDraggedItems().contains(event.getDropTargetRow().get())) {
  341. List<T> sourceItemsList = (List<T>) sourceItems;
  342. shiftedDropIndex = sourceItemsList
  343. .indexOf(event.getDropTargetRow().get());
  344. shiftedDropIndex -= getDraggedItems().stream().filter(
  345. item -> sourceItemsList.indexOf(item) < shiftedDropIndex)
  346. .count();
  347. }
  348. sourceItems.removeAll(droppedItems);
  349. // if reordering the same grid, DataProvider's refresh will be done later
  350. if (getGridDragSource().getGrid() != getGridDropTarget().getGrid()) {
  351. listDataProvider.refreshAll();
  352. }
  353. }
  354. private void handleTargetGridDrop(GridDropEvent<T> event, final int index,
  355. Collection<T> droppedItems) {
  356. Grid<T> target = getGridDropTarget().getGrid();
  357. if (getTargetDataProviderUpdater() != null) {
  358. getTargetDataProviderUpdater().onDrop(event.getDropEffect(),
  359. target.getDataProvider(), index, droppedItems);
  360. return;
  361. }
  362. ListDataProvider<T> listDataProvider = (ListDataProvider<T>) target
  363. .getDataProvider();
  364. // update the existing to keep filters etc.
  365. List<T> targetItems = (List<T>) listDataProvider.getItems();
  366. if (index != Integer.MAX_VALUE) {
  367. targetItems.addAll(index, droppedItems);
  368. } else {
  369. targetItems.addAll(droppedItems);
  370. }
  371. // instead of using setItems or creating a new data provider,
  372. // refresh the existing one to keep filters etc. in place
  373. listDataProvider.refreshAll();
  374. // if dropped to the end of the grid, the grid should scroll there so
  375. // that the dropped row is visible, but that is just recommended in
  376. // documentation and left for the users to take into use
  377. }
  378. private int calculateDropIndex(GridDropEvent<T> event) {
  379. // use custom calculator if present
  380. if (getDropIndexCalculator() != null) {
  381. return getDropIndexCalculator().calculateDropIndex(event);
  382. }
  383. // if the source and target grids are the same, then the index has been
  384. // calculated before removing the items. In this case the drop location
  385. // is always above, since the items will be starting from that point on
  386. if (shiftedDropIndex != -1) {
  387. return shiftedDropIndex;
  388. }
  389. ListDataProvider<T> targetDataProvider = (ListDataProvider<T>) getGridDropTarget()
  390. .getGrid().getDataProvider();
  391. List<T> items = (List<T>) targetDataProvider.getItems();
  392. int index = items.size();
  393. Optional<T> dropTargetRow = event.getDropTargetRow();
  394. if (dropTargetRow.isPresent()) {
  395. index = items.indexOf(dropTargetRow.get())
  396. + (event.getDropLocation() == DropLocation.BELOW ? 1 : 0);
  397. }
  398. return index;
  399. }
  400. private void verifySupportedDataProviders() {
  401. verifySourceDataProvider();
  402. verifyTargetDataProvider();
  403. }
  404. @SuppressWarnings("unchecked")
  405. private void verifySourceDataProvider() {
  406. if (getSourceDataProviderUpdater() != null) {
  407. return; // custom updater is always fine
  408. }
  409. if (!(getSourceDataProvider() instanceof ListDataProvider)) {
  410. throwUnsupportedOperationExceptionForUnsupportedDataProvider(true);
  411. }
  412. if (!(((ListDataProvider<T>) getSourceDataProvider())
  413. .getItems() instanceof List)) {
  414. throwUnsupportedOperationExceptionForUnsupportedCollectionInListDataProvider(
  415. true);
  416. }
  417. }
  418. @SuppressWarnings("unchecked")
  419. private void verifyTargetDataProvider() {
  420. if (getTargetDataProviderUpdater() != null
  421. && getDropIndexCalculator() != null) {
  422. return; // custom updater and calculator is always fine
  423. }
  424. if (!(getTargetDataProvider() instanceof ListDataProvider)) {
  425. throwUnsupportedOperationExceptionForUnsupportedDataProvider(false);
  426. }
  427. if (!(((ListDataProvider<T>) getTargetDataProvider())
  428. .getItems() instanceof List)) {
  429. throwUnsupportedOperationExceptionForUnsupportedCollectionInListDataProvider(
  430. false);
  431. }
  432. }
  433. private DataProvider<T, ?> getSourceDataProvider() {
  434. return getGridDragSource().getGrid().getDataProvider();
  435. }
  436. private DataProvider<T, ?> getTargetDataProvider() {
  437. return getGridDropTarget().getGrid().getDataProvider();
  438. }
  439. private static void throwUnsupportedOperationExceptionForUnsupportedDataProvider(
  440. boolean sourceGrid) {
  441. throw new UnsupportedOperationException(new StringBuilder()
  442. .append(sourceGrid ? "Source " : "Target ")
  443. .append("grid does not have a ListDataProvider, cannot automatically ")
  444. .append(sourceGrid ? "remove " : "add ")
  445. .append("items. Use GridRowDragger.set")
  446. .append(sourceGrid ? "Source" : "Target")
  447. .append("DataProviderUpdater(...) ")
  448. .append(sourceGrid ? ""
  449. : "and setDropIndexCalculator(...) "
  450. + "to customize how to handle updating the data provider.")
  451. .toString());
  452. }
  453. private static void throwUnsupportedOperationExceptionForUnsupportedCollectionInListDataProvider(
  454. boolean sourceGrid) {
  455. throw new UnsupportedOperationException(new StringBuilder()
  456. .append(sourceGrid ? "Source " : "Target ")
  457. .append("grid's ListDataProvider is not backed by a List-collection, cannot ")
  458. .append(sourceGrid ? "remove " : "add ")
  459. .append("items. Use a ListDataProvider backed by a List, or use GridRowDragger.set")
  460. .append(sourceGrid ? "Source" : "Target")
  461. .append("DataProviderUpdater(...) ")
  462. .append(sourceGrid ? "" : "and setDropIndexCalculator(...) ")
  463. .append(" to customize how to handle updating the data provider to customize how to handle updating the data provider.")
  464. .toString());
  465. }
  466. }