/* * Copyright (c) 2014, Michael Grossmann * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the jo-widgets.org nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. */ package org.jowidgets.impl.widgets.composed; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import org.jowidgets.api.color.Colors; import org.jowidgets.api.controller.IDisposeListener; import org.jowidgets.api.model.tree.ITreeNodeModel; import org.jowidgets.api.model.tree.ITreeNodeModelListener; import org.jowidgets.api.types.CheckedState; import org.jowidgets.api.types.TreeAutoCheckPolicy; import org.jowidgets.api.types.TreeViewerCreationPolicy; import org.jowidgets.api.widgets.ITree; import org.jowidgets.api.widgets.ITreeContainer; import org.jowidgets.api.widgets.ITreeNode; import org.jowidgets.api.widgets.ITreeViewer; import org.jowidgets.api.widgets.descriptor.setup.ITreeViewerSetup; import org.jowidgets.common.types.Markup; import org.jowidgets.common.widgets.controller.ITreeNodeListener; import org.jowidgets.i18n.api.IMessage; import org.jowidgets.tools.controller.TreeNodeAdapter; import org.jowidgets.tools.model.tree.TreeNodeModelAdapter; import org.jowidgets.tools.widgets.wrapper.TreeWrapper; import org.jowidgets.util.Assert; import org.jowidgets.util.ICallback; import org.jowidgets.util.NullCompatibleEquivalence; public final class TreeViewerImpl<ROOT_NODE_VALUE_TYPE> extends TreeWrapper implements ITreeViewer<ROOT_NODE_VALUE_TYPE> { private static final String DUMMY_NODE_NAME = UUID.randomUUID().toString(); private static final IMessage MORE = Messages.getMessage("TreeViewerImpl.more"); private final ITreeNodeModel<ROOT_NODE_VALUE_TYPE> rootNodeModel; private final TreeViewerCreationPolicy creationPolicy; private final Integer pageSize; private final boolean autoCheckMode; private final TreeAutoCheckPolicy autoCheckPolicy; private final Map<ITreeContainer, ModelNodeBinding> bindings; public TreeViewerImpl(final ITree tree, final ITreeViewerSetup<ROOT_NODE_VALUE_TYPE> setup) { super(tree); Assert.paramNotNull(setup.getRootNodeModel(), "setup.getRootNodeModel()"); Assert.paramNotNull(setup.getCreationPolicy(), "setup.getCreationPolicy()"); this.rootNodeModel = setup.getRootNodeModel(); this.creationPolicy = setup.getCreationPolicy(); this.pageSize = setup.getPageSize(); this.autoCheckPolicy = setup.getAutoCheckPolicy(); this.autoCheckMode = autoCheckPolicy != TreeAutoCheckPolicy.OFF && setup.isChecked(); this.bindings = new HashMap<ITreeContainer, TreeViewerImpl<ROOT_NODE_VALUE_TYPE>.ModelNodeBinding>(); final ModelNodeBinding rootBinding = new ModelNodeBinding(tree, rootNodeModel); bindings.put(tree, rootBinding); rootNodeModel.addTreeNodeModelListener(new TreeNodeModelAdapter() { @Override public void dispose() { disposeBindings(); } }); tree.addDisposeListener(new IDisposeListener() { @Override public void onDispose() { disposeBindings(); } }); } private void disposeBindings() { for (final ModelNodeBinding binding : bindings.values()) { binding.dispose(); } } @Override public ITreeNodeModel<ROOT_NODE_VALUE_TYPE> getRootNodeModel() { return rootNodeModel; } private final class ModelNodeBinding { private final ITreeContainer parentNode; private final ITreeNodeModel<?> parentNodeModel; private final ITreeNodeModelListener dataListener; private final ITreeNodeModelListener childrenListener; private final ITreeNodeListener treeNodeListener; private ITreeNode pagingNode; private int currentPage; private ModelNodeBinding(final ITreeContainer parentNode, final ITreeNodeModel<?> parentNodeModel) { Assert.paramNotNull(parentNode, "parentNode"); Assert.paramNotNull(parentNodeModel, "parentNodeModel"); this.parentNode = parentNode; this.parentNodeModel = parentNodeModel; this.currentPage = 0; this.dataListener = new DataListener(); this.childrenListener = new ChildrenListener(); this.treeNodeListener = new TreeNodeListener(); if (parentNode instanceof ITreeNode) { final ITreeNode treeNode = (ITreeNode) parentNode; renderDataChanged(parentNodeModel, treeNode); renderSelectionChanged(parentNodeModel, treeNode); renderCheckedChanged(parentNodeModel, treeNode); if (TreeViewerCreationPolicy.CREATE_COMPLETE.equals(creationPolicy)) { onChildrenChanged(); } else if (parentNodeModel.getChildrenCount() > 0) { if (parentNodeModel.isExpanded()) { onChildrenChanged(); } else { final ITreeNode dummyNode = parentNode.addNode(); dummyNode.setText(DUMMY_NODE_NAME); dummyNode.addTreeNodeListener(new DummyNodeListener(dummyNode)); if (parentNodeModel.getChildrenCount() > 1) { //add a second dummy node for consistency with single path auto check mode parentNode.addNode().setText(DUMMY_NODE_NAME); } } } } else { onChildrenChanged(); } if (parentNode instanceof ITreeNode) { final ITreeNode treeNode = (ITreeNode) parentNode; renderExpansionChanged(parentNodeModel, treeNode); parentNodeModel.addTreeNodeModelListener(dataListener); treeNode.addTreeNodeListener(treeNodeListener); } parentNodeModel.addTreeNodeModelListener(childrenListener); } private ITreeNodeModel<?> getParentNodeModel() { return parentNodeModel; } private void onChildrenChanged() { final boolean wasExpanded; final CheckedState lastCheckedState; if (parentNode instanceof ITreeNode) { final ITreeNode treeNode = (ITreeNode) parentNode; wasExpanded = treeNode.isExpanded(); lastCheckedState = treeNode.getCheckedState(); } else { wasExpanded = false; lastCheckedState = null; } //Brute force, remove all nodes and add new ones removeChildren(); //than add the new nodes addChildren(null); if (parentNode instanceof ITreeNode) { final ITreeNode treeNode = (ITreeNode) parentNode; if (wasExpanded && !treeNode.isExpanded()) { treeNode.removeTreeNodeListener(treeNodeListener); treeNode.setExpanded(true); treeNode.addTreeNodeListener(treeNodeListener); } if (!NullCompatibleEquivalence.equals(lastCheckedState, treeNode.getCheckedState())) { treeNode.removeTreeNodeListener(treeNodeListener); treeNode.setCheckedState(lastCheckedState); treeNode.addTreeNodeListener(treeNodeListener); } } } private void eagerDisposeChildren() { final boolean wasExpanded; final CheckedState lastCheckedState; if (parentNode instanceof ITreeNode) { final ITreeNode treeNode = (ITreeNode) parentNode; wasExpanded = treeNode.isExpanded(); lastCheckedState = treeNode.getCheckedState(); } else { wasExpanded = false; lastCheckedState = null; } removeChildren(); if (parentNodeModel.getChildrenCount() > 0) { final ITreeNode dummyNode = parentNode.addNode(); dummyNode.setText(DUMMY_NODE_NAME); } if (parentNode instanceof ITreeNode) { final ITreeNode treeNode = (ITreeNode) parentNode; if (wasExpanded && !treeNode.isExpanded()) { treeNode.removeTreeNodeListener(treeNodeListener); treeNode.setExpanded(true); treeNode.addTreeNodeListener(treeNodeListener); } if (!NullCompatibleEquivalence.equals(lastCheckedState, treeNode.getCheckedState())) { treeNode.removeTreeNodeListener(treeNodeListener); treeNode.setCheckedState(lastCheckedState); treeNode.addTreeNodeListener(treeNodeListener); } } } private void removeChildren() { for (final ITreeNode childNode : parentNode.getChildren()) { final ModelNodeBinding binding = bindings.remove(childNode); if (binding != null) { final ITreeNodeModel<?> childNodeModel = binding.getParentNodeModel(); renderDisposeNode(childNodeModel, childNode); binding.dispose(); } } parentNode.removeAllNodes(); currentPage = 0; pagingNode = null; } private void addChildren(final CheckedState checkedState) { final int pageSizeInt = pageSize != null ? pageSize.intValue() : Integer.MAX_VALUE; final int pageStart = currentPage * pageSizeInt; final int residualNodes = parentNodeModel.getChildrenCount() - pageStart; if (residualNodes < 0) { //no nodes to add return; } final int pageSizeTrunc = Math.min(residualNodes, pageSizeInt); final int pageEnd = pageStart + pageSizeTrunc; for (int i = pageStart; i < pageEnd; i++) { final ITreeNodeModel<?> childNodeModel = parentNodeModel.getChildNode(i); if (checkedState != null) { childNodeModel.setCheckedState(checkedState); } final ITreeNode childNode = parentNode.addNode(); renderNodeCreated(childNodeModel, childNode); final ModelNodeBinding childBinding = new ModelNodeBinding(childNode, childNodeModel); bindings.put(childNode, childBinding); if (childNodeModel.isExpanded()) { childNode.setExpanded(true); } if (!childNodeModel.isCheckable()) { childNode.setCheckable(false); } childNode.setCheckedState(childNodeModel.getCheckedState()); if (childNodeModel.isSelected()) { childNode.setSelected(true); } } currentPage++; if (residualNodes > pageSizeInt) { this.pagingNode = createPagingNode(checkedState); } } private ITreeNode createPagingNode(final CheckedState checkedState) { final ITreeNode result = parentNode.addNode(); result.setMarkup(Markup.EMPHASIZED); result.setForegroundColor(Colors.STRONG); result.setText(MORE.get()); if (checkedState != null) { result.setCheckedState(checkedState); } result.addTreeNodeListener(new TreeNodeAdapter() { @Override public void selectionChanged(final boolean selected) { loadNextPage(); } private void loadNextPage() { final CheckedState currentCheckedState = autoCheckMode ? result.getCheckedState() : null; parentNode.removeNode(pagingNode); addChildren(currentCheckedState); } }); return result; } private boolean hasDummyChildNode() { final List<ITreeNode> children = parentNode.getChildren(); return children.size() > 0 && DUMMY_NODE_NAME.equals(children.iterator().next().getText()); } @SuppressWarnings({"rawtypes", "unchecked"}) private void renderNodeCreated(final ITreeNodeModel model, final ITreeNode node) { model.getRenderer().nodeCreated(model.getData(), node); } @SuppressWarnings({"rawtypes", "unchecked"}) private void renderDataChanged(final ITreeNodeModel model, final ITreeNode node) { model.getRenderer().dataChanged(model.getData(), node); } @SuppressWarnings({"rawtypes", "unchecked"}) private void renderDisposeNode(final ITreeNodeModel model, final ITreeNode node) { model.getRenderer().disposeNode(model.getData(), node); } @SuppressWarnings({"rawtypes", "unchecked"}) private void renderSelectionChanged(final ITreeNodeModel model, final ITreeNode node) { model.getRenderer().selectionChanged(model.getData(), node); } @SuppressWarnings({"rawtypes", "unchecked"}) private void renderCheckedChanged(final ITreeNodeModel model, final ITreeNode node) { model.getRenderer().checkedChanged(model.getData(), node); } @SuppressWarnings({"rawtypes", "unchecked"}) private void renderExpansionChanged(final ITreeNodeModel model, final ITreeNode node) { model.getRenderer().expansionChanged(model.getData(), node); } private void dispose() { if (parentNode instanceof ITreeNode) { parentNodeModel.removeTreeNodeModelListener(dataListener); ((ITreeNode) parentNode).removeTreeNodeListener(treeNodeListener); } parentNodeModel.removeTreeNodeModelListener(childrenListener); } private final class DataListener extends TreeNodeModelAdapter { @Override public void dataChanged() { renderDataChanged(parentNodeModel, (ITreeNode) parentNode); } @Override public void checkableChanged() { if (parentNode instanceof ITreeNode) { ((ITreeNode) parentNode).setCheckable(parentNodeModel.isCheckable()); } } } private final class ChildrenListener extends TreeNodeModelAdapter { @Override public void childrenChanged() { onChildrenChanged(); } } private final class TreeNodeListener extends TreeNodeAdapter { @Override public void selectionChanged(final boolean selected) { parentNodeModel.setSelected(selected); renderSelectionChanged(parentNodeModel, (ITreeNode) parentNode); } @Override public void expandedChanged(final boolean expanded) { parentNodeModel.setExpanded(expanded); renderExpansionChanged(parentNodeModel, (ITreeNode) parentNode); if (expanded && hasDummyChildNode()) { onChildrenChanged(); } else if (!expanded && !hasDummyChildNode() && TreeViewerCreationPolicy.CREATE_ON_EXPAND_DISPOSE_ON_COLLAPSE.equals(creationPolicy)) { eagerDisposeChildren(); } } @Override public void checkedChanged(final boolean checked) { final ITreeNode treeNode = (ITreeNode) parentNode; parentNodeModel.setCheckedState(treeNode.getCheckedState()); renderCheckedChanged(parentNodeModel, treeNode); } } private final class DummyNodeListener extends TreeNodeAdapter { private final ITreeNode dummyNode; private DummyNodeListener(final ITreeNode dummyNode) { Assert.paramNotNull(dummyNode, "dummyNode"); this.dummyNode = dummyNode; onCheckedChanged(); } @Override public void checkedChanged(final boolean checked) { onCheckedChanged(); } private void onCheckedChanged() { final CheckedState checkedState = dummyNode.getCheckedState(); if (CheckedState.UNCHECKED.equals(checkedState)) { invokeOnChildren(parentNodeModel, false, new ICallback<ITreeNodeModel<?>>() { @Override public void call(final ITreeNodeModel<?> model) { model.setCheckedState(CheckedState.UNCHECKED); } }); } else if (CheckedState.CHECKED.equals(checkedState)) { final boolean firstChildOnly = TreeAutoCheckPolicy.SINGLE_PATH.equals(autoCheckPolicy); invokeOnChildren(parentNodeModel, firstChildOnly, new ICallback<ITreeNodeModel<?>>() { @Override public void call(final ITreeNodeModel<?> model) { model.setCheckedState(CheckedState.CHECKED); } }); } } private void invokeOnChildren( final ITreeNodeModel<?> node, final boolean firstChildOnly, final ICallback<ITreeNodeModel<?>> callback) { for (int i = 0; i < node.getChildrenCount(); i++) { final ITreeNodeModel<?> childNode = node.getChildNode(i); if (childNode.isCheckable()) { callback.call(childNode); invokeOnChildren(childNode, firstChildOnly, callback); if (firstChildOnly) { break; } } } } } } }