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.

TreeGridConnector.java 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  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.client.ui.treegrid;
  17. import java.util.Collection;
  18. import java.util.HashSet;
  19. import java.util.List;
  20. import java.util.Objects;
  21. import java.util.Set;
  22. import java.util.logging.Logger;
  23. import com.google.gwt.core.client.Scheduler;
  24. import com.google.gwt.dom.client.BrowserEvents;
  25. import com.google.gwt.dom.client.Element;
  26. import com.google.gwt.event.dom.client.KeyCodes;
  27. import com.google.gwt.user.client.Event;
  28. import com.vaadin.client.annotations.OnStateChange;
  29. import com.vaadin.client.connectors.grid.ColumnConnector.CustomColumn;
  30. import com.vaadin.client.connectors.grid.GridConnector;
  31. import com.vaadin.client.data.AbstractRemoteDataSource;
  32. import com.vaadin.client.data.DataChangeHandler;
  33. import com.vaadin.client.data.DataSource;
  34. import com.vaadin.client.renderers.HierarchyRenderer;
  35. import com.vaadin.client.widget.grid.EventCellReference;
  36. import com.vaadin.client.widget.grid.GridEventHandler;
  37. import com.vaadin.client.widget.treegrid.TreeGrid;
  38. import com.vaadin.client.widgets.Grid;
  39. import com.vaadin.shared.Range;
  40. import com.vaadin.shared.data.DataCommunicatorConstants;
  41. import com.vaadin.shared.data.HierarchicalDataCommunicatorConstants;
  42. import com.vaadin.shared.ui.Connect;
  43. import com.vaadin.shared.ui.treegrid.FocusParentRpc;
  44. import com.vaadin.shared.ui.treegrid.FocusRpc;
  45. import com.vaadin.shared.ui.treegrid.NodeCollapseRpc;
  46. import com.vaadin.shared.ui.treegrid.TreeGridClientRpc;
  47. import com.vaadin.shared.ui.treegrid.TreeGridState;
  48. import elemental.json.JsonObject;
  49. /**
  50. * A connector class for the TreeGrid component.
  51. *
  52. * @author Vaadin Ltd
  53. * @since 8.1
  54. */
  55. @Connect(com.vaadin.ui.TreeGrid.class)
  56. public class TreeGridConnector extends GridConnector {
  57. private static enum AwaitingRowsState {
  58. NONE, COLLAPSE, EXPAND
  59. }
  60. public TreeGridConnector() {
  61. registerRpc(FocusRpc.class, (rowIndex, cellIndex) -> getWidget()
  62. .focusCell(rowIndex, cellIndex));
  63. }
  64. private String hierarchyColumnId;
  65. private HierarchyRenderer hierarchyRenderer;
  66. private Set<String> rowKeysPendingExpand = new HashSet<>();
  67. private AwaitingRowsState awaitingRowsState = AwaitingRowsState.NONE;
  68. private boolean hierarchyColumnUpdateScheduled = false;
  69. @Override
  70. public TreeGrid getWidget() {
  71. return (TreeGrid) super.getWidget();
  72. }
  73. @Override
  74. public TreeGridState getState() {
  75. return (TreeGridState) super.getState();
  76. }
  77. /**
  78. * This method has been scheduled finally to avoid possible race conditions
  79. * between state change handling for the Grid and its columns. The renderer
  80. * of the column is set in a state change handler, and might not be
  81. * available when this method is executed.
  82. */
  83. @OnStateChange("hierarchyColumnId")
  84. void updateHierarchyColumn() {
  85. if (hierarchyColumnUpdateScheduled) {
  86. return;
  87. }
  88. Scheduler.get().scheduleFinally(() -> {
  89. hierarchyColumnUpdateScheduled = false;
  90. // Id of old hierarchy column
  91. String oldHierarchyColumnId = hierarchyColumnId;
  92. // Id of new hierarchy column. Choose first when nothing explicitly
  93. // set
  94. String newHierarchyColumnId = getState().hierarchyColumnId;
  95. if (newHierarchyColumnId == null
  96. && !getState().columnOrder.isEmpty()) {
  97. newHierarchyColumnId = getState().columnOrder.get(0);
  98. }
  99. // Columns
  100. Grid.Column<?, ?> newColumn = getColumn(newHierarchyColumnId);
  101. Grid.Column<?, ?> oldColumn = getColumn(oldHierarchyColumnId);
  102. if (newColumn == null && oldColumn == null) {
  103. // No hierarchy column defined
  104. return;
  105. }
  106. // Unwrap renderer of old column
  107. if (oldColumn != null
  108. && oldColumn.getRenderer() instanceof HierarchyRenderer) {
  109. oldColumn.setRenderer(
  110. ((HierarchyRenderer) oldColumn.getRenderer())
  111. .getInnerRenderer());
  112. }
  113. // Wrap renderer of new column
  114. if (newColumn != null) {
  115. HierarchyRenderer wrapperRenderer = getHierarchyRenderer();
  116. wrapperRenderer.setInnerRenderer(newColumn.getRenderer());
  117. newColumn.setRenderer(wrapperRenderer);
  118. // Set frozen columns again after setting hierarchy column as
  119. // setRenderer() replaces DOM elements
  120. getWidget().setFrozenColumnCount(getState().frozenColumnCount);
  121. hierarchyColumnId = newHierarchyColumnId;
  122. } else {
  123. Logger.getLogger(TreeGridConnector.class.getName()).warning(
  124. "Couldn't find column: " + newHierarchyColumnId);
  125. }
  126. });
  127. hierarchyColumnUpdateScheduled = true;
  128. }
  129. private HierarchyRenderer getHierarchyRenderer() {
  130. if (hierarchyRenderer == null) {
  131. hierarchyRenderer = new HierarchyRenderer(this::setCollapsed,
  132. getState().primaryStyleName);
  133. }
  134. return hierarchyRenderer;
  135. }
  136. @Override
  137. protected void init() {
  138. super.init();
  139. // Swap Grid's CellFocusEventHandler to this custom one
  140. // The handler is identical to the original one except for the child
  141. // widget check
  142. replaceCellFocusEventHandler(getWidget(), new CellFocusEventHandler());
  143. getWidget().addBrowserEventHandler(5, new NavigationEventHandler());
  144. registerRpc(TreeGridClientRpc.class, new TreeGridClientRpc() {
  145. @Override
  146. public void setExpanded(List<String> keys) {
  147. rowKeysPendingExpand.addAll(keys);
  148. checkExpand();
  149. }
  150. @Override
  151. public void setCollapsed(List<String> keys) {
  152. rowKeysPendingExpand.removeAll(keys);
  153. }
  154. @Override
  155. public void clearPendingExpands() {
  156. rowKeysPendingExpand.clear();
  157. }
  158. });
  159. }
  160. @Override
  161. public void setDataSource(DataSource<JsonObject> dataSource) {
  162. super.setDataSource(dataSource);
  163. dataSource.addDataChangeHandler(new DataChangeHandler() {
  164. @Override
  165. public void dataUpdated(int firstRowIndex, int numberOfRows) {
  166. checkExpand(firstRowIndex, numberOfRows);
  167. }
  168. @Override
  169. public void dataRemoved(int firstRowIndex, int numberOfRows) {
  170. if (awaitingRowsState == AwaitingRowsState.COLLAPSE) {
  171. awaitingRowsState = AwaitingRowsState.NONE;
  172. // make sure the cache stays up to date with the collapsing
  173. Range visibleRowRange = getWidget().getEscalator()
  174. .getVisibleRowRange();
  175. getDataSource().ensureAvailability(
  176. visibleRowRange.getStart(),
  177. visibleRowRange.getEnd());
  178. }
  179. checkExpand();
  180. }
  181. @Override
  182. public void dataAdded(int firstRowIndex, int numberOfRows) {
  183. if (awaitingRowsState == AwaitingRowsState.EXPAND) {
  184. awaitingRowsState = AwaitingRowsState.NONE;
  185. // make sure the cache stays up to date with the expanding
  186. Range visibleRowRange = getWidget().getEscalator()
  187. .getVisibleRowRange();
  188. getDataSource().ensureAvailability(
  189. visibleRowRange.getStart(),
  190. visibleRowRange.getEnd());
  191. }
  192. checkExpand();
  193. }
  194. @Override
  195. public void dataAvailable(int firstRowIndex, int numberOfRows) {
  196. // NO-OP
  197. }
  198. @Override
  199. public void resetDataAndSize(int estimatedNewDataSize) {
  200. awaitingRowsState = AwaitingRowsState.NONE;
  201. }
  202. });
  203. }
  204. @OnStateChange("primaryStyleName")
  205. private void updateHierarchyRendererStyleName() {
  206. getHierarchyRenderer().setStyleNames(getState().primaryStyleName);
  207. }
  208. private native void replaceCellFocusEventHandler(Grid<?> grid,
  209. GridEventHandler<?> eventHandler)
  210. /*-{
  211. var browserEventHandlers = grid.@com.vaadin.client.widgets.Grid::browserEventHandlers;
  212. // FocusEventHandler is initially 5th in the list of browser event handlers
  213. browserEventHandlers.@java.util.List::set(*)(5, eventHandler);
  214. }-*/;
  215. private native EventCellReference<?> getEventCell(Grid<?> grid)
  216. /*-{
  217. return grid.@com.vaadin.client.widgets.Grid::eventCell;
  218. }-*/;
  219. private boolean isHierarchyColumn(EventCellReference<JsonObject> cell) {
  220. return cell.getColumn().getRenderer() instanceof HierarchyRenderer;
  221. }
  222. /**
  223. * Delegates to {@link #setCollapsed(int, boolean, boolean)}, with
  224. * {@code userOriginated} as {@code true}.
  225. *
  226. * @see #setCollapsed(int, boolean, boolean)
  227. */
  228. private void setCollapsed(int rowIndex, boolean collapsed) {
  229. setCollapsed(rowIndex, collapsed, true);
  230. }
  231. /**
  232. * Set the collapse state for the row in the given index.
  233. * <p>
  234. * Calling this method will have no effect if a response has not yet been
  235. * received for a previous call to this method.
  236. *
  237. * @param rowIndex
  238. * index of the row to set the state for
  239. * @param collapsed
  240. * {@code true} to collapse the row, {@code false} to expand the
  241. * row
  242. * @param userOriginated
  243. * whether this method was originated from a user interaction
  244. */
  245. private void setCollapsed(int rowIndex, boolean collapsed,
  246. boolean userOriginated) {
  247. if (isAwaitingRowChange()) {
  248. return;
  249. }
  250. if (collapsed) {
  251. awaitingRowsState = AwaitingRowsState.COLLAPSE;
  252. } else {
  253. awaitingRowsState = AwaitingRowsState.EXPAND;
  254. }
  255. String rowKey = getRowKey(getDataSource().getRow(rowIndex));
  256. getRpcProxy(NodeCollapseRpc.class).setNodeCollapsed(rowKey, rowIndex,
  257. collapsed, userOriginated);
  258. }
  259. /**
  260. * Class to replace
  261. * {@link com.vaadin.client.widgets.Grid.CellFocusEventHandler}. The only
  262. * difference is that it handles events originated from widgets in hierarchy
  263. * cells.
  264. */
  265. private class CellFocusEventHandler
  266. implements GridEventHandler<JsonObject> {
  267. @Override
  268. public void onEvent(Grid.GridEvent<JsonObject> event) {
  269. Element target = Element.as(event.getDomEvent().getEventTarget());
  270. boolean elementInChildWidget = getWidget()
  271. .isElementInChildWidget(target);
  272. // Ignore if event was handled by keyboard navigation handler
  273. if (event.isHandled() && !elementInChildWidget) {
  274. return;
  275. }
  276. // Ignore target in child widget but handle hierarchy widget
  277. if (elementInChildWidget
  278. && !HierarchyRenderer.isElementInHierarchyWidget(target)) {
  279. return;
  280. }
  281. Collection<String> navigation = getNavigationEvents(getWidget());
  282. if (navigation.contains(event.getDomEvent().getType())) {
  283. handleNavigationEvent(getWidget(), event);
  284. }
  285. }
  286. private native Collection<String> getNavigationEvents(Grid<?> grid)
  287. /*-{
  288. return grid.@com.vaadin.client.widgets.Grid::cellFocusHandler
  289. .@com.vaadin.client.widgets.Grid.CellFocusHandler::getNavigationEvents()();
  290. }-*/;
  291. private native void handleNavigationEvent(Grid<?> grid,
  292. Grid.GridEvent<JsonObject> event)
  293. /*-{
  294. grid.@com.vaadin.client.widgets.Grid::cellFocusHandler
  295. .@com.vaadin.client.widgets.Grid.CellFocusHandler::handleNavigationEvent(*)(
  296. event.@com.vaadin.client.widgets.Grid.GridEvent::getDomEvent()(),
  297. event.@com.vaadin.client.widgets.Grid.GridEvent::getCell()())
  298. }-*/;
  299. }
  300. private class NavigationEventHandler
  301. implements GridEventHandler<JsonObject> {
  302. @Override
  303. public void onEvent(Grid.GridEvent<JsonObject> event) {
  304. if (event.isHandled()) {
  305. return;
  306. }
  307. Event domEvent = event.getDomEvent();
  308. if (!domEvent.getType().equals(BrowserEvents.KEYDOWN)) {
  309. return;
  310. }
  311. // Navigate within hierarchy with ARROW KEYs
  312. if (domEvent.getKeyCode() == KeyCodes.KEY_LEFT
  313. || domEvent.getKeyCode() == KeyCodes.KEY_RIGHT) {
  314. event.setHandled(true);
  315. EventCellReference<JsonObject> cell = event.getCell();
  316. // Hierarchy metadata
  317. JsonObject rowData = cell.getRow();
  318. if (rowData == null) {
  319. // Row data is lost from the cache, i.e. the row is at least
  320. // outside the visual area,
  321. // let's scroll the row into the view
  322. getWidget().scrollToRow(cell.getRowIndex());
  323. } else if (rowData.hasKey(
  324. HierarchicalDataCommunicatorConstants.ROW_HIERARCHY_DESCRIPTION)) {
  325. JsonObject rowDescription = rowData.getObject(
  326. HierarchicalDataCommunicatorConstants.ROW_HIERARCHY_DESCRIPTION);
  327. boolean leaf = rowDescription.getBoolean(
  328. HierarchicalDataCommunicatorConstants.ROW_LEAF);
  329. boolean collapsed = isCollapsed(rowData);
  330. switch (domEvent.getKeyCode()) {
  331. case KeyCodes.KEY_RIGHT:
  332. if (collapsed && !leaf) {
  333. setCollapsed(cell.getRowIndex(), false);
  334. }
  335. break;
  336. case KeyCodes.KEY_LEFT:
  337. if (collapsed || leaf) {
  338. // navigate up
  339. int columnIndex = cell.getColumnIndex();
  340. getRpcProxy(FocusParentRpc.class).focusParent(
  341. getRowKey(cell.getRow()), columnIndex);
  342. } else if (isCollapseAllowed(rowDescription)) {
  343. setCollapsed(cell.getRowIndex(), true);
  344. }
  345. break;
  346. }
  347. }
  348. }
  349. }
  350. }
  351. private boolean isAwaitingRowChange() {
  352. return awaitingRowsState != AwaitingRowsState.NONE;
  353. }
  354. private void checkExpand() {
  355. Range cache = ((AbstractRemoteDataSource) getDataSource())
  356. .getCachedRange();
  357. checkExpand(cache.getStart(), cache.length());
  358. }
  359. private void checkExpand(int firstRowIndex, int numberOfRows) {
  360. if (rowKeysPendingExpand.isEmpty() || isAwaitingRowChange()) {
  361. // will not perform the check if an expand or collapse action is
  362. // already pending or there are no rows pending expand
  363. return;
  364. }
  365. for (int rowIndex = firstRowIndex; rowIndex < firstRowIndex
  366. + numberOfRows; rowIndex++) {
  367. String rowKey = getDataSource().getRow(rowIndex)
  368. .getString(DataCommunicatorConstants.KEY);
  369. if (rowKeysPendingExpand.remove(rowKey)) {
  370. setCollapsed(rowIndex, false, false);
  371. return;
  372. }
  373. }
  374. }
  375. private static boolean isCollapsed(JsonObject rowData) {
  376. assert rowData.hasKey(
  377. HierarchicalDataCommunicatorConstants.ROW_HIERARCHY_DESCRIPTION) : "missing hierarchy data for row "
  378. + rowData.asString();
  379. return rowData.getObject(
  380. HierarchicalDataCommunicatorConstants.ROW_HIERARCHY_DESCRIPTION)
  381. .getBoolean(
  382. HierarchicalDataCommunicatorConstants.ROW_COLLAPSED);
  383. }
  384. /**
  385. * Checks if the item can be collapsed.
  386. *
  387. * @param row
  388. * the item row
  389. * @return {@code true} if the item is allowed to be collapsed,
  390. * {@code false} otherwise.
  391. */
  392. public static boolean isCollapseAllowed(JsonObject row) {
  393. return row.getBoolean(
  394. HierarchicalDataCommunicatorConstants.ROW_COLLAPSE_ALLOWED);
  395. }
  396. @Override
  397. public void onColumnRendererChanged(CustomColumn column) {
  398. super.onColumnRendererChanged(column);
  399. if (Objects.equals(getColumnId(column), hierarchyColumnId)) {
  400. updateHierarchyColumn();
  401. }
  402. }
  403. }