/* Copyright (C) 2003 EBI, GRL This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.ensembl.mart.explorer; import java.awt.Dimension; import java.awt.Point; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.dnd.DnDConstants; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.DragGestureListener; import java.awt.dnd.DragSource; import java.awt.dnd.DragSourceDragEvent; import java.awt.dnd.DragSourceDropEvent; import java.awt.dnd.DragSourceEvent; import java.awt.dnd.DragSourceListener; import java.awt.dnd.DropTarget; import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDropEvent; import java.awt.dnd.DropTargetEvent; import java.awt.dnd.DropTargetListener; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.util.logging.Logger; import javax.sql.DataSource; import javax.swing.AbstractAction; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.KeyStroke; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import org.ensembl.mart.guiutils.QuickFrame; import org.ensembl.mart.lib.Attribute; import org.ensembl.mart.lib.BasicFilter; import org.ensembl.mart.lib.DetailedDataSource; import org.ensembl.mart.lib.FieldAttribute; import org.ensembl.mart.lib.Filter; import org.ensembl.mart.lib.InvalidQueryException; import org.ensembl.mart.lib.Query; import org.ensembl.mart.lib.QueryListener; import org.ensembl.mart.lib.SequenceDescription; import org.ensembl.mart.lib.config.AttributeDescription; import org.ensembl.mart.lib.config.BaseNamedConfigurationObject; import org.ensembl.mart.lib.config.CompositeDSConfigAdaptor; import org.ensembl.mart.lib.config.DSConfigAdaptor; import org.ensembl.mart.lib.config.DatasetConfig; /** * Tree config showing the current state of the query. Allows the user * to select nodes and delete attributes and filters. * * @author <a href="mailto:craig@ebi.ac.uk">Craig Melsopp</a> * */ public class QueryTreeView extends JTree implements QueryListener { /** * Handles all DnD behaviour for the tree. Uses several call back * methods defined by the interfaces it implements to respond to user actions. */ private class DnDHandler implements DragSourceListener, DragGestureListener, DropTargetListener { private JTree jTree; // We need to create and pass a transferable around even though we // don't use it so we need to create one. private Transferable dummyTransferable = new StringSelection("DUMMY DATA - NOT USED"); private DefaultMutableTreeNode selected; private DragSource dragSource; /** * Initialises dnd source and target for the tree and registers * itself as a listener. */ private DnDHandler(JTree jTree) { this.jTree = jTree; DropTarget target = new DropTarget(jTree, this); dragSource = new DragSource(); dragSource.createDefaultDragGestureRecognizer( jTree, DnDConstants.ACTION_MOVE, this); } private TreeNode getNodeForLocation(Point p) { TreePath path = jTree.getClosestPathForLocation(p.x, p.y); return (TreeNode) path.getLastPathComponent(); } public void dragEnter(DropTargetDragEvent dtde) { dragOver(dtde); } /** * Determine whether drop is allowed. Attributes can be dropped * on other "attribute" nodes or the "atributes" node. Filters can be * dropped on other "filter" nodes or the "filters" node. */ public void dragOver(DropTargetDragEvent dtde) { TreeNode node = getNodeForLocation(dtde.getLocation()); if (node == attributesNode || attributesNode.isNodeChild(node) && node != selected) dtde.acceptDrag(DnDConstants.ACTION_MOVE); else dtde.rejectDrag(); } /** * Move the desired attribute or filter to it's new position. Called * in response to a drop action. */ public void drop(DropTargetDropEvent dtde) { TreeNode target = getNodeForLocation(dtde.getLocation()); // remove node from old position int oldIndex = attributesNode.getIndex(selected); Attribute attribute = query.getAttributes()[oldIndex]; query.removeAttribute(attribute); // insert selected node into the tree by adding to the query in the // correct position. int newIndex = -1; if (target == attributesNode) newIndex = 0; else if (attributesNode.isNodeChild(target)) newIndex = attributesNode.getIndex(target) + 1; query.addAttribute(newIndex, attribute); dtde.getDropTargetContext().dropComplete(true); } /** * Set cursor to show that a drop is allowed. Called after * dragOver(DropTargetDragEvent dtde) if * dtde.acceptDrag(DnDConstants.ACTION_MOVE) was called. */ public void dragOver(DragSourceDragEvent dsde) { dsde.getDragSourceContext().setCursor(DragSource.DefaultMoveDrop); } /** * Set cursor to show that the drop is not allowed.Called after * dragOver(DropTargetDragEvent dtde) if dtde.reject() was called. */ public void dragExit(DragSourceEvent dse) { dse.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop); } /** * Start the drag if an attribute is selected, otherwise do nothing. */ public void dragGestureRecognized(DragGestureEvent dge) { TreePath path = jTree.getSelectionPath(); if (path == null || path.getPathCount() <= 1) return; selected = (DefaultMutableTreeNode) path.getLastPathComponent(); // only allow attributes to be dragged if (!attributesNode.isNodeChild(selected)) return; // And start the drag process. We start with a no-drop cursor, assuming that the // user won't want to drop the item right where she picked it up. dragSource.startDrag( dge, DragSource.DefaultMoveNoDrop, dummyTransferable, this); } public void dropActionChanged(DropTargetDragEvent dtde) { } public void dragExit(DropTargetEvent dte) { } public void dragEnter(DragSourceDragEvent dsde) { } public void dropActionChanged(DragSourceDragEvent dsde) { } public void dragDropEnd(DragSourceDropEvent dsde) { } } /** * If an attribute or filter is currently selected delete it. */ private final class DeleteAction extends AbstractAction { public void actionPerformed(ActionEvent e) { TreePath path = getSelectionModel().getSelectionPath(); if (path == null) return; DefaultMutableTreeNode child = (DefaultMutableTreeNode) path.getLastPathComponent(); TreeNode parent = child.getParent(); int index = parent.getIndex(child); if (parent == attributesNode) { Attribute att = query.getAttributes()[index]; if (query.hasAttribute(att)) query.removeAttribute(att); else if ( ( att.getField().indexOf('.') > 0 ) && ( query.getSequenceDescription() != null ) ) query.setSequenceDescription(null); } else if (parent == filtersNode) query.removeFilter(query.getFilters()[index]); } } private Feedback feedback = new Feedback(this); private String dsvInternalName; private DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(); private DefaultMutableTreeNode dataSourceNode = new DefaultMutableTreeNode(TreeNodeData.createDataSourceNode()); private DefaultMutableTreeNode datasetNode = new DefaultMutableTreeNode(TreeNodeData.createDatasetNode()); private DefaultMutableTreeNode attributesNode = new DefaultMutableTreeNode(TreeNodeData.createAttributesNode()); private DefaultMutableTreeNode filtersNode = new DefaultMutableTreeNode(TreeNodeData.createFilterNode()); private DefaultMutableTreeNode formatNode = new DefaultMutableTreeNode(TreeNodeData.createFormatNode()); private DefaultTreeModel treeModel = new DefaultTreeModel(rootNode); private final static Logger logger = Logger.getLogger(QueryTreeView.class.getName()); private DSConfigAdaptor dsvAdaptor; private Query query; /** * Tree config showing the current state of the query. The current datasetConfig * is retrieved from the adaptor and this is used to determine how to render * the values stored in the query. * * @param query Query represented by tree. * @param dsvAdaptor source of DatasetConfigs used to interpret query. */ public QueryTreeView(Query query, DSConfigAdaptor dsvAdaptor) { super(); this.query = query; this.dsvAdaptor = dsvAdaptor; query.addQueryChangeListener(this); setModel(treeModel); setRootVisible(false); rootNode.add(dataSourceNode); rootNode.add(datasetNode); rootNode.add(attributesNode); rootNode.add(filtersNode); rootNode.add(formatNode); getSelectionModel().setSelectionMode( TreeSelectionModel.SINGLE_TREE_SELECTION); // ensure the 1st level of nodes are visible TreePath path = new TreePath(rootNode).pathByAddingChild(dataSourceNode); makeVisible(path); getInputMap().put( KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "doDelete"); getActionMap().put("doDelete", new DeleteAction()); // handles dnd for this component new DnDHandler(this); } private static DSConfigAdaptor testAdaptor; /** * Runs an interactive test program where the user can interact * with the QueryTreeView. */ public static void main(String[] args) throws Exception { // default adaptor for retrieving datasetconfigs testAdaptor = QueryEditor.testDSConfigAdaptor(new CompositeDSConfigAdaptor()); final Query query = new Query(); final QueryTreeView qtv = new QueryTreeView(query, testAdaptor); Dimension d = new Dimension(500, 600); qtv.setPreferredSize(d); qtv.setMinimumSize(d); Box c = Box.createVerticalBox(); qtv.addTreeSelectionListener(new TreeSelectionListener() { public void valueChanged(TreeSelectionEvent e) { if (e != null && e.getNewLeadSelectionPath() != null) logger.info( "Selected:" + e.getNewLeadSelectionPath().getLastPathComponent()); } }); c.add(new JLabel("Delete key should work for attributes+filters")); JButton b; Box box; box = Box.createHorizontalBox(); c.add(box); b = new JButton("Add attribute"); box.add(b); b.addActionListener(new ActionListener() { private int count = 0; public void actionPerformed(ActionEvent e) { int index = (int) (query.getAttributes().length * Math.random()); Attribute a = new FieldAttribute("attribute" + count++); query.addAttribute(index, a); } }); b = new JButton("Remove attribute"); box.add(b); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Attribute[] a = query.getAttributes(); if (a.length > 0) { int index = (int) (Math.random() * a.length); logger.info("Removing attribute " + index + " " + a[index]); query.removeAttribute(a[index]); } } }); // --------- box = Box.createHorizontalBox(); c.add(box); b = new JButton("Add sequence attribute"); box.add(b); b.addActionListener(new ActionListener() { private int count = 0; public void actionPerformed(ActionEvent e) { int index = (int) (query.getAttributes().length * Math.random()); Attribute a = new FieldAttribute("attribute" + count++); //TODO: remove hard-coded sequence description try { query.setSequenceDescription( new SequenceDescription("hsapiens_gene_ensembl","hsapiens_genomic_sequence","coding", testAdaptor)); } catch (InvalidQueryException e1) { e1.printStackTrace(); } } }); b = new JButton("Remove sequence attribute"); box.add(b); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { query.setSequenceDescription(null); } }); // --------- box = Box.createHorizontalBox(); c.add(box); b = new JButton("Add filter"); box.add(b); b.addActionListener(new ActionListener() { private int count = 0; public void actionPerformed(ActionEvent e) { int index = (int) (query.getFilters().length * Math.random()); Filter f = new BasicFilter("filterField" + count++, "=", "value"); query.addFilter(index, f); } }); b = new JButton("Remove random filter"); box.add(b); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Filter[] f = query.getFilters(); if (f.length > 0) { int index = (int) (Math.random() * f.length); logger.info("Removing filter " + index + " " + f[index]); query.removeFilter(f[index]); } } }); box = Box.createHorizontalBox(); c.add(box); b = new JButton("Print Query"); box.add(b); b.addActionListener(new ActionListener() { private int count = 0; public void actionPerformed(ActionEvent e) { System.out.println(query.toString()); } }); c.add(new JScrollPane(qtv)); JFrame f = new QuickFrame("QueryTreeView unit test", c); // preload some default settings //query.setDatasetConfig( adaptor.getDatasetConfigs()[0] ); //query.addAttribute(new FieldAttribute("ensembl_gene_id")); query.addFilter(new BasicFilter("ensembl_gene_id", "=", "ENSG001")); query.addFilter(new BasicFilter("chr_name", "=", "3")); } /** * Do nothing. * @see org.ensembl.mart.lib.QueryChangeListener#queryNameChanged(org.ensembl.mart.lib.Query, java.lang.String, java.lang.String) */ public void queryNameChanged( Query sourceQuery, String oldName, String newName) { } /** * Update the name of the dataset shown in the tree. * @see org.ensembl.mart.lib.QueryChangeListener#datasetChanged(org.ensembl.mart.lib.Query, java.lang.String, java.lang.String) */ public void datasetChanged( Query source, String oldDataset, String newDataset) { String s = (newDataset != null) ? newDataset : ""; ((TreeNodeData) datasetNode.getUserObject()).setRightText(s); treeModel.reload(datasetNode); } /** * Set the Datasource node with the newDatasource. * @see org.ensembl.mart.lib.QueryChangeListener#datasourceChanged(org.ensembl.mart.lib.Query, javax.sql.DataSource, javax.sql.DataSource) */ public void datasourceChanged( Query sourceQuery, DataSource oldDatasource, DataSource newDatasource) { String s = (newDatasource != null) ? newDatasource.toString() : ""; // TODO query.datasource:DataSource -> DetailedDataSource and propagate changes // through QueryListener. if (newDatasource != null && newDatasource instanceof DetailedDataSource) { DetailedDataSource ds = (DetailedDataSource) newDatasource; s = ds.getName(); } ((TreeNodeData) dataSourceNode.getUserObject()).setRightText(s); treeModel.reload(dataSourceNode); } /** * Adds a child node to attributesNode at the specified index. * If dsv->attributeDescription->displayName is available that is used for the * label, otherwise attribute.fieldName is used. * position in the list of attributes. * @see org.ensembl.mart.lib.QueryChangeListener#queryAttributeAdded(org.ensembl.mart.lib.Query, int, org.ensembl.mart.lib.Attribute) */ public void attributeAdded( Query sourceQuery, int index, Attribute attribute) { // Try to get a user friendly labelName, // otherwise use the raw one from attribute AttributeDescription ad = null; if (query.getDatasetConfig() != null) ad = query .getDatasetConfig() .getAttributeDescriptionByFieldNameTableConstraint( attribute.getField(), attribute.getTableConstraint()); String nodeLabel = (ad != null) ? ad.getDisplayName() : attribute.getField(); TreeNodeData userObject = new TreeNodeData(null, null, nodeLabel, attribute); DefaultMutableTreeNode treeNode = new DefaultMutableTreeNode(userObject); attributesNode.insert(treeNode, index); treeModel.reload(attributesNode); select(attributesNode, index, false); } /** * Select a node. * @param treeNode */ private void select( DefaultMutableTreeNode parentNode, int selectedChildIndex, boolean select) { DefaultMutableTreeNode next = parentNode; int nChildren = parentNode.getChildCount(); if (nChildren > 0) if (selectedChildIndex < nChildren) next = (DefaultMutableTreeNode) parentNode.getChildAt(selectedChildIndex); else next = (DefaultMutableTreeNode) parentNode.getChildAt(nChildren - 1); TreePath path = new TreePath(next.getPath()); scrollPathToVisible(path); if (select) setSelectionPath(path); } /** * Remove node from tree that corresponds to the attribute and select * the next attribute if available, otherwise the attributesNode. * @see org.ensembl.mart.lib.QueryChangeListener#queryAttributeRemoved(org.ensembl.mart.lib.Query, int, org.ensembl.mart.lib.Attribute) */ public void attributeRemoved( Query sourceQuery, int index, Attribute attribute) { attributesNode.remove(index); treeModel.reload(attributesNode); //sequence queries behave differently if (sourceQuery.getSequenceDescription() != null && index > 0) index--; select(attributesNode, index, true); } /** * Adds a child node to the filtersNode to represent the filter at the specified index. * The node label is partly derived from a corresponding FilterDescription retrieved * from the dsConfig, otherwise it is based solely on the filter. * @see org.ensembl.mart.lib.QueryChangeListener#queryFilterAdded(org.ensembl.mart.lib.Query, int, org.ensembl.mart.lib.Filter) */ public void filterAdded(Query sourceQuery, int index, Filter filter) { DefaultMutableTreeNode treeNode = new DefaultMutableTreeNode(new TreeNodeData(query, filter)); filtersNode.insert(treeNode, index); treeModel.reload(filtersNode); select(filtersNode, index, false); } /** * Removes filter at specified index from tree. */ public void filterRemoved(Query sourceQuery, int index, Filter filter) { filtersNode.remove(index); treeModel.reload(filtersNode); select(filtersNode, index, true); } /** * Replaces oldFilter in tree at the specified index with newFilter. */ public void filterChanged( Query sourceQuery, int index, Filter oldFilter, Filter newFilter) { DefaultMutableTreeNode node = new DefaultMutableTreeNode(new TreeNodeData(sourceQuery, newFilter)); filtersNode.remove(index); filtersNode.insert(node, index); treeModel.reload(filtersNode); select(filtersNode, index, false); } /** * Add / remove sequence attribute to / from end of attributes list. * * The query tree view represents the sequence description as a single node at the end of the * attribute branch. If newSequenceDescription * is null remove any existing tree node. If newSequenceDescription is not null then add * a tree node representing the sequence description, replace any existing sequence description * node. * */ public void sequenceDescriptionChanged( Query sourceQuery, SequenceDescription oldSequenceDescription, SequenceDescription newSequenceDescription) { if (oldSequenceDescription != null) { attributesNode.remove(attributesNode.getChildCount() - 1); treeModel.reload(attributesNode); } DefaultMutableTreeNode node = null; if (newSequenceDescription != null) { node = new DefaultMutableTreeNode(new TreeNodeData(newSequenceDescription)); attributesNode.add(node); treeModel.reload(attributesNode); select(attributesNode, attributesNode.getIndex(node), true); } else { int last = attributesNode.getChildCount() - 1; if (last > -1) select(attributesNode, last, true); else select(rootNode, rootNode.getIndex(attributesNode), true); } } /** * Do nothing. * @see org.ensembl.mart.lib.QueryChangeListener#queryLimitChanged(org.ensembl.mart.lib.Query, int, int) */ public void limitChanged(Query query, int oldLimit, int newLimit) { } /** * Do nothing. * @see org.ensembl.mart.lib.QueryChangeListener#queryStarBasesChanged(org.ensembl.mart.lib.Query, java.lang.String[], java.lang.String[]) */ public void starBasesChanged( Query sourceQuery, String[] oldStarBases, String[] newStarBases) { } /** * Do nothing. * @see org.ensembl.mart.lib.QueryChangeListener#queryPrimaryKeysChanged(org.ensembl.mart.lib.Query, java.lang.String[], java.lang.String[]) */ public void primaryKeysChanged( Query sourceQuery, String[] oldPrimaryKeys, String[] newPrimaryKeys) { } /** * Do nothing. */ public void datasetConfigChanged( Query query, DatasetConfig oldDatasetConfig, DatasetConfig newDatasetConfig) { } protected boolean skipConfigurationObject(BaseNamedConfigurationObject obj) { if (obj == null) return false; //let caller handle null objects itself if (obj.getHidden() != null && obj.getHidden().equals("true")) return true; if (obj.getDisplay() != null && obj.getDisplay().equals("true")) return true; return false; } }