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}