001package jmri.jmrit.catalog;
002
003import java.awt.BorderLayout;
004import java.awt.Color;
005import java.awt.Component;
006import java.awt.Dimension;
007import java.awt.FlowLayout;
008import java.awt.Frame;
009import java.awt.GridBagConstraints;
010import java.awt.GridBagLayout;
011import java.awt.Insets;
012import java.awt.Point;
013import java.awt.datatransfer.DataFlavor;
014import java.awt.datatransfer.Transferable;
015import java.awt.datatransfer.UnsupportedFlavorException;
016import java.awt.dnd.DnDConstants;
017import java.awt.dnd.DropTarget;
018import java.awt.dnd.DropTargetDragEvent;
019import java.awt.dnd.DropTargetDropEvent;
020import java.awt.dnd.DropTargetEvent;
021import java.awt.dnd.DropTargetListener;
022import java.awt.event.ActionEvent;
023import java.awt.image.BufferedImage;
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.Enumeration;
027import java.util.List;
028import javax.swing.*;
029import javax.swing.event.TreeSelectionEvent;
030import javax.swing.tree.*;
031
032import jmri.CatalogTree;
033import jmri.CatalogTreeNode;
034import jmri.CatalogTreeLeaf;
035import jmri.CatalogTreeManager;
036import jmri.InstanceManager;
037import jmri.jmrit.display.Editor;
038import jmri.jmrit.display.palette.IconItemPanel;
039import jmri.util.FileUtil;
040import jmri.util.swing.DrawSquares;
041import jmri.util.swing.ImagePanel;
042import jmri.util.swing.JmriMouseEvent;
043import jmri.util.swing.JmriMouseListener;
044import jmri.util.swing.JmriJOptionPane;
045
046/**
047 * Create a JPanel containing trees of resources to replace default icons. The
048 * panel also displays image files contained in a node of a tree. Drag and
049 * Drop is implemented to drag a display of an icon to the display of an icon
050 * that may be added to the panel.
051 * <p>
052 * This panel is used in the Icon Editors and also in the {@link ImageIndexEditor}.
053 *
054 * @author Pete Cressman Copyright 2009, 2018
055 * @author Egbert Broerse Copyright 2017
056 */
057public class CatalogPanel extends JPanel {
058
059    private static final Object _lock = new Object();
060
061    public static final double ICON_SCALE = 0.020;
062    public static final int ICON_WIDTH = 100;
063    public static final int ICON_HEIGHT = 100;
064
065    private IconDisplayPanel _selectedImage;
066    private IconItemPanel _parent;      // IconItemPanel could implement an interface if other classes use deselectIcon()
067    private JSplitPane _splitPane;
068    static Color _grayColor = new Color(235, 235, 235);
069    static Color _darkGrayColor = new Color(150, 150, 150);
070    protected Color[] colorChoice = new Color[] {Color.white, _grayColor, _darkGrayColor};
071    /**
072     * Array of BufferedImage backgrounds loaded as background image in Preview.
073     */
074    protected BufferedImage[] _backgrounds;
075
076    JScrollPane _iconPane;
077    JLabel _previewLabel = new JLabel(" ");
078    protected ImagePanel _preview;
079    private boolean _treeDnd;
080    private boolean _dragIcons;
081
082    private JScrollPane _treePane;
083    private JTree _dTree;
084    private DefaultTreeModel _model;
085    private final ArrayList<CatalogTree> _branchModel = new ArrayList<>();
086
087    /**
088     * Constructor
089     *
090     * The constructor is private to force using the method makeCatalogPanel
091     */
092    private CatalogPanel() {
093        _model = new DefaultTreeModel(new CatalogTreeNode("mainRoot"));
094    }
095
096    /**
097     * Ctor for a named icon catalog split pane. Make sure both properties keys exist.
098     *
099     * The constructor is private to force using the method makeCatalogPanel
100     *
101     * @param label1 properties key to be used as the label for the icon tree
102     * @param label2 properties key to be used as the instruction
103     * @param addButtonPanel adds background select comboBox
104     */
105    private CatalogPanel(String label1, String label2, boolean addButtonPanel) {
106        super(true);
107        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
108        setLayout(new BorderLayout());
109        add(new JLabel(Bundle.getMessage(label2)), BorderLayout.NORTH);
110        _splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
111                makeTreePanel(label1), makePreviewPanel()); // create left and right icon tree views
112        _splitPane.setContinuousLayout(true);
113        _splitPane.setOneTouchExpandable(true);
114        add(_splitPane, BorderLayout.CENTER);
115        if (addButtonPanel) {
116            add(makeButtonPanel(), BorderLayout.SOUTH); // add the background chooser
117        }
118    }
119
120    /**
121     * Ctor for a named icon catalog split pane. Make sure both properties keys exist.
122     *
123     * The constructor is private to force using the method makeCatalogPanel
124     *
125     * @param label1 properties key to be used as the label for the icon tree
126     * @param label2 properties key to be used as the instruction
127     */
128    private CatalogPanel(String label1, String label2) {
129        this(label1, label2, true);
130    }
131
132    @Override
133    public void setToolTipText(String tip) {
134        if (_dTree != null) {
135            _dTree.setToolTipText(tip);
136        }
137        if (_treePane != null) {
138            _treePane.setToolTipText(tip);
139        }
140        super.setToolTipText(tip);
141    }
142
143    /**
144     * Customize CatalogPanel to be used either as editing/creating an ImageEditor
145     * or as a panel to display or deliver icons to widgets
146     * @param treeDnD true allows dropping into tree or panel
147     * @param dragIcons true allows dragging icons from panel
148     */
149    private void init(boolean treeDnD, boolean dragIcons) {
150        _model = new DefaultTreeModel(new CatalogTreeNode("mainRoot"));
151        if (treeDnD) { // index editor (right pane)
152            _dTree = new DropJTree(_model);
153            setTransferHandler(new DropOnPanelToNode());
154        } else {       // Catalog (left pane index editor or all icon editors)
155            _dTree = new JTree(_model);
156        }
157        _treeDnd = treeDnD;
158        _dragIcons = dragIcons;
159        log.debug("CatalogPanel.init _treeDnd= {}, _dragIcons= {}", _treeDnd, _dragIcons);
160        DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer();
161        renderer.setLeafIcon(renderer.getClosedIcon());
162        _dTree.setCellRenderer(renderer);
163        _dTree.setRootVisible(false);
164        _dTree.setShowsRootHandles(true);
165        _dTree.setScrollsOnExpand(true);
166        _dTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
167
168        _dTree.addTreeSelectionListener((TreeSelectionEvent e) -> updatePanel());
169        _dTree.setExpandsSelectedPaths(true);
170        _treePane.setViewportView(_dTree);
171    }
172
173    public void setParent(IconItemPanel p) {
174        _parent = p;
175    }
176
177    public void updatePanel() {
178        log.debug("updatePanel: _dTree.isSelectionEmpty()= {} _dTree.getSelectionPath() is {}null",
179                _dTree.isSelectionEmpty(), (_dTree.getSelectionPath() == null) ? "" : "not ");
180        if (!_dTree.isSelectionEmpty() && _dTree.getSelectionPath() != null) {
181            try {
182                _previewLabel.setText(setIcons());
183            } catch (OutOfMemoryError oome) {
184                resetPanel();
185                log.debug("setIcons threw OutOfMemoryError", oome);
186            }
187        } else {
188            _previewLabel.setText(" ");
189        }
190    }
191
192    /**
193     * Create a new model and add it to the main root.
194     * <p>
195     * Can be called from off the GUI thread.
196     *
197     * @param systemName the system name for the catalog
198     * @param userName   the user name for the catalog
199     * @param path       the path on the new branch
200     */
201    public void createNewBranch(String systemName, String userName, String path) {
202        synchronized (_lock) {
203            CatalogTreeManager manager = InstanceManager.getDefault(jmri.CatalogTreeManager.class);
204            CatalogTree tree = manager.getBySystemName(systemName);
205            if (tree != null) {
206                jmri.util.ThreadingUtil.runOnGUI(() -> addTree(tree));
207            } else {
208                final CatalogTree t = manager.newCatalogTree(systemName, userName);
209                t.insertNodes(path);
210                jmri.util.ThreadingUtil.runOnGUI(() -> addTree(t));
211            }
212        }
213    }
214
215    /**
216     * For Index Editor to able to edit its tree
217     * @return tree
218     */
219    protected JTree getTree() {
220        return _dTree;
221    }
222
223    /**
224     * Extend the Catalog by adding a tree to the root.
225     *
226     * @param tree the tree to add to the catalog
227     */
228    public void addTree(CatalogTree tree) {
229        String name = tree.getSystemName();
230        for (CatalogTree t : _branchModel) {
231            if (name.equals(t.getSystemName())) {
232                return;
233            }
234        }
235        addTreeBranch(tree.getRoot());
236        _branchModel.add(tree);
237        _model.reload();
238    }
239
240    /**
241     * Recursively add the branch nodes to the display tree.
242     */
243    private void addTreeBranch(CatalogTreeNode node) {
244        if (log.isDebugEnabled()) {
245            log.debug("addTreeBranch called for node= {}, has {} children.",
246                    node, node.getChildCount());
247        }
248        CatalogTreeNode root = (CatalogTreeNode) _model.getRoot();
249        Enumeration<TreeNode> e = node.children();
250        while (e.hasMoreElements()) {
251            CatalogTreeNode n = (CatalogTreeNode)e.nextElement();
252            addNode(root, n);
253        }
254    }
255
256    /**
257     * Clone the node and adds to parent.
258     */
259    private void addNode(CatalogTreeNode parent, CatalogTreeNode n) {
260        CatalogTreeNode node = new CatalogTreeNode((String) n.getUserObject());
261        node.setLeaves(n.getLeaves());
262        parent.add(node);
263        Enumeration<TreeNode> e = n.children();
264        while (e.hasMoreElements()) {
265            CatalogTreeNode nChild = (CatalogTreeNode)e.nextElement();
266            addNode(node, nChild);
267        }
268    }
269
270    /**
271     * The tree held in the CatalogTreeManager must be kept in sync with the
272     * tree displayed as the Image Index. Required in order to save the Index to
273     * disc.
274     */
275    private CatalogTreeNode getCorrespondingNode(CatalogTreeNode node) {
276        TreeNode[] nodes = node.getPath();
277        CatalogTreeNode cNode = null;
278        for (CatalogTree t : _branchModel) {
279            CatalogTreeNode cRoot = t.getRoot();
280            cNode = match(cRoot, nodes, 1);
281            if (cNode != null) {
282                break;
283            }
284        }
285        return cNode;
286    }
287
288    /**
289     * Find the corresponding node in a CatalogTreeManager tree with a displayed
290     * node.
291     */
292    private CatalogTreeNode match(CatalogTreeNode cRoot, TreeNode[] nodes, int idx) {
293        if (idx == nodes.length) {
294            return cRoot;
295        }
296        Enumeration<TreeNode> e = cRoot.children();
297        CatalogTreeNode result = null;
298        while (e.hasMoreElements()) {
299            CatalogTreeNode cNode = (CatalogTreeNode)e.nextElement();
300            if (nodes[idx].toString().equals(cNode.toString())) {
301                result = match(cNode, nodes, idx + 1);
302                break;
303            }
304        }
305        return result;
306    }
307
308    /**
309     * Find the corresponding CatalogTreeManager tree to the displayed branch.
310     */
311    private CatalogTree getCorespondingModel(CatalogTreeNode node) {
312        TreeNode[] nodes = node.getPath();
313        CatalogTree model = null;
314        for (CatalogTree t : _branchModel) {
315            model = t;
316            CatalogTreeNode cRoot = model.getRoot();
317            if (match(cRoot, nodes, 1) != null) {
318                break;
319            }
320        }
321        return model;
322    }
323
324    /**
325     * Insert a new node into the displayed tree.
326     *
327     * @param name   the name of the new node
328     * @param parent the parent of name
329     * @return true if the node was inserted
330     */
331    protected boolean insertNodeIntoModel(String name, CatalogTreeNode parent) {
332        if (!nameOK(parent, name)) {
333            return false;
334        }
335        int index = 0;
336        Enumeration<TreeNode> e = parent.children();
337        while (e.hasMoreElements()) {
338            CatalogTreeNode n = (CatalogTreeNode)e.nextElement();
339            if (name.compareTo(n.toString()) < 0) {
340                break;
341            }
342            index++;
343        }
344        CatalogTreeNode newChild = new CatalogTreeNode(name);
345        _model.insertNodeInto(newChild, parent, index);
346
347        CatalogTreeNode cParent = getCorrespondingNode(parent);
348        CatalogTreeNode node = new CatalogTreeNode(name);
349        AbstractCatalogTree tree = (AbstractCatalogTree) getCorespondingModel(parent);
350        if(tree!=null) {
351            tree.insertNodeInto(node, cParent, index);
352            InstanceManager.getDefault(CatalogTreeManager.class).indexChanged(true);
353        }
354        return true;
355    }
356
357    /**
358     * Delete a node from the displayed tree.
359     *
360     * @param node the node to delete
361     */
362    protected void removeNodeFromModel(CatalogTreeNode node) {
363        AbstractCatalogTree tree = (AbstractCatalogTree) getCorespondingModel(node);
364        if(tree!=null) {
365            tree.removeNodeFromParent(getCorrespondingNode(node));
366            _model.removeNodeFromParent(node);
367            InstanceManager.getDefault(CatalogTreeManager.class).indexChanged(true);
368        }
369    }
370
371    /**
372     * Make a change to a node in the displayed tree. Either its name or the
373     * contents of its leaves (image references).
374     *
375     * @param node the node to change
376     * @param name new name for the node
377     * @return true if the change was successful
378     */
379    protected boolean renameNode(CatalogTreeNode node, String name) {
380        if (!nameOK((CatalogTreeNode)node.getParent(), name)) {
381            return false;
382        }
383        CatalogTreeNode cNode = getCorrespondingNode(node);
384        AbstractCatalogTree tree = (AbstractCatalogTree) getCorespondingModel(node);
385        if (cNode != null && tree != null) {
386            cNode.setLeaves(node.getLeaves());
387
388            cNode.setUserObject(name);
389            tree.nodeChanged(cNode);
390            node.setUserObject(name);
391            _model.nodeChanged(node);
392            InstanceManager.getDefault(CatalogTreeManager.class).indexChanged(true);
393            updatePanel();
394            return true;
395        }
396        return false;
397    }
398
399    private void addLeaf(CatalogTreeNode node, NamedIcon icon) {
400        node.addLeaf(icon.getName(), icon.getURL());
401
402        CatalogTreeNode cNode = getCorrespondingNode(node);
403        AbstractCatalogTree tree = (AbstractCatalogTree) getCorespondingModel(node);
404        if (cNode != null && tree != null) {
405            cNode.setLeaves(node.getLeaves());
406
407            cNode.setUserObject(node.toString());
408            tree.nodeChanged(cNode);
409            _model.nodeChanged(node);
410
411            InstanceManager.getDefault(CatalogTreeManager.class).indexChanged(true);
412        }
413        if (node.equals(getSelectedNode())) {
414            updatePanel();
415        }
416    }
417
418    /**
419     * Check that Node names in the path to the root are unique.
420     */
421    private boolean nameOK(CatalogTreeNode node, String name) {
422        TreeNode[] nodes = node.getPath();
423        for (TreeNode node1 : nodes) {
424            if (name.equals(node1.toString())) {
425                return false;
426            }
427        }
428        return true;
429    }
430
431    /**
432     * Only call when log.isDebugEnabled() is true
433     *
434     * public void enumerateTree() { CatalogTreeNode root =
435     * (CatalogTreeNode)_model.getRoot(); log.debug("enumerateTree called for
436     * root= "+root.toString()+ ", has "+root.getChildCount()+" children");
437     * Enumeration e =root.depthFirstEnumeration(); while (e.hasMoreElements())
438     * { CatalogTreeNode n = (CatalogTreeNode)e.nextElement();
439     * log.debug("nodeName= "+n.getUserObject()+" has "+n.getLeaves().size()+"
440     * leaves."); } }
441     */
442
443    private JPanel makeTreePanel(String label) {
444        JPanel panel = new JPanel();
445        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
446        _treePane = new JScrollPane(_dTree);
447        panel.add(new JLabel(Bundle.getMessage(label)));
448        _treePane.setMinimumSize(new Dimension(30, 100));
449        panel.add(_treePane);
450        return panel;
451    }
452    /**
453     * Set up a display pane for a tree that shows only directory nodes (no file
454     * leaves). The leaves (icon images) will be displayed in this panel.
455     */
456    private JPanel makePreviewPanel() {
457        JPanel previewPanel = new JPanel();
458        previewPanel.setLayout(new BoxLayout(previewPanel, BoxLayout.Y_AXIS));
459        previewPanel.add(_previewLabel);
460        _preview = new ImagePanel();
461        _preview.setOpaque(false);
462        _iconPane =  new JScrollPane(_preview);
463        previewPanel.add(_iconPane);
464        _iconPane.setMinimumSize(new Dimension(30, 100));
465        _iconPane.setPreferredSize(new Dimension(2*ICON_WIDTH, 2*ICON_HEIGHT));
466        return previewPanel;
467    }
468
469    /**
470     * Create panel element containing a "View on:" drop down list.
471     * Employs a normal JComboBox, no Panel Background option.
472     *
473     * @return the JPanel with label and drop down
474     */
475    private JPanel makeButtonPanel() {
476        // create array of backgrounds
477        if (_backgrounds == null) {
478            _backgrounds = new BufferedImage[4];
479            for (int i = 0; i <= 2; i++) {
480                _backgrounds[i] = DrawSquares.getImage(300, 400, 10, colorChoice[i], colorChoice[i]);
481            }
482            _backgrounds[3] = DrawSquares.getImage(300, 400, 10, Color.white, _grayColor);
483        }
484        JComboBox<String> bgColorBox = new JComboBox<>();
485        bgColorBox.addItem(Bundle.getMessage("White"));
486        bgColorBox.addItem(Bundle.getMessage("LightGray"));
487        bgColorBox.addItem(Bundle.getMessage("DarkGray"));
488        bgColorBox.addItem(Bundle.getMessage("Checkers")); // checkers option
489        bgColorBox.setSelectedIndex(0); // start as "White"
490        bgColorBox.addActionListener((ActionEvent e) -> {
491            // load background image
492            _preview.setImage(_backgrounds[bgColorBox.getSelectedIndex()]);
493            log.debug("Catalog setImage called");
494            _preview.setOpaque(false);
495            _preview.invalidate();
496        });
497
498        JPanel backgroundPanel = new JPanel();
499        backgroundPanel.setLayout(new BoxLayout(backgroundPanel, BoxLayout.Y_AXIS));
500        JPanel pp = new JPanel();
501        pp.setLayout(new FlowLayout(FlowLayout.CENTER));
502        pp.add(new JLabel(Bundle.getMessage("setBackground")));
503        pp.add(bgColorBox);
504        backgroundPanel.add(pp);
505        backgroundPanel.setMaximumSize(backgroundPanel.getPreferredSize());
506        return backgroundPanel;
507    }
508
509    /**
510     * Allows ItemPalette to set the preview panel background to match that of
511     * the icon set being edited.
512     *
513     * @return Preview panel
514     */
515    public ImagePanel getPreviewPanel() {
516        return _preview;
517    }
518
519    protected void resetPanel() {
520        _selectedImage = null;
521        if (_preview == null) {
522            return;
523        }
524        if (log.isDebugEnabled()) {
525            log.debug("_preview.removeAll done.");
526        }
527        _preview.removeAll();
528        _preview.repaint();
529    }
530
531    // called by palette.IconItemPanel to get user's selection from catalog
532    public NamedIcon getIcon() {
533        if (_selectedImage != null) {
534            return _selectedImage.getIcon();
535        }
536        return null;
537    }
538
539    // called by palette.IconItemPanel when selection is made for its iconMap
540    public void deselectIcon() {
541        if (_selectedImage !=null) {
542            _selectedImage.setBorder(null);
543            _selectedImage = null;
544        }
545    }
546
547    protected void setSelection(IconDisplayPanel panel) {
548        if (_parent == null) {
549            return;
550        }
551        if (_selectedImage != null && !panel.equals(_selectedImage)) {
552            deselectIcon();
553        }
554        if (panel != null) {
555            panel.setBorder(BorderFactory.createLineBorder(Color.red, 2));
556            _selectedImage = panel;
557        } else {
558            deselectIcon();
559        }
560        _parent.deselectIcon();
561    }
562
563    public class MemoryExceptionHandler implements Thread.UncaughtExceptionHandler {
564
565        @Override
566        public void uncaughtException(Thread t, Throwable e) {
567            _noMemory = true;
568            log.error("MemoryExceptionHandler", e);
569        }
570    }
571
572    private boolean _noMemory = false;
573
574    /**
575     * Display the icons in the preview panel.
576     */
577    private String setIcons() {
578        Thread.UncaughtExceptionHandler exceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
579        resetPanel();
580        CatalogTreeNode node = getSelectedNode();
581        if (node == null) {
582            return null;
583        }
584        List<CatalogTreeLeaf> leaves = node.getLeaves();
585        if (leaves == null) {
586            return null;
587        }
588        int numCol = 1;
589        while (numCol * numCol < leaves.size()) {
590            numCol++;
591        }
592        if (numCol > 1) {
593            numCol--;
594        }
595        int numRow = leaves.size() / numCol;
596        boolean newCol = false;
597        _noMemory = false;
598        // VM launches another thread to run ImageFetcher.
599        // This handler will catch memory exceptions from that thread
600        Thread.setDefaultUncaughtExceptionHandler(new MemoryExceptionHandler());
601        GridBagLayout gridbag = new GridBagLayout();
602        _preview.setLayout(gridbag);
603        GridBagConstraints c = new GridBagConstraints();
604        c.fill = GridBagConstraints.NONE;
605        c.anchor = GridBagConstraints.CENTER;
606        c.weightx = 1.0;
607        c.weighty = 1.0;
608        c.gridy = 0;
609        c.gridx = -1;
610        for (int i = 0; i < leaves.size(); i++) {
611            if (_noMemory) {
612                continue;
613            }
614            CatalogTreeLeaf leaf = leaves.get(i);
615            NamedIcon icon = new NamedIcon(leaf.getPath(), leaf.getName());
616            if (_noMemory) {
617                continue;
618            }
619            if (c.gridx < numCol) {
620                c.gridx++;
621            } else if (c.gridy < numRow) { // start next row
622                c.gridy++;
623                if (!newCol) {
624                    c.gridx = 0;
625                }
626            } else if (!newCol) { // start new column
627                c.gridx++;
628                c.gridy = 0;
629                newCol = true;
630            } else {  // start new row
631                c.gridy++;
632                c.gridx = 0;
633                newCol = false;
634            }
635            c.insets = new Insets(5, 5, 0, 0);
636
637            JPanel p = new IconDisplayPanel(leaf.getName(), icon);
638            gridbag.setConstraints(p, c);
639            _preview.add(p);
640            log.debug("{} inserted at ({}, {})", leaf.getName(), c.gridx, c.gridy);
641        }
642        _preview.invalidate();
643
644        Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
645        return Bundle.getMessage("numImagesInNode", node.getUserObject(), leaves.size());
646    }
647
648    class IconListener implements JmriMouseListener {
649        @Override
650        public void mouseClicked(JmriMouseEvent event) {
651            if (event.getSource() instanceof IconDisplayPanel) {
652                IconDisplayPanel panel = (IconDisplayPanel)event.getSource();
653                setSelection(panel);
654            } else if(event.getSource() instanceof ImagePanel) {
655                deselectIcon();
656           }
657        }
658        @Override
659        public void mousePressed(JmriMouseEvent event) {
660            // no handling provided for mousePressed events
661        }
662        @Override
663        public void mouseReleased(JmriMouseEvent e) {
664            if (log.isDebugEnabled()) {
665                log.debug("IconListener mouseReleased, _treeDnd= {}, popup= {}, source= {}",
666                        _treeDnd, e.isPopupTrigger(), e.getSource().getClass().getName());
667            }
668           if (_treeDnd && e.isPopupTrigger()) {
669               if (e.getSource() instanceof IconDisplayPanel) {
670                   IconDisplayPanel panel = (IconDisplayPanel)e.getSource();
671                   setSelection(panel);
672                   NamedIcon icon = panel.getIcon();
673                   showPopUp(e, icon);
674               } else if (e.getSource() instanceof JLabel) {
675                   JLabel label = (JLabel)e.getSource();
676                   NamedIcon icon = (NamedIcon)label.getIcon();
677                   if (icon !=null) {
678                       showPopUp(e, icon);
679                  }
680               }
681            }
682        }
683        @Override
684        public void mouseEntered(JmriMouseEvent event) {
685            // no handling provided for mouseEntered events
686        }
687        @Override
688        public void mouseExited(JmriMouseEvent event) {
689            // no handling provided for mouseExited events
690        }
691    }
692
693    public static CatalogPanel makeDefaultCatalog() {
694        log.trace("call to makeDefaultCatalog()", new Exception("traceback"));
695        log.debug("CatalogPanel catalog requested");
696        synchronized(_lock) {
697            return makeDefaultCatalog(true, false, true); // deactivate dragNdrop? (true, true, false)
698        }
699    }
700
701    public static CatalogPanel makeDefaultCatalog(boolean addButtonPanel, boolean treeDrop, boolean dragIcon) {
702        log.trace("call to makeDefaultCatalog({},{},{})", addButtonPanel, treeDrop, dragIcon, new Exception("traceback"));
703        synchronized(_lock) {
704            CatalogPanel catalog = new CatalogPanel("catalogs", "selectNode", addButtonPanel);
705            catalog.init(treeDrop, dragIcon);
706            CatalogTreeManager manager = InstanceManager.getDefault(jmri.CatalogTreeManager.class);
707            manager.loadImageIndex();
708            for (CatalogTree tree : manager.getNamedBeanSet()) {
709                if (tree.getSystemName().charAt(0) == 'I') {
710                    catalog.addTree(tree);
711                }
712            }
713            catalog.createNewBranch("IFJAR", "Program Directory", "resources");
714            FileUtil.createDirectory(FileUtil.getUserResourcePath());
715            catalog.createNewBranch("IFPREF", "Preferences Directory", FileUtil.getUserResourcePath());
716            return catalog;
717        }
718    }
719
720    /**
721     * Create a named icon catalog split pane. Make sure both properties keys exist.
722     *
723     * @param label1 properties key to be used as the label for the icon tree
724     * @param label2 properties key to be used as the instruction
725     * @param addButtonPanel adds background select comboBox
726     * @param treeDnD true allows dropping into tree or panel
727     * @param dragIcons true allows dragging icons from panel
728     * @return the created CatalogPanel
729     */
730    public static CatalogPanel makeCatalog(String label1, String label2, boolean addButtonPanel, boolean treeDnD, boolean dragIcons) {
731        log.trace("call to makeCatalog", new Exception("traceback"));
732        synchronized(_lock) {
733            CatalogPanel cp = new CatalogPanel(label1, label2, addButtonPanel);
734            cp.init(treeDnD, dragIcons);
735            return cp;
736        }
737    }
738
739    public static Frame getParentFrame(Component comp) {
740        while (true) {
741            if (comp instanceof Frame) {
742                return (Frame) comp;
743            }
744            if (comp == null) {
745                return null;
746            }
747            comp = comp.getParent();
748        }
749    }
750
751    public static void packParentFrame(Component comp) {
752        Frame frame = getParentFrame(comp);
753        if (frame != null) {
754            frame.pack();
755        }
756    }
757
758    /**
759     * Utility returning a number as a string.
760     *
761     * @param z             double
762     * @param decimalPlaces number of decimal places
763     * @return String       a formatted number
764     */
765    public static String printDbl(double z, int decimalPlaces) {
766        if (Double.isNaN(z) || decimalPlaces > 8) {
767            return Double.toString(z);
768        } else if (decimalPlaces <= 0) {
769            return Integer.toString((int) Math.rint(z));
770        }
771        StringBuilder sb = new StringBuilder();
772        if (z < 0) {
773            sb.append('-');
774        }
775        z = Math.abs(z);
776        int num = 1;
777        int d = decimalPlaces;
778        while (d-- > 0) {
779            num *= 10;
780        }
781        int x = (int) Math.rint(z * num);
782        int ix = x / num;                     // integer part
783        int dx = x - ix * num;
784        sb.append(ix);
785        if (dx == 0) {
786            return sb.toString();
787        }
788        sb.append('.');
789        num /= 10;
790        while (num > dx) {
791            sb.append('0');
792            num /= 10;
793        }
794        sb.append(dx);
795        return sb.toString();
796    }
797
798    protected void setSelectedNode(CatalogTreeNode node) {
799        _dTree.setExpandsSelectedPaths(true);
800        if (log.isDebugEnabled()) {
801            log.debug("setSelectedNode node: {}", node);
802        }
803        if (node != null) {
804            _dTree.setSelectionPath(new TreePath(node.getPath()));
805        } else {
806            _dTree.setSelectionRow(0);
807        }
808    }
809
810    protected void scrollPathToVisible(String[] names) {
811        _dTree.setExpandsSelectedPaths(true);
812        CatalogTreeNode[] path = new CatalogTreeNode[names.length];
813        for (int i = 0; i < names.length; i++) {
814            path[i] = new CatalogTreeNode(names[i]);
815        }
816        _dTree.scrollPathToVisible(new TreePath(path));
817    }
818
819    /**
820     * Return the node the user has selected.
821     *
822     * @return CatalogTreeNode
823     */
824    protected CatalogTreeNode getSelectedNode() {
825        if (!_dTree.isSelectionEmpty() && _dTree.getSelectionPath() != null) {
826            // somebody has been selected
827            TreePath path = _dTree.getSelectionPath();
828            if (log.isDebugEnabled()) {
829                log.debug("getSelectedNode TreePath: {}, lastComponent= {}", path, path.getLastPathComponent());
830            }
831            return (CatalogTreeNode) path.getLastPathComponent();
832        }
833        return null;
834    }
835
836    private void delete(NamedIcon icon) {
837        CatalogTreeNode node = getSelectedNode();
838        if (node == null) {
839            return;
840        }
841        log.debug("delete icon {} from node {}", icon.getName(), node);
842        node.deleteLeaf(icon.getName(), icon.getURL());
843        _model.nodeChanged(node);
844        updatePanel();
845        InstanceManager.getDefault(CatalogTreeManager.class).indexChanged(true);
846    }
847
848    private void rename(NamedIcon icon) {
849        CatalogTreeNode node = getSelectedNode();
850        if (node == null) {
851            return;
852        }
853        String name = JmriJOptionPane.showInputDialog(getParentFrame(this),
854                Bundle.getMessage("newIconName"), icon.getName());
855        if (name != null && name.length() > 0) {
856            log.debug("rename icon {} to {} from node {}", icon.getName(), name, node);
857            CatalogTreeLeaf leaf = node.getLeaf(icon.getName(), icon.getURL());
858            if (leaf != null) {
859                leaf.setName(name);
860            }
861            TreePath path = _dTree.getSelectionPath();
862            // deselect to refresh panel
863            _dTree.setSelectionPath(null);
864            _dTree.setSelectionPath(path);
865            InstanceManager.getDefault(CatalogTreeManager.class).indexChanged(true);
866        }
867    }
868
869    private void showPopUp(JmriMouseEvent evt, NamedIcon icon) {
870        if (log.isDebugEnabled()) {
871            log.debug("showPopUp {}", icon);
872        }
873        JPopupMenu popup = new JPopupMenu();
874        popup.add(new JMenuItem(icon.getName()));
875        popup.add(new JMenuItem(icon.getURL()));
876        popup.add(new javax.swing.JPopupMenu.Separator());
877
878        popup.add(new AbstractAction(Bundle.getMessage("RenameIcon")) {
879            NamedIcon icon;
880
881            @Override
882            public void actionPerformed(ActionEvent e) {
883                rename(icon);
884            }
885
886            AbstractAction init(NamedIcon i) {
887                icon = i;
888                return this;
889            }
890        }.init(icon));
891        popup.add(new javax.swing.JPopupMenu.Separator());
892
893        popup.add(new AbstractAction(Bundle.getMessage("DeleteIcon")) {
894            NamedIcon icon;
895
896            @Override
897            public void actionPerformed(ActionEvent e) {
898                delete(icon);
899            }
900
901            AbstractAction init(NamedIcon i) {
902                icon = i;
903                return this;
904            }
905        }.init(icon));
906        popup.show(evt.getComponent(), evt.getX(), evt.getY());
907    }
908
909    class DropOnPanelToNode extends TransferHandler {
910
911        DataFlavor dataFlavor;
912
913        DropOnPanelToNode() {
914            try {
915                dataFlavor = new DataFlavor(ImageIndexEditor.IconDataFlavorMime);
916            } catch (ClassNotFoundException cnfe) {
917                log.warn("DropOnPanelToNode Unable to create data flavor", cnfe);
918            }
919        }
920
921        @Override
922        public boolean canImport(TransferHandler.TransferSupport support) {
923            if (!support.isDataFlavorSupported(dataFlavor)) {
924                return false;
925            }
926            support.setDropAction(COPY);
927            return true;
928        }
929
930        @Override
931        public boolean importData(TransferHandler.TransferSupport support) {
932            if (!canImport(support)) {
933                return false;
934            }
935            CatalogTreeNode node = getSelectedNode();
936            if (node == null) {
937                return false;
938            }
939            try {
940                Transferable t = support.getTransferable();
941                NamedIcon icon = (NamedIcon) t.getTransferData(dataFlavor);
942                addLeaf(node, icon);
943                if (log.isDebugEnabled()) {
944                    log.debug("DropOnPanelToNode.drop COMPLETED for {} into {}", icon.getURL(), node);
945                }
946                return true;
947            } catch (IOException | UnsupportedFlavorException ex) {
948                log.warn("DropOnPanelToNode unable to drag and drop", ex);
949            }
950            return false;
951        }
952    }
953
954    class DropJTree extends JTree implements DropTargetListener {
955
956        DataFlavor dataFlavor;
957
958        DropJTree(TreeModel model) {
959            super(model);
960            try {
961                dataFlavor = new DataFlavor(ImageIndexEditor.IconDataFlavorMime);
962            } catch (ClassNotFoundException cnfe) {
963                log.warn("DropJTree Unable to create data flavor", cnfe);
964            }
965            new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, this);
966            log.debug("DropJTree ctor");
967        }
968
969        @Override
970        public void dragExit(DropTargetEvent dte) {
971            log.debug("DropJTree.dragExit");
972        }
973
974        @Override
975        public void dragEnter(DropTargetDragEvent dtde) {
976            log.debug("DropJTree.dragEnter");
977        }
978
979        @Override
980        public void dragOver(DropTargetDragEvent dtde) {
981            log.debug("DropJTree.dragOver");
982        }
983
984        @Override
985        public void dropActionChanged(DropTargetDragEvent dtde) {
986            log.debug("DropJTree.dropActionChanged");
987        }
988
989        @Override
990        public void drop(DropTargetDropEvent e) {
991            try {
992                Transferable tr = e.getTransferable();
993                if (e.isDataFlavorSupported(dataFlavor)) {
994                    NamedIcon icon = (NamedIcon) tr.getTransferData(dataFlavor);
995                    Point pt = e.getLocation();
996                    TreePath path = _dTree.getPathForLocation(pt.x, pt.y);
997                    if (path != null) {
998                        CatalogTreeNode node = (CatalogTreeNode) path.getLastPathComponent();
999                        e.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
1000                        addLeaf(node, icon);
1001                        e.dropComplete(true);
1002                        if (log.isDebugEnabled()) {
1003                            log.debug("DropJTree.drop COMPLETED for {} into {}", icon.getURL(), node);
1004                        }
1005                        return;
1006                    }
1007                }
1008            } catch (IOException | UnsupportedFlavorException ex) {
1009                log.warn("DropJTree unable to drag and drop", ex);
1010            }
1011            log.debug("DropJTree.drop REJECTED!");
1012            e.rejectDrop();
1013        }
1014    }
1015
1016    public class IconDisplayPanel extends JPanel implements JmriMouseListener{
1017        String _name;
1018        NamedIcon _icon;
1019
1020        public IconDisplayPanel(String leafName, NamedIcon icon) {
1021            super();
1022            _name = leafName;
1023            _icon = icon;
1024            setLayout(new BorderLayout());
1025            setOpaque(false);
1026            if (_name != null) {
1027                setBorderAndIcon(icon);
1028            }
1029            addMouseListener(JmriMouseListener.adapt(new IconListener()));
1030        }
1031
1032        NamedIcon getIcon() {
1033            return _icon;
1034        }
1035
1036        void setBorderAndIcon(NamedIcon icon) {
1037            if (icon == null) {
1038                log.error("IconDisplayPanel: No icon for \"{}\"", _name);
1039                return;
1040            }
1041            try {
1042                JLabel image;
1043                if (_dragIcons) {
1044                    image = new DragJLabel(new DataFlavor(ImageIndexEditor.IconDataFlavorMime));
1045                } else {
1046                    image = new JLabel();
1047                }
1048                image.setOpaque(false);
1049                image.setName(_name);
1050                image.setToolTipText(icon.getName());
1051                double scale;
1052                if (icon.getIconWidth() < 1 || icon.getIconHeight() < 1) {
1053                    image.setText(Bundle.getMessage("invisibleIcon"));
1054                    image.setForeground(Color.lightGray);
1055                    scale = 0;
1056                } else {
1057                    scale = icon.reduceTo(ICON_WIDTH, ICON_HEIGHT, ICON_SCALE);
1058                }
1059                image.setIcon(icon);
1060                image.setHorizontalAlignment(SwingConstants.CENTER);
1061                image.addMouseListener(JmriMouseListener.adapt(new IconListener()));
1062                add(image, BorderLayout.NORTH);
1063
1064                String scaleMessage = Bundle.getMessage("scale", CatalogPanel.printDbl(scale, 2));
1065                JLabel label = new JLabel(scaleMessage);
1066                label.setOpaque(false);
1067                label.setHorizontalAlignment(SwingConstants.CENTER);
1068                add(label, BorderLayout.CENTER);
1069                label = new JLabel(_name);
1070                label.setOpaque(false);
1071                label.setHorizontalAlignment(SwingConstants.CENTER);
1072                add(label, BorderLayout.SOUTH);
1073                setBorder(BorderFactory.createEmptyBorder(2,2,2,2));
1074            } catch (java.lang.ClassNotFoundException cnfe) {
1075                log.error("Unable to find class supporting {}", Editor.POSITIONABLE_FLAVOR, cnfe);
1076            }
1077        }
1078
1079        public String getIconName() {
1080            return _name;
1081        }
1082        @Override
1083        public void mouseClicked(JmriMouseEvent event) {
1084            if (event.getSource() instanceof JLabel ) {
1085                setSelection(this);
1086            }
1087        }
1088        @Override
1089        public void mousePressed(JmriMouseEvent event) {
1090            // no handling provided for mousePressed events
1091        }
1092        @Override
1093        public void mouseReleased(JmriMouseEvent event) {
1094            // no handling provided for mouseReleased events
1095        }
1096        @Override
1097        public void mouseEntered(JmriMouseEvent event) {
1098            // no handling provided for mouseEntered events
1099        }
1100        @Override
1101        public void mouseExited(JmriMouseEvent event) {
1102            // no handling provided for mouseExited events
1103        }
1104    }
1105
1106    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CatalogPanel.class);
1107
1108}