All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.databasesandlife.util.swing.MasterDetailTreeNode Maven / Gradle / Ivy

There is a newer version: 21.0.1
Show newest version
package com.databasesandlife.util.swing;

import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.dnd.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;

/**
 * Represents a tree node in a hierarchy/detail situation, such as in the 
 * Windows Explorer. Has following features:
 * 
    *
  • Each node belongs to exactly one JTree with exactly one TreeModel. * The root node stores these. Through a node's parents one gets to the root, and thus these. *
  • Assumes that children are cached, and that subclass implements fetchChildren, called once. *
  • Assumes that per program only one such tree exists (public static TreeModel). *
  • Assumes that parent is known at node creation time. Parent can be changed later. *
  • If parent==null then this node is root. In which case it stores table model and tree. * All nodes have getTableModel etc methods, they just call parent.getTableModel until * root is found. *
  • Has utility methods such as deleteNode0, redraw, getTreePath etc. *
  • Comes with a Node.CellRenderer, allowing icon,text,extra-text to be displayed *
  • Supports creation of context menus, through newPopupMenu *
  • Installs itself into a Tree (e.g. created in NetBeans), remembers static TreeModel. *
  • On tree right-click, pop-up menu opened *
  • On tree left-click, detail panel changed (with default "no detail panel") *
* Usage: *
 *    Tree t = new Tree(); // from NetBeans UI designer
 *    MyRootNode n = new MyNode(null); // extends MasterDetailTreeNode, parent==null
 *    n.installIntoTreeAsRootNode(t);
 * 
* * @author This source is copyright Adrian Smith and licensed under the LGPL 3. * @see Project on GitHub */ @SuppressWarnings("serial") public abstract class MasterDetailTreeNode implements TreeNode, java.io.Serializable { protected class CellRenderer extends JPanel implements TreeCellRenderer { JLabel icon = new JLabel(), text = new JLabel(), extraText = new JLabel(); public CellRenderer() { setLayout(new BorderLayout()); add(icon, BorderLayout.WEST); var p = new JPanel(); p.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); p.add(text); p.add(extraText); p.setOpaque(false); add(p, BorderLayout.CENTER); icon.setPreferredSize(new Dimension(18, 200)); text.setOpaque(true); extraText.setOpaque(true); setOpaque(false); } public Component getTreeCellRendererComponent( JTree jTree, Object obj, boolean isSelected, boolean isExpanded, boolean isLeaf, int row, boolean hasFocus ) { var node = (MasterDetailTreeNode) obj; var isDropTarget = (node == candidateDropTarget); text.setBackground(isSelected ? UIManager.getColor("Tree.selectionBackground") : isDropTarget ? Color.LIGHT_GRAY : UIManager.getColor("Tree.textBackground")); text.setForeground(isSelected ? UIManager.getColor("Tree.selectionForeground") : UIManager.getColor("Tree.textForeground")); text.setText(" " + node.getTreeText()); icon.setIcon(node.getTreeIcon()); extraText.setText(node.getExtraText() + " "); extraText.setBackground(text.getBackground()); extraText.setForeground(node.getExtraTextColor().equals(Color.black) ? text.getForeground() : node.getExtraTextColor()); setPreferredSize(new Dimension(text.getPreferredSize().width + icon.getPreferredSize().width + extraText.getPreferredSize().width, 20)); // height irrelevant return this; } } protected class TreeListener extends MouseAdapter implements TreeSelectionListener, DropTargetListener { public JPanel detailPanelContainer, defaultDetailPanel; public TreeListener(JPanel cont, JPanel def) { detailPanelContainer=cont; defaultDetailPanel=def; } protected MasterDetailTreeNode getNodeUnderXY(int x, int y) { var selectedRow = tree.getRowForLocation(x, y); var selectedPath = tree.getPathForLocation(x, y); if(selectedRow == -1) return null; return (MasterDetailTreeNode) selectedPath.getLastPathComponent(); } public void mouseReleased(java.awt.event.MouseEvent evt) { if (evt.isPopupTrigger()) { var node = getNodeUnderXY(evt.getX(), evt.getY()); if (node != null) { tree.setSelectionPath(node.newTreePath()); final JPopupMenu menu = node.newPopupMenu(); // if not invokeLater, then new tree selection is only shown // after pop-up menu disappears final MouseEvent evt2 = evt; if (menu != null) { SwingUtilities.invokeLater(new Runnable() { public void run() { tree.add(menu); menu.show(tree, evt2.getX(), evt2.getY()); } }); } } } } /** Node selection changed */ public void valueChanged(javax.swing.event.TreeSelectionEvent evt) { JPanel detailPanel; var newTP = evt.getNewLeadSelectionPath(); if (newTP != null) { var node = (MasterDetailTreeNode) newTP.getLastPathComponent(); detailPanel = node.newDetailJPanel(); } else { detailPanel = defaultDetailPanel; } detailPanelContainer.removeAll(); detailPanelContainer.add(detailPanel); detailPanel.requestFocus(); detailPanelContainer.revalidate(); } protected boolean isDragOk(DropTargetDragEvent e, MasterDetailTreeNode node) { // 1. flavor ok ? var flavorOk = false; DataFlavor[] flavors = node.getAcceptableDropFlavors(); for (var i = 0; i < flavors.length; i++) if (e.isDataFlavorSupported(flavors[i])) flavorOk = true; if ( ! flavorOk) return false; // 2. action ok ? if ((e.getSourceActions() & node.getAcceptableDropActions()) == 0) return false; return true; } protected void change(MasterDetailTreeNode newNode) { var oldNode = candidateDropTarget; candidateDropTarget = newNode; if (oldNode == newNode) return; if (oldNode != null) oldNode.redraw(); if (newNode != null) newNode.redraw(); } protected void accept(DropTargetDragEvent e) { if (candidateDropTarget != null) e.acceptDrag(candidateDropTarget.getAcceptableDropActions()); else e.rejectDrag(); } public void dragEnter(DropTargetDragEvent e) { dragOver(e); } public void dragOver(DropTargetDragEvent e) { var node = getNodeUnderXY((int)e.getLocation().getX(), (int)e.getLocation().getY()); if (node == null) { change(null); accept(e); return; } if ( ! isDragOk(e, node)) { change(null); accept(e); return; } change(node); accept(e); } public void dropActionChanged(DropTargetDragEvent e) { accept(e); } public void dragExit(DropTargetEvent e) { change(null); } public void drop(DropTargetDropEvent e) { candidateDropTarget.transferrableHasBeenDropped(e); e.dropComplete(true); change(null); } } /** Contains children if loaded, or null meaning not yet loaded */ protected MasterDetailTreeNode children[] = null; /** Must always be set, or null if root node */ protected MasterDetailTreeNode parent; protected MasterDetailTreeNode(MasterDetailTreeNode parent) { this.parent = parent; } public MasterDetailTreeNode[] getChildren() { if (children != null) return children; else return children = fetchChildren(); } public void setParent(MasterDetailTreeNode n) { parent = n; } /** if root, candidate drop target, otherwise null */ protected MasterDetailTreeNode candidateDropTarget = null; /** if root, assigned to a tree, this is a value, otherwise null */ protected DefaultTreeModel treeModel = null; /** if root, assigned to a tree, this is a value, otherwise null */ protected JTree tree = null; protected JTree getTree() { if (parent == null) return tree; else return parent.getTree(); } protected DefaultTreeModel getTreeModel() { if (parent == null) return treeModel; else return parent.getTreeModel(); } protected MasterDetailTreeNode getRootNode() { if (parent == null) return this; else return parent.getRootNode(); } /** Sets Node.treeModel and makes this the first node * of it. Also sets some properties of the tree such as the cell renderer. * This can only be called on root nodes (where parent==null) * @param detailPanelContainer panel which should contain the detail panels * @param defaultDetailPanel if no node selected, this panel is shown in detailContainingPanel */ public void installIntoTreeAsRootNode(JTree tree, JPanel detailPanelContainer, JPanel defaultDetailPanel) { if (parent != null) throw new IllegalStateException("trying to install non-root node into tree as root node"); this.tree = tree; tree.setModel(treeModel = new DefaultTreeModel(this)); tree.setCellRenderer(new CellRenderer()); tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); tree.setExpandsSelectedPaths(true); var l = new TreeListener(detailPanelContainer, defaultDetailPanel); tree.addMouseListener(l); tree.addTreeSelectionListener(l); new DropTarget(tree, DnDConstants.ACTION_COPY, l); } protected String getLeafNameForNodeKey(String key) { var dotPosition = -1; while (true) { int newDotPosition; if (dotPosition == -1) newDotPosition = key.indexOf("."); else newDotPosition = key.indexOf(".", dotPosition+1); if (newDotPosition == -1) break; else dotPosition = newDotPosition; } return key.substring(dotPosition+1); } /** Deletes a node, which must be a member of this's children. Tree * model is informed. */ protected void deleteNode0(MasterDetailTreeNode node) { // create new children MasterDetailTreeNode[] newChildren = new MasterDetailTreeNode[children.length - 1]; var newChildrenPos = 0; var oldElementInOldChildren = -1; for (var childrenPos = 0; childrenPos < children.length; childrenPos++) if (children[childrenPos] == node) oldElementInOldChildren = childrenPos; else newChildren[newChildrenPos++] = children[childrenPos]; if (oldElementInOldChildren == -1 || newChildrenPos != newChildren.length) throw new RuntimeException("elements found != 1"); children = newChildren; // inform tree model getTreeModel().nodesWereRemoved(this, new int[] { oldElementInOldChildren }, new MasterDetailTreeNode[] { node }); } protected void informTreeModelThatNodeHasRadicallyChanged() { MasterDetailTreeNode[] siblings = parent.getChildren(); var thisIndexInParent = -1; for (var i = 0; i < siblings.length; i++) if (siblings[i] == this) thisIndexInParent = i; if (thisIndexInParent == -1) throw new RuntimeException("this not found"); getTreeModel().nodesChanged(parent, new int[] { thisIndexInParent }); } public TreePath newTreePath() { var path = new Vector<>(); var n = this; while (n != null) { path.insertElementAt(n, 0); n = n.parent; } return new TreePath(path.toArray()); } public void select() { var tree = getTree(); var path = newTreePath(); tree.setSelectionPath(path); tree.scrollPathToVisible(path); } public void redraw() { getTreeModel().nodeChanged(this); } // from http://www.jguru.com/faq/view.jsp?EID=513951 protected int expandJTreeNode (javax.swing.JTree tree, javax.swing.tree.TreeModel model, MasterDetailTreeNode node, int row) { if (node != null && !model.isLeaf(node)) { tree.expandRow(row); for (var index = 0; row + 1 < tree.getRowCount() && index < model.getChildCount(node); index++) { row++; var child = (MasterDetailTreeNode) model.getChild(node, index); if (child == null) break; javax.swing.tree.TreePath path; while ((path = tree.getPathForRow(row)) != null && path.getLastPathComponent() != child) row++; if (path == null) break; row = expandJTreeNode(tree, model, child, row); } } return row; } public void expandAllChildren() { var tree = getTree(); var model = getTreeModel(); var thisRow = tree.getRowForPath(newTreePath()); expandJTreeNode(tree, model, this, thisRow); } /** Can be called anywhere on the tree. Assumes only one node selected. * Returns null if no node selected. */ public MasterDetailTreeNode getSelectedNode() { var p = tree.getSelectionPath(); if (p == null) return null; return (MasterDetailTreeNode) p.getLastPathComponent(); } // ----------------------------------------------------------------------- // Must be implemented by subclasses // ----------------------------------------------------------------------- /** Create and return children. No caching need be done, this is done * by getChildren() */ protected abstract MasterDetailTreeNode[] fetchChildren(); /** Create and return a Panel which can be displayed on the right hand * side of the window, when this node is selected. */ public abstract JPanel newDetailJPanel(); /** Create and return a JPopupMenu which can be displayed when the user * right-clicks on this node, or null for no pop-up menu */ public JPopupMenu newPopupMenu() { return null; } /** Returns an ImageIcon which represents this icon, to be displayed to the * left of it in the tree-view */ public abstract ImageIcon getTreeIcon(); /** Returns the text which can be used to display this icon in the * tree-view */ public abstract String getTreeText(); /** Returns the text which is in color at the end of the name. */ public String getExtraText() { return ""; } /** Returns the color which should be used for the extra text. */ public Color getExtraTextColor() { return Color.black; } /** Returns array of Flavors which can be accepted, in the case a drag & drop * attempts to drop on this node. */ public DataFlavor[] getAcceptableDropFlavors() { return new DataFlavor[0]; } /** Returns a bitmask of acceptable actions such as * DnDConstants.ACTION_COPY. */ public int getAcceptableDropActions() { return 0; } /** When a Transferrable is dropped on this node, this method * is called. In the default implementation this should never be reached, * as a default node cannot accept any DataFlavors or accept any actions. */ public void transferrableHasBeenDropped(DropTargetDropEvent e) { throw new RuntimeException("unreachable"); } // ----------------------------------------------------------------------- // javax.swing.tree.TreeNode API // ----------------------------------------------------------------------- public Enumeration children() { return new Vector<>(Arrays.asList(getChildren())).elements(); } public TreeNode getChildAt(int param) { return getChildren()[param]; } public int getChildCount() { return getChildren().length; } public TreeNode getParent() { return parent; } public boolean isLeaf() { return getChildren().length == 0; } public int getIndex(TreeNode treeNode) { TreeNode[] children = getChildren(); for (var i = 0; i < children.length; i++) if (treeNode == children[i]) return i; return -1; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy