/*
* Copyright (c) 2011, 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 jo-widgets.org 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.spi.impl.swt.common.widgets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.events.MenuDetectEvent;
import org.eclipse.swt.events.MenuDetectListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.TreeEvent;
import org.eclipse.swt.events.TreeListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.ToolTip;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.swt.widgets.Widget;
import org.jowidgets.common.color.ColorValue;
import org.jowidgets.common.color.IColorConstant;
import org.jowidgets.common.dnd.DropMode;
import org.jowidgets.common.image.IImageConstant;
import org.jowidgets.common.types.Markup;
import org.jowidgets.common.types.Position;
import org.jowidgets.common.types.SelectionPolicy;
import org.jowidgets.common.widgets.controller.ITreeNodeListener;
import org.jowidgets.spi.dnd.ITreeDropLocationSpi.TreeDropPositionSpi;
import org.jowidgets.spi.impl.controller.TreeSelectionObservableSpi;
import org.jowidgets.spi.impl.dnd.TreeDropLocationSpiImpl;
import org.jowidgets.spi.impl.swt.common.color.ColorCache;
import org.jowidgets.spi.impl.swt.common.dnd.IDropSelectionProvider;
import org.jowidgets.spi.impl.swt.common.image.SwtImageRegistry;
import org.jowidgets.spi.impl.swt.common.options.SwtOptions;
import org.jowidgets.spi.impl.swt.common.util.PositionConvert;
import org.jowidgets.spi.widgets.ITreeNodeSpi;
import org.jowidgets.spi.widgets.ITreeSpi;
import org.jowidgets.spi.widgets.controller.ITreeSelectionListenerSpi;
import org.jowidgets.spi.widgets.setup.ITreeSetupSpi;
import org.jowidgets.util.Assert;
public class TreeImpl extends SwtControl implements ITreeSpi, ITreeNodeSpi, IDropSelectionProvider {
static final ColorValue DISABLED_COLOR = new ColorValue(130, 130, 130);
private final SwtImageRegistry imageRegistry;
private final boolean multiSelection;
private final Map<TreeItem, TreeNodeImpl> items;
private final Set<TreeItem> uncheckableItems;
private final TreeSelectionObservableSpi treeObservable;
private final IColorConstant selectedForeground;
private final IColorConstant selectedBackground;
private final IColorConstant selectedBorder;
private final IColorConstant disabledSelectedForeground;
private final IColorConstant disabledSelectedBackground;
private final IColorConstant disabledSelectedBorder;
private ToolTip toolTip;
private List<TreeItem> lastSelection;
public TreeImpl(final Object parentUiReference, final ITreeSetupSpi setup, final SwtImageRegistry imageRegistry) {
super(new Tree((Composite) parentUiReference, getStyle(setup)), imageRegistry);
this.imageRegistry = imageRegistry;
this.lastSelection = new LinkedList<TreeItem>();
this.treeObservable = new TreeSelectionObservableSpi();
this.items = new HashMap<TreeItem, TreeNodeImpl>();
this.uncheckableItems = new HashSet<TreeItem>();
this.multiSelection = setup.getSelectionPolicy() == SelectionPolicy.MULTI_SELECTION;
this.selectedForeground = setup.getSelectedForegroundColor() != null
? setup.getSelectedForegroundColor() : SwtOptions.getTreeSelectedForegroundColor();
this.selectedBackground = setup.getSelectedBackgroundColor() != null
? setup.getSelectedBackgroundColor() : SwtOptions.getTreeSelectedBackgroundColor();
this.selectedBorder = setup.getSelectedBorderColor() != null
? setup.getSelectedBorderColor() : SwtOptions.getTreeSelectedBorderColor();
this.disabledSelectedForeground = setup.getDisabledSelectedForegroundColor() != null
? setup.getDisabledSelectedForegroundColor() : SwtOptions.getTreeDisabledSelectedForegroundColor();
this.disabledSelectedBackground = setup.getDisabledSelectedBackgroundColor() != null
? setup.getDisabledSelectedBackgroundColor() : SwtOptions.getTreeDisabledSelectedBackgroundColor();
this.disabledSelectedBorder = setup.getDisabledSelectedBorderColor() != null
? setup.getDisabledSelectedBorderColor() : SwtOptions.getTreeDisabledSelectedBorderColor();
setMenuDetectListener(new MenuDetectListener() {
@Override
public void menuDetected(final MenuDetectEvent e) {
final Point position = getUiReference().toControl(e.x, e.y);
final TreeItem item = getUiReference().getItem(position);
if (item == null) {
getPopupDetectionObservable().firePopupDetected(new Position(position.x, position.y));
}
else {
final TreeNodeImpl itemImpl = items.get(item);
itemImpl.firePopupDetected(new Position(position.x, position.y));
}
}
});
getUiReference().addTreeListener(new TreeListener() {
@Override
public void treeExpanded(final TreeEvent e) {
fireExpandedChanged(e, true);
}
@Override
public void treeCollapsed(final TreeEvent e) {
fireExpandedChanged(e, false);
}
private void fireExpandedChanged(final TreeEvent event, final boolean expanded) {
final TreeNodeImpl itemImpl = items.get(event.item);
if (itemImpl != null) {
itemImpl.fireExpandedChanged(expanded);
}
else {
throw new IllegalStateException("No item impl registered for item '"
+ event.item
+ "'. This seems to be a bug");
}
}
});
final SelectionListener selectionListener = new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent e) {
if (e.item.isDisposed()) {
return;
}
if (e.detail == SWT.CHECK) {
final TreeNodeImpl itemImpl = items.get(e.item);
if (itemImpl != null) {
if (itemImpl.isCheckable()) {
itemImpl.fireCheckedChanged(itemImpl.isChecked());
}
else {
itemImpl.getUiReference().setChecked(itemImpl.getUiReference().getGrayed());
}
}
else {
throw new IllegalStateException("No item impl registered for item '"
+ e.item
+ "'. This seems to be a bug");
}
}
else {
if (!multiSelection) {
getUiReference().removeSelectionListener(this);
if ((e.stateMask & SWT.CTRL) > 0) {
final TreeItem[] selection = getUiReference().getSelection();
if (selection != null && selection.length > 1) {
getUiReference().setSelection(new TreeItem[] {(TreeItem) e.item});
}
else {
getUiReference().setSelection(new TreeItem[] {});
}
}
else {
getUiReference().setSelection(new TreeItem[] {(TreeItem) e.item});
}
getUiReference().addSelectionListener(this);
}
fireSelectionChange(getUiReference().getSelection());
}
}
};
getUiReference().addSelectionListener(selectionListener);
// ToolTip support
try {
this.toolTip = new ToolTip(getUiReference().getShell(), SWT.NONE);
}
catch (final NoClassDefFoundError error) {
//TODO MG rwt has no tooltip, may use a window instead.
//(New rwt version supports tooltips)
}
if (toolTip != null) {
final ToolTipListener toolTipListener = new ToolTipListener();
final Tree tree = getUiReference();
tree.addListener(SWT.Dispose, toolTipListener);
tree.addListener(SWT.KeyDown, toolTipListener);
tree.addListener(SWT.MouseHover, toolTipListener);
tree.addListener(SWT.MouseMove, toolTipListener);
}
if (selectedForeground != null
|| selectedBackground != null
|| selectedBorder != null
|| disabledSelectedForeground != null
|| disabledSelectedBackground != null
|| disabledSelectedBorder != null) {
getUiReference().addListener(SWT.EraseItem, new CustomSelectionRenderingListener());
}
}
@Override
public Tree getUiReference() {
return (Tree) super.getUiReference();
}
@Override
public Object getDropSelection(final Widget item, final Position position, final int dropFeedback) {
final TreeNodeImpl node = items.get(item);
if (node != null) {
if ((dropFeedback & DND.FEEDBACK_INSERT_BEFORE) != 0) {
return new TreeDropLocationSpiImpl(node, TreeDropPositionSpi.BEFORE);
}
else if ((dropFeedback & DND.FEEDBACK_INSERT_AFTER) != 0) {
return new TreeDropLocationSpiImpl(node, TreeDropPositionSpi.AFTER);
}
else if ((dropFeedback & DND.FEEDBACK_SELECT) != 0) {
return new TreeDropLocationSpiImpl(node, TreeDropPositionSpi.ON);
}
}
return null;
}
@Override
public Integer getFeedback(final Widget widget, final Position position, final DropMode dropMode) {
final int result = DND.FEEDBACK_SCROLL | DND.FEEDBACK_EXPAND;
if (DropMode.SELECT.equals(dropMode)) {
return result | DND.FEEDBACK_SELECT;
}
else if (DropMode.SELECT_OR_INSERT.equals(dropMode)) {
if (widget instanceof TreeItem) {
final TreeItem item = (TreeItem) widget;
if (item.isDisposed()) {//pragmatic approach, no clear why swt throws events for disposed items
return result;
}
final int y = position.getY();
final Rectangle bounds = item.getBounds();
if (y < bounds.y + bounds.height / 3) {
return result | DND.FEEDBACK_INSERT_BEFORE;
}
else if (y > bounds.y + 2 * bounds.height / 3) {
return result | DND.FEEDBACK_INSERT_AFTER;
}
else {
return result | DND.FEEDBACK_SELECT;
}
}
}
else if (DropMode.INSERT.equals(dropMode)) {
if (widget instanceof TreeItem) {
final TreeItem item = (TreeItem) widget;
if (item.isDisposed()) {//pragmatic approach, no clear why swt throws events for disposed items
return result;
}
final int y = position.getY();
final Rectangle bounds = item.getBounds();
if (y < bounds.y + bounds.height / 2) {
return result | DND.FEEDBACK_INSERT_BEFORE;
}
else {
return result | DND.FEEDBACK_INSERT_AFTER;
}
}
}
return null;
}
@Override
public ITreeNodeSpi getRootNode() {
return this;
}
@Override
public List<ITreeNodeSpi> getSelectedNodes() {
final List<ITreeNodeSpi> result = new LinkedList<ITreeNodeSpi>();
for (final TreeItem item : getUiReference().getSelection()) {
result.add(items.get(item));
}
return result;
}
@Override
public ITreeNodeSpi addNode(final Integer index) {
final TreeNodeImpl result = new TreeNodeImpl(this, null, index, imageRegistry);
registerItem(result.getUiReference(), result);
return result;
}
@Override
public void removeNode(final int index) {
final TreeItem child = getUiReference().getItem(index);
if (child != null) {
if (isNodeSelected(child)) {
final TreeNodeImpl treeNode = items.get(child);
if (treeNode != null) {
setSelected(treeNode, false);
}
}
unRegisterItem(child);
child.dispose();
}
}
@Override
public ITreeNodeSpi getNodeAt(final Position position) {
Assert.paramNotNull(position, "position");
final TreeItem item = getUiReference().getItem(PositionConvert.convert(position));
if (item != null) {
return items.get(item);
}
else {
return null;
}
}
@Override
public void addTreeSelectionListener(final ITreeSelectionListenerSpi listener) {
treeObservable.addTreeSelectionListener(listener);
}
@Override
public void removeTreeSelectionListener(final ITreeSelectionListenerSpi listener) {
treeObservable.removeTreeSelectionListener(listener);
}
@Override
public void setMarkup(final Markup markup) {
throw new UnsupportedOperationException("setMarkup is not possible on the root node");
}
@Override
public void setExpanded(final boolean expanded) {
throw new UnsupportedOperationException("setExpanded is not possible on the root node");
}
@Override
public boolean isExpanded() {
throw new UnsupportedOperationException("isExpanded is not possible on the root node");
}
@Override
public void setSelected(final boolean selected) {
throw new UnsupportedOperationException("setSelected is not possible on the root node");
}
@Override
public boolean isSelected() {
throw new UnsupportedOperationException("isSelected is not possible on the root node");
}
@Override
public void setText(final String text) {
throw new UnsupportedOperationException("setText is not possible on the root node");
}
@Override
public void setToolTipText(final String text) {
throw new UnsupportedOperationException("setToolTipText is not possible on the root node");
}
@Override
public void setIcon(final IImageConstant icon) {
throw new UnsupportedOperationException("setIcon is not possible on the root node");
}
@Override
public void addTreeNodeListener(final ITreeNodeListener listener) {
throw new UnsupportedOperationException("addTreeNodeListener is not possible on the root node");
}
@Override
public void removeTreeNodeListener(final ITreeNodeListener listener) {
throw new UnsupportedOperationException("removeTreeNodeListener is not possible on the root node");
}
@Override
public void setChecked(final boolean checked) {
throw new UnsupportedOperationException("setChecked is not possible on the root node");
}
@Override
public boolean isChecked() {
throw new UnsupportedOperationException("isChecked is not possible on the root node");
}
@Override
public void setGreyed(final boolean greyed) {
throw new UnsupportedOperationException("setGreyed is not possible on the root node");
}
@Override
public boolean isGreyed() {
throw new UnsupportedOperationException("isGreyed is not possible on the root node");
}
@Override
public void setCheckable(final boolean checkable) {
throw new UnsupportedOperationException("setCheckable is not possible on the root node");
}
private void showToolTip(final String message) {
toolTip.setMessage(message);
final Point location = Display.getCurrent().getCursorLocation();
toolTip.setLocation(location.x + 16, location.y + 16);
toolTip.setVisible(true);
}
protected void setSelected(final TreeNodeImpl treeNode, final boolean selected) {
if (selected != treeNode.isSelected()) {
final TreeItem[] newSelection;
if (multiSelection) {
if (selected) {//add item to selection
TreeItem[] oldSelection = getUiReference().getSelection();
if (oldSelection == null) {
oldSelection = new TreeItem[0];
}
newSelection = new TreeItem[oldSelection.length + 1];
newSelection[0] = treeNode.getUiReference();
for (int i = 0; i < oldSelection.length; i++) {
newSelection[i + 1] = oldSelection[i];
}
}
else {//not selected, so remove item from selection
final TreeItem[] oldSelection = getUiReference().getSelection();
if (oldSelection == null || oldSelection.length == 0) {
//nothing to remove from, so return
return;
}
newSelection = new TreeItem[oldSelection.length - 1];
int offset = 0;
for (int i = 0; i < newSelection.length; i++) {
if (oldSelection[i] == treeNode.getUiReference()) {
offset = 1;
}
else {
newSelection[i] = oldSelection[i + offset];
}
}
}
}
else {//single selection
if (selected) {
newSelection = new TreeItem[1];
newSelection[0] = treeNode.getUiReference();
}
else {
newSelection = new TreeItem[0];
}
}
getUiReference().setSelection(newSelection);
fireSelectionChange(newSelection);
}
}
boolean isNodeSelected(final TreeItem item) {
final TreeNodeImpl treeNode = getTreeNodeItem(item);
if (treeNode != null) {
return treeNode.isSelected();
}
else {
return false;
}
}
protected TreeNodeImpl getTreeNodeItem(final TreeItem item) {
return items.get(item);
}
protected void registerItem(final TreeItem item, final TreeNodeImpl treeNodeImpl) {
items.put(item, treeNodeImpl);
}
protected void unRegisterItem(final TreeItem item) {
items.remove(item);
uncheckableItems.remove(item);
}
void addUncheckableItem(final TreeItem item) {
Assert.paramNotNull(item, "item");
uncheckableItems.add(item);
}
void removeUncheckableItem(final TreeItem item) {
Assert.paramNotNull(item, "item");
uncheckableItems.remove(item);
}
private void fireSelectionChange(final TreeItem[] newSelection) {
final List<TreeItem> newSelectionList = Arrays.asList(newSelection);
boolean selectionChanged = false;
for (final TreeItem wasSelected : lastSelection) {
if (!newSelectionList.contains(wasSelected)) {
final TreeNodeImpl treeNodeImpl = items.get(wasSelected);
if (treeNodeImpl != null) {
selectionChanged = true;
treeNodeImpl.fireSelectionChanged(false);
}
}
}
for (final TreeItem isSelected : newSelectionList) {
if (!lastSelection.contains(isSelected)) {
final TreeNodeImpl treeNodeImpl = items.get(isSelected);
if (treeNodeImpl != null) {
selectionChanged = true;
treeNodeImpl.fireSelectionChanged(true);
}
}
}
lastSelection = newSelectionList;
if (selectionChanged) {
treeObservable.fireSelectionChanged();
}
}
private static int getStyle(final ITreeSetupSpi setup) {
//do not use the single selection mode of SWT, it behaves strange!!!
//e.g. tree get auto selected when it get the focus
//selection could no disabled with clicking on item together with CTRL
//single selection will be simulated by selection listener
int result = SWT.MULTI;
if (setup.isChecked()) {
result = result | SWT.CHECK;
}
if (!setup.isContentScrolled()) {
result = result | SWT.NO_SCROLL;
}
return result;
}
final class ToolTipListener implements Listener {
@Override
public void handleEvent(final Event event) {
if (event.type == SWT.MouseHover) {
final TreeItem item = getUiReference().getItem(new Point(event.x, event.y));
if (item != null) {
final TreeNodeImpl itemImpl = items.get(item);
toolTip.setVisible(false);
if (itemImpl.getToolTipText() != null) {
showToolTip(itemImpl.getToolTipText());
}
}
}
else {
if (toolTip != null && !toolTip.isDisposed()) {
toolTip.setVisible(false);
}
}
}
}
private final class CustomSelectionRenderingListener implements Listener {
@Override
public void handleEvent(final Event event) {
final TreeItem item = (TreeItem) event.item;
final GC gc = event.gc;
final Color currentBackground = gc.getBackground();
final Color currentForeground = gc.getForeground();
final boolean checkable = !uncheckableItems.contains(item);
if ((event.detail & SWT.SELECTED) != 0) {
final Rectangle rect = item.getBounds(0);
if (selectedBackground != null && checkable) {
gc.setBackground(ColorCache.getInstance().getColor(selectedBackground));
gc.fillRectangle(rect);
event.detail &= ~SWT.SELECTED;
}
else if (disabledSelectedBackground != null && !checkable) {
gc.setBackground(ColorCache.getInstance().getColor(disabledSelectedBackground));
gc.fillRectangle(rect);
event.detail &= ~SWT.SELECTED;
}
else {
gc.setBackground(currentBackground);
}
if (selectedBorder != null && checkable) {
gc.setForeground(ColorCache.getInstance().getColor(selectedBorder));
gc.drawRectangle(rect.x, rect.y, rect.width, rect.height - 1);
event.detail &= ~SWT.SELECTED;
}
else if (disabledSelectedBorder != null && !checkable) {
gc.setForeground(ColorCache.getInstance().getColor(disabledSelectedBorder));
gc.drawRectangle(rect.x, rect.y, rect.width, rect.height - 1);
event.detail &= ~SWT.SELECTED;
}
if (selectedForeground != null && checkable) {
gc.setForeground(ColorCache.getInstance().getColor(selectedForeground));
event.detail &= ~SWT.SELECTED;
}
else if (disabledSelectedForeground != null && !checkable) {
gc.setForeground(ColorCache.getInstance().getColor(disabledSelectedForeground));
event.detail &= ~SWT.SELECTED;
}
else {
gc.setForeground(currentForeground);
}
}
else if ((event.detail & SWT.HOT) != 0) {
event.detail &= ~SWT.HOT;
return;
}
}
}
}