blob: b4d298d03cfb6094c76b8d4b62715f343cc4c60a [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014-2017 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
******************************************************************************/
package org.eclipse.scout.rt.ui.html.json.tree;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.scout.rt.client.job.ModelJobs;
import org.eclipse.scout.rt.client.ui.AbstractEventBuffer;
import org.eclipse.scout.rt.client.ui.IEventHistory;
import org.eclipse.scout.rt.client.ui.MouseButton;
import org.eclipse.scout.rt.client.ui.action.keystroke.IKeyStroke;
import org.eclipse.scout.rt.client.ui.action.menu.root.IContextMenu;
import org.eclipse.scout.rt.client.ui.basic.cell.ICell;
import org.eclipse.scout.rt.client.ui.basic.tree.ITree;
import org.eclipse.scout.rt.client.ui.basic.tree.ITreeNode;
import org.eclipse.scout.rt.client.ui.basic.tree.TreeAdapter;
import org.eclipse.scout.rt.client.ui.basic.tree.TreeEvent;
import org.eclipse.scout.rt.client.ui.basic.tree.TreeListener;
import org.eclipse.scout.rt.client.ui.basic.tree.TreeUtility;
import org.eclipse.scout.rt.client.ui.dnd.IDNDSupport;
import org.eclipse.scout.rt.client.ui.dnd.ResourceListTransferObject;
import org.eclipse.scout.rt.platform.resource.BinaryResource;
import org.eclipse.scout.rt.platform.util.CollectionUtility;
import org.eclipse.scout.rt.platform.util.StringUtility;
import org.eclipse.scout.rt.platform.util.visitor.DepthFirstTreeVisitor;
import org.eclipse.scout.rt.platform.util.visitor.TreeVisitResult;
import org.eclipse.scout.rt.ui.html.IUiSession;
import org.eclipse.scout.rt.ui.html.UiException;
import org.eclipse.scout.rt.ui.html.json.AbstractJsonWidget;
import org.eclipse.scout.rt.ui.html.json.FilteredJsonAdapterIds;
import org.eclipse.scout.rt.ui.html.json.IJsonAdapter;
import org.eclipse.scout.rt.ui.html.json.JsonEvent;
import org.eclipse.scout.rt.ui.html.json.JsonObjectUtility;
import org.eclipse.scout.rt.ui.html.json.JsonProperty;
import org.eclipse.scout.rt.ui.html.json.action.DisplayableActionFilter;
import org.eclipse.scout.rt.ui.html.json.form.fields.JsonAdapterProperty;
import org.eclipse.scout.rt.ui.html.json.form.fields.JsonAdapterPropertyConfig;
import org.eclipse.scout.rt.ui.html.json.form.fields.JsonAdapterPropertyConfigBuilder;
import org.eclipse.scout.rt.ui.html.json.menu.IJsonContextMenuOwner;
import org.eclipse.scout.rt.ui.html.json.menu.JsonContextMenu;
import org.eclipse.scout.rt.ui.html.res.BinaryResourceUrlUtility;
import org.eclipse.scout.rt.ui.html.res.IBinaryResourceConsumer;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JsonTree<TREE extends ITree> extends AbstractJsonWidget<TREE> implements IJsonContextMenuOwner, IBinaryResourceConsumer {
private static final Logger LOG = LoggerFactory.getLogger(JsonTree.class);
public static final String EVENT_NODES_INSERTED = "nodesInserted";
public static final String EVENT_NODES_UPDATED = "nodesUpdated";
public static final String EVENT_NODES_DELETED = "nodesDeleted";
public static final String EVENT_ALL_CHILD_NODES_DELETED = "allChildNodesDeleted";
public static final String EVENT_NODES_SELECTED = "nodesSelected";
public static final String EVENT_NODE_CLICK = "nodeClick";
public static final String EVENT_NODE_ACTION = "nodeAction";
public static final String EVENT_NODE_EXPANDED = "nodeExpanded";
public static final String EVENT_NODE_CHANGED = "nodeChanged";
public static final String EVENT_CHILD_NODE_ORDER_CHANGED = "childNodeOrderChanged";
public static final String EVENT_NODES_CHECKED = "nodesChecked";
public static final String EVENT_REQUEST_FOCUS = "requestFocus";
public static final String EVENT_SCROLL_TO_SELECTION = "scrollToSelection";
public static final String PROP_NODE_ID = "nodeId";
public static final String PROP_NODE_IDS = "nodeIds";
public static final String PROP_COMMON_PARENT_NODE_ID = "commonParentNodeId";
public static final String PROP_NODE = "node";
public static final String PROP_NODES = "nodes";
public static final String PROP_EXPANDED = "expanded";
public static final String PROP_EXPANDED_LAZY = "expandedLazy";
public static final String PROP_SELECTED_NODES = "selectedNodes";
private TreeListener m_treeListener;
private final Map<String, ITreeNode> m_treeNodes;
private final Map<ITreeNode, String> m_treeNodeIds;
/**
* Keep the parent/child hierarchy to that nodes may be disposed properly. In case of delete events the model is
* already updated so it is not always possible anymore to visit all the child nodes.
*/
private final Map<ITreeNode, Set<ITreeNode>> m_childNodes;
private final Map<ITreeNode, ITreeNode> m_parentNodes;
private final TreeEventFilter m_treeEventFilter;
private final AbstractEventBuffer<TreeEvent> m_eventBuffer;
private JsonContextMenu<IContextMenu> m_jsonContextMenu;
public JsonTree(TREE model, IUiSession uiSession, String id, IJsonAdapter<?> parent) {
super(model, uiSession, id, parent);
m_treeNodes = new HashMap<>();
m_treeNodeIds = new HashMap<>();
m_childNodes = new HashMap<>();
m_parentNodes = new HashMap<>();
m_treeEventFilter = new TreeEventFilter(this);
m_eventBuffer = model.createEventBuffer();
}
@Override
public String getObjectType() {
return "Tree";
}
public JsonContextMenu<IContextMenu> getJsonContextMenu() {
return m_jsonContextMenu;
}
@Override
public void init() {
super.init();
// Replay missed events
IEventHistory<TreeEvent> eventHistory = getModel().getEventHistory();
if (eventHistory != null) {
for (TreeEvent event : eventHistory.getRecentEvents()) {
// Immediately execute events (no buffering), because this method is not called
// from the model but from the JSON layer. If Response.toJson() is in progress,
// adding this adapter to the list of buffered event providers would cause
// an exception.
processBufferedEvent(event);
}
}
}
@Override
protected void initJsonProperties(TREE model) {
super.initJsonProperties(model);
putJsonProperty(new JsonProperty<TREE>(ITree.PROP_TITLE, model) {
@Override
protected String modelValue() {
return getModel().getTitle();
}
});
putJsonProperty(new JsonProperty<TREE>(ITree.PROP_ICON_ID, model) {
@Override
protected String modelValue() {
return getModel().getIconId();
}
@Override
public Object prepareValueForToJson(Object value) {
return BinaryResourceUrlUtility.createIconUrl((String) value);
}
});
putJsonProperty(new JsonProperty<TREE>(ITree.PROP_CHECKABLE, model) {
@Override
protected Boolean modelValue() {
return getModel().isCheckable();
}
});
putJsonProperty(new JsonProperty<TREE>(ITree.PROP_MULTI_CHECK, model) {
@Override
protected Boolean modelValue() {
return getModel().isMultiCheck();
}
});
putJsonProperty(new JsonProperty<TREE>(ITree.PROP_ENABLED, model) {
@Override
protected Boolean modelValue() {
return getModel().isEnabled();
}
});
putJsonProperty(new JsonProperty<TREE>(ITree.PROP_LAZY_EXPANDING_ENABLED, model) {
@Override
protected Boolean modelValue() {
return getModel().isLazyExpandingEnabled();
}
});
putJsonProperty(new JsonProperty<TREE>(ITree.PROP_AUTO_CHECK_CHILDREN, model) {
@Override
protected Boolean modelValue() {
return getModel().isAutoCheckChildNodes();
}
});
putJsonProperty(new JsonProperty<ITree>(ITree.PROP_SCROLL_TO_SELECTION, model) {
@Override
protected Boolean modelValue() {
return getModel().isScrollToSelection();
}
});
putJsonProperty(new JsonProperty<ITree>(ITree.PROP_DROP_TYPE, model) {
@Override
protected Integer modelValue() {
return getModel().getDropType();
}
});
putJsonProperty(new JsonProperty<ITree>(ITree.PROP_DROP_MAXIMUM_SIZE, model) {
@Override
protected Long modelValue() {
return getModel().getDropMaximumSize();
}
});
putJsonProperty(new JsonAdapterProperty<ITree>(ITree.PROP_KEY_STROKES, model, getUiSession()) {
@Override
protected JsonAdapterPropertyConfig createConfig() {
return new JsonAdapterPropertyConfigBuilder().filter(new DisplayableActionFilter<>()).build();
}
@Override
protected List<IKeyStroke> modelValue() {
return getModel().getKeyStrokes();
}
});
putJsonProperty(new JsonProperty<TREE>(ITree.PROP_DISPLAY_STYLE, model) {
@Override
protected String modelValue() {
return getModel().getDisplayStyle();
}
});
putJsonProperty(new JsonProperty<TREE>(ITree.PROP_TOGGLE_BREADCRUMB_STYLE_ENABLED, model) {
@Override
protected Boolean modelValue() {
return getModel().isToggleBreadcrumbStyleEnabled();
}
});
}
@Override
protected void attachChildAdapters() {
super.attachChildAdapters();
m_jsonContextMenu = createJsonContextMenu();
m_jsonContextMenu.init();
attachNodes(getTopLevelNodes(), true);
}
protected JsonContextMenu<IContextMenu> createJsonContextMenu() {
return new JsonContextMenu<>(getModel().getContextMenu(), this);
}
@Override
protected void disposeChildAdapters() {
disposeAllNodes();
m_jsonContextMenu.dispose();
super.disposeChildAdapters();
}
@Override
protected void attachModel() {
super.attachModel();
if (m_treeListener != null) {
throw new IllegalStateException();
}
m_treeListener = new P_TreeListener();
getModel().addUITreeListener(m_treeListener);
}
@Override
protected void detachModel() {
super.detachModel();
if (m_treeListener == null) {
throw new IllegalStateException();
}
getModel().removeTreeListener(m_treeListener);
m_treeListener = null;
}
protected void attachNodeInternal(ITreeNode node) {
// We create a node id because it can happen that we handle events
// concerning nodes which have not yet been assigned a node id.
// Rather than requiring callers to ensure that the nodes on which
// their events operate exist, we create them here ourselves.
getOrCreateNodeId(node);
Set<ITreeNode> children = getChildNodes(node.getParentNode());
children.add(node);
m_childNodes.put(node.getParentNode(), children);
m_parentNodes.put(node, node.getParentNode());
}
protected void attachNode(ITreeNode node, boolean attachChildren) {
if (!isNodeAccepted(node)) {
return;
}
attachNodeInternal(node);
if (attachChildren) {
attachNodes(node.getChildNodes(), true);
}
}
protected void attachNodes(Collection<ITreeNode> nodes, boolean attachChildren) {
for (ITreeNode node : nodes) {
attachNode(node, attachChildren);
}
}
/**
* Removes all node mappings without querying the model.
*/
protected void disposeAllNodes() {
m_treeNodeIds.clear();
m_treeNodes.clear();
m_childNodes.clear();
m_parentNodes.clear();
}
protected void disposeNode(ITreeNode node, boolean disposeChildren) {
if (disposeChildren) {
disposeNodes(getChildNodes(node), disposeChildren);
}
String nodeId = m_treeNodeIds.get(node);
m_treeNodeIds.remove(node);
m_treeNodes.remove(nodeId);
// Remove node from parent/child hierarchy maps.
// The node will be removed from its parent childNodes list later in unlinkFromParentNode
m_childNodes.remove(node);
m_parentNodes.remove(node);
}
/**
* @return the child nodes of the given nodes which are kept by the map {@link #m_childNodes}. This method is
* typically used on delete operations because {@link ITreeNode#getChildNodes()} may not contain the deleted
* nodes anymore.
* @see #m_childNodes
*/
protected Set<ITreeNode> getChildNodes(ITreeNode node) {
Set<ITreeNode> children = m_childNodes.get(node);
if (children == null) {
return new HashSet<>();
}
return children;
}
protected ITreeNode getParentNode(ITreeNode node) {
return m_parentNodes.get(node);
}
/**
* Removes the given node from the child list of the parent node ({@link #m_childNodes}). Does not remove it from the
* {@link #m_parentNodes} list because it is not necessary as it will be done in
* {@link #disposeNode(ITreeNode, boolean)}.
*/
protected void unlinkFromParentNode(ITreeNode node) {
ITreeNode parentNode = getParentNode(node);
Set<ITreeNode> childrenOfParent = getChildNodes(parentNode);
childrenOfParent.remove(node);
}
protected void disposeNodes(Collection<ITreeNode> nodes, boolean disposeChildren) {
for (ITreeNode node : nodes) {
disposeNode(node, disposeChildren);
}
}
@Override
public JSONObject toJson() {
JSONObject json = super.toJson();
JSONArray jsonNodes = new JSONArray();
for (ITreeNode childNode : getTopLevelNodes()) {
if (!isNodeAccepted(childNode)) {
continue;
}
jsonNodes.put(treeNodeToJson(childNode));
}
putProperty(json, PROP_NODES, jsonNodes);
putProperty(json, PROP_SELECTED_NODES, nodeIdsToJson(getModel().getSelectedNodes(), true, true));
putProperty(json, PROP_MENUS, getJsonContextMenu().childActionsToJson());
return json;
}
protected void handleModelTreeEvent(TreeEvent event) {
event = m_treeEventFilter.filter(event);
if (event == null) {
return;
}
// Add event to buffer instead of handling it immediately. (This allows coalescing the events at JSON response level.)
bufferModelEvent(event);
registerAsBufferedEventsAdapter();
}
protected void bufferModelEvent(final TreeEvent event) {
switch (event.getType()) {
case TreeEvent.TYPE_NODE_FILTER_CHANGED: {
// Convert the "filter changed" event to a NODES_DELETED and a NODES_INSERTED event. This prevents sending unnecessary
// data to the UI. We convert the event before adding it to the event buffer to allow coalescing on UI-level.
// NOTE: This may lead to a temporary inconsistent situation, where node events exist in the buffer after the
// node itself is deleted. This is because the node is not really deleted from the model. However, when processing
// the buffered events, the "wrong" events will be ignored and everything is fixed again.
applyFilterChangedEventToUiRec(Collections.singletonList(getModel().getRootNode()));
break;
}
default: {
m_eventBuffer.add(event);
}
}
}
/**
* Recursively traverses through the given nodes (and its child nodes) and checks which of the model nodes are hidden
* by tree filters.
* <ul>
* <li>For every newly hidden node (i.e. a node that is currently visible on the UI) a NODES_DELETED event is created.
* <li>For every newly visible node (i.e. a node that is currently invisible on the UI) a NODES_INSERTED event is
* created.
* </ul>
* All new events are added to the event buffer, where they might be coalesced later.
*/
protected void applyFilterChangedEventToUiRec(List<ITreeNode> nodes) {
for (ITreeNode node : nodes) {
boolean processChildNodes = true;
if (!isInvisibleRootNode(node) && node.getTree() != null) {
String existingNodeId = optNodeId(node);
if (node.isFilterAccepted()) {
if (existingNodeId == null) {
// Node is not filtered but JsonTree does not know it yet --> handle as insertion event
m_eventBuffer.add(new TreeEvent(node.getTree(), TreeEvent.TYPE_NODES_INSERTED, node));
// Stop recursion, because this node (including its child nodes) is already inserted
processChildNodes = false;
}
}
else if (!node.isRejectedByUser()) {
if (existingNodeId != null) {
// Node is filtered, but JsonTree has it in its list --> handle as deletion event
m_eventBuffer.add(new TreeEvent(node.getTree(), TreeEvent.TYPE_NODES_DELETED, node));
}
// Stop recursion, because this node (including its child nodes) is already deleted
processChildNodes = false;
}
}
// Recursion
if (processChildNodes) {
applyFilterChangedEventToUiRec(node.getChildNodes());
}
}
}
@Override
public void processBufferedEvents() {
if (m_eventBuffer.isEmpty()) {
return;
}
List<TreeEvent> coalescedEvents = m_eventBuffer.consumeAndCoalesceEvents();
for (TreeEvent event : coalescedEvents) {
processBufferedEvent(event);
}
}
protected void processBufferedEvent(TreeEvent event) {
switch (event.getType()) {
case TreeEvent.TYPE_NODES_INSERTED:
handleModelNodesInserted(event);
break;
case TreeEvent.TYPE_NODES_UPDATED:
handleModelNodesUpdated(event);
break;
case TreeEvent.TYPE_NODES_DELETED:
handleModelNodesDeleted(event);
break;
case TreeEvent.TYPE_ALL_CHILD_NODES_DELETED:
handleModelAllChildNodesDeleted(event);
break;
case TreeEvent.TYPE_NODE_EXPANDED:
case TreeEvent.TYPE_NODE_COLLAPSED:
if (!isInvisibleRootNode(event.getNode())) { // Not necessary to send events for invisible root node
handleModelNodeExpanded(event.getNode(), false);
}
break;
case TreeEvent.TYPE_NODE_EXPANDED_RECURSIVE:
case TreeEvent.TYPE_NODE_COLLAPSED_RECURSIVE:
if (isInvisibleRootNode(event.getNode())) { // Send event for all child nodes
for (ITreeNode childNode : event.getNode().getChildNodes()) {
handleModelNodeExpanded(childNode, true);
}
}
else {
handleModelNodeExpanded(event.getNode(), true);
}
break;
case TreeEvent.TYPE_NODES_SELECTED:
handleModelNodesSelected(event.getNodes());
break;
case TreeEvent.TYPE_NODES_CHECKED:
handleModelNodesChecked(event.getNodes());
break;
case TreeEvent.TYPE_NODE_CHANGED:
handleModelNodeChanged(event.getNode());
break;
case TreeEvent.TYPE_NODE_FILTER_CHANGED:
// See special handling in bufferModelEvent()
throw new IllegalStateException("Unsupported event type: " + event);
case TreeEvent.TYPE_CHILD_NODE_ORDER_CHANGED:
handleModelChildNodeOrderChanged(event);
break;
case TreeEvent.TYPE_REQUEST_FOCUS:
handleModelRequestFocus(event);
break;
case TreeEvent.TYPE_SCROLL_TO_SELECTION:
handleModelScrollToSelection(event);
break;
default:
handleModelOtherTreeEvent(event);
break;
}
// TODO [7.0] bsh: Tree | Events not yet implemented:
// - TYPE_NODE_REQUEST_FOCUS
// - TYPE_NODE_ENSURE_VISIBLE what is the difference to scroll_to_selection? delete in treeevent
// - TYPE_NODES_DRAG_REQUEST
// - TYPE_DRAG_FINISHED
// - TYPE_NODE_DROP_ACTION, partly implemented with consumeBinaryResource(...)
// - TYPE_NODE_DROP_TARGET_CHANGED
}
/**
* Default impl. does nothing. Override this method to handle custom tree-events.
*/
protected void handleModelOtherTreeEvent(TreeEvent event) {
// empty default implementation
}
protected void handleModelNodeExpanded(ITreeNode modelNode, boolean recursive) {
if (!isNodeAccepted(modelNode)) {
return;
}
String nodeId = optNodeId(modelNode);
if (nodeId == null) { // Ignore nodes that are not yet sent to the UI (may happen due to asynchronous event processing)
return;
}
JSONObject jsonEvent = new JSONObject();
putProperty(jsonEvent, PROP_NODE_ID, nodeId);
putProperty(jsonEvent, PROP_EXPANDED, modelNode.isExpanded());
putProperty(jsonEvent, PROP_EXPANDED_LAZY, modelNode.isExpandedLazy());
putProperty(jsonEvent, "recursive", recursive);
addActionEvent(EVENT_NODE_EXPANDED, jsonEvent);
}
protected void handleModelNodesInserted(TreeEvent event) {
JSONArray jsonNodes = new JSONArray();
attachNodes(event.getNodes(), true); // TODO [7.0] cgu: why not inside loop? attaching for rejected nodes?
for (ITreeNode node : event.getNodes()) {
if (isNodeAccepted(node)) {
jsonNodes.put(treeNodeToJson(node));
}
}
if (jsonNodes.length() == 0) {
return;
}
JSONObject jsonEvent = new JSONObject();
putProperty(jsonEvent, PROP_NODES, jsonNodes);
putProperty(jsonEvent, PROP_COMMON_PARENT_NODE_ID, getOrCreateNodeId(event.getCommonParentNode()));
addActionEvent(EVENT_NODES_INSERTED, jsonEvent);
}
protected void handleModelNodesUpdated(TreeEvent event) {
JSONArray jsonNodes = new JSONArray();
for (ITreeNode node : event.getNodes()) {
if (!isNodeAccepted(node)) {
continue;
}
String nodeId = optNodeId(node);
if (nodeId == null) { // Ignore nodes that are not yet sent to the UI (may happen due to asynchronous event processing)
continue;
}
JSONObject jsonNode = new JSONObject();
putProperty(jsonNode, "id", nodeId);
// Only send _some_ of the properties. Everything else (e.g. "checked", "expanded") will be handled with separate events.
// --> See also: Tree.js/_onNodesUpdated()
putProperty(jsonNode, "leaf", node.isLeaf());
putProperty(jsonNode, "enabled", node.isEnabled());
putProperty(jsonNode, "lazyExpandingEnabled", node.isLazyExpandingEnabled());
jsonNodes.put(jsonNode);
}
if (jsonNodes.length() == 0) {
return;
}
JSONObject jsonEvent = new JSONObject();
putProperty(jsonEvent, PROP_NODES, jsonNodes);
putProperty(jsonEvent, PROP_COMMON_PARENT_NODE_ID, optNodeId(event.getCommonParentNode()));
addActionEvent(EVENT_NODES_UPDATED, jsonEvent);
}
protected void handleModelNodesDeleted(TreeEvent event) {
Collection<ITreeNode> nodes = event.getNodes();
JSONObject jsonEvent = new JSONObject();
putProperty(jsonEvent, PROP_COMMON_PARENT_NODE_ID, optNodeId(event.getCommonParentNode()));
// Small optimization: If no nodes remain, just
// send "all" instead of every single nodeId. (However, the nodes must be disposed individually.)
// Caveat: This can only be optimized when no nodes were inserted again in the same "tree changing" scope.
if (event.getCommonParentNode() != null && getFilteredNodeCount(event.getCommonParentNode()) == 0) {
addActionEvent(EVENT_ALL_CHILD_NODES_DELETED, jsonEvent);
}
else {
JSONArray jsonNodeIds = nodeIdsToJson(nodes, false, false);
if (jsonNodeIds.length() > 0) {
putProperty(jsonEvent, PROP_NODE_IDS, jsonNodeIds);
addActionEvent(EVENT_NODES_DELETED, jsonEvent);
}
}
for (ITreeNode node : nodes) {
unlinkFromParentNode(node);
}
disposeNodes(nodes, true);
}
protected void handleModelAllChildNodesDeleted(TreeEvent event) {
JSONObject jsonEvent = new JSONObject();
putProperty(jsonEvent, PROP_COMMON_PARENT_NODE_ID, getNodeId(event.getCommonParentNode()));
addActionEvent(EVENT_ALL_CHILD_NODES_DELETED, jsonEvent);
// Read the removed nodes from the event, because they are no longer contained in the model
for (ITreeNode node : event.getChildNodes()) {
unlinkFromParentNode(node);
}
disposeNodes(event.getChildNodes(), true);
}
protected void handleModelNodesSelected(Collection<ITreeNode> modelNodes) {
JSONArray jsonNodeIds = nodeIdsToJson(modelNodes, true, false);
JSONObject jsonEvent = new JSONObject();
putProperty(jsonEvent, PROP_NODE_IDS, jsonNodeIds);
addActionEvent(EVENT_NODES_SELECTED, jsonEvent);
}
protected void handleModelNodesChecked(Collection<ITreeNode> modelNodes) {
JSONArray jsonNodes = new JSONArray();
for (ITreeNode node : modelNodes) {
addJsonNodesChecked(jsonNodes, node);
if (getModel().isAutoCheckChildNodes()) {
for (ITreeNode childNode : collectChildNodesCheckedRec(node)) {
addJsonNodesChecked(jsonNodes, childNode);
}
}
}
if (jsonNodes.length() == 0) {
return;
}
JSONObject jsonEvent = new JSONObject();
putProperty(jsonEvent, PROP_NODES, jsonNodes);
addActionEvent(EVENT_NODES_CHECKED, jsonEvent);
}
protected void addJsonNodesChecked(JSONArray jsonNodes, ITreeNode node) {
if (!isNodeAccepted(node)) {
return;
}
String nodeId = optNodeId(node);
if (nodeId == null) { // Ignore nodes that are not yet sent to the UI (may happen due to asynchronous event processing)
return;
}
jsonNodes.put(nodeCheckedToJson(nodeId, node));
}
protected JSONObject nodeCheckedToJson(String nodeId, ITreeNode node) {
JSONObject json = new JSONObject();
putProperty(json, "id", nodeId);
putProperty(json, "checked", node.isChecked());
return json;
}
protected Collection<ITreeNode> collectChildNodesCheckedRec(ITreeNode node) {
P_ChildNodesVisitor visitor = new P_ChildNodesVisitor();
TreeUtility.visitNodes(node.getChildNodes(), visitor);
return visitor.getNodes();
}
protected void handleModelNodeChanged(ITreeNode modelNode) {
if (!isNodeAccepted(modelNode)) {
return;
}
String nodeId = optNodeId(modelNode);
if (nodeId == null) { // Ignore nodes that are not yet sent to the UI (may happen due to asynchronous event processing)
return;
}
JSONObject jsonEvent = new JSONObject();
putProperty(jsonEvent, PROP_NODE_ID, nodeId);
putCellProperties(jsonEvent, modelNode.getCell());
addActionEvent(EVENT_NODE_CHANGED, jsonEvent);
}
protected void handleModelChildNodeOrderChanged(TreeEvent event) {
JSONObject jsonEvent = new JSONObject();
jsonEvent.put("parentNodeId", getNodeId(event.getCommonParentNode()));
boolean hasNodeIds = false;
for (ITreeNode childNode : event.getChildNodes()) {
if (!isNodeAccepted(childNode)) {
continue;
}
String childNodeId = optNodeId(childNode);
if (childNodeId == null) { // Ignore nodes that are not yet sent to the UI (may happen due to asynchronous event processing)
continue;
}
jsonEvent.append("childNodeIds", childNodeId);
hasNodeIds = true;
}
if (hasNodeIds) {
addActionEvent(EVENT_CHILD_NODE_ORDER_CHANGED, jsonEvent);
}
}
protected void handleModelRequestFocus(TreeEvent event) {
addActionEvent(EVENT_REQUEST_FOCUS).protect();
}
protected void handleModelScrollToSelection(TreeEvent event) {
addActionEvent(EVENT_SCROLL_TO_SELECTION).protect();
}
@Override
public void handleModelContextMenuChanged(FilteredJsonAdapterIds<?> filteredAdapters) {
addPropertyChangeEvent(PROP_MENUS, filteredAdapters);
}
@Override
public void consumeBinaryResource(List<BinaryResource> binaryResources, Map<String, String> uploadProperties) {
if ((getModel().getDropType() & IDNDSupport.TYPE_FILE_TRANSFER) == IDNDSupport.TYPE_FILE_TRANSFER) {
ResourceListTransferObject transferObject = new ResourceListTransferObject(binaryResources);
ITreeNode node = null;
if (uploadProperties != null && uploadProperties.containsKey("nodeId")) {
String nodeId = uploadProperties.get("nodeId");
if (!StringUtility.isNullOrEmpty(nodeId)) {
node = getTreeNodeForNodeId(nodeId);
}
}
getModel().getUIFacade().fireNodeDropActionFromUI(node, transferObject);
}
}
@Override
public long getMaximumBinaryResourceUploadSize() {
return getModel().getDropMaximumSize();
}
protected JSONArray nodeIdsToJson(Collection<ITreeNode> modelNodes, boolean autoCreateNodeId) {
return nodeIdsToJson(modelNodes, true, autoCreateNodeId);
}
protected JSONArray nodeIdsToJson(Collection<ITreeNode> modelNodes, boolean checkNodeAccepted, boolean autoCreateNodeId) {
JSONArray jsonNodeIds = new JSONArray();
for (ITreeNode node : modelNodes) {
if (checkNodeAccepted && !isNodeAccepted(node)) {
continue;
}
String nodeId;
if (autoCreateNodeId) {
nodeId = getOrCreateNodeId(node);
}
else {
nodeId = optNodeId(node);
if (nodeId == null) { // Ignore nodes that are not yet sent to the UI (may happen due to asynchronous event processing)
continue;
}
}
// May be null if its the invisible root node
if (nodeId != null) {
jsonNodeIds.put(nodeId);
}
}
return jsonNodeIds;
}
public String getOrCreateNodeId(ITreeNode node) {
if (node == null) {
return null;
}
if (isInvisibleRootNode(node)) {
return null;
}
String id = m_treeNodeIds.get(node);
if (id != null) {
return id;
}
id = getUiSession().createUniqueId();
m_treeNodes.put(id, node);
m_treeNodeIds.put(node, id);
return id;
}
/**
* @return the nodeIdfor the given node. Returns <code>null</code> if the node is the invisible root node or
* <code>null</code> itself. Use {@link #optNodeId(ITreeNode)} to prevent an exception when no nodeId could be
* found.
* @throws UiException
* when no nodeId is found for the given node
*/
protected String getNodeId(ITreeNode node) {
if (node == null) {
return null;
}
if (isInvisibleRootNode(node)) {
return null;
}
String nodeId = m_treeNodeIds.get(node);
if (nodeId == null) {
throw new UiException("Unknown node: " + node);
}
return nodeId;
}
/**
* @return the nodeId for the given node or <code>null</code> if the node has no nodeId assigned. Also returns
* <code>null</code> if the node is the invisible root node or <code>null</code> itself.
*/
protected String optNodeId(ITreeNode node) {
if (node == null) {
return null;
}
if (isInvisibleRootNode(node)) {
return null;
}
return m_treeNodeIds.get(node);
}
protected boolean isInvisibleRootNode(ITreeNode node) {
if (!getModel().isRootNodeVisible()) {
return (node == getModel().getRootNode());
}
return false;
}
protected List<ITreeNode> getTopLevelNodes() {
ITreeNode rootNode = getModel().getRootNode();
if (getModel().isRootNodeVisible()) {
return CollectionUtility.arrayList(rootNode);
}
return rootNode.getChildNodes();
}
protected void putCellProperties(JSONObject json, ICell cell) {
// We deliberately don't use JsonCell here, because most properties are not supported in a tree anyway
json.put("text", cell.getText());
json.put("iconId", BinaryResourceUrlUtility.createIconUrl(cell.getIconId()));
json.put("cssClass", (cell.getCssClass()));
json.put("tooltipText", cell.getTooltipText());
json.put("foregroundColor", cell.getForegroundColor());
json.put("backgroundColor", cell.getBackgroundColor());
json.put("font", (cell.getFont() == null ? null : cell.getFont().toPattern()));
json.put("htmlEnabled", cell.isHtmlEnabled());
}
/**
* If the given node has a parent node, the value for the property "childNodeIndex" is calculated and added to the
* given JSON object.
* <p>
* Note that the calculated value may differ from the model's {@link ITreeNode#getChildNodeIndex()} value! This is
* because not all model nodes are sent to the UI. The calculated value only counts nodes sent to the UI, e.g. a node
* with childNodeIndex=50 may result in "childNodeIndex: 3" if 47 of the preceding nodes are filtered.
*/
protected void putChildNodeIndex(JSONObject json, ITreeNode node) {
if (node.getParentNode() != null && node.getParentNode().getChildNodeCount() > 0) {
int childNodeIndex = 0;
// Find the node in the parents childNodes list (skipping non-accepted nodes)
for (ITreeNode childNode : node.getParentNode().getChildNodes()) {
// Only count accepted nodes
if (isNodeAccepted(childNode)) {
if (childNode == node) {
// We have found our node!
break;
}
childNodeIndex++;
}
}
putProperty(json, "childNodeIndex", childNodeIndex);
}
}
protected JSONObject treeNodeToJson(ITreeNode node) {
JSONObject json = new JSONObject();
putProperty(json, "id", getOrCreateNodeId(node));
putProperty(json, "expanded", node.isExpanded());
putProperty(json, "expandedLazy", node.isExpandedLazy());
putProperty(json, "lazyExpandingEnabled", node.isLazyExpandingEnabled());
putProperty(json, "leaf", node.isLeaf());
putProperty(json, "checked", node.isChecked());
putProperty(json, "enabled", node.isEnabled());
putProperty(json, "iconId", BinaryResourceUrlUtility.createIconUrl(node.getCell().getIconId()));
putProperty(json, "initialExpanded", node.isInitialExpanded());
putChildNodeIndex(json, node);
putCellProperties(json, node.getCell());
JSONArray jsonChildNodes = new JSONArray();
if (node.getChildNodeCount() > 0) {
for (ITreeNode childNode : node.getChildNodes()) {
if (!isNodeAccepted(childNode)) {
continue;
}
jsonChildNodes.put(treeNodeToJson(childNode));
}
}
putProperty(json, "childNodes", jsonChildNodes);
JsonObjectUtility.filterDefaultValues(json, "TreeNode");
return json;
}
/**
* Returns a treeNode for the given nodeId, or null when no node is found for the given nodeId.
*/
public ITreeNode optTreeNodeForNodeId(String nodeId) {
return m_treeNodes.get(nodeId);
}
/**
* Returns a treeNode for the given nodeId.
*
* @throws UiException
* when no node is found for the given nodeId
*/
public ITreeNode getTreeNodeForNodeId(String nodeId) {
ITreeNode node = optTreeNodeForNodeId(nodeId);
if (node == null) {
throw new UiException("No node found for id " + nodeId);
}
return node;
}
public List<ITreeNode> extractTreeNodes(JSONObject json) {
JSONArray nodeIds = json.getJSONArray(PROP_NODE_IDS);
List<ITreeNode> nodes = new ArrayList<>(nodeIds.length());
for (int i = 0; i < nodeIds.length(); i++) {
ITreeNode node = optTreeNodeForNodeId(nodeIds.getString(i));
if (node != null) {
nodes.add(node);
}
}
return nodes;
}
@Override
public void handleUiEvent(JsonEvent event) {
if (EVENT_NODE_CLICK.equals(event.getType())) {
handleUiNodeClick(event);
}
else if (EVENT_NODE_ACTION.equals(event.getType())) {
handleUiNodeAction(event);
}
else if (EVENT_NODES_SELECTED.equals(event.getType())) {
handleUiNodesSelected(event);
}
else if (EVENT_NODE_EXPANDED.equals(event.getType())) {
handleUiNodeExpanded(event);
}
else if (EVENT_NODES_CHECKED.equals(event.getType())) {
handleUiNodesChecked(event);
}
else {
super.handleUiEvent(event);
}
}
protected void handleUiNodesChecked(JsonEvent event) {
CheckedInfo checkedInfo = jsonToCheckedInfo(event.getData());
addTreeEventFilterCondition(TreeEvent.TYPE_NODES_CHECKED).setCheckedNodes(checkedInfo.getCheckedNodes(), checkedInfo.getUncheckedNodes());
if (!checkedInfo.getCheckedNodes().isEmpty()) {
getModel().getUIFacade().setNodesCheckedFromUI(checkedInfo.getCheckedNodes(), true);
}
if (!checkedInfo.getUncheckedNodes().isEmpty()) {
getModel().getUIFacade().setNodesCheckedFromUI(checkedInfo.getUncheckedNodes(), false);
}
}
protected void handleUiNodeClick(JsonEvent event) {
String nodeId = event.getData().getString(PROP_NODE_ID);
ITreeNode node = optTreeNodeForNodeId(nodeId);
if (node == null) {
LOG.info("Requested tree-node with ID {} doesn't exist. Skip nodeClicked event", nodeId);
return;
}
getModel().getUIFacade().fireNodeClickFromUI(node, MouseButton.Left);
}
protected void handleUiNodeAction(JsonEvent event) {
String nodeId = event.getData().getString(PROP_NODE_ID);
ITreeNode node = optTreeNodeForNodeId(nodeId);
if (node == null) {
LOG.info("Requested tree-node with ID {} doesn't exist. Skip nodeAction event", nodeId);
return;
}
getModel().getUIFacade().fireNodeActionFromUI(node);
}
protected void handleUiNodesSelected(JsonEvent event) {
final List<ITreeNode> nodes = extractTreeNodes(event.getData());
if (nodes.isEmpty() && event.getData().getJSONArray(PROP_NODE_IDS).length() > 0) {
LOG.info("Ignored inconsistent selection event from UI: {} (current model selection: {})", event.getData(), nodeIdsToJson(getModel().getSelectedNodes(), false));
return;
}
addTreeEventFilterCondition(TreeEvent.TYPE_NODES_SELECTED).setNodes(nodes);
getModel().getUIFacade().setNodesSelectedFromUI(nodes);
}
protected void handleUiNodeExpanded(JsonEvent event) {
String nodeId = event.getData().getString(PROP_NODE_ID);
ITreeNode node = optTreeNodeForNodeId(nodeId);
if (node == null) {
LOG.info("Requested tree-node with ID {} doesn't exist. Skip nodeExpanded event", nodeId);
return;
}
boolean expanded = event.getData().getBoolean(PROP_EXPANDED);
boolean lazy = event.getData().getBoolean(PROP_EXPANDED_LAZY);
int eventType = expanded ? TreeEvent.TYPE_NODE_EXPANDED : TreeEvent.TYPE_NODE_COLLAPSED;
addTreeEventFilterCondition(eventType).setNodes(CollectionUtility.arrayList(node));
getModel().getUIFacade().setNodeExpandedFromUI(node, expanded, lazy);
}
@Override
protected void handleUiPropertyChange(String propertyName, JSONObject data) {
if (ITree.PROP_DISPLAY_STYLE.equals(propertyName)) {
String displayStyle = data.getString(propertyName);
addPropertyEventFilterCondition(propertyName, displayStyle);
getModel().getUIFacade().setDisplayStyleFromUI(displayStyle);
}
else {
super.handleUiPropertyChange(propertyName, data);
}
}
/**
* Ignore deleted or filtered nodes, because for the UI, they don't exist
*/
protected boolean isNodeAccepted(ITreeNode node) {
if (node.isStatusDeleted()) {
return false;
}
if (node.isFilterAccepted()) {
return true;
}
// Accept if rejected by user row filter because gui is and should be aware of that row
return node.isRejectedByUser();
}
/**
* @return the filtered node count excluding nodes filtered by the user
*/
protected int getFilteredNodeCount(ITreeNode parentNode) {
if (getModel().getNodeFilters().isEmpty()) {
return parentNode.getChildNodeCount();
}
int filteredNodeCount = 0;
for (ITreeNode node : parentNode.getChildNodes()) {
if (node.isFilterAccepted() || node.isRejectedByUser()) {
filteredNodeCount++;
}
}
return filteredNodeCount;
}
protected AbstractEventBuffer<TreeEvent> eventBuffer() {
return m_eventBuffer;
}
protected final TreeEventFilter getTreeEventFilter() {
return m_treeEventFilter;
}
protected TreeEventFilterCondition addTreeEventFilterCondition(int treeEventType) {
TreeEventFilterCondition condition = new TreeEventFilterCondition(treeEventType);
m_treeEventFilter.addCondition(condition);
return condition;
}
@Override
public void cleanUpEventFilters() {
super.cleanUpEventFilters();
m_treeEventFilter.removeAllConditions();
}
protected CheckedInfo jsonToCheckedInfo(JSONObject data) {
JSONArray jsonNodes = data.optJSONArray("nodes");
CheckedInfo checkInfo = new CheckedInfo();
for (int i = 0; i < jsonNodes.length(); i++) {
JSONObject jsonObject = jsonNodes.optJSONObject(i);
ITreeNode row = m_treeNodes.get(jsonObject.getString("nodeId"));
checkInfo.getAllNodes().add(row);
if (jsonObject.optBoolean("checked")) {
checkInfo.getCheckedNodes().add(row);
}
else {
checkInfo.getUncheckedNodes().add(row);
}
}
return checkInfo;
}
protected static class CheckedInfo {
private final List<ITreeNode> m_allNodes = new ArrayList<>();
private final List<ITreeNode> m_checkedNodes = new ArrayList<>();
private final List<ITreeNode> m_uncheckedNodes = new ArrayList<>();
public List<ITreeNode> getAllNodes() {
return m_allNodes;
}
public List<ITreeNode> getCheckedNodes() {
return m_checkedNodes;
}
public List<ITreeNode> getUncheckedNodes() {
return m_uncheckedNodes;
}
}
protected class P_TreeListener extends TreeAdapter {
@Override
public void treeChanged(final TreeEvent e) {
ModelJobs.assertModelThread();
handleModelTreeEvent(e);
}
}
protected class P_ChildNodesVisitor extends DepthFirstTreeVisitor<ITreeNode> {
private final Set<ITreeNode> m_nodes = new HashSet<>();
@Override
public TreeVisitResult preVisit(ITreeNode node, int level, int index) {
m_nodes.add(node);
return TreeVisitResult.CONTINUE;
}
public Set<ITreeNode> getNodes() {
return m_nodes;
}
}
}