/*******************************************************************************
* Copyright 2006, CHISEL Group, University of Victoria, Victoria, BC, Canada.
* 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:
* The Chisel Group, University of Victoria
*******************************************************************************/
package ca.uvic.cs.tagsea.editing;
import java.util.ArrayList;
import java.util.Iterator;
import org.eclipse.jface.bindings.keys.IKeyLookup;
import org.eclipse.jface.bindings.keys.KeyLookupFactory;
import org.eclipse.jface.dialogs.IInputValidator;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSource;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DragSourceListener;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetAdapter;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Item;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import ca.uvic.cs.tagsea.core.Tag;
import ca.uvic.cs.tagsea.editing.events.ItemDragEvent;
import ca.uvic.cs.tagsea.editing.events.TreeItemCopyEvent;
import ca.uvic.cs.tagsea.editing.events.TreeItemDeleteEvent;
import ca.uvic.cs.tagsea.editing.events.TreeItemListener;
import ca.uvic.cs.tagsea.editing.events.TreeItemMoveEvent;
/**
* Sets up drag and dropping of nodes within an SWT Tree.
* Also handles renaming and deleting TreeItems.
*
* TODO should this class be changed to work with TreeViewers instead of Trees?
*
* @author Chris Callendar
*/
public class TreeItemWorker {
private TreeViewer treeViewer = null;
private Tree tree;
private static Item[] draggedItems = new Item[0];
private int dropDetail = DND.DROP_MOVE;
private InlineTreeItemRenamer renamer;
private ArrayList<TreeItemListener> listeners;
private boolean childOrderImportant = true;
/**
* Initializes the worker to handle drag and drop, inline rename, and deletion.
* Sets up the drag source and the drag target as the tree.
* This constructor takes a tree viewer which is used to update selections.
* @param treeViewer the tree viewer - its tree will be monitored for drags, renames, and deletions
* @param childOrderImportant if the index of a Tree or a TreeItem's children matters.
* If this is false then moving a TreeItem to the same parent (different position) won't do anything.
*/
public TreeItemWorker(TreeViewer treeViewer, boolean childOrderImportant) {
this(treeViewer.getTree(), childOrderImportant);
this.treeViewer = treeViewer;
}
/**
* Initializes the drag operation.
* Sets up the drag source and the drag target as the tree.
* @param tree the tree which will be monitored for drags, renames, and deletions
* @param childOrderImportant if the index of a Tree or a TreeItem's children matters.
* If this is false then moving a TreeItem to the same parent (different position) won't do anything.
*/
public TreeItemWorker(Tree tree, boolean childOrderImportant) {
this.tree = tree;
this.listeners = new ArrayList<TreeItemListener>();
this.renamer = new InlineTreeItemRenamer(tree);
this.childOrderImportant = childOrderImportant;
createDragSource();
createDragTarget();
addKeyListener();
}
/** Adds a listener to the list. */
public void addListener(TreeItemListener listener) {
listeners.add(listener);
renamer.addListener(listener);
}
/** Removes a listener from the list. */
public void removeListener(TreeItemListener listener) {
listeners.remove(listener);
renamer.addListener(listener);
}
public boolean isChildOrderImportant() {
return childOrderImportant;
}
/**
* Sets the validator which determines if a rename is valid.
* @param validator
*/
public void setRenameValidator(IInputValidator validator) {
renamer.setValidator(validator);
}
/**
* Selects the given item and shows the inline tree editor.
* @param item the item to rename
*/
public void renameTreeItem(TreeItem item) {
boolean doit = RefactorHelp.requestSave(new Tag[] {(Tag)item.getData()});
if (!doit) return;
renamer.renameTreeItem(item);
}
/**
* Creates a new TreeItem from the existing item.
* @param item the item to copy
* @param parent the parent (can be null)
* @param index the index to insert into the parent
* @return TreeItem
*/
public TreeItem copyItem(Item item, TreeItem parent, int index) {
if (item.getData() instanceof Tag) {
boolean doit = RefactorHelp.requestSave(new Tag[] {(Tag)item.getData()});
if (!doit) return null;
}
TreeItem newItem = copy(item, parent, index);
tree.showItem(newItem);
TreeItemCopyEvent copyEvent = new TreeItemCopyEvent(item, newItem);
fireAfterCopyEvent(copyEvent);
return newItem;
}
/**
* Deletes the given tree item. An event is fired before and after the deletion, and then the
* finishedDelete() method is called. This way the listeners have the choice of cancelling
* the event. The tree item can be disposed afterwards.
* @param item the item to delete
* @param dispose if the tree items should be disposed
*/
public void deleteTreeItem(TreeItem item, boolean dispose) {
deleteTreeItems(new TreeItem[] { item }, dispose);
}
/**
* Deletes the given tree items. An event is fired before and after each deletion, and after
* all have been deleted then finishedDelete() method is called. This way the listeners have
* the choice of cancelling the event. The tree item can be disposed afterwards.
* @param items the items to delete
* @param dispose if the tree items should be disposed
*/
public void deleteTreeItems(TreeItem[] items, boolean dispose) {
ArrayList<Tag> tags = new ArrayList<Tag>();
for (TreeItem item : items) {
if (item.getData() instanceof Tag)
tags.add((Tag)item.getData());
}
Tag[] tagsArray = new Tag[tags.size()];
tags.toArray(tagsArray);
if (tagsArray.length > 0) {
boolean doit = RefactorHelp.requestSave(tagsArray);
if (!doit) return;
}
for (TreeItem item : items) {
deleteTreeItemInternal(item, dispose);
}
setSelection(new TreeItem[0]);
fireFinishedDelete();
}
/**
* Doesn't fire the finishedDelete() event or set the selection.
* @param item the item to delete
* @param dispose
*/
private void deleteTreeItemInternal(TreeItem item, boolean dispose) {
if ((item != null) && !item.isDisposed()) {
TreeItemDeleteEvent event = new TreeItemDeleteEvent(item);
fireBeforeDeleteEvent(event);
if (event.doit) {
if (dispose) {
/*
if (item.getImage() != null) {
item.getImage().dispose();
}
*/
item.dispose();
}
fireAfterDeleteEvent(event);
}
}
}
/**
* Moves a tree item to be a root item (directly under the Tree)
* It the item is already a root item it is returned.
* @param item the item to move (not disposed)
* @param disposeItem if the old item should be disposed after the move (if a new item is created)
* @return TreeItem the new item
*/
public TreeItem moveTreeItemToRoot(TreeItem item, boolean disposeItem) {
TreeItem newItem = item;
if (item != null && !item.isDisposed()) {
if (item.getParentItem() != null) {
newItem = moveTreeItem(null, tree.getItemCount(), item, disposeItem);
} else {
//System.out.println("Already a root node");
}
}
return newItem;
}
/**
* Moves a tree item to the end of the list of items of the parent.
* If the parent is null then it is added to the Tree (root).
* @param parent The parent tree item, or null to use the Tree as the parent
* @param item The old item which is being moved
* @param disposeItem if the old item should be disposed after the move (a new item is created)
* @return TreeItem the new item
*/
public TreeItem moveTreeItem(TreeItem parent, TreeItem item, boolean disposeItem) {
TreeItem newItem = item;
if (item != null) {
if (parent != null) {
newItem = moveTreeItem(parent, parent.getItemCount(), item, disposeItem);
} else {
newItem = moveTreeItem(null, tree.getItemCount(), item, disposeItem);
}
}
return newItem;
}
/**
* Moves a tree item. Its parent will be the given parent TreeItem if not null,
* otherwise its parent will be the Tree.
* The text, image, and data are copied into the new item from the old item.
* The children of the old item are also created.
* @param parent The parent tree item, or null to use the Tree as the parent
* @param index The position to insert
* @param item The old item which is being moved
* @param disposeItem if the old item should be disposed after the move (a new item is created)
* @return TreeITem the new item
*/
public TreeItem moveTreeItem(TreeItem parent, int index, TreeItem item, boolean disposeItem) {
if ((item == null) || item.isDisposed())
return item;
// check if we need to move
TreeItem oldParent = item.getParentItem();
// parent is a child of the item (or parent is the item)?
// The above drag'n'drop code shouldn't allow this, but anyone can call this method
TreeItem check = parent;
while (check != null) {
if (check == item) {
//System.out.println("Parent is a child of the item");
return item;
}
check = check.getParentItem();
}
// check for same parent, then if the order matters check the position/index
if (/*(oldParent != null) && (parent != null) &&*/ (oldParent == parent)) {
if (!childOrderImportant) {
// same parent - no need to move
return item;
} else {
// check if position has changed
int pos = (item.getParentItem() == null ? tree.indexOf(item) : item.getParentItem().indexOf(item));
if ((index == pos) || (index == (pos + 1))) {
return item;
}
}
}
// already a root node?
if ((oldParent == null) && (parent == null)) {
//System.out.println("No need to move - already a root");
return item;
}
// ok, proceed with the move (really a create/delete)
// create the move event for the old item
TreeItemMoveEvent event = new TreeItemMoveEvent(item);
if (event.data instanceof Tag) {
Tag tag = (Tag) event.data;
event.doit = RefactorHelp.requestSave(new Tag[] {tag});
}
if (!event.doit) return item;
fireBeforeMovedEvent(event);
// if the event was cancelled then we don't create a new TreeItem
if (!event.doit)
return item;
// okay the event wasn't cancelled, so create the new item
TreeItem newItem = copy(item, parent, index);
// create the move event for the new item
event = new TreeItemMoveEvent(newItem);
// recursively add the children
addChildren(newItem, item.getItems());
// now ensure this new item is visible and maybe expanded
newItem.setExpanded(item.getExpanded());
tree.showItem(newItem);
setSelection(new TreeItem[] { newItem });
// notify listeners of move event
fireAfterMovedEvent(event);
if (disposeItem) {
item.dispose();
}
return newItem;
}
/**
* Creates a new TreeItem from the given item, copying over the text,
* image, and data. Then a TreeItemCopyEvent is fired giving any listeners
* the opportunity to copy over keyed data.
* @param item the item to copy
* @param parent the parent for the new item
* @param index the index for the new item in the parent's list of children
* @return TreeItem the created item
*/
private TreeItem copy(Item item, TreeItem parent, int index) {
TreeItem newItem;
if (parent == null) {
newItem = new TreeItem(tree, SWT.NONE, index);
} else {
newItem = new TreeItem(parent, SWT.NONE, index);
}
newItem.setText(item.getText());
newItem.setImage(copyImage16(item));
// can't copy keyed data!
newItem.setData(item.getData());
// send copy event to let the listener copy over any keyed data
TreeItemCopyEvent event = new TreeItemCopyEvent(item, newItem);
fireCopyDataEvent(event);
return newItem;
}
/**
* Returns the 16x16 image from the item (if not null or disposed).
* TreeItem objects can only have 16x16 images.
* @param item the item whose image will be returned.
* @return Image (16x16)
*/
private Image copyImage16(Item item) {
Image img = null;
if ((item.getImage() != null) && !item.getImage().isDisposed()) {
img = item.getImage();
Image img16 = new Image(img.getDevice(), 16, 16);
GC gc = new GC(img16);
gc.drawImage(img, 0, 0, 16, 16, 0, 0, 16, 16);
img = img16;
gc.dispose();
}
return img;
}
/**
* Recursively adds all the children items.
* @param item the new item to add children to
* @param children the children to add
*/
private void addChildren(TreeItem item, TreeItem[] children) {
for (int i = 0; i < children.length; i++) {
TreeItem oldChild = children[i];
TreeItem child = new TreeItem(item, SWT.NONE);
child.setText(oldChild.getText());
child.setImage(oldChild.getImage());
child.setData(oldChild.getData());
addChildren(child, oldChild.getItems());
// have to set expanded after the children are added
child.setExpanded(oldChild.getExpanded());
}
}
protected void fireBeforeMovedEvent(TreeItemMoveEvent event) {
for (Iterator<TreeItemListener> iter = listeners.iterator(); iter.hasNext(); ) {
iter.next().beforeMove(event);
if (!event.doit)
break;
}
}
protected void fireCopyDataEvent(TreeItemCopyEvent event) {
for (Iterator<TreeItemListener> iter = listeners.iterator(); iter.hasNext(); ) {
iter.next().copyData(event);
if (!event.doit)
break;
}
}
protected void fireAfterCopyEvent(TreeItemCopyEvent event) {
for (Iterator<TreeItemListener> iter = listeners.iterator(); iter.hasNext(); ) {
iter.next().afterCopy(event);
}
}
protected void fireAfterMovedEvent(TreeItemMoveEvent event) {
for (Iterator<TreeItemListener> iter = listeners.iterator(); iter.hasNext(); ) {
iter.next().afterMove(event);
}
}
protected void fireBeforeDeleteEvent(TreeItemDeleteEvent event) {
for (Iterator<TreeItemListener> iter = listeners.iterator(); iter.hasNext(); ) {
iter.next().beforeDelete(event);
if (!event.doit)
break;
}
}
protected void fireAfterDeleteEvent(TreeItemDeleteEvent event) {
for (Iterator<TreeItemListener> iter = listeners.iterator(); iter.hasNext(); ) {
iter.next().afterDelete(event);
}
}
protected void fireDragStartEvent(ItemDragEvent event) {
for (Iterator<TreeItemListener> iter = listeners.iterator(); iter.hasNext(); ) {
iter.next().dragStart(event);
}
}
protected void fireDragOverEvent(ItemDragEvent event) {
for (Iterator<TreeItemListener> iter = listeners.iterator(); iter.hasNext(); ) {
iter.next().dragOver(event);
}
}
protected void fireDragDropEvent(ItemDragEvent event) {
for (Iterator<TreeItemListener> iter = listeners.iterator(); iter.hasNext(); ) {
iter.next().drop(event);
}
}
protected void fireFinishedMove(Item[] movedData) {
for (Iterator<TreeItemListener> iter = listeners.iterator(); iter.hasNext(); ) {
iter.next().finishedMove(movedData);
}
}
protected void fireFinishedDelete() {
for (Iterator<TreeItemListener> iter = listeners.iterator(); iter.hasNext(); ) {
iter.next().finishedDelete();
}
}
/**
* Adds a drag source and listener to the Table.
* This allows you to drag TableItems onto the tree.
*/
public void addTableDragSource(final Table table)
{
//@tag bug(1522066) : temporary fix until the model is updated.
if (table.getData("DragSource") != null) return; //$NON-NLS$
DragSource source = new DragSource(table, DND.DROP_COPY);
source.setTransfer(new Transfer[] { TextTransfer.getInstance() });
source.addDragListener(new DragSourceListener() {
/** Starts the drag - sets the draggedItem. */
public void dragStart(DragSourceEvent event) {
TableItem[] selection = table.getSelection();
if (selection.length > 0) {
draggedItems = selection;
dropDetail = DND.DROP_COPY;
ItemDragEvent dragEvent = new ItemDragEvent(draggedItems);
dragEvent.dragType = dropDetail;
fireDragStartEvent(dragEvent);
event.doit = dragEvent.doit;
if (!dragEvent.doit) {
draggedItems = new Item[0];
}
} else {
event.doit = false;
}
}
public void dragSetData(DragSourceEvent event) {
if (draggedItems.length > 0) {
event.data = draggedItems[0].getText();
}
}
public void dragFinished(DragSourceEvent event) {
draggedItems = new Item[0];
}
});
}
/**
* Creates the drag source and listener for the Tree.
*/
private void createDragSource()
{
DragSource source = new DragSource(tree, DND.DROP_MOVE);
source.setTransfer(new Transfer[] { TextTransfer.getInstance() });
source.addDragListener(new DragSourceListener() {
/** Starts the drag - sets the draggedItem. */
public void dragStart(DragSourceEvent event)
{
renamer.stopEditting(); // stop any inline editing
TreeItem[] selection = tree.getSelection();
if (selection.length > 0)
{
draggedItems = selection;
dropDetail = DND.DROP_MOVE;
ItemDragEvent dragEvent = new ItemDragEvent(draggedItems);
dragEvent.dragType = dropDetail;
fireDragStartEvent(dragEvent);
event.doit = dragEvent.doit;
if (!dragEvent.doit)
{
draggedItems = new Item[0];
}
}
else
{
event.doit = false;
}
}
public void dragSetData(DragSourceEvent event) {
if (draggedItems.length > 0) {
event.data = draggedItems[0].getText();
}
}
public void dragFinished(DragSourceEvent event) {
draggedItems = new Item[0];
}
});
}
/**
* Creates the drop target and the drop listener.
*/
private void createDragTarget()
{
DropTarget target = new DropTarget(tree, DND.DROP_MOVE | DND.DROP_COPY);
target.setTransfer(new Transfer[] { TextTransfer.getInstance() });
target.addDropListener(new DropTargetAdapter() {
/** Sets up the drop feedback. */
public void dragOver(DropTargetEvent event) {
event.feedback = DND.FEEDBACK_EXPAND | DND.FEEDBACK_SCROLL;
if (event.item != null) {
TreeItem item = (TreeItem)event.item;
// check if the target item is a child (or the same) of the draggedItems
TreeItem check = item;
while (check != null) {
for (Item draggedItem : draggedItems) {
if (check == draggedItem) {
event.detail = DND.DROP_NONE;
return;
}
}
check = check.getParentItem();
}
TreeItem target = item.getParentItem();
Point pt = Display.getDefault().map(null, tree, event.x, event.y);
Rectangle bounds = item.getBounds();
int feedback;
if (pt.y < bounds.y + bounds.height/3) {
feedback = DND.FEEDBACK_INSERT_BEFORE;
} else if (pt.y > bounds.y + 2*bounds.height/3) {
feedback = DND.FEEDBACK_INSERT_AFTER;
} else {
target = item;
feedback = DND.FEEDBACK_SELECT;
}
// fire the dragOver event
ItemDragEvent dragEvent = new ItemDragEvent(draggedItems, target);
dragEvent.dragType = dropDetail;
fireDragOverEvent(dragEvent);
if (dragEvent.doit) {
event.detail = dropDetail; // dropDetail set in drag start (copy/move)
event.feedback |= feedback;
} else {
event.detail = DND.DROP_NONE;
}
} else {
event.detail = DND.DROP_NONE;
}
}
/** Handles the drop event. */
public void drop(DropTargetEvent event) {
if (event.data == null) {
event.detail = DND.DROP_NONE;
return;
}
ItemDragEvent dragEvent = new ItemDragEvent(draggedItems, (TreeItem)event.item);
dragEvent.dragType = dropDetail;
fireDragDropEvent(dragEvent);
if (!dragEvent.doit) {
event.detail = DND.DROP_NONE;
return;
}
if (event.item == null) {
for (Item draggedItem : draggedItems) {
if (event.detail == DND.DROP_MOVE) {
moveTreeItem(null, (TreeItem)draggedItem, true);
} else if (event.detail == DND.DROP_COPY) {
copyItem(draggedItem, null, tree.getItemCount());
}
}
fireFinishedMove(draggedItems);
} else {
TreeItem item = (TreeItem)event.item;
Point pt = Display.getDefault().map(null, tree, event.x, event.y);
Rectangle bounds = item.getBounds();
// check if the position is above or below the item
boolean insertBefore = (pt.y < bounds.y + bounds.height/3);
boolean insertAfter = (pt.y > bounds.y + 2*bounds.height/3);
TreeItem parent = item.getParentItem();
TreeItem[] items = (parent != null ? parent.getItems() : tree.getItems());
int index = items.length;
for (int i = 0; i < items.length; i++) {
if (items[i] == item) {
index = i;
break;
}
}
// update the index and parent vars
if (insertBefore) {
} else if (insertAfter) {
index = index + 1;
} else {
parent = item;
index = parent.getItemCount();
}
for (Item draggedItem : draggedItems) {
if (event.detail == DND.DROP_MOVE) {
moveTreeItem(parent, index, (TreeItem)draggedItem, true);
} else if (event.detail == DND.DROP_COPY) {
copyItem(draggedItem, parent, index);
}
index++;
}
fireFinishedMove(draggedItems);
}
}
});
}
/**
* Adds a key listener to watch for delete events.
*/
private void addKeyListener() {
final IKeyLookup lookup = KeyLookupFactory.getSWTKeyLookup();
final int DEL = lookup.formalKeyLookup(IKeyLookup.DEL_NAME);
final int DEL2 = lookup.formalKeyLookup(IKeyLookup.DELETE_NAME);
tree.addKeyListener(new KeyAdapter() {
public void keyReleased(KeyEvent e) {
int code = e.keyCode;
if (code == DEL || code == DEL2) {
TreeItem[] selection = tree.getSelection();
deleteTreeItems(selection, true);
}
}
});
}
/**
* Sets the selection on the tree.
* Also updates the TreeViewer selection if it is not null.
* The tree viewer's selection will be the data Object from each TreeItem.
* @param selectedItems the tree items that are to be selected
*/
protected void setSelection(TreeItem[] selectedItems) {
tree.setSelection(selectedItems);
if (treeViewer != null) {
ArrayList<Object> selectedData = new ArrayList<Object>(selectedItems.length);
for (TreeItem item : selectedItems) {
if ((item != null) && !item.isDisposed()) {
Object data = item.getData();
if (data != null) {
selectedData.add(data);
}
}
}
treeViewer.setSelection(new StructuredSelection(selectedData));
}
}
}