Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

TreeGridConnector.java 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  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. }
  173. checkExpand();
  174. }
  175. @Override
  176. public void dataAdded(int firstRowIndex, int numberOfRows) {
  177. if (awaitingRowsState == AwaitingRowsState.EXPAND) {
  178. awaitingRowsState = AwaitingRowsState.NONE;
  179. }
  180. checkExpand();
  181. }
  182. @Override
  183. public void dataAvailable(int firstRowIndex, int numberOfRows) {
  184. // NO-OP
  185. }
  186. @Override
  187. public void resetDataAndSize(int estimatedNewDataSize) {
  188. awaitingRowsState = AwaitingRowsState.NONE;
  189. }
  190. });
  191. }
  192. @OnStateChange("primaryStyleName")
  193. private void updateHierarchyRendererStyleName() {
  194. getHierarchyRenderer().setStyleNames(getState().primaryStyleName);
  195. }
  196. private native void replaceCellFocusEventHandler(Grid<?> grid,
  197. GridEventHandler<?> eventHandler)
  198. /*-{
  199. var browserEventHandlers = grid.@com.vaadin.client.widgets.Grid::browserEventHandlers;
  200. // FocusEventHandler is initially 5th in the list of browser event handlers
  201. browserEventHandlers.@java.util.List::set(*)(5, eventHandler);
  202. }-*/;
  203. private native EventCellReference<?> getEventCell(Grid<?> grid)
  204. /*-{
  205. return grid.@com.vaadin.client.widgets.Grid::eventCell;
  206. }-*/;
  207. private boolean isHierarchyColumn(EventCellReference<JsonObject> cell) {
  208. return cell.getColumn().getRenderer() instanceof HierarchyRenderer;
  209. }
  210. /**
  211. * Delegates to {@link #setCollapsed(int, boolean, boolean)}, with
  212. * {@code userOriginated} as {@code true}.
  213. *
  214. * @see #setCollapsed(int, boolean, boolean)
  215. */
  216. private void setCollapsed(int rowIndex, boolean collapsed) {
  217. setCollapsed(rowIndex, collapsed, true);
  218. }
  219. /**
  220. * Set the collapse state for the row in the given index.
  221. * <p>
  222. * Calling this method will have no effect if a response has not yet been
  223. * received for a previous call to this method.
  224. *
  225. * @param rowIndex
  226. * index of the row to set the state for
  227. * @param collapsed
  228. * {@code true} to collapse the row, {@code false} to expand the
  229. * row
  230. * @param userOriginated
  231. * whether this method was originated from a user interaction
  232. */
  233. private void setCollapsed(int rowIndex, boolean collapsed,
  234. boolean userOriginated) {
  235. if (isAwaitingRowChange()) {
  236. return;
  237. }
  238. if (collapsed) {
  239. awaitingRowsState = AwaitingRowsState.COLLAPSE;
  240. } else {
  241. awaitingRowsState = AwaitingRowsState.EXPAND;
  242. }
  243. String rowKey = getRowKey(getDataSource().getRow(rowIndex));
  244. getRpcProxy(NodeCollapseRpc.class).setNodeCollapsed(rowKey, rowIndex,
  245. collapsed, userOriginated);
  246. }
  247. /**
  248. * Class to replace
  249. * {@link com.vaadin.client.widgets.Grid.CellFocusEventHandler}. The only
  250. * difference is that it handles events originated from widgets in hierarchy
  251. * cells.
  252. */
  253. private class CellFocusEventHandler
  254. implements GridEventHandler<JsonObject> {
  255. @Override
  256. public void onEvent(Grid.GridEvent<JsonObject> event) {
  257. Element target = Element.as(event.getDomEvent().getEventTarget());
  258. boolean elementInChildWidget = getWidget()
  259. .isElementInChildWidget(target);
  260. // Ignore if event was handled by keyboard navigation handler
  261. if (event.isHandled() && !elementInChildWidget) {
  262. return;
  263. }
  264. // Ignore target in child widget but handle hierarchy widget
  265. if (elementInChildWidget
  266. && !HierarchyRenderer.isElementInHierarchyWidget(target)) {
  267. return;
  268. }
  269. Collection<String> navigation = getNavigationEvents(getWidget());
  270. if (navigation.contains(event.getDomEvent().getType())) {
  271. handleNavigationEvent(getWidget(), event);
  272. }
  273. }
  274. private native Collection<String> getNavigationEvents(Grid<?> grid)
  275. /*-{
  276. return grid.@com.vaadin.client.widgets.Grid::cellFocusHandler
  277. .@com.vaadin.client.widgets.Grid.CellFocusHandler::getNavigationEvents()();
  278. }-*/;
  279. private native void handleNavigationEvent(Grid<?> grid,
  280. Grid.GridEvent<JsonObject> event)
  281. /*-{
  282. grid.@com.vaadin.client.widgets.Grid::cellFocusHandler
  283. .@com.vaadin.client.widgets.Grid.CellFocusHandler::handleNavigationEvent(*)(
  284. event.@com.vaadin.client.widgets.Grid.GridEvent::getDomEvent()(),
  285. event.@com.vaadin.client.widgets.Grid.GridEvent::getCell()())
  286. }-*/;
  287. }
  288. private class NavigationEventHandler
  289. implements GridEventHandler<JsonObject> {
  290. @Override
  291. public void onEvent(Grid.GridEvent<JsonObject> event) {
  292. if (event.isHandled()) {
  293. return;
  294. }
  295. Event domEvent = event.getDomEvent();
  296. if (!domEvent.getType().equals(BrowserEvents.KEYDOWN)) {
  297. return;
  298. }
  299. // Navigate within hierarchy with ARROW KEYs
  300. if (domEvent.getKeyCode() == KeyCodes.KEY_LEFT
  301. || domEvent.getKeyCode() == KeyCodes.KEY_RIGHT) {
  302. event.setHandled(true);
  303. EventCellReference<JsonObject> cell = event.getCell();
  304. // Hierarchy metadata
  305. JsonObject rowData = cell.getRow();
  306. if (rowData == null) {
  307. // Row data is lost from the cache, i.e. the row is at least
  308. // outside the visual area,
  309. // let's scroll the row into the view
  310. getWidget().scrollToRow(cell.getRowIndex());
  311. } else if (rowData.hasKey(
  312. HierarchicalDataCommunicatorConstants.ROW_HIERARCHY_DESCRIPTION)) {
  313. JsonObject rowDescription = rowData.getObject(
  314. HierarchicalDataCommunicatorConstants.ROW_HIERARCHY_DESCRIPTION);
  315. boolean leaf = rowDescription.getBoolean(
  316. HierarchicalDataCommunicatorConstants.ROW_LEAF);
  317. boolean collapsed = isCollapsed(rowData);
  318. switch (domEvent.getKeyCode()) {
  319. case KeyCodes.KEY_RIGHT:
  320. if (collapsed && !leaf) {
  321. setCollapsed(cell.getRowIndex(), false);
  322. }
  323. break;
  324. case KeyCodes.KEY_LEFT:
  325. if (collapsed || leaf) {
  326. // navigate up
  327. int columnIndex = cell.getColumnIndex();
  328. getRpcProxy(FocusParentRpc.class).focusParent(
  329. getRowKey(cell.getRow()), columnIndex);
  330. } else if (isCollapseAllowed(rowDescription)) {
  331. setCollapsed(cell.getRowIndex(), true);
  332. }
  333. break;
  334. }
  335. }
  336. }
  337. }
  338. }
  339. private boolean isAwaitingRowChange() {
  340. return awaitingRowsState != AwaitingRowsState.NONE;
  341. }
  342. private void checkExpand() {
  343. Range cache = ((AbstractRemoteDataSource) getDataSource())
  344. .getCachedRange();
  345. checkExpand(cache.getStart(), cache.length());
  346. }
  347. private void checkExpand(int firstRowIndex, int numberOfRows) {
  348. if (rowKeysPendingExpand.isEmpty() || isAwaitingRowChange()) {
  349. // will not perform the check if an expand or collapse action is
  350. // already pending or there are no rows pending expand
  351. return;
  352. }
  353. for (int rowIndex = firstRowIndex; rowIndex < firstRowIndex
  354. + numberOfRows; rowIndex++) {
  355. String rowKey = getDataSource().getRow(rowIndex)
  356. .getString(DataCommunicatorConstants.KEY);
  357. if (rowKeysPendingExpand.remove(rowKey)) {
  358. setCollapsed(rowIndex, false, false);
  359. return;
  360. }
  361. }
  362. }
  363. private static boolean isCollapsed(JsonObject rowData) {
  364. assert rowData.hasKey(
  365. HierarchicalDataCommunicatorConstants.ROW_HIERARCHY_DESCRIPTION) : "missing hierarchy data for row "
  366. + rowData.asString();
  367. return rowData.getObject(
  368. HierarchicalDataCommunicatorConstants.ROW_HIERARCHY_DESCRIPTION)
  369. .getBoolean(
  370. HierarchicalDataCommunicatorConstants.ROW_COLLAPSED);
  371. }
  372. /**
  373. * Checks if the item can be collapsed.
  374. *
  375. * @param row
  376. * the item row
  377. * @return {@code true} if the item is allowed to be collapsed,
  378. * {@code false} otherwise.
  379. */
  380. public static boolean isCollapseAllowed(JsonObject row) {
  381. return row.getBoolean(
  382. HierarchicalDataCommunicatorConstants.ROW_COLLAPSE_ALLOWED);
  383. }
  384. @Override
  385. public void onColumnRendererChanged(CustomColumn column) {
  386. super.onColumnRendererChanged(column);
  387. if (Objects.equals(getColumnId(column), hierarchyColumnId)) {
  388. updateHierarchyColumn();
  389. }
  390. }
  391. }