001package jmri.jmrit.symbolicprog;
002
003import java.awt.BorderLayout;
004import java.awt.event.ActionEvent;
005import java.awt.event.MouseAdapter;
006import java.awt.event.MouseEvent;
007import java.util.ArrayList;
008import java.util.Enumeration;
009import java.util.HashMap;
010import java.util.List;
011import javax.swing.ButtonGroup;
012import javax.swing.JLabel;
013import javax.swing.JPanel;
014import javax.swing.JRadioButton;
015import javax.swing.JScrollPane;
016import javax.swing.JTree;
017import javax.swing.event.TreeSelectionEvent;
018import javax.swing.event.TreeSelectionListener;
019import javax.swing.tree.DefaultMutableTreeNode;
020import javax.swing.tree.DefaultTreeModel;
021import javax.swing.tree.DefaultTreeSelectionModel;
022import javax.swing.tree.TreeNode;
023import javax.swing.tree.TreePath;
024import jmri.GlobalProgrammerManager;
025import jmri.InstanceManager;
026import jmri.jmrit.decoderdefn.DecoderFile;
027import jmri.jmrit.decoderdefn.DecoderIndexFile;
028import jmri.jmrit.progsupport.ProgModeSelector;
029import jmri.jmrit.roster.Roster;
030import jmri.jmrit.roster.RosterEntry;
031import jmri.util.StringUtil;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035/**
036 * Provide GUI controls to select a known loco and/or new decoder.
037 * <p>
038 * This is an extension of the CombinedLocoSelPane class to use a JTree instead
039 * of a JComboBox for the decoder selection. The loco selection (Roster
040 * manipulation) parts are unchanged.
041 * <p>
042 * The JComboBox implementation always had to have selected entries, so we added
043 * dummy "select from .." items at the top and used those to indicate
044 * that there was no selection in that box. Here, the lack of a selection
045 * indicates there's no selection.
046 * <p>
047 * Internally, the "filter" is used to only show identified models (leaf nodes).
048 * This is implemented in internal InvisibleTreeModel and DecoderTreeNode
049 * classes.
050 * <p>
051 * The decoder definition {@link jmri.jmrit.decoderdefn.DecoderFile.Showable}
052 * attribute also interacts with those.
053 *
054 * @author Bob Jacobsen Copyright (C) 2001, 2002, 2013
055 */
056public class CombinedLocoSelTreePane extends CombinedLocoSelPane {
057
058    /**
059     * The decoder selection tree.
060     */
061    protected JTree dTree;
062
063    /**
064     * A panel immediately below the decoder selection tree.
065     * <br><br>
066     * Used for tree action buttons.
067     */
068    protected JPanel viewButtons;
069
070    /**
071     * The listener for the decoder selection tree.
072     */
073    protected transient volatile TreeSelectionListener dListener;
074    InvisibleTreeModel dModel;
075    DecoderTreeNode dRoot;
076    JRadioButton showAll;
077    JRadioButton showMatched;
078    ArrayList<TreePath> selectedPath = new ArrayList<>();
079
080    /**
081     * Provide GUI controls to select a known loco and/or new decoder.
082     *
083     * @param s        Reference to a JLabel that should be updated with status
084     *                 information as identification happens.
085     *
086     * @param selector Reference to a
087     *                 {@link jmri.jmrit.progsupport.ProgModeSelector} panel
088     *                 that configures the programming mode.
089     */
090    public CombinedLocoSelTreePane(JLabel s, ProgModeSelector selector) {
091        super(s, selector);
092    }
093
094    /**
095     * Create the panel used to select the decoder.
096     *
097     * @return a JPanel for handling the decoder-selection GUI
098     */
099    @Override
100    protected JPanel layoutDecoderSelection() {
101        JPanel pane1a = new JPanel(new BorderLayout());
102        pane1a.add(new JLabel(Bundle.getMessage("LabelDecoderInstalled")), BorderLayout.NORTH);
103        // create the list of manufacturers; get the list of decoders, and add elements
104        dRoot = new DecoderTreeNode("Root");
105        dModel = new InvisibleTreeModel(dRoot);
106        dTree = new JTree(dModel) {
107
108            @Override
109            public String getToolTipText(MouseEvent evt) {
110                if (getRowForLocation(evt.getX(), evt.getY()) == -1) {
111                    return null;
112                }
113                TreePath curPath = getPathForLocation(evt.getX(), evt.getY());
114                return ((DecoderTreeNode) curPath.getLastPathComponent()).getToolTipText();
115            }
116        };
117        dTree.setToolTipText("");
118
119        createDecoderTypeContents();
120
121        // build the tree GUI
122        pane1a.add(new JScrollPane(dTree), BorderLayout.CENTER);
123        dTree.expandPath(new TreePath(dRoot));
124        dTree.setRootVisible(false);
125        dTree.setShowsRootHandles(true);
126        dTree.setScrollsOnExpand(true);
127        dTree.setExpandsSelectedPaths(true);
128
129        dTree.getSelectionModel().setSelectionMode(DefaultTreeSelectionModel.SINGLE_TREE_SELECTION);
130        // tree listener
131        dTree.addTreeSelectionListener(dListener = (TreeSelectionEvent e) -> {
132            log.debug("selection changed {} {}", dTree.isSelectionEmpty(), dTree.getSelectionPath());
133            if (!dTree.isSelectionEmpty() && dTree.getSelectionPath() != null
134                    && // can't be just a mfg, has to be at least a family
135                    dTree.getSelectionPath().getPathCount() > 2
136                    && // can't be a multiple decoder selection
137                    dTree.getSelectionCount() < 2) {
138                // decoder selected - reset and disable loco selection
139                log.debug("Selection event with {}", dTree.getSelectionPath());
140                if (locoBox != null) {
141                    locoBox.setSelectedIndex(0);
142                }
143                go2.setEnabled(true);
144                go2.setRequestFocusEnabled(true);
145                go2.requestFocus();
146                go2.setToolTipText(Bundle.getMessage("TipClickToOpen"));
147            } else {
148                // decoder not selected - require one
149                go2.setEnabled(false);
150                go2.setToolTipText(Bundle.getMessage("TipSelectLoco"));
151            }
152        });
153
154//      Mouselistener for doubleclick activation of programmer
155        dTree.addMouseListener(new MouseAdapter() {
156
157            @Override
158            public void mouseClicked(MouseEvent me) {
159                // Clear any status messages and ensure the tree is in single path select mode
160                if (_statusLabel != null) {
161                    _statusLabel.setText(Bundle.getMessage("StateIdle"));
162                }
163                dTree.getSelectionModel().setSelectionMode(DefaultTreeSelectionModel.SINGLE_TREE_SELECTION);
164
165                if (me.getClickCount() == 2) {
166                    if (go2.isEnabled() && ((TreeNode) dTree.getSelectionPath().getLastPathComponent()).isLeaf()) {
167                        go2.doClick();
168                    }
169                }
170            }
171        });
172
173        viewButtons = new JPanel();
174        iddecoder = addDecoderIdentButton();
175        if (iddecoder != null) {
176            viewButtons.add(iddecoder);
177        }
178        showAll = new JRadioButton(Bundle.getMessage("LabelAll"));
179        showAll.getAccessibleContext().setAccessibleName(Bundle.getMessage("LabelAll"));
180        showAll.setSelected(true);
181        showMatched = new JRadioButton(Bundle.getMessage("LabelMatched"));
182        showMatched.getAccessibleContext().setAccessibleName(Bundle.getMessage("LabelMatched"));
183
184        if (InstanceManager.getNullableDefault(GlobalProgrammerManager.class) != null
185                && InstanceManager.getDefault(GlobalProgrammerManager.class).isGlobalProgrammerAvailable()) {
186            ButtonGroup group = new ButtonGroup();
187            group.add(showAll);
188            group.add(showMatched);
189            viewButtons.add(showAll);
190            viewButtons.add(showMatched);
191
192            pane1a.add(viewButtons, BorderLayout.SOUTH);
193            showAll.addActionListener((ActionEvent e) -> {
194                setShowMatchedOnly(false);
195            });
196            showMatched.addActionListener((ActionEvent e) -> {
197                setShowMatchedOnly(true);
198            });
199        }
200
201        return pane1a;
202    }
203
204    /**
205     * Sets the Loco Selection Pane to "Matched Only" {@code (true)} or "Show
206     * All" {@code (false)}.
207     * <br><br>
208     * Changes the Decoder Tree Display and the Radio Buttons.
209     *
210     * @param state the desired state
211     */
212    public void setShowMatchedOnly(boolean state) {
213        showMatched.setSelected(state);
214        showAll.setSelected(!state);
215        dModel.activateFilter(state);
216        dModel.reload();
217        for (TreePath path : selectedPath) {
218            log.debug("action selects path: {}", path);
219            dTree.expandPath(path);
220            dTree.addSelectionPath(path);
221            dTree.scrollPathToVisible(path);
222        }
223    }
224
225    /**
226     * Reads the available decoders and loads them into the dModel tree model.
227     */
228    void createDecoderTypeContents() {
229        List<DecoderFile> decoders = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, null, null, null, null, null);
230        int len = decoders.size();
231        DecoderTreeNode mfgElement = null;
232        DecoderTreeNode familyElement = null;
233        HashMap<String, DecoderTreeNode> familyNameNode = new HashMap<>();
234        for (int i = 0; i < len; i++) {
235            DecoderFile decoder = decoders.get(i);
236            String mfg = decoder.getMfg();
237            String family = decoder.getFamily();
238            String model = decoder.getModel();
239            log.debug(" process {}/{}/{} on nodes {}/{}", mfg, family, model, mfgElement == null ? "<null>" : mfgElement.toString() + "(" + mfgElement.getChildCount() + ")", familyElement == null ? "<null>" : familyElement.toString() + "(" + familyElement.getChildCount() + ")");
240
241            // build elements
242            if (mfgElement == null || !mfg.equals(mfgElement.toString())) {
243                // need new mfg node
244                mfgElement = new DecoderTreeNode(mfg,
245                        "CV8 = " + InstanceManager.getDefault(DecoderIndexFile.class).mfgIdFromName(mfg), "");
246                dModel.insertNodeInto(mfgElement, dRoot, dRoot.getChildCount());
247                familyNameNode = new HashMap<>();
248                familyElement = null;
249            }
250            String famComment = decoders.get(i).getFamilyComment();
251            String verString = decoders.get(i).getVersionsAsString();
252            if (familyElement == null || (!family.equals(familyElement.toString()) && !familyNameNode.containsKey(family))) {
253                // need new family node - is there only one model? Expect the
254                // family element, plus the model element, so check i+2
255                // to see if its the same, or if a single-decoder family
256                // appears to have decoder names separate from the family name
257                if ((i + 2 >= len)
258                        || decoders.get(i + 2).getFamily().equals(family)
259                        || !decoders.get(i + 1).getModel().equals(family)) {
260                    // normal here; insert the new family element & exit
261                    log.debug("normal family update case: {}", family);
262                    familyElement = new DecoderTreeNode(family,
263                            getHoverText(verString, famComment),
264                            decoders.get(i).titleString());
265                    dModel.insertNodeInto(familyElement, mfgElement, mfgElement.getChildCount());
266                    familyNameNode.put(family, familyElement);
267                    continue;
268                } else {
269                    // this is short case; insert decoder entry (next) here
270                    log.debug("short case, i={} family={} next {}", i, family, decoders.get(i + 1).getModel());
271                    if (i + 1 > len) {
272                        log.error("Unexpected single entry for family: {}", family);
273                    }
274                    family = decoders.get(i + 1).getModel();
275                    familyElement = new DecoderTreeNode(family,
276                            getHoverText(verString, famComment),
277                            decoders.get(i).titleString());
278                    dModel.insertNodeInto(familyElement, mfgElement, mfgElement.getChildCount());
279                    familyNameNode.put(family, familyElement);
280                    i = i + 1;
281                    continue;
282                }
283            }
284            // insert at the decoder level, except if family name is the same
285            if (!family.equals(model)) {
286                if (familyNameNode.containsKey(family)) {
287                    familyElement = familyNameNode.get(family);
288                }
289                String modelComment = decoders.get(i).getModelComment();
290                DecoderTreeNode decoderNameNode = new DecoderTreeNode(model,
291                        getHoverText(verString, modelComment),
292                        decoders.get(i).titleString());
293                decoderNameNode.setShowable(decoder.getShowable());
294                dModel.insertNodeInto(decoderNameNode, familyElement, familyElement.getChildCount());
295            }
296        }  // end of loop over decoders
297    }
298
299    /**
300     * Provide tooltip text: Decoder comment, with CV version info, formatted as
301     * best we can.
302     *
303     * @param verString version string, typically from
304     *                  {@link jmri.jmrit.decoderdefn.DecoderFile#getVersionsAsString DecoderFile.getVersionsAsString()}
305     * @param comment   version string, typically from
306     *                  {@link jmri.jmrit.decoderdefn.DecoderFile#getModelComment DecoderFile.getModelComment()}
307     *                  or
308     *                  {@link jmri.jmrit.decoderdefn.DecoderFile#getFamilyComment DecoderFile.getFamilyComment()}
309     * @return the combined formatted string.
310     */
311    String getHoverText(String verString, String comment) {
312        if (comment == null || comment.equals("")) {
313            if (!verString.equals("")) {
314                return "CV7=" + verString;
315            } else {
316                return "";
317            }
318        } else {
319            if (verString.equals("")) {
320                return comment;
321            } else {
322                return StringUtil.concatTextHtmlAware(comment, " (CV7=" + verString + ")");
323            }
324        }
325    }
326
327    /**
328     * Identify loco button pressed, start the identify operation. This defines
329     * what happens when the identify is done.
330     * <br><br>
331     * This {@code @Override} method invokes
332     * {@link #resetSelections resetSelections} before starting.
333     */
334    @Override
335    protected synchronized void startIdentifyDecoder() {
336        // start identifying a decoder
337        resetSelections();
338        super.startIdentifyDecoder();
339    }
340
341    /**
342     * Resets the Decoder Tree Display selections and sets the state to "Show
343     * All".
344     */
345    public void resetSelections() {
346        Enumeration<TreeNode> e = dRoot.breadthFirstEnumeration();
347        while (e.hasMoreElements()) {
348            ((DecoderTreeNode) e.nextElement()).setIdentified(false);
349        }
350        setShowMatchedOnly(false);
351        selectedPath = new ArrayList<>();
352        dTree.expandPath(new TreePath(dRoot));
353        dTree.setExpandsSelectedPaths(true);
354        int row = dTree.getRowCount() - 1;
355        while (row >= 0) {
356            dTree.collapseRow(row);
357            row--;
358        }
359    }
360
361    /**
362     * Decoder identify has matched one or more specific types.
363     *
364     * @param pList a list of decoders
365     */
366    @Override
367    public void updateForDecoderTypeID(List<DecoderFile> pList) {
368        // find and select the first item
369        if (log.isDebugEnabled()) {
370            StringBuilder buf = new StringBuilder();
371            for (int i = 0; i < pList.size(); i++) {
372                buf.append(pList.get(i).getModel()).append(":");
373            }
374            log.debug("Identified {} matches: {}", pList.size() , buf );
375        }
376        if (pList.size() <= 0) {
377            log.error("Found empty list in updateForDecoderTypeID, should not happen");
378            return;
379        }
380        dTree.clearSelection();
381        // If there are multiple matches change tree to allow multiple selections by the program
382        // and issue a warning instruction in the status bar
383        if (pList.size() > 1) {
384            dTree.getSelectionModel().setSelectionMode(DefaultTreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
385            _statusLabel.setText(Bundle.getMessage("StateMultipleMatch"));
386        } else {
387            dTree.getSelectionModel().setSelectionMode(DefaultTreeSelectionModel.SINGLE_TREE_SELECTION);
388            _statusLabel.setText(Bundle.getMessage("StateIdle"));
389        }
390
391        // set everybody not identified
392        Enumeration<TreeNode> e = dRoot.breadthFirstEnumeration();
393        while (e.hasMoreElements()) { // loop over the tree
394            DecoderTreeNode node = ((DecoderTreeNode) e.nextElement());
395            node.setIdentified(false);
396        }
397
398        selectedPath = new ArrayList<>();
399
400        // Find decoder nodes in tree and set selected
401        for (int i = 0; i < pList.size(); i++) { // loop over selected decoders
402            e = dRoot.breadthFirstEnumeration();
403
404            DecoderFile f = pList.get(i);
405            String findMfg = f.getMfg();
406            String findFamily = f.getFamily();
407            String findModel = f.getModel();
408
409            while (e.hasMoreElements()) { // loop over the tree & find node
410                DecoderTreeNode node = ((DecoderTreeNode) e.nextElement());
411                // never match show=NO nodes
412                if (node.getShowable() == DecoderFile.Showable.NO) {
413                    continue;
414                }
415                // convert path to comparison string
416                TreeNode[] list = node.getPath();
417                if (list.length == 3) {
418                    // check for match to mfg, model (as family)
419                    if (list[1].toString().equals(findMfg)
420                            && list[2].toString().equals(findModel)) {
421                        log.debug("match length 3");
422                        node.setIdentified(true);
423                        dModel.reload();
424                        ((DecoderTreeNode) list[1]).setIdentified(true);
425                        ((DecoderTreeNode) list[2]).setIdentified(true);
426                        TreePath path = new TreePath(node.getPath());
427                        selectedPath.add(path);
428                        break;
429                    }
430                } else if (list.length == 4) {
431                    // check for match to mfg, family, model
432                    if (list[1].toString().equals(findMfg)
433                            && list[2].toString().equals(findFamily)
434                            && list[3].toString().equals(findModel)) {
435                        log.debug("match length 4");
436                        node.setIdentified(true);
437                        dModel.reload();
438                        ((DecoderTreeNode) list[1]).setIdentified(true);
439                        ((DecoderTreeNode) list[2]).setIdentified(true);
440                        ((DecoderTreeNode) list[3]).setIdentified(true);
441                        TreePath path = new TreePath(node.getPath());
442                        selectedPath.add(path);
443                        break;
444                    }
445                }
446            }
447        }
448        // now select and show paths in tree
449        for (TreePath path : selectedPath) {
450            dTree.addSelectionPath(path);
451            dTree.expandPath(path);
452            dTree.scrollPathToVisible(path);
453        }
454    }
455
456    /**
457     * Decoder identify has not matched specific types, but did find
458     * manufacturer match.
459     *
460     * @param pMfg     Manufacturer name. This is passed to save time, as it has
461     *                 already been determined once.
462     * @param pMfgID   Manufacturer ID number (CV8)
463     * @param pModelID Model ID number (CV7)
464     */
465    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST",
466        justification="String also built for display in _statusLabel")
467    @Override
468    void updateForDecoderMfgID(String pMfg, int pMfgID, int pModelID) {
469        String msg = "Found mfg " + pMfgID + " (" + pMfg + ") version " + pModelID + "; no such decoder defined";
470        log.warn(msg);
471        _statusLabel.setText(msg);
472        // find this mfg to select it
473        dTree.clearSelection();
474
475        Enumeration<TreeNode> e = dRoot.breadthFirstEnumeration();
476
477        ArrayList<DecoderTreeNode> selected = new ArrayList<>();
478        selectedPath = new ArrayList<>();
479        while (e.hasMoreElements()) {
480            DecoderTreeNode node = (DecoderTreeNode) e.nextElement();
481            if (node.getParent() != null && node.getParent().toString().equals("Root")) {
482                if (node.toString().equals(pMfg)) {
483                    TreePath path = new TreePath(node.getPath());
484                    dTree.expandPath(path);
485                    dTree.addSelectionPath(path);
486                    dTree.scrollPathToVisible(path);
487                    selectedPath.add(path);
488                    node.setIdentified(true);
489                    selected.add(node);
490                }
491            } else {
492                node.setIdentified(false);
493            }
494        }
495        for (DecoderTreeNode node : selected) {
496            node.setIdentified(true);
497
498            Enumeration<TreeNode> es = dRoot.breadthFirstEnumeration();
499
500            while (es.hasMoreElements()) {
501                ((DecoderTreeNode) es.nextElement()).setIdentified(true);
502            }
503        }
504        if (showMatched.isSelected()) {
505            dModel.activateFilter(true);
506            dModel.reload();
507        }
508    }
509
510    /**
511     * Decoder identify did not match anything, warn and clear selection.
512     *
513     * @param pMfgID   Manufacturer ID number (CV8)
514     * @param pModelID Model ID number (CV7)
515     */
516    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST",
517        justification="String also built for display in _statusLabel")
518    @Override
519    void updateForDecoderNotID(int pMfgID, int pModelID) {
520        String msg = "Found mfg " + pMfgID + " version " + pModelID + "; no such manufacturer defined";
521        log.warn(msg);
522        _statusLabel.setText(msg);
523        dTree.clearSelection();
524    }
525
526    /**
527     * Set the decoder selection to a specific decoder from a selected Loco.
528     * <p>
529     * This must not trigger an update event from the Tree selection, so we
530     * remove and replace the listener.
531     *
532     * @param loco the loco name
533     */
534    @Override
535    void setDecoderSelectionFromLoco(String loco) {
536        // if there's a valid loco entry...
537        RosterEntry locoEntry = Roster.getDefault().entryFromTitle(loco);
538        if (locoEntry == null) {
539            return;
540        }
541        dTree.removeTreeSelectionListener(dListener);
542        dTree.clearSelection();
543        // get the decoder type, it has to be there (assumption!),
544        String modelString = locoEntry.getDecoderModel();
545        String familyString = locoEntry.getDecoderFamily();
546
547        // close the entire GUI (not currently done, users want left open)
548        //collapseAll();
549        // find this one to select it
550        Enumeration<TreeNode> e = dRoot.breadthFirstEnumeration();
551
552        while (e.hasMoreElements()) {
553            DecoderTreeNode node = (DecoderTreeNode) e.nextElement();
554            DecoderTreeNode parentNode = (DecoderTreeNode) node.getParent();
555            if (node.toString().equals(modelString)
556                    && parentNode.toString().equals(familyString)) {
557                TreePath path = new TreePath(node.getPath());
558                node.setIdentified(true);
559                parentNode.setIdentified(true);
560                dModel.reload();
561                dTree.addSelectionPath(path);
562                dTree.scrollPathToVisible(path);
563                break;
564            }
565        }
566        // and restore the listener
567        dTree.addTreeSelectionListener(dListener);
568    }
569
570    /**
571     * Convert the decoder selection UI result into a name.
572     *
573     * @return The selected decoder type name, or null if none selected.
574     */
575    @Override
576    protected String selectedDecoderType() {
577        if (!isDecoderSelected()) {
578            return null;
579        } else {
580            return ((DecoderTreeNode) dTree.getLastSelectedPathComponent()).getTitle();
581        }
582    }
583
584    /**
585     * Has the user selected a decoder type, either manually or via a successful
586     * event?
587     *
588     * @return true if a decoder type is selected
589     */
590    @Override
591    boolean isDecoderSelected() {
592        return !dTree.isSelectionEmpty();
593    }
594    private final static Logger log = LoggerFactory.getLogger(CombinedLocoSelTreePane.class);
595
596    /**
597     * The following has been taken from an example given in..
598     * http://www.java2s.com/Code/Java/Swing-Components/DecoderTreeNodeTreeExample.htm
599     * with extracts from http://www.codeguru.com/java/articles/143.shtml
600     */
601    static class InvisibleTreeModel extends DefaultTreeModel {
602
603        protected boolean filterIsActive;
604
605        public InvisibleTreeModel(TreeNode root) {
606            this(root, false);
607        }
608
609        public InvisibleTreeModel(TreeNode root, boolean asksAllowsChildren) {
610            this(root, false, false);
611        }
612
613        public InvisibleTreeModel(TreeNode root, boolean asksAllowsChildren,
614                boolean filterIsActive) {
615            super(root, asksAllowsChildren);
616            this.filterIsActive = filterIsActive;
617        }
618
619        public void activateFilter(boolean newValue) {
620            filterIsActive = newValue;
621        }
622
623        public boolean isActivatedFilter() {
624            return filterIsActive;
625        }
626
627        @Override
628        public Object getChild(Object parent, int index) {
629            if (parent instanceof DecoderTreeNode) {
630                return ((DecoderTreeNode) parent).getChildAt(index,
631                        filterIsActive);
632            }
633            return ((TreeNode) parent).getChildAt(index);
634        }
635
636        @Override
637        public int getChildCount(Object parent) {
638            if (parent instanceof DecoderTreeNode) {
639                return ((DecoderTreeNode) parent).getChildCount(filterIsActive);
640            }
641            return ((TreeNode) parent).getChildCount();
642        }
643    }
644
645    static class DecoderTreeNode extends DefaultMutableTreeNode {
646
647        protected boolean isIdentified;
648        private String toolTipText;
649        private String title;
650        DecoderFile.Showable showable = DecoderFile.Showable.YES;  // default
651
652        public DecoderTreeNode(String str, String toolTipText, String title) {
653            this(str);
654            this.toolTipText = toolTipText;
655            this.title = title;
656        }
657
658        @Override
659        public Enumeration<TreeNode> breadthFirstEnumeration() { // JDK 9 typing
660            return super.breadthFirstEnumeration();
661        }
662
663        public String getTitle() {
664            return title;
665        }
666
667        public String getToolTipText() {
668            return toolTipText;
669        }
670
671        public DecoderTreeNode(Object userObject) {
672            this(userObject, true, false, DecoderFile.Showable.YES);
673        }
674
675        public DecoderTreeNode(Object userObject, boolean allowsChildren,
676                boolean isIdentified, DecoderFile.Showable showable) {
677            super(userObject, allowsChildren);
678            this.isIdentified = isIdentified;
679            this.showable = showable;
680        }
681
682        public TreeNode getChildAt(int index, boolean filterIsActive) {
683            if (children == null) {
684                throw new ArrayIndexOutOfBoundsException("node has no children");
685            }
686
687            int realIndex = -1;
688            int visibleIndex = -1;
689            Enumeration<?> e = children.elements();
690            while (e.hasMoreElements()) {
691                DecoderTreeNode node = (DecoderTreeNode) e.nextElement();
692                if (node.isVisible(filterIsActive)) {
693                    visibleIndex++;
694                }
695                realIndex++;
696                if (visibleIndex == index) {
697                    return children.elementAt(realIndex);
698                }
699            }
700
701            throw new ArrayIndexOutOfBoundsException("index unmatched");
702            //return (TreeNode)children.elementAt(index);
703        }
704
705        public int getChildCount(boolean filterIsActive) {
706            if (children == null) {
707                return 0;
708            }
709
710            int count = 0;
711            Enumeration<?> e = children.elements();
712            while (e.hasMoreElements()) {
713                DecoderTreeNode node = (DecoderTreeNode) e.nextElement();
714                if (node.isVisible(filterIsActive)) {
715                    count++;
716                }
717            }
718
719            return count;
720        }
721
722        public void setIdentified(boolean isIdentified) {
723            this.isIdentified = isIdentified;
724        }
725
726        public void setShowable(DecoderFile.Showable showable) {
727            this.showable = showable;
728        }
729
730        public DecoderFile.Showable getShowable() {
731            return this.showable;
732        }
733
734        private boolean isVisible(boolean filterIsActive) {
735            // if there are children, are any visible?
736            if (children != null) {
737                Enumeration<?> e = children.elements();
738                while (e.hasMoreElements()) {
739                    DecoderTreeNode node = (DecoderTreeNode) e.nextElement();
740                    if (node.isVisible(filterIsActive)) {
741                        return true;
742                    }
743                }
744                return false;
745            }
746            // no children
747            return isIdentified || (!filterIsActive && (showable == DecoderFile.Showable.YES));
748        }
749    }
750}