//----------------------------------------------------------------------------// // // // S c o r e T r e e // // // //----------------------------------------------------------------------------// // <editor-fold defaultstate="collapsed" desc="hdr"> // // Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. // // This software is released under the GNU General Public License. // // Goto http://kenai.com/projects/audiveris to report bugs or suggestions. // //----------------------------------------------------------------------------// // </editor-fold> package omr.score.ui; import omr.Main; import omr.constant.Constant; import omr.constant.ConstantSet; import omr.score.Score; import omr.score.entity.Container; import omr.ui.MainGui; import omr.util.Dumper; import omr.util.Dumping.PackageRelevance; import omr.util.Dumping.Relevance; import omr.util.Navigable; import org.jdesktop.application.ResourceMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import javax.swing.AbstractAction; import javax.swing.JButton; import javax.swing.JEditorPane; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JToolBar; import javax.swing.JTree; import javax.swing.WindowConstants; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; /** * Class {@code ScoreTree} provides a user interface (a frame) where the * whole score hierarchy can be browsed as a tree. * * @author Hervé Bitteur */ public class ScoreTree { //~ Static fields/initializers --------------------------------------------- /** Specific application parameters */ private static final Constants constants = new Constants(); /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(ScoreTree.class); /** The filter for relevant classes and fields */ private static final Relevance filter = new PackageRelevance( Main.class.getPackage()); //~ Instance fields -------------------------------------------------------- /** Concrete UI component */ private final JPanel component; /** The right panel for HTML display */ private final JEditorPane htmlPane; /** The related score */ private final Score score; /** Cache to avoid recomputing sets of children */ private final HashMap<Object, List<Object>> nodeMap = new HashMap<>(); /** The tree model */ private final Model model; /** The enclosing frame */ private JFrame frame; //~ Constructors ----------------------------------------------------------- //-----------// // ScoreTree // //-----------// /** * Creates a new ScoreTree object. * * @param score the related score */ public ScoreTree (Score score) { this.score = score; component = new JPanel(); // Set up the tree model = new Model(score); ///model.addTreeModelListener(new ModelListener()); // Debug /** The tree entity */ JTree tree = new JTree(model); // Build left-side view JScrollPane treeView = new JScrollPane(tree); // Build right-side view htmlPane = new JEditorPane("text/html", ""); htmlPane.setEditable(false); JScrollPane htmlView = new JScrollPane(htmlPane); // Allow only single selections tree.getSelectionModel().setSelectionMode( TreeSelectionModel.SINGLE_TREE_SELECTION); // Display lines to explicit relationships tree.putClientProperty("JTree.lineStyle", "Angled"); // Wire the two views together. Use a selection listener // created with an anonymous inner-class adapter. // Listen for when the selection changes. tree.addTreeSelectionListener(new SelectionListener()); // To be notified of expansion / collapse actions (debug ...) ///tree.addTreeExpansionListener(new ExpansionListener()); // Build split-pane view JSplitPane splitPane = new JSplitPane( JSplitPane.HORIZONTAL_SPLIT, treeView, htmlView); splitPane.setName("treeHtmlSplitPane"); splitPane.setContinuousLayout(true); splitPane.setBorder(null); splitPane.setDividerSize(2); // Add GUI components component.setLayout(new BorderLayout()); component.add("Center", splitPane); } //~ Methods ---------------------------------------------------------------- //-------// // close // //-------// public void close () { if (frame != null) { frame.dispose(); frame = null; } } //----------// // getFrame // //----------// /** * Report the enclosing frame of this entity * * @return the frame of the score browser */ public JFrame getFrame () { if (frame == null) { // Set up a GUI framework frame = new JFrame(); frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); frame.setName("scoreTreeFrame"); // Add a REFRESH button JToolBar toolBar = new JToolBar(JToolBar.HORIZONTAL); frame.getContentPane().add(toolBar, BorderLayout.NORTH); // Set up the views, and display it all JButton refreshButton = new JButton( new AbstractAction() { @Override public void actionPerformed (ActionEvent e) { refresh(); } }); refreshButton.setName("refreshButton"); toolBar.add(refreshButton); frame.add(component); // Resources injection ResourceMap resource = MainGui.getInstance().getContext(). getResourceMap(getClass()); resource.injectComponents(frame); frame.setTitle( resource.getString("frameTitleMask", score.getRadix())); } return frame; } //---------// // refresh // //---------// /** * Refresh the whole display, to be in sync with latest data */ public void refresh () { model.refreshAll(); } //~ Inner Classes ---------------------------------------------------------- //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- /** Should we hide empty dummy containers */ Constant.Boolean hideEmptyDummies = new Constant.Boolean( false, "Should we hide empty dummy containers"); } //-----------------// // NamedCollection // //-----------------// private static class NamedCollection { //~ Instance fields ---------------------------------------------------- private final String name; private final Collection<?> collection; //~ Constructors ------------------------------------------------------- public NamedCollection (String name, Collection<?> collection) { this.name = name; this.collection = collection; } //~ Methods ------------------------------------------------------------ @Override public String toString () { return name; } } //-----------// // NamedData // //-----------// private static class NamedData { //~ Instance fields ---------------------------------------------------- private final String name; private final Object data; //~ Constructors ------------------------------------------------------- public NamedData (String name, Object data) { this.name = name; this.data = data; } //~ Methods ------------------------------------------------------------ @Override public String toString () { return name + ":" + data; } } //-------// // Model // //-------// // This adapter converts the current Score into a JTree model. private class Model implements TreeModel { //~ Instance fields ---------------------------------------------------- private List<TreeModelListener> listeners = new ArrayList<>(); private Score score; //~ Constructors ------------------------------------------------------- //---------// // Model // //---------// public Model (Score score) { this.score = score; } //~ Methods ------------------------------------------------------------ //----------------------// // addTreeModelListener // //----------------------// @Override public void addTreeModelListener (TreeModelListener listener) { if ((listener != null) && !listeners.contains(listener)) { listeners.add(listener); } } //----------// // getChild // //----------// @Override public Object getChild (Object parent, int index) { return getRelevantChildren(parent).toArray()[index]; } //---------------// // getChildCount // //---------------// @Override public int getChildCount (Object parent) { return getRelevantChildren(parent).size(); } //-----------------// // getIndexOfChild // //-----------------// @Override public int getIndexOfChild (Object parent, Object child) { int i = 0; for (Iterator<Object> it = getRelevantChildren(parent).iterator(); it.hasNext();) { if (it.next() == child) { return i; } i++; } throw new RuntimeException( "'" + child + "' not child of '" + parent + "'"); } //---------// // getRoot // //---------// @Override public Object getRoot () { return score; } //--------// // isLeaf // //--------// @Override public boolean isLeaf (Object node) { // Determines whether the icon shows up to the left. // Return true for any node with no children logger.debug("isLeaf. node={} {}", node, getChildCount(node) == 0); return getChildCount(node) == 0; } //------------// // refreshAll // //------------// public void refreshAll () { nodeMap.clear(); TreeModelEvent modelEvent = new TreeModelEvent( this, new Object[]{score}); for (TreeModelListener listener : listeners) { listener.treeStructureChanged(modelEvent); } } //-------------// // refreshPath // //-------------// public void refreshPath (TreePath path) { TreeModelEvent modelEvent = new TreeModelEvent(this, path); for (TreeModelListener listener : listeners) { listener.treeStructureChanged(modelEvent); } } //-------------------------// // removeTreeModelListener // //-------------------------// @Override public void removeTreeModelListener (TreeModelListener listener) { if (listener != null) { listeners.remove(listener); } } //---------------------// // valueForPathChanged // //---------------------// @Override public void valueForPathChanged (TreePath path, Object newValue) { // Null. We won't be making changes in the GUI. If we did, we would // ensure the new value was really new and then fire a // TreeNodesChanged event. } //---------------------// // getRelevantChildren // //---------------------// /** * Report the set of children of the provided node that are * relevant for display in the tree hierarchy (left pane) * * @param node the node to investigate * @return the collection of relevant children */ private List<Object> getRelevantChildren (Object node) { // First check the cache List<Object> relevants = nodeMap.get(node); if (relevants != null) { return relevants; } // Not found, so let's build it logger.debug("Retrieving relevants of {} {}", node, node.getClass()); // Case of Named Collection if (node instanceof NamedCollection) { ///logger.info("named collection: " + node); NamedCollection nc = (NamedCollection) node; relevants = new ArrayList<>(); nodeMap.put(node, relevants); for (Object n : nc.collection) { if (isRelevant(n)) { relevants.add(n); } } if (logger.isDebugEnabled()) { logger.debug("{} nb={}", node, relevants.size()); } return relevants; } // Case of Named Data if (node instanceof NamedData) { ///logger.info("named data: " + node); relevants = getRelevantChildren(((NamedData) node).data); nodeMap.put(node, relevants); if (logger.isDebugEnabled()) { logger.debug("{} nb={}", node, relevants.size()); } return relevants; } Class<?> classe = node.getClass(); relevants = new ArrayList<>(); nodeMap.put(node, relevants); // Walk up the inheritance tree do { // Browse the declared fields of the class at hand for (Field field : classe.getDeclaredFields()) { // Skip field if annotated as non navigable Navigable navigable = field.getAnnotation(Navigable.class); if ((navigable != null) && (navigable.value() == false)) { if (logger.isDebugEnabled()) { logger.debug("skipping {}", field); } continue; } ///logger.info("fieldName:" + field.getName()); try { // No static or inner class if (!filter.isFieldRelevant(field)) { ///System.out.println(" [field not relevant]"); continue; } field.setAccessible(true); Object object = field.get(node); // No null field if (object == null) { ///System.out.println(" [null]"); continue; } Class<?> objClass = object.getClass(); // Skip primitive members if (objClass.isPrimitive()) { ///System.out.println(" [primitive]"); continue; } // Special handling of collections if (object instanceof Collection) { Collection<?> coll = (Collection<?>) object; if (!coll.isEmpty()) { relevants.add( new NamedCollection(field.getName(), coll)); } continue; } if (!filter.isClassRelevant(objClass)) { ///System.out.println(" [CLASS not relevant]"); continue; } // No leaf on left pane if (getChildCount(object) == 0) { continue; } ///System.out.println(" ...OK"); relevants.add(new NamedData(field.getName(), object)); } catch (Exception ex) { logger.warn("Error in accessing field", ex); } } // Walk up the inheritance tree classe = classe.getSuperclass(); } while (filter.isClassRelevant(classe)); if (logger.isDebugEnabled()) { logger.debug("{} nb={}", node, relevants.size()); } return relevants; } //------------// // isRelevant // //------------// private boolean isRelevant (Object node) { // return !isLeaf(node); // We display dummy containers only when they are not empty if (constants.hideEmptyDummies.getValue() && (node instanceof NamedCollection || (node instanceof Container))) { return getChildCount(node) > 0; } else { return true; } } } //-------------------// // SelectionListener // //-------------------// private class SelectionListener implements TreeSelectionListener { //~ Methods ------------------------------------------------------------ @Override public void valueChanged (TreeSelectionEvent e) { TreePath p = e.getNewLeadSelectionPath(); if (p != null) { Object obj = p.getLastPathComponent(); if (obj instanceof NamedData) { NamedData nd = (NamedData) obj; obj = nd.data; } htmlPane.setText(new Dumper.Html(filter, obj).toString()); } } } }