package de.uni_passau.fim.infosun.prophet.experimentEditor.qTree;
import java.awt.Point;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.TooManyListenersException;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import de.uni_passau.fim.infosun.prophet.util.qTree.QTreeNode;
import static de.uni_passau.fim.infosun.prophet.util.language.UIElementNames.getLocalized;
import static de.uni_passau.fim.infosun.prophet.util.qTree.QTreeNode.Type.CATEGORY;
import static de.uni_passau.fim.infosun.prophet.util.qTree.QTreeNode.Type.EXPERIMENT;
import static de.uni_passau.fim.infosun.prophet.util.qTree.QTreeNode.Type.QUESTION;
/**
* A <code>JTree</code> subclass displaying a tree of <code>QTreeNode</code> objects.
*/
public class QTree extends JTree {
/**
* Popup menus for the different types of nodes.
*/
private JPopupMenu experimentPopup;
private JPopupMenu categoryPopup;
private JPopupMenu questionPopup;
private JPopupMenu treePopup;
/**
* Menu items and the clipboard reference to support copy and paste.
*/
private JMenuItem experimentPasteItem;
private JMenuItem categoryPasteItem;
private QTreeNode clipboard;
/**
* ActionCommand String constants for the popup menus.
*/
private static final String ACTION_NEW_CATEGORY = "newcategory";
private static final String ACTION_NEW_QUESTION = "newquestion";
private static final String ACTION_NEW_EXPERIMENT = "newexperiment";
private static final String ACTION_RENAME = "rename";
private static final String ACTION_REMOVE = "remove";
private static final String ACTION_COPY = "copy";
private static final String ACTION_PASTE = "paste";
private QTreeModel model;
/**
* Constructs a new <code>QTree</code>.
*/
public QTree() {
super(new QTreeModel(null));
model = (QTreeModel) getModel();
getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
buildPopupMenus();
try {
setupDragAndDrop();
} catch (TooManyListenersException ignored) {}
setupShortcuts();
}
/**
* Sets up the shortcuts.
*/
private void setupShortcuts() {
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (!e.isControlDown()) {
return;
}
TreePath selPath = getSelectionPath();
if (selPath == null) {
return;
}
QTreeNode selNode = (QTreeNode) selPath.getLastPathComponent();
switch (e.getKeyCode()) {
case KeyEvent.VK_N:
if (selNode.getType() == EXPERIMENT) {
newCategory(selNode);
} else if (selNode.getType() == CATEGORY) {
newQuestion(selNode);
}
break;
case KeyEvent.VK_R:
rename(selNode);
break;
case KeyEvent.VK_Q:
remove(selNode);
break;
case KeyEvent.VK_C:
copy(selNode);
break;
case KeyEvent.VK_V:
paste(selNode);
break;
default:
}
}
});
}
/**
* Sets up the drag and drop actions for the tree.
*
* @throws TooManyListenersException
* if a <code>DropTargetListener</code> is already added to the <code>QTree</code>
*/
private void setupDragAndDrop() throws TooManyListenersException {
setDragEnabled(true);
setDropTarget(new DropTarget());
getDropTarget().addDropTargetListener(new DropTargetAdapter() {
@Override
public void dragOver(DropTargetDragEvent dtde) {
QTreeNode dragComponent = (QTreeNode) getSelectionPath().getLastPathComponent();
// you can't drag the experiment node
if (dragComponent.getType() == EXPERIMENT) {
dtde.rejectDrag();
return;
}
Point mouseLocation = dtde.getLocation();
TreePath selPath = getPathForLocation(mouseLocation.x, mouseLocation.y);
// you can only drag onto a tree component
if (selPath == null) {
dtde.rejectDrag();
return;
}
QTreeNode target = (QTreeNode) selPath.getLastPathComponent();
// you can't drag onto the experiment node
if (target.getType() == EXPERIMENT) {
dtde.rejectDrag();
// you can't drag a category onto a question
} else if (dragComponent.getType() == CATEGORY && target.getType() != CATEGORY) {
dtde.rejectDrag();
} else {
dtde.acceptDrag(dtde.getDropAction());
}
}
@Override
public void drop(DropTargetDropEvent dtde) {
Point dropLocation = dtde.getLocation();
TreePath dropPath = getPathForLocation(dropLocation.x, dropLocation.y);
QTreeNode dragComponent = (QTreeNode) getSelectionPath().getLastPathComponent();
if (dropPath == null) {
dtde.rejectDrop();
return;
}
QTreeNode target = (QTreeNode) dropPath.getLastPathComponent();
switch (dragComponent.getType()) {
case CATEGORY: {
moveCategory(dragComponent, target);
}
break;
case QUESTION: {
moveQuestion(dragComponent, target);
}
break;
default:
System.err.println("Can't move nodes of type " + dragComponent.getType());
}
dtde.dropComplete(true);
}
});
}
/**
* Builds the popup menus and sets up the actions available through them.
*/
private void buildPopupMenus() {
JMenuItem menuItem;
ActionListener popupListener;
experimentPopup = new JPopupMenu();
categoryPopup = new JPopupMenu();
questionPopup = new JPopupMenu();
treePopup = new JPopupMenu();
// action listener for handling all actions coming from the popup menus
popupListener = event -> {
QTreeNode selNode = null;
TreePath selPath = getSelectionPath();
if (selPath != null) {
selNode = (QTreeNode) selPath.getLastPathComponent();
}
switch (event.getActionCommand()) {
case ACTION_NEW_CATEGORY: {
newCategory(selNode);
}
break;
case ACTION_NEW_QUESTION: {
newQuestion(selNode);
}
break;
case ACTION_NEW_EXPERIMENT: {
newExperiment();
}
break;
case ACTION_RENAME: {
rename(selNode);
}
break;
case ACTION_REMOVE: {
remove(selNode);
}
break;
case ACTION_COPY: {
copy(selNode);
}
break;
case ACTION_PASTE: {
paste(selNode);
}
break;
}
};
// setup for the experiment popup menu
menuItem = new JMenuItem(getLocalized("TREE.POPUP.NEW_CATEGORY"));
menuItem.setActionCommand(ACTION_NEW_CATEGORY);
menuItem.addActionListener(popupListener);
experimentPopup.add(menuItem);
experimentPopup.add(new JPopupMenu.Separator());
menuItem = new JMenuItem(getLocalized("TREE.POPUP.RENAME"));
menuItem.setActionCommand(ACTION_RENAME);
menuItem.addActionListener(popupListener);
experimentPopup.add(menuItem);
experimentPopup.add(new JPopupMenu.Separator());
experimentPasteItem = new JMenuItem(getLocalized("TREE.POPUP.PASTE"));
experimentPasteItem.setActionCommand(ACTION_PASTE);
experimentPasteItem.addActionListener(popupListener);
experimentPasteItem.setEnabled(false);
experimentPopup.add(experimentPasteItem);
// setup for the category popup menu
menuItem = new JMenuItem(getLocalized("TREE.POPUP.NEW_QUESTION"));
menuItem.setActionCommand(ACTION_NEW_QUESTION);
menuItem.addActionListener(popupListener);
categoryPopup.add(menuItem);
categoryPopup.add(new JPopupMenu.Separator());
menuItem = new JMenuItem(getLocalized("TREE.POPUP.RENAME"));
menuItem.setActionCommand(ACTION_RENAME);
menuItem.addActionListener(popupListener);
categoryPopup.add(menuItem);
menuItem = new JMenuItem(getLocalized("TREE.POPUP.REMOVE"));
menuItem.setActionCommand(ACTION_REMOVE);
menuItem.addActionListener(popupListener);
categoryPopup.add(menuItem);
categoryPopup.add(new JPopupMenu.Separator());
menuItem = new JMenuItem(getLocalized("TREE.POPUP.COPY"));
menuItem.setActionCommand(ACTION_COPY);
menuItem.addActionListener(popupListener);
categoryPopup.add(menuItem);
categoryPasteItem = new JMenuItem(getLocalized("TREE.POPUP.PASTE"));
categoryPasteItem.setActionCommand(ACTION_PASTE);
categoryPasteItem.addActionListener(popupListener);
categoryPasteItem.setEnabled(false);
categoryPopup.add(categoryPasteItem);
// setup for the question popup menu
menuItem = new JMenuItem(getLocalized("TREE.POPUP.RENAME"));
menuItem.setActionCommand(ACTION_RENAME);
menuItem.addActionListener(popupListener);
questionPopup.add(menuItem);
questionPopup.add(new JPopupMenu.Separator());
menuItem = new JMenuItem(getLocalized("TREE.POPUP.REMOVE"));
menuItem.setActionCommand(ACTION_REMOVE);
menuItem.addActionListener(popupListener);
questionPopup.add(menuItem);
questionPopup.add(new JPopupMenu.Separator());
menuItem = new JMenuItem(getLocalized("TREE.POPUP.COPY"));
menuItem.setActionCommand(ACTION_COPY);
menuItem.addActionListener(popupListener);
questionPopup.add(menuItem);
menuItem = new JMenuItem(getLocalized("TREE.POPUP.NEW_EXPERIMENT"));
menuItem.setActionCommand(ACTION_NEW_EXPERIMENT);
menuItem.addActionListener(popupListener);
treePopup.add(menuItem);
// mouse listener for showing the popup menus when a node of the tree is right clicked
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (!SwingUtilities.isRightMouseButton(e)) {
return;
}
int selX = e.getX();
int selY = e.getY();
TreePath path = getPathForLocation(selX, selY);
if (path == null) {
treePopup.show(QTree.this, selX, selY);
return;
}
setSelectionPath(path);
QTreeNode selNode = (QTreeNode) path.getLastPathComponent();
switch (selNode.getType()) {
case EXPERIMENT:
experimentPopup.show(QTree.this, selX, selY);
break;
case CATEGORY:
categoryPopup.show(QTree.this, selX, selY);
break;
case QUESTION:
questionPopup.show(QTree.this, selX, selY);
break;
default:
System.err.println("No popup for type " + selNode.getType());
}
}
});
}
/**
* Moves a category. Categories can only be dragged onto other categories and will be placed after the
* node <code>target</code> in the targets parent.
*
* @param dragComponent
* the component that is being dragged
* @param target
* the component <code>dragComponent</code> is being dropped on
*/
private void moveCategory(QTreeNode dragComponent, QTreeNode target) {
model.removeFromParent(dragComponent);
int index = target.getParent().getIndexOfChild(target) + 1;
dragComponent.setParent(target.getParent());
model.addChild(target.getParent(), dragComponent, index);
}
/**
* Moves a question. Questions can be dragged onto categories or other questions. If the are dragged onto
* a category they will be added as the last child of the category, if the are dragged onto a question
* they will be added after the node <code>target</code> in the targets parent.
*
* @param dragComponent
* the component that is being dragged
* @param target
* the component <code>dragComponent</code> is being dropped on
*/
private void moveQuestion(QTreeNode dragComponent, QTreeNode target) {
switch (target.getType()) {
case CATEGORY: {
model.removeFromParent(dragComponent);
int index = target.getChildCount();
dragComponent.setParent(target);
model.addChild(target, dragComponent, index);
}
break;
case QUESTION: {
model.removeFromParent(dragComponent);
int index = target.getParent().getIndexOfChild(target) + 1;
dragComponent.setParent(target.getParent());
model.addChild(target.getParent(), dragComponent, index);
}
break;
default:
System.err.println("Can not move QUESTION onto " + target.getType());
}
}
/**
* Copies the given node.
*
* @param selNode
* the node to be copied
*/
private void copy(QTreeNode selNode) {
clipboard = selNode;
if (selNode.getType() == QUESTION) {
categoryPasteItem.setEnabled(true);
experimentPasteItem.setEnabled(false);
} else if (selNode.getType() == CATEGORY) {
categoryPasteItem.setEnabled(false);
experimentPasteItem.setEnabled(true);
}
}
/**
* Pasts the node currently in <code>clipboard</code> into the given node.
*
* @param selNode
* the node into which to paste
*/
private void paste(QTreeNode selNode) {
boolean categoryToExperiment = selNode.getType() == EXPERIMENT && clipboard.getType() == CATEGORY;
boolean questionToCategory = selNode.getType() == CATEGORY && clipboard.getType() == QUESTION;
if (categoryToExperiment || questionToCategory) {
QTreeNode copy;
try {
copy = (QTreeNode) clipboard.clone();
} catch (CloneNotSupportedException e) {
JOptionPane.showMessageDialog(this, getLocalized("TREE.POPUP.COPY_FAILED"),
getLocalized("TREE.POPUP.ERROR"), JOptionPane.ERROR_MESSAGE);
return;
}
copy.setParent(selNode);
model.addChild(selNode, copy);
} else {
JOptionPane.showMessageDialog(this, getLocalized("TREE.POPUP.COPY_IMPOSSIBLE"),
getLocalized("TREE.POPUP.ERROR"), JOptionPane.ERROR_MESSAGE);
}
}
/**
* Replaces the current experiment with a new one.
*/
private void newExperiment() {
if (model.getRoot() != null) {
int choice = JOptionPane
.showOptionDialog(this, getLocalized("TREE.POPUP.CONFIRM.NEW_EXPERIMENT"),
getLocalized("TREE.POPUP.NEW_EXPERIMENT"), JOptionPane.OK_CANCEL_OPTION,
JOptionPane.WARNING_MESSAGE, null, null, null);
if (choice != JOptionPane.OK_OPTION) {
return;
}
}
String name = JOptionPane.showInputDialog(this, getLocalized("TREE.POPUP.NAME"),
getLocalized("TREE.POPUP.NEW_EXPERIMENT"), JOptionPane.QUESTION_MESSAGE);
if (name == null) {
return;
}
model.setRoot(new QTreeNode(null, EXPERIMENT, name));
}
/**
* Removes the given node from the tree.
*
* @param selNode
* the node to be removed
*/
private void remove(QTreeNode selNode) {
model.removeFromParent(selNode);
}
/**
* Renames the given node.
*
* @param selNode
* the node to be renamed
*/
private void rename(QTreeNode selNode) {
String name = JOptionPane.showInputDialog(this, getLocalized("TREE.POPUP.NAME"),
getLocalized("TREE.POPUP.RENAME"), JOptionPane.QUESTION_MESSAGE);
if (name == null) {
return;
}
model.rename(selNode, name);
}
/**
* Adds a new question to the given node.
*
* @param selNode
* the node to which a new question is to be added
*/
private void newQuestion(QTreeNode selNode) {
String name = JOptionPane.showInputDialog(this, getLocalized("TREE.POPUP.NAME"),
getLocalized("TREE.POPUP.NEW_QUESTION"), JOptionPane.QUESTION_MESSAGE);
if (name == null) {
return;
}
model.addChild(selNode, new QTreeNode(selNode, QUESTION, name));
expandPath(getSelectionPath());
}
/**
* Adds a new category to the given node.
*
* @param selNode
* the node to which a new category is to be added
*/
private void newCategory(QTreeNode selNode) {
String name = JOptionPane.showInputDialog(this, getLocalized("TREE.POPUP.NAME"),
getLocalized("TREE.POPUP.NEW_CATEGORY"), JOptionPane.QUESTION_MESSAGE);
if (name == null) {
return;
}
model.addChild(selNode, new QTreeNode(selNode, CATEGORY, name));
expandPath(getSelectionPath());
}
}