blob: 99a643812bfd73863968dd1633cae15c7ca1972b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015 Tasktop Technologies and others.
* 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:
* Tasktop Technologies - initial API and implementation
*******************************************************************************/
package org.eclipse.mylyn.internal.context.ui;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.mylyn.commons.ui.CommonImages;
import org.eclipse.mylyn.commons.ui.PlatformUiUtil;
import org.eclipse.mylyn.commons.workbench.WorkbenchUtil;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
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.ui.PlatformUI;
/**
* @author Mik Kersten
*/
public class FilteredChildrenDecorationDrawer implements Listener {
private final class MoveListener implements MouseMoveListener, MouseListener, MouseTrackListener {
private TreeItem lastItem;
private TreeItem currentItem;
private long lastMoveTime;
private long startMoveTime;
private final TreeViewer viewer;
private final BrowseFilteredListener browseFilteredListener;
private ToolTip toolTip;
public MoveListener(TreeViewer viewer, BrowseFilteredListener browseFilteredListener) {
this.viewer = viewer;
this.browseFilteredListener = browseFilteredListener;
}
public void mouseEnter(MouseEvent e) {
mouseMove(e);
}
public void mouseExit(MouseEvent e) {
if (lastItem != null && !lastItem.isDisposed()) {
lastItem.setData(ID_HOVER, NodeState.LESS);
}
if (toolTip != null && !toolTip.isDisposed()) {
toolTip.setVisible(false);
toolTip.dispose();
toolTip = null;
}
redrawTree(lastItem);
lastItem = null;
currentItem = null;
}
private void redrawTree(TreeItem item) {
if (viewer.getTree() != null && !viewer.getTree().isDisposed()) {
if (item != null && !item.isDisposed()) {
Rectangle bounds = item.getBounds();
Rectangle treeBounds = viewer.getTree().getBounds();
viewer.getTree().redraw(0, bounds.y, treeBounds.width, bounds.height, true);
} else {
viewer.getTree().redraw();
}
}
}
public void mouseHover(MouseEvent e) {
if (toolTip == null || toolTip.isDisposed()) {
toolTip = new ToolTip(WorkbenchUtil.getShell(), PlatformUiUtil.getSwtTooltipStyle());
}
if (toolTip != null && !toolTip.isDisposed()) {
Tree tree = (Tree) e.widget;
TreeItem item = findItem(tree, e.y);
if (item != null && !item.isDisposed()) {
Object data = item.getData(ID_HOVER);
if (data == NodeState.MORE) {
toolTip.setMessage(Messages.FilteredChildrenDecorationDrawer_Show_Filtered_Children);
} else {
toolTip.setMessage(Messages.FilteredChildrenDecorationDrawer_No_Filtered_Children);
}
if (inImageBounds(tree, item, e)) {
toolTip.setVisible(true);
}
}
}
}
private boolean inImageBounds(Tree tree, TreeItem item, MouseEvent e) {
int selectedX = e.x;
int imageStartX = getImageStartX(item.getBounds().x, item.getBounds().width, tree);
int imageEndX = imageStartX + moreImage.getBounds().width;
return selectedX > imageStartX && selectedX < imageEndX;
}
public void mouseMove(MouseEvent e) {
if (toolTip != null && !toolTip.isDisposed()) {
toolTip.setVisible(false);
toolTip.dispose();
toolTip = null;
}
if (!(e.widget instanceof Tree) || e.widget.isDisposed()) {
return;
}
Tree tree = (Tree) e.widget;
final TreeItem item = findItem(tree, e.y);
if (item != null && !item.isDisposed()) {
if (lastItem != null && !lastItem.isDisposed() && !lastItem.equals(item)) {
boolean redraw = lastItem.getData(ID_HOVER) != NodeState.LESS;
lastItem.setData(ID_HOVER, NodeState.LESS);
if (redraw) {// hide the + immediately
redrawTree(lastItem);
}
}
currentItem = item;
long currentTime = System.currentTimeMillis();
if (currentTime - lastMoveTime > 250) {// user paused movement
startMoveTime = currentTime;
}
lastMoveTime = currentTime;
// be responsive for small moves but delay more for bigger ones
int delay = Math.min(100, (int) ((currentTime - startMoveTime) / 4.0));
PlatformUI.getWorkbench().getDisplay().timerExec(delay, new Runnable() {
public void run() {// do nothing if we aren't using the most recent item
if (currentItem == item && !item.isDisposed()) {
if (item.getData(ID_HOVER) != NodeState.MORE_ERROR) {
item.setData(ID_HOVER, NodeState.MORE);
}
if (lastItem == null || (!lastItem.isDisposed() && !lastItem.equals(item))) {
redrawTree(lastItem);
redrawTree(item);
}
lastItem = item;
}
}
});
} else {
if (lastItem != null && !lastItem.isDisposed() && !lastItem.equals(item)) {
lastItem.setData(ID_HOVER, NodeState.LESS);
redrawTree(lastItem);
}
lastItem = item;
}
}
public void dispose() {
if (toolTip != null && !toolTip.isDisposed()) {
toolTip.dispose();
}
}
public void mouseDoubleClick(MouseEvent e) {
// ignore
}
public void mouseDown(MouseEvent e) {
if (toolTip != null && !toolTip.isDisposed()) {
toolTip.setVisible(false);
toolTip.dispose();
toolTip = null;
}
if (!(e.widget instanceof Tree) || e.widget.isDisposed()) {
// we only handle tree's
return;
}
Tree tree = (Tree) e.widget;
TreeItem item = findItem(tree, e.y);
if (item == null || item.isDisposed() || e.button != 1) {
// we can't do anything if we cant find the tree items
return;
}
int prevNumberItems = item.getItemCount();
boolean prevHasData = true;
if (prevNumberItems > 0) {
prevHasData = item.getItem(0).getData() != null;
}
if (inImageBounds(tree, item, e)) {
browseFilteredListener.setWasExternalClick(true);
browseFilteredListener.unfilterSelection(viewer, new StructuredSelection(item.getData()));
int newNumItems = item.getItemCount();
if (newNumItems == prevNumberItems && prevHasData) {
item.setData(ID_HOVER, NodeState.MORE_ERROR);
redrawTree(item);
}
}
}
public void mouseUp(MouseEvent e) {
// ignore
}
}
private static final int IMAGE_PADDING = 5;
// XXX Update Images
private final Image moreImage = CommonImages.getImage(CommonImages.EXPAND);
private final Image moreErrorImage = CommonImages.getImage(CommonImages.REMOVE);
enum NodeState {
MORE, LESS, MORE_ERROR
};
private static final String ID_HOVER = "mylyn-context-hover"; //$NON-NLS-1$
private final TreeViewer treeViewer;
private MoveListener listener;
private final BrowseFilteredListener browseFilteredListener;
public FilteredChildrenDecorationDrawer(TreeViewer treeViewer, BrowseFilteredListener browseFilteredListener) {
this.treeViewer = treeViewer;
this.browseFilteredListener = browseFilteredListener;
}
public boolean applyToTreeViewer() {
Object data = treeViewer.getData(FilteredChildrenDecorationDrawer.class.getName());
if (data == null || Boolean.FALSE.equals(data)) {
treeViewer.setData(FilteredChildrenDecorationDrawer.class.getName(), Boolean.TRUE);
if (treeViewer.getTree() != null && !treeViewer.getTree().isDisposed()) {
treeViewer.getTree().addListener(SWT.PaintItem, this);
listener = new MoveListener(treeViewer, browseFilteredListener);
treeViewer.getTree().addMouseMoveListener(listener);
treeViewer.getTree().addMouseListener(listener);
treeViewer.getTree().addMouseTrackListener(listener);
return true;
}
}
return false;
}
public void dispose() {
if (treeViewer.getTree() == null || treeViewer.getTree().isDisposed()) {
return;
}
treeViewer.setData(FilteredChildrenDecorationDrawer.class.getName(), Boolean.FALSE);
treeViewer.getTree().removeListener(SWT.PaintItem, this);
if (listener != null) {
treeViewer.getTree().removeMouseMoveListener(listener);
treeViewer.getTree().removeMouseListener(listener);
treeViewer.getTree().removeMouseTrackListener(listener);
listener.dispose();
}
}
/*
* NOTE: MeasureItem, PaintItem and EraseItem are called repeatedly.
* Therefore, it is critical for performance that these methods be as
* efficient as possible.
*/
public void handleEvent(Event event) {
if (!(event.widget instanceof Tree)) {
// we only handle tree's
return;
}
switch (event.type) {
case SWT.PaintItem: {
Tree tree = (Tree) event.widget;
if (tree.isDisposed() || event.index != 0) {
return;
}
TreeItem item = findItem(tree, event.y);
if (item == null || item.isDisposed()) {
return;
}
int imageStartX = getImageStartX(event.x, event.width, tree);
NodeState value = (NodeState) item.getData(ID_HOVER);
int imageStartY = event.y;
int imageHeight = moreImage.getBounds().height;
if (value != null && value.equals(NodeState.MORE_ERROR)) {
imageHeight = moreErrorImage.getBounds().height;
}
int offset = Math.round(((float) event.height) / 2 - ((float) imageHeight) / 2);
imageStartY += offset;
Rectangle clipping = event.gc.getClipping();
if (clipping.width < imageStartX && clipping.width > 0) {
clipping.width += IMAGE_PADDING + moreImage.getBounds().width;
event.gc.setClipping(clipping);
}
if (value != null && value.equals(NodeState.MORE)) {
event.gc.drawImage(moreImage, imageStartX, imageStartY);
} else if (value != null && value.equals(NodeState.MORE_ERROR)) {
event.gc.drawImage(moreErrorImage, imageStartX, imageStartY);
}
break;
}
}
}
private int getImageStartX(int x, int width, Tree tree) {
int imageStartX = x + width + IMAGE_PADDING;
int imageEndX = imageStartX + moreImage.getBounds().width;
Rectangle clientArea = tree.getClientArea();
int currentTreeBounds = clientArea.x + clientArea.width;
if (imageStartX > currentTreeBounds) {
imageStartX = currentTreeBounds - moreImage.getBounds().width;
}
float tolerance = 0;//moreImage.getBounds().width - (((float) moreImage.getBounds().width) / 3 * 2); // draw over item if more than 33% of the image is hidden
if (imageEndX > currentTreeBounds && imageEndX - currentTreeBounds > tolerance) {
imageStartX = currentTreeBounds - moreImage.getBounds().width;
}
return imageStartX;
}
private TreeItem findItem(Tree tree, int y) {
TreeItem item = null;
Point size = tree.getSize();
final int RATE = 17;
for (int i = 0; i <= RATE && item == null; i++) {
int position = size.x / RATE + (i * size.x / RATE);
item = tree.getItem(new Point(position, y));
}
return item;
}
}