001package jmri.jmrit.display.palette;
002
003import java.awt.BorderLayout;
004import java.awt.Component;
005import java.awt.Dimension;
006import java.awt.GraphicsEnvironment;
007import java.awt.event.ActionEvent;
008import java.net.URL;
009import java.util.Enumeration;
010import java.util.HashMap;
011import java.util.Iterator;
012import java.util.List;
013import java.util.Map.Entry;
014import javax.annotation.Nonnull;
015import javax.annotation.concurrent.GuardedBy;
016import javax.swing.*;
017import javax.swing.event.ChangeEvent;
018import javax.swing.event.ChangeListener;
019import javax.swing.tree.TreeNode;
020
021import jmri.CatalogTree;
022import jmri.CatalogTreeManager;
023import jmri.InstanceManager;
024import jmri.CatalogTreeLeaf;
025import jmri.CatalogTreeNode;
026import jmri.jmrit.catalog.DirectorySearcher;
027import jmri.jmrit.catalog.ImageIndexEditor;
028import jmri.jmrit.catalog.NamedIcon;
029import jmri.jmrit.display.DisplayFrame;
030import jmri.jmrit.display.Editor;
031import jmri.jmrit.picker.PickListModel;
032import jmri.util.FileUtil;
033import jmri.util.swing.JmriJOptionPane;
034
035import org.jdom2.Element;
036
037import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
038
039/**
040 * Container for adding items to Control Panels. Starting point for palette package.
041 * <p>
042 * Loads and stores icons used in Control Panel Editor panels.
043 * For background colors to work on a particular editor instance, select the
044 * 'Item Palette' item under 'Add Items' menu and configure the 'Backgrounds' tab
045 * ItemPalette for that editor. Otherwise any item can be dragged and
046 * dropped to any editor.
047 * <p>
048 * The icons are displayed on the background of the last editor to call the
049 * ItemPalette instance. In session the user can set it to another color or a white/gray
050 * squares pattern using the "View on:" combo. This choice is shared across tabs
051 * as a field on the {@link jmri.jmrit.display.DisplayFrame} parent frame.
052 * <p>
053 * <a href="doc-files/ItemPalette-ClassDiagram.png"><img src="doc-files/ItemPalette-ClassDiagram.png" alt="UML Class diagram" height="50%" width="50%"></a>
054 *
055 * @author Pete Cressman Copyright (c) 2010, 2018
056 * @author Egbert Broerse Copyright (c) 2017
057 */
058/*
059@startuml jmri/jmrit/display/palette/doc-files/ItemPalette-ClassDiagram.png
060
061abstract class JPanel
062package "jmri.util.swing.ImagePanel" {
063   class ImagePanel {
064-BufferedImage back
065+setImage()
066+paintComponent()
067}
068}
069package "jmri.util.swing.DrawSquares" {
070   class "DrawSquares" {
071+DrawSquares()
072}
073}
074abstract class ItemPanel {
075-String type
076#int previewBgSet
077#BufferedImage[] _backgrounds
078#MakeBgCombo()
079}
080JPanel --|> ItemPanel
081abstract class FamilyItemPanel
082class TableItemPanel
083class IndicatorItemPanel
084IndicatorItemPanel : type = "Indicator"
085object viewOnCombo
086viewOnCombo : -int choice
087viewOnCombo : +EventListener InitListener
088object preview
089preview : -image = 1
090preview : +EventListener comboListener
091object TurnoutItemPanel
092TurnoutItemPanel : type = "Turnout"
093TableItemPanel -- TurnoutItemPanel
094object SensorItemPanel
095SensorItemPanel : type = "Sensor"
096TableItemPanel -- SensorItemPanel
097class SignalMastItemPanel
098SignalMastItemPanel : type = "SignalMast"
099TableItemPanel --|> SignalMastItemPanel
100class MultiSensorItemPanel
101MultiSensorItemPanel : type = "MultiSensor"
102TableItemPanel --|> MultiSensorItemPanel
103class IconItemPanel
104class BackgroundItemPanel
105BackgroundItemPanel : type = "Background"
106IconItemPanel --|> BackgroundItemPanel
107class ClockItemPanel
108ClockItemPanel : type = "Clock"
109IconItemPanel --|> ClockItemPanel
110class DecoratorPanel
111DecoratorPanel : #int previewBgSet
112DecoratorPanel : #BufferedImage[] _backgrounds
113JPanel --|> DecoratorPanel
114abstract class DragJComponent
115JPanel --|> DragJComponent
116class TextItemPanel
117TextItemPanel : type = "Text"
118
119ItemPanel --|> FamilyItemPanel
120FamilyItemPanel --|> TableItemPanel
121FamilyItemPanel --|> IndicatorItemPanel
122DecoratorPanel *-- viewOnCombo
123FamilyItemPanel *-- viewOnCombo : if != SignalMast
124FamilyItemPanel *-- preview
125IconItemPanel *-- viewOnCombo : if != Background
126SignalMastItemPanel *-- viewOnCombo
127viewOnCombo ..> preview: setImage[n]
128viewOnCombo -- DrawSquares
129ItemPanel --|> IconItemPanel
130ItemPanel --|> TextItemPanel
131DecoratorPanel -- TextItemPanel
132ImagePanel -- preview
133DragJComponent --|> ReporterItemPanel
134ReporterItemPanel *-- preview
135' MemoryItemPanel not shown
136
137@enduml
138*/
139
140public class ItemPalette extends DisplayFrame implements ChangeListener {
141
142    public static final int STRUT_SIZE = 5;
143    static final String RED_X = "resources/icons/misc/X-red.gif";
144
145    @GuardedBy("ItemPalette")
146    static JTabbedPane _tabPane;
147    @GuardedBy("ItemPalette")
148    static HashMap<String, ItemPanel> _tabIndex;
149
150    private static volatile HashMap<String, HashMap<String, HashMap<String, NamedIcon>>> _iconMaps;
151    // for now, special case 4 level maps since IndicatorTO is the only case.
152    private static volatile HashMap<String, HashMap<String, HashMap<String, HashMap<String, NamedIcon>>>> _indicatorTOMaps;
153    private static int tabWidth;
154    private ItemPanel _currentItemPanel;
155
156    /**
157     * Store palette icons in preferences file catalogTrees.xml
158     */
159    public static void storeIcons() {
160        if (_iconMaps == null) {
161            return;     // never loaded
162        }
163        CatalogTreeManager manager = InstanceManager.getDefault(jmri.CatalogTreeManager.class);
164        // unfiltered, xml-stored, item palette icon tree
165        CatalogTree tree = manager.getBySystemName("NXPI");
166        // discard old version
167        if (tree != null) {
168            manager.deregister(tree);
169        }
170        tree = manager.newCatalogTree("NXPI", "Item Palette");
171        CatalogTreeNode root = tree.getRoot();
172
173        for (Entry<String, HashMap<String, HashMap<String, NamedIcon>>> entry : _iconMaps.entrySet()) {
174            root.add(store3levelMap(entry.getKey(), entry.getValue()));
175            if (log.isDebugEnabled()) {
176                log.debug("Add type node {}", entry.getKey());
177            }
178        }
179
180        for (Entry<String, HashMap<String, HashMap<String, HashMap<String, NamedIcon>>>> entry : _indicatorTOMaps.entrySet()) {
181            CatalogTreeNode typeNode = new CatalogTreeNode(entry.getKey());
182            for (Entry<String, HashMap<String, HashMap<String, NamedIcon>>> ent : entry.getValue().entrySet()) {
183                typeNode.add(store3levelMap(ent.getKey(), ent.getValue()));
184                log.debug("Add IndicatorTO node {}", ent.getKey());
185            }
186            root.add(typeNode);
187            log.debug("Add IndicatorTO node {}", entry.getKey());
188        }
189    }
190
191    static CatalogTreeNode store3levelMap(String type, HashMap<String, HashMap<String, NamedIcon>> familyMap) {
192        CatalogTreeNode typeNode = new CatalogTreeNode(type);
193        for (Entry<String, HashMap<String, NamedIcon>> mapEntry : familyMap.entrySet()) {
194            String family = mapEntry.getKey();
195            CatalogTreeNode familyNode = new CatalogTreeNode(family);
196            HashMap<String, NamedIcon> iconMap = mapEntry.getValue();
197            for (Entry<String, NamedIcon> iconEntry : iconMap.entrySet()) {
198                String state = iconEntry.getKey();
199                String path = iconEntry.getValue().getURL();
200                familyNode.addLeaf(state, path);
201            }
202            typeNode.add(familyNode);
203            log.debug("Add familyNode {}", familyNode);
204        }
205        return typeNode;
206    }
207
208    public static void loadIcons() {
209        if (_iconMaps == null) {
210            // long t = System.currentTimeMillis();
211            InstanceManager.getDefault(jmri.CatalogTreeManager.class).loadImageIndex();
212            _iconMaps = new HashMap<>();
213            _indicatorTOMaps = new HashMap<>();
214
215            if (!loadSavedIcons()) {
216                loadDefaultIcons();
217            }
218        }
219    }
220
221    static boolean loadSavedIcons() {
222        CatalogTreeManager manager = InstanceManager.getDefault(jmri.CatalogTreeManager.class);
223        CatalogTree tree = manager.getBySystemName("NXPI");
224        if (tree != null) {
225            CatalogTreeNode root = tree.getRoot();
226            Enumeration<TreeNode> e = root.children();
227            while (e.hasMoreElements()) {
228                CatalogTreeNode node = (CatalogTreeNode)e.nextElement();
229                String typeName = (String) node.getUserObject();
230                // detect this is a 4 level map collection.
231                // not very elegant (i.e. extensible), but maybe all that's needed.
232                if (typeName.equals("IndicatorTO")) {
233                    HashMap<String, HashMap<String, HashMap<String, NamedIcon>>> familyTOMap
234                            = loadIndicatorFamilyMap(node);
235                    log.debug("Add {} indicatorTO families to item type {} for _indicatorTOMaps.",
236                            familyTOMap.size(), typeName );
237                    _indicatorTOMaps.put(typeName, familyTOMap);
238                } else {
239                    HashMap<String, HashMap<String, NamedIcon>> familyMap
240                            = loadFamilyMap(node);
241                    _iconMaps.put(typeName, familyMap);
242                    log.debug("Add item type {} to _iconMaps.", typeName);
243                }
244            }
245            log.debug("Icon Map has {} members", _iconMaps.size());
246            return true;
247        }
248        return false;
249    }
250
251    static HashMap<String, HashMap<String, HashMap<String, NamedIcon>>>
252            loadIndicatorFamilyMap(CatalogTreeNode node) {
253        HashMap<String, HashMap<String, HashMap<String, NamedIcon>>> familyMap
254                = new HashMap<>();
255        Enumeration<TreeNode> ee = node.children();
256        while (ee.hasMoreElements()) {
257            CatalogTreeNode famNode = (CatalogTreeNode)ee.nextElement();
258            String name = (String) famNode.getUserObject();
259            familyMap.put(name, loadFamilyMap(famNode));
260        }
261        return familyMap;
262    }
263
264    static HashMap<String, HashMap<String, NamedIcon>> loadFamilyMap(CatalogTreeNode node) {
265        HashMap<String, HashMap<String, NamedIcon>> familyMap = new HashMap<>();
266        Enumeration<TreeNode> ee = node.children();
267        while (ee.hasMoreElements()) {
268            CatalogTreeNode famNode = (CatalogTreeNode)ee.nextElement();
269            String familyName = (String)famNode.getUserObject();
270            HashMap<String, NamedIcon> iconMap = new HashMap<>();
271            List<CatalogTreeLeaf> list = famNode.getLeaves();
272            for (CatalogTreeLeaf catalogTreeLeaf : list) {
273                String iconName = catalogTreeLeaf.getName();
274                String path = catalogTreeLeaf.getPath();
275                NamedIcon icon = NamedIcon.getIconByName(path);
276                if (icon == null) {
277                    log.warn("loadFamilyMap cannot find icon \"{}\" in family\"{}\" at path \"{}\"", iconName, familyName, path);
278                    String fileName = RED_X;
279                    icon = new NamedIcon(fileName, fileName);
280                }
281                iconMap.put(iconName, icon);
282                log.debug("Add {} icon to family \"{}\"", iconName, familyName);
283            }
284            familyMap.put(familyName, iconMap);
285        }
286        return familyMap;
287    }
288
289    static List<Element> getDefaultIconItemTypes() throws org.jdom2.JDOMException, java.io.IOException {
290        URL file = FileUtil.findURL("xml/defaultPanelIcons.xml");
291        if (file == null) {
292            throw new IllegalArgumentException("defaultPanelIcons file (xml/defaultPanelIcons.xml) doesn't exist.");
293        }
294        jmri.jmrit.XmlFile xf = new jmri.jmrit.XmlFile() {
295        };
296        Element root = xf.rootFromURL(file);
297        return (root.getChild("ItemTypes").getChildren());
298    }
299
300    static void loadDefaultIcons() {
301        try {
302            List<Element> typeList = getDefaultIconItemTypes();
303            for (Element type : typeList) {
304                String typeName = type.getName();
305                List<Element> families = type.getChildren();
306                loadFamilies(typeName, families);
307            }
308        } catch (org.jdom2.JDOMException | java.io.IOException e) {
309            log.error("error reading file \"defaultPanelIcons.xml\" due to: ", e);
310        }
311    }
312
313    static void loadFamilies(String typeName, List<Element> families) {
314        // detect this is a 4 level map collection.
315        // not very elegant (i.e. extensible), but maybe all that's needed.
316        if (typeName.equals("IndicatorTO")) {
317            HashMap<String, HashMap<String, HashMap<String, NamedIcon>>> familyTOMap
318                    = loadDefaultIndicatorTOMap(families, null);
319            _indicatorTOMaps.put(typeName, familyTOMap);
320            log.debug("Add {} indicatorTO families to item type {} to _indicatorTOMaps.",
321                    familyTOMap.size(), typeName);
322        } else {
323            HashMap<String, HashMap<String, NamedIcon>> familyMap = loadDefaultFamilyMap(families, null);
324            _iconMaps.put(typeName, familyMap);
325            log.debug("Add {} families to item type \"{}\" to _iconMaps.",
326                    familyMap.size(), typeName);
327        }
328    }
329
330    @SuppressFBWarnings(value="DLS_DEAD_LOCAL_STORE",justification="Stores are not dead. Both statements APPEND additional items into their maps")
331    static public void loadMissingItemType(String itemType) {
332        try {
333            Element thisType = null;
334            List<Element> typeList = getDefaultIconItemTypes();
335            for (Element type : typeList) {
336                String typeName = type.getName();
337                if (typeName.equals(itemType)) {
338                    thisType = type;
339                    break;
340                }
341            }
342            if (thisType == null) {
343                log.error("No type \"{}\" in file \"defaultPanelIcons.xml\"", itemType);
344                return;
345            }
346            List<Element> families = thisType.getChildren();
347            if (itemType.equals("IndicatorTO")) {
348                HashMap<String, HashMap<String, HashMap<String, NamedIcon>>> familyMaps = _indicatorTOMaps.get(itemType);
349                familyMaps = loadDefaultIndicatorTOMap(families, familyMaps);
350            } else {
351                HashMap<String, HashMap<String, NamedIcon>> familyMap = ItemPalette.getFamilyMaps(itemType);
352                familyMap = loadDefaultFamilyMap(families, familyMap);
353            }
354            InstanceManager.getDefault(CatalogTreeManager.class).indexChanged(true);
355        } catch (org.jdom2.JDOMException | java.io.IOException ex) {
356            log.error("error reading file \"defaultPanelIcons.xml\" due to: ", ex);
357        }
358    }
359
360    static HashMap<String, HashMap<String, NamedIcon>> loadDefaultFamilyMap(
361            List<Element> families, HashMap<String, HashMap<String, NamedIcon>> familyMap) {
362        if (familyMap == null) {
363            familyMap = new HashMap<>();
364        }
365        for (Element family : families) {
366            String familyName = family.getName();
367            HashMap<String, NamedIcon> iconMap = new HashMap<>();
368            // Map of all icons of in family, familyName
369            List<Element> iconfiles = family.getChildren();
370            for (Element iconfile : iconfiles) {
371                String iconName = iconfile.getName();
372                String fileName = iconfile.getText().trim();
373                if (fileName.length() == 0) {
374                    log.warn("loadDefaultFamilyMap: icon \"{}\" in family \"{}\" has no image file.", iconName, familyName);
375                    fileName = RED_X;
376                }
377                NamedIcon icon = NamedIcon.getIconByName(fileName);
378                if (icon == null) {
379                    log.warn("loadDefaultFamilyMap: icon \"{}\" in family \"{}\" cannot get icon from file \"{}\".", iconName, familyName, fileName);
380                    fileName = RED_X;
381                    icon = new NamedIcon(fileName, fileName);
382                }
383                iconMap.put(iconName, icon);
384            }
385            familyMap.put(familyName, iconMap);
386            if (log.isDebugEnabled()) {
387                log.debug("Add {}  icons to family \"{}\"", iconMap.size(), familyName);
388            }
389        }
390        return familyMap;
391    }
392
393    static HashMap<String, HashMap<String, HashMap<String, NamedIcon>>>
394            loadDefaultIndicatorTOMap(List<Element> typeList,  HashMap<String, HashMap<String, HashMap<String, NamedIcon>>> familyTOMap) {
395        if (familyTOMap == null) {
396            familyTOMap = new HashMap<>();
397        }
398      //  HashMap<String, HashMap<String, HashMap<String, NamedIcon>>> familyTOMap = new HashMap<>();
399        // Map of all families of type, typeName
400        for (Element element : typeList) {
401            String familyName = element.getName();
402            List<Element> types = element.getChildren();
403            HashMap<String, HashMap<String, NamedIcon>> familyMap = loadDefaultFamilyMap(types, null);
404            familyTOMap.put(familyName, familyMap);
405            if (log.isDebugEnabled()) {
406                log.debug("Add {} IndicatorTO sub-families to item type {}  to IndicatorTO families.", familyMap.size(), familyName);
407            }
408        }
409        return familyTOMap;
410    }
411
412    public static ItemPalette getDefault(String title, @Nonnull Editor ed) {
413        if (GraphicsEnvironment.isHeadless()) {
414            return null;
415        }
416        ItemPalette instance = InstanceManager.getOptionalDefault(ItemPalette.class).orElseGet(() -> InstanceManager.setDefault(ItemPalette.class, new ItemPalette(title, ed)));
417        if (!ed.equals(instance.getEditor())) {
418            instance.updateBackground(ed);
419            InstanceManager.getDefault(jmri.util.PlaceWindow.class).nextTo(ed, null, instance);
420        }
421        // pack before setLocation
422        instance.pack();
423        InstanceManager.getDefault(jmri.util.PlaceWindow.class).nextTo(ed, null, instance);
424        instance.setVisible(true);
425        return instance;
426    }
427
428    public void setEditor(Editor ed) {
429        updateBackground(ed);
430        InstanceManager.getDefault(jmri.util.PlaceWindow.class).nextTo(ed, null, this);
431    }
432
433    public ItemPalette(String title, Editor ed) {
434        super(title, ed);
435        init();
436        setTitle(Bundle.getMessage("ItemPaletteTitle"));
437    }
438
439    private void init() {
440        loadIcons();
441        addWindowListener(new java.awt.event.WindowAdapter() {
442            @Override
443            public void windowClosing(java.awt.event.WindowEvent e) {
444                closePanels(e);
445            }
446        });
447
448        makeMenus();
449        buildTabPane(this);
450
451        setLayout(new BorderLayout(5, 5));
452        synchronized (this) {
453            add(_tabPane, BorderLayout.CENTER);
454            JScrollPane sp = (JScrollPane) _tabPane.getSelectedComponent();
455            _currentItemPanel = (ItemPanel) sp.getViewport().getView();
456            _currentItemPanel.hideIcons();
457        }
458    }
459
460    /*
461     * Add the tabs on the Control Panel Editor.
462     */
463    static void buildTabPane(ItemPalette palette) {
464        //synchronized (ItemPalette.class) {
465            _tabPane = new JTabbedPane();
466            _tabIndex = new HashMap<>();
467        //}
468        tabWidth = getTabWidth();
469
470        ItemPanel itemPanel = new TableItemPanel<>(palette, "Turnout", null,
471                PickListModel.turnoutPickModelInstance());
472        addItemTab(itemPanel, "Turnout", "BeanNameTurnout");
473        // panel shown on start
474
475        itemPanel = new TableItemPanel<>(palette, "Sensor", null,
476                PickListModel.sensorPickModelInstance());
477        addItemTab(itemPanel, "Sensor", "BeanNameSensor");
478
479        itemPanel = new SignalHeadItemPanel(palette, "SignalHead", null,
480                PickListModel.signalHeadPickModelInstance());
481        addItemTab(itemPanel, "SignalHead", "BeanNameSignalHead");
482
483        itemPanel = new SignalMastItemPanel(palette, "SignalMast", null,
484                PickListModel.signalMastPickModelInstance());
485        addItemTab(itemPanel, "SignalMast", "BeanNameSignalMast");
486
487        itemPanel = new MemoryItemPanel(palette, "Memory", null,
488                PickListModel.memoryPickModelInstance());
489        addItemTab(itemPanel, "Memory", "BeanNameMemory");
490
491        itemPanel = new GlobalVariableItemPanel(palette, "Global Variable", null,
492                PickListModel.globalVariablePickModelInstance());
493        addItemTab(itemPanel, "GlobalVariable", "BeanNameGlobalVariable");
494
495        itemPanel = new ReporterItemPanel(palette, "Reporter", null,
496                PickListModel.reporterPickModelInstance());
497        addItemTab(itemPanel, "Reporter", "BeanNameReporter");
498
499        itemPanel = new TableItemPanel<>(palette, "Light", null,
500                PickListModel.lightPickModelInstance());
501        addItemTab(itemPanel, "Light", "BeanNameLight");
502
503        itemPanel = new MultiSensorItemPanel(palette, "MultiSensor", null,
504                PickListModel.multiSensorPickModelInstance());
505        addItemTab(itemPanel, "MultiSensor", "MultiSensor");
506
507        itemPanel = new IconItemPanel(palette, "Icon");
508        addItemTab(itemPanel, "Icon", "Icon");
509
510        itemPanel = new BackgroundItemPanel(palette, "Background");
511        addItemTab(itemPanel, "Background", "Background");
512
513        itemPanel = new TextItemPanel(palette, "Text");
514        addItemTab(itemPanel, "Text", "Text");
515
516        itemPanel = new RPSItemPanel(palette, "RPSReporter", null);
517        addItemTab(itemPanel, "RPSReporter", "RPSreporter");
518
519        itemPanel = new ClockItemPanel(palette, "FastClock");
520        addItemTab(itemPanel, "FastClock", "FastClock");
521
522        itemPanel = new IndicatorItemPanel(palette, "IndicatorTrack", null);
523        addItemTab(itemPanel, "IndicatorTrack", "IndicatorTrack");
524
525        itemPanel = new IndicatorTOItemPanel(palette, "IndicatorTO", null,
526                PickListModel.turnoutPickModelInstance());
527        addItemTab(itemPanel, "IndicatorTO", "IndicatorTO");
528
529        itemPanel = new PortalItemPanel(palette, "Portal", null);
530        addItemTab(itemPanel, "Portal", "BeanNamePortal");
531
532        setTabs();
533        _tabPane.addChangeListener(palette);
534    }
535
536    static void addItemTab(ItemPanel itemPanel, String key, String tabTitle) {
537        itemPanel.init();
538        JScrollPane scrollPane = new JScrollPane(itemPanel);
539        synchronized (ItemPalette.class) {
540            _tabPane.add(Bundle.getMessage(tabTitle), scrollPane);
541            _tabIndex.put(key, itemPanel);
542            log.debug("_tabIndex.size()={}", _tabIndex.size());
543        }
544    }
545
546    static int getTabWidth() {
547        int maxTabWidth = 0;
548        for (Entry<String, String> t : ItemPanel.NAME_MAP.entrySet()) {
549            maxTabWidth = Math.max(maxTabWidth, new JLabel(Bundle.getMessage(t.getValue())).getWidth());
550        }
551        return maxTabWidth;
552    }
553
554    static void setTabs() {
555        JLabel lab = new JLabel();
556        lab.setPreferredSize(new Dimension(tabWidth, 30));
557        synchronized (ItemPalette.class) {
558            for (int i = 0; i == _tabPane.getTabCount(); i++) {
559                _tabPane.setTabComponentAt(i, lab);
560            }
561        }
562    }
563
564//    @Override
565//    public void setPreviewBg(int index) {
566//        super.setPreviewBg(index);
567//        // introduced loop, deprecated and replaced by updating at the moment a tab is brought to front.
568//    }
569
570    @Override
571    public void stateChanged(ChangeEvent e) {
572        JTabbedPane tp = (JTabbedPane) e.getSource();
573        JScrollPane sp = (JScrollPane) tp.getSelectedComponent();
574        ItemPanel newItemPanel = (ItemPanel) sp.getViewport().getView();
575        newItemPanel.closeDialogs();
576        newItemPanel.previewColorChange();
577        newItemPanel.revalidate();
578        // elegantly close previous ItemPanel
579        if (_currentItemPanel != null) {
580            _currentItemPanel.closeDialogs();
581        }
582        _currentItemPanel = newItemPanel;
583    }
584
585    private void makeMenus() {
586        JMenuBar menuBar = new JMenuBar();
587        JMenu findIcon = new JMenu(Bundle.getMessage("findIconMenu"));
588        menuBar.add(findIcon);
589
590        JMenuItem editItem = new JMenuItem(Bundle.getMessage("editIndexMenu"));
591        editItem.addActionListener((ActionEvent e) -> {
592                ImageIndexEditor ii = InstanceManager.getDefault(ImageIndexEditor.class);
593                ii.pack();
594                ii.setVisible(true);
595        });
596        findIcon.add(editItem);
597        findIcon.addSeparator();
598
599        JMenuItem openItem = new JMenuItem(Bundle.getMessage("openDirMenu"));
600        openItem.addActionListener((ActionEvent e) -> InstanceManager.getDefault(DirectorySearcher.class).openDirectory());
601        findIcon.add(openItem);
602
603         JMenuItem searchItem = new JMenuItem(Bundle.getMessage("searchFSMenu"));
604         searchItem.addActionListener((ActionEvent e) -> DirectorySearcher.instance().searchFS());
605         findIcon.add(searchItem);
606
607        setJMenuBar(menuBar);
608        addHelpMenu("package.jmri.jmrit.display.ItemPalette", true);
609    }
610
611    private void closePanels(java.awt.event.WindowEvent e) {
612        synchronized(this) {
613            java.awt.Component[] comps = _tabPane.getComponents();
614            if (log.isDebugEnabled()) {
615                log.debug("closePanels: tab count= {}", _tabPane.getTabCount());
616            }
617            for (Component comp : comps) {
618                javax.swing.JViewport vp = (javax.swing.JViewport) ((JScrollPane) comp).getComponent(0);
619                Component ip = vp.getView();
620                if (ip instanceof ItemPanel) {
621                    ((ItemPanel) ip).closeDialogs();
622                }
623            }
624        }
625        super.windowClosing(e);
626    }
627
628    /*
629     * Look for duplicate name of family in the iterated set.
630     */
631    static private boolean familyNameOK(String type, String family, Iterator<String> it) {
632        if (family == null || family.length() == 0) {
633            JmriJOptionPane.showMessageDialog(null,
634                    Bundle.getMessage("EnterFamilyName"),
635                    Bundle.getMessage("WarningTitle"), JmriJOptionPane.WARNING_MESSAGE);
636            return false;
637        }
638        while (it.hasNext()) {
639            String f = it.next();
640            if (family.equals(f)) {
641                JmriJOptionPane.showMessageDialog(null,
642                        java.text.MessageFormat.format(Bundle.getMessage("DuplicateFamilyName"), family, type),
643                        Bundle.getMessage("WarningTitle"), JmriJOptionPane.WARNING_MESSAGE);
644                return false;
645            }
646        }
647        return true;
648    }
649
650    /**
651     * Add a new Family of icons to the device type.
652     *
653     * @param type type to retrieve
654     * @param family name for iconMap "family"
655     * @param iconMap icon HashMap providing the images
656     * @return result
657     */
658    static protected boolean addFamily(String type, String family, HashMap<String, NamedIcon> iconMap) {
659        if (family == null) {
660            return false;
661        }
662        HashMap<String, HashMap<String, NamedIcon>> typeMap = ItemPalette.getFamilyMaps(type);
663        typeMap.put(family, iconMap);
664        InstanceManager.getDefault(CatalogTreeManager.class).indexChanged(true);
665        return true;
666    }
667
668    /**
669     * Get all the Families of icons for a given device type.
670     *
671     * @param type type
672     * @return map of families
673     */
674    static public @Nonnull HashMap<String, HashMap<String, NamedIcon>> getFamilyMaps(String type) {
675        return _iconMaps.computeIfAbsent(type, k -> new HashMap<>());
676    }
677
678    /**
679     * Remove a Family of icons from the device type.
680     *
681     * @param type type
682     * @param family family
683     */
684    static protected void removeIconMap(String type, String family) {
685        if (log.isDebugEnabled()) {
686            log.debug("removeIconMap for family \"{}\" in type \"{}\"", family, type);
687        }
688        HashMap<String, HashMap<String, NamedIcon>> families = getFamilyMaps(type);
689        families.remove(family);
690        InstanceManager.getDefault(CatalogTreeManager.class).indexChanged(true);
691        if (log.isDebugEnabled()) {
692            for (String s : families.keySet()) {
693                log.debug("removeIconMap remaining Keys: family \"{}\" in type \"{}\"", s, type);
694            }
695        }
696    }
697
698    /*
699     * Get a clone of the Family of icons for a given device type and family.
700     */
701    static public HashMap<String, NamedIcon> getIconMap(String type, String family) {
702        HashMap<String, HashMap<String, NamedIcon>> itemMap = _iconMaps.get(type);
703        if (itemMap == null) {
704            log.error("getIconMap failed. item type \"{}\" not found.", type);
705            return null;
706        }
707        HashMap<String, NamedIcon> iconMap = itemMap.get(family);
708        if (iconMap == null) {
709            log.warn("getIconMap failed. family \"{}\" not found in item type \"{}\"", family, type);
710            return null;
711        }
712        return cloneMap(iconMap);
713    }
714
715    /**
716     * ************ Currently only needed for IndicatorTO type **************
717     * @param type type
718     * @param family family
719     * @param iconMap iconMap
720     * @return result
721     */
722    // add entire family
723    static protected boolean addLevel4Family(String type, String family,
724            HashMap<String, HashMap<String, NamedIcon>> iconMap) {
725        Iterator<String> iter = getLevel4FamilyMaps(type).keySet().iterator();
726        if (familyNameOK(type, family, iter)) {
727            getLevel4FamilyMaps(type).put(family, iconMap);
728            InstanceManager.getDefault(CatalogTreeManager.class).indexChanged(true);
729            return true;
730        }
731        return false;
732    }
733
734    // add entire family
735    static protected void addLevel4FamilyMap(String type, String family,
736            String key, HashMap<String, NamedIcon> iconMap) {
737        HashMap<String, HashMap<String, NamedIcon>> familyMap = getLevel4Family(type, family);
738        familyMap.put(key, iconMap);
739        InstanceManager.getDefault(CatalogTreeManager.class).indexChanged(true);
740    }
741
742    // Currently only needed for IndicatorTO type
743    static protected HashMap<String, HashMap<String, HashMap<String, NamedIcon>>>
744            getLevel4FamilyMaps(String type) {
745        return _indicatorTOMaps.get(type);
746    }
747
748    // Currently only needed for IndicatorTO type
749    static protected HashMap<String, HashMap<String, NamedIcon>>
750            getLevel4Family(String type, String family) {
751        HashMap<String, HashMap<String, HashMap<String, NamedIcon>>> map = _indicatorTOMaps.get(type);
752        return map.get(family);
753    }
754
755    // Currently only needed for IndicatorTO type
756    static protected void removeLevel4IconMap(String type, String family, String key) {
757        if (log.isDebugEnabled()) {
758            log.debug("removeLevel4IconMap for indicator family \"{}\" in type \"{}\" with key \"{}\"",
759                    family, type, key);
760        }
761        if (key != null) {
762            _indicatorTOMaps.get(type).get(family).remove(key);
763        } else {
764            _indicatorTOMaps.get(type).remove(family);
765        }
766        InstanceManager.getDefault(CatalogTreeManager.class).indexChanged(true);
767    }
768
769    ///////////////////////////////////////////////////////////////////////////////
770
771    static protected HashMap<String, NamedIcon> cloneMap(HashMap<String, NamedIcon> map) {
772        HashMap<String, NamedIcon> clone = new HashMap<>();
773        if (map != null) {
774            for (Entry<String, NamedIcon> entry : map.entrySet()) {
775                String name = entry.getKey();
776                NamedIcon icon = new NamedIcon(entry.getValue());
777                clone.put(name, icon);
778            }
779        }
780        return clone;
781    }
782
783    /**
784     * Default key names as listed in defaultPanelIcons.xml are Bundle keys and
785     * nodes in the CatalogTree. However users also define icon sets and store
786     * them in the CatalogTree. The names the user has defined for these sets
787     * (i.e.family" name) are also nodes in the CatalogTree. So it is expected
788     * that such names will fall through as an Exception.  Thus these names are
789     * returned as the user has entered them.  There is no failure of I18N here.
790     * @param name key name
791     * @return usable UI display name
792     */
793    static public String convertText(String name) {
794        String cName;
795        try {
796            // NOI18N
797            cName = Bundle.getMessage(name);
798        } catch (Exception e) {
799            cName = name;
800        }
801        return cName;
802    }
803
804    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ItemPalette.class);
805
806}