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

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