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.

GridDragger.java 18KB

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