001package jmri.jmrit.display.palette;
002
003import java.awt.Color;
004import java.awt.Dimension;
005import java.awt.FontMetrics;
006import java.awt.GridBagConstraints;
007import java.awt.GridBagLayout;
008import java.awt.event.ActionListener;
009import java.util.HashMap;
010import java.util.Map.Entry;
011
012import javax.annotation.Nonnull;
013import javax.swing.BorderFactory;
014import javax.swing.Box;
015import javax.swing.BoxLayout;
016import javax.swing.JButton;
017import javax.swing.JLabel;
018import javax.swing.JPanel;
019import javax.swing.JTextField;
020
021import jmri.jmrit.catalog.CatalogPanel;
022import jmri.jmrit.catalog.NamedIcon;
023import jmri.jmrit.display.DisplayFrame;
024import jmri.jmrit.display.PreviewPanel;
025import jmri.jmrit.display.controlPanelEditor.PortalIcon;
026import jmri.util.swing.ImagePanel;
027import jmri.util.swing.JmriJOptionPane;
028
029/**
030 * JPanels for the various item types that can be added to a Panel - e.g. Sensors,
031 * Turnouts, etc.
032 *
033 * Devices such as these have sets of icons to display their various states.
034 * Such sets are called a "family" in the code. These devices then may have sets
035 * of families to provide the user with a choice of the icon set to use for a
036 * particular device.
037 * These sets/families are defined in an xml file stored as xml/defaultPanelIcons.xml
038 * including the icon file paths, to be loaded by an iterator.
039 * The subclass FamilyItemPanel.java and its subclasses handles these devices.
040 *
041 * Other devices, e.g. Backgrounds or Memory, may use only one or no icon to
042 * display. The subclass IconItemPanel.java and its subclasses handles these
043 * devices.
044 * @see jmri.jmrit.display.DisplayFrame for class diagram for the palette package.
045 *
046 * @author Pete Cressman Copyright (c) 2010, 2020
047 * @author Egbert Broerse Copyright 2017, 2021
048 */
049public abstract class ItemPanel extends JPanel  {
050
051    protected DisplayFrame _frame;
052    protected String _itemType;
053    protected boolean _initialized = false; // has init() been run
054    protected boolean _update = false;      // editing existing icon, do not allow icon dragging. Set in init()
055    protected boolean _suppressDragging;
056    protected boolean _askOnce = false;
057    protected JTextField _linkName = new JTextField(30);
058    protected PreviewPanel _previewPanel; // contains _iconPanel and optionally _dragIconPanel when used to create a panel object
059    protected HashMap<String, NamedIcon> _currentIconMap;
060    protected ImagePanel _iconPanel;   // a panel on _iconFamilyPanel - all icons in family, shown upon [Show Icons]
061    protected JPanel _iconFamilyPanel; // Holds _previewPanel, _familyButtonPanel.
062    protected JPanel _bottomPanel; // contains function buttons for panel
063    protected ActionListener _doneAction;   // update done action return
064    protected boolean _wasEmpty;
065    protected JPanel _instructions;
066
067    /*
068     * ****** Default family icon names *******
069     *
070     * NOTE: Names supplied must be available as properties keys and also match the
071     * element names defined in xml/defaultPanelIcons.xml
072     */
073    static final String[] TURNOUT = {"TurnoutStateClosed", "TurnoutStateThrown",
074            "BeanStateInconsistent", "BeanStateUnknown"};
075    static final String[] SENSOR = {"SensorStateActive", "SensorStateInactive",
076            "BeanStateInconsistent", "BeanStateUnknown"};
077    static final String[] SIGNALHEAD = {"SignalHeadStateRed", "SignalHeadStateYellow",
078            "SignalHeadStateGreen", "SignalHeadStateDark",
079            "SignalHeadStateHeld", "SignalHeadStateLunar",
080            "SignalHeadStateFlashingRed", "SignalHeadStateFlashingYellow",
081            "SignalHeadStateFlashingGreen", "SignalHeadStateFlashingLunar"};
082    static final String[] LIGHT = {"StateOff", "StateOn",
083            "BeanStateInconsistent", "BeanStateUnknown"};
084    static final String[] MULTISENSOR = {"SensorStateInactive", "BeanStateInconsistent",
085            "BeanStateUnknown", "first", "second", "third"};
086    // SIGNALMAST family is empty is signal system
087    static final String[] RPSREPORTER = {"active", "error"};
088    final static String[] INDICATOR_TRACK = {"ClearTrack", "OccupiedTrack", "PositionTrack",
089            "AllocatedTrack", "DontUseTrack", "ErrorTrack"};
090    static final String[] PORTAL = {PortalIcon.HIDDEN, PortalIcon.VISIBLE, PortalIcon.PATH,
091            PortalIcon.TO_ARROW, PortalIcon.FROM_ARROW};
092
093    protected static HashMap<String, String[]> STATE_MAP = new HashMap<>();
094    static {
095        STATE_MAP.put("Turnout", TURNOUT);
096        STATE_MAP.put("Sensor", SENSOR);
097        STATE_MAP.put("SignalHead", SIGNALHEAD);
098        STATE_MAP.put("Light", LIGHT);
099        STATE_MAP.put("MultiSensor", MULTISENSOR);
100        STATE_MAP.put("RPSReporter", RPSREPORTER);
101        STATE_MAP.put("IndicatorTrack", INDICATOR_TRACK);
102        STATE_MAP.put("IndicatorTO", INDICATOR_TRACK);
103        STATE_MAP.put("Portal", PORTAL);
104    }
105
106    protected static HashMap<String, String> NAME_MAP = new HashMap<>();
107    static {
108        NAME_MAP.put("Turnout", "BeanNameTurnout");
109        NAME_MAP.put("Sensor", "BeanNameSensor");
110        NAME_MAP.put("SignalHead", "BeanNameSignalHead");
111        NAME_MAP.put("Light", "BeanNameLight");
112        NAME_MAP.put("SignalMast", "BeanNameSignalMast");
113        NAME_MAP.put("MultiSensor", "MultiSensor");
114        NAME_MAP.put("Memory", "BeanNameMemory");
115        NAME_MAP.put("Reporter", "BeanNameReporter");
116        NAME_MAP.put("RPSReporter", "RPSreporter");
117        NAME_MAP.put("IndicatorTrack", "IndicatorTrack");
118        NAME_MAP.put("IndicatorTO", "IndicatorTO");
119        NAME_MAP.put("Portal", "BeanNamePortal");
120        NAME_MAP.put("Icon", "Icon");
121        NAME_MAP.put("Background", "Background");
122        NAME_MAP.put("Text", "Text");
123        NAME_MAP.put("FastClock", "FastClock");
124    }
125
126    /**
127     * Constructor for all item types.
128     *
129     * @param parentFrame ItemPalette instance
130     * @param type        identifier of the ItemPanel type
131     */
132    public ItemPanel(DisplayFrame parentFrame, @Nonnull String type) {
133        _frame = parentFrame;
134        _itemType = type;
135        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
136        add(Box.createVerticalGlue());
137    }
138
139    /**
140     * Initialize panel for selecting a new Control Panel item or for updating
141     * an existing item. Adds table if item is a bean. i.e. customizes for the
142     * item type.
143     * Called by enclosing TabbedPanel on change of displayed tab Pane.
144     */
145    public void init() {
146        if (!_initialized) {
147            _update = false;
148            _suppressDragging = false;
149            initIconFamiliesPanel();
150            _initialized = true;
151        }
152    }
153
154    @Nonnull
155    protected HashMap<String, NamedIcon> makeNewIconMap(String type) {
156        HashMap<String, NamedIcon> newMap = new HashMap<>();
157        for (String name : STATE_MAP.get(type)) {
158            NamedIcon icon = new NamedIcon(ItemPalette.RED_X, ItemPalette.RED_X);
159            newMap.put(name, icon);
160        }
161        return newMap;
162    }
163
164    static protected void checkIconMap(String type, HashMap<String, NamedIcon> map) {
165        for (String name : STATE_MAP.get(type)) {
166            if (map.get(name) == null) {
167                NamedIcon icon = new NamedIcon(ItemPalette.RED_X, ItemPalette.RED_X);
168                // store RedX as default icon if icon not set
169                map.put(name, icon);
170            }
171        }
172    }
173
174    protected void previewColorChange() {
175        if (_previewPanel != null) {
176            _previewPanel.setBackgroundSelection(_frame.getPreviewBg());
177            _previewPanel.invalidate();
178        }
179    }
180
181    public void closeDialogs() {
182    }
183
184    /**
185     * Make a button panel that can populate an empty ItemPanel
186     * @param update edit icons on a panel
187     * @return the panel
188     */
189    abstract protected JPanel makeSpecialBottomPanel(boolean update);
190
191    /**
192     * Make a button panel to populate editing an ItemPanel
193     * @return the panel
194     */
195    abstract protected JPanel makeItemButtonPanel();
196
197    /**
198     * Add [Update] button to _bottom1Panel.
199     * @param doneAction Action for button
200     * @return button with doneAction Action
201     */
202    protected JButton makeUpdateButton(ActionListener doneAction) {
203        JButton updateButton = new JButton(Bundle.getMessage("updateButton")); // custom update label
204        updateButton.addActionListener(doneAction);
205        updateButton.setToolTipText(Bundle.getMessage("ToolTipPickFromTable"));
206        return updateButton;
207    }
208
209
210    protected void makeBottomPanel(boolean isEmpty) {
211        if (isEmpty) {
212            _bottomPanel = makeSpecialBottomPanel(_update);
213        } else {
214            _bottomPanel = makeItemButtonPanel();
215        }
216        if (_doneAction != null) {
217            _bottomPanel.add(makeUpdateButton(_doneAction));
218        }
219        _bottomPanel.invalidate();
220        add(_bottomPanel);
221    }
222
223    /**
224     * Initialize or reset an ItemPanel.
225     */
226    protected void initIconFamiliesPanel() {
227        if (_iconPanel == null) {
228            _iconPanel = new ImagePanel();
229            _iconPanel.setBorder(BorderFactory.createLineBorder(Color.black));
230            _iconPanel.setImage(_frame.getPreviewBackground());
231            _iconPanel.setOpaque(false);
232        }
233        if (_iconFamilyPanel == null) {
234            _iconFamilyPanel = new JPanel();
235            _iconFamilyPanel.setLayout(new BoxLayout(_iconFamilyPanel, BoxLayout.Y_AXIS));
236            add(_iconFamilyPanel);
237        }
238        makeFamiliesPanel();
239        if (log.isDebugEnabled()) {
240            log.debug("initIconFamiliesPanel done for {}, update= {}", _itemType, _update);
241        }
242    }
243
244    protected void makePreviewPanel(boolean hasMaps, ImagePanel dragIconPanel) {
245        if (_previewPanel == null) {
246            if (!_update && !_suppressDragging) {
247                _previewPanel = new PreviewPanel(_frame, _iconPanel, dragIconPanel, true);
248                _instructions = instructions();
249                _previewPanel.add(_instructions, 0);
250            } else {
251                _previewPanel = new PreviewPanel(_frame, _iconPanel, null, false);
252                _previewPanel.setVisible(false);
253            }
254            _iconFamilyPanel.add(_previewPanel);
255        }
256        _previewPanel.setVisible(true);
257        _previewPanel.invalidate();
258    }
259
260    /**
261     * Add the current set of icons to a Show Icons pane. Used in several
262     * ways by different ItemPanels. 
263     * When dropIcon is true, call may be from an editing dialog and the
264     * caller may allow the icon to dropped upon (replaced) or be the
265     * source of dragging it - (e.g. IconItemPanel). When_showIconsButton 
266     * pressed, dropIcon will be false.
267     * 
268     * @see #hideIcons()
269     * @param iconMap   family maps
270     * @param iconPanel panel to fill with icons
271     * @param dropIcon  true for ability to drop new image on icon to change
272     *                  icon source
273     */
274    protected void addIconsToPanel(HashMap<String, NamedIcon> iconMap, ImagePanel iconPanel, boolean dropIcon) {
275        if (iconMap == null) {
276            log.debug("_currentIconMap is null for type {}", _itemType);
277            return;
278        }
279        iconPanel.removeAll();
280
281        GridBagLayout gridbag = new GridBagLayout();
282        iconPanel.setLayout(gridbag);
283
284        int numCol = 4;
285        GridBagConstraints c = ItemPanel.itemGridBagConstraint();
286
287        if (iconMap.isEmpty()) {
288            iconPanel.add(Box.createRigidArea(new Dimension(70,70)));
289        }
290        int cnt = 0;
291        for (String key : iconMap.keySet()) {
292            JPanel panel = makeIconDisplayPanel(key, iconMap, dropIcon);
293            
294            iconPanel.add(panel, c);
295            if (c.gridx > numCol) { // start next row
296                c.gridy++;
297                c.gridx = 0;
298            }
299            c.gridx++;
300            cnt++;
301            gridbag.setConstraints(panel, c);
302        }
303        if (log.isDebugEnabled()) {
304            log.debug("addIconsToPanel adds {} icons (map size {}) to iconPanel for {}", cnt, iconMap.size(), _itemType);
305        }
306        iconPanel.invalidate();
307    }
308
309    /**
310     * Utility for above method. Implementation returns a JPanel extension
311     * containing a bordered JLabel extension of icon and labels
312     * 
313     * @param key name of icon
314     * @param iconMap containing icon for possible replacement
315     * @param dropIcon JLabel extension may be replaceable or dragable.
316     * @return the JPanel
317     */
318    abstract protected JPanel makeIconDisplayPanel(String key, HashMap<String, NamedIcon> iconMap, boolean dropIcon);
319
320    /**
321     * Utility used by implementations of above 'makeIconDisplayPanel' method to wrap its panel
322     * @param icon icon held by a JLabel
323     * @param image background image for panel
324     * @param panel holds image and JLable
325     * @param key key of icon in its set - name for the icon can be extracted from it
326     */
327    protected void wrapIconImage(NamedIcon icon, JLabel image, JPanel panel, String key) {
328        String borderName = ItemPalette.convertText(key);
329        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
330        panel.setOpaque(false);
331        // I18N use existing NamedBeanBundle keys
332        panel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black), borderName));
333        image.setOpaque(false);
334        image.setToolTipText(icon.getName());
335        image.setName(key);
336        JPanel iPanel = new JPanel();
337        iPanel.setOpaque(false);
338        iPanel.add(image);
339        panel.add(iPanel);
340
341        double scale;
342        if (icon.getIconWidth() < 1 || icon.getIconHeight() < 1) {
343            image.setText(Bundle.getMessage("invisibleIcon"));
344            scale = 0;
345        } else {
346            scale = icon.reduceTo(CatalogPanel.ICON_WIDTH, CatalogPanel.ICON_HEIGHT, CatalogPanel.ICON_SCALE);
347        }
348        String scaleText = java.text.MessageFormat.format(Bundle.getMessage("scale"), CatalogPanel.printDbl(scale, 2));
349        JLabel label = new JLabel(scaleText);
350        JPanel sPanel = new JPanel();
351        sPanel.setOpaque(false);
352        sPanel.add(label);
353        panel.add(sPanel);       
354
355        FontMetrics fm = getFontMetrics(panel.getFont());
356        int width = fm.stringWidth(borderName) + 5;
357        width = Math.max(Math.max(width, CatalogPanel.ICON_WIDTH), icon.getIconWidth() + 5);
358        int height = panel.getPreferredSize().height;
359        panel.setPreferredSize(new Dimension(width, height));
360    }
361
362    abstract protected JPanel instructions();
363
364    /**
365     * Part of the initialization and reseting of an ItemPanel.
366     * Allows divergence for different panel needs.
367     */
368    abstract protected void makeFamiliesPanel();
369
370    abstract protected void hideIcons();
371    
372    /**
373     * See if the map is supported by the family map. "Equals" in
374     * this context means that each map is the same size the keys are equal and
375     * the urls for the icons are equal. Note that icons with different urls may
376     * be or appear to be the same.
377     * The item type "SignalHead" allows for unequal sizes but 'mapOne'
378     * must contain 'mapTwo' elements.
379     * 
380     * @param mapOne an icon HashMap
381     * @param mapTwo another icon HashMap
382     * @return true if all of signal head entries have matching entries in the
383     *         family map.
384     */
385    protected boolean mapsAreEqual(HashMap<String, NamedIcon> mapOne, HashMap<String, NamedIcon> mapTwo) {
386        if (  !_itemType.equals("SignalHead") && mapOne.size() != mapTwo.size()) {
387            return false;
388        }
389        for (Entry<String, NamedIcon> mapTwoEntry : mapTwo.entrySet()) {
390            NamedIcon mapOneIcon = mapOne.get(mapTwoEntry.getKey());
391            if (mapOneIcon == null) {
392                return false;
393            }
394            String url = mapOneIcon.getURL();
395            if (url == null || !url.equals(mapTwoEntry.getValue().getURL())) {
396                return false;
397            }
398        }
399        return true;
400    }
401
402    protected void loadDefaultType() {
403        ItemPalette.loadMissingItemType(_itemType);
404        // Check for duplicate names or duplicate icon sets
405        java.util.ArrayList<String> deletes = new java.util.ArrayList<>();
406        if (!_itemType.equals("IndicatorTO")) {
407            HashMap<String, HashMap<String, NamedIcon>> families = ItemPalette.getFamilyMaps(_itemType);
408            java.util.Set<String> keys = families.keySet();
409            String[] key = new String[keys.size()];
410            key = keys.toArray(key);
411            for (int i=0; i<key.length; i++) {
412                for (int j=i+1; j<key.length; j++) {
413                    HashMap<String, NamedIcon> mapK = families.get(key[i]);
414                    if (mapsAreEqual(mapK, families.get(key[j]))) {
415                        deletes.add(queryWhichToDelete(key[i], key[j]));
416                        break;
417                    }
418                }
419            }
420            for (String k : deletes) {
421                ItemPalette.removeIconMap(_itemType, k);
422            }
423            if (this instanceof FamilyItemPanel) {
424                ((FamilyItemPanel)this)._family = null;
425            }
426        } else {
427            IndicatorTOItemPanel p = (IndicatorTOItemPanel)this;
428            HashMap<String, HashMap<String, HashMap<String, NamedIcon>>> 
429                                families = ItemPalette.getLevel4FamilyMaps(_itemType);
430            java.util.Set<String> keys = families.keySet();
431            String[] key = new String[keys.size()];
432            key = keys.toArray(key);
433            for (int i=0; i<key.length; i++) {
434                for (int j=i+1; j<key.length; j++) {
435                    HashMap<String, HashMap<String, NamedIcon>> mapK = families.get(key[i]);
436                    if (p.familiesAreEqual(mapK, families.get(key[j]))) {
437                        deletes.add(queryWhichToDelete(key[i], key[j]));
438                        break;
439                    }
440                }
441            }
442            for (String k : deletes) {
443                ItemPalette.removeLevel4IconMap(_itemType, k, null);
444            }
445            p._family = null;
446        }
447        if (!_initialized) {
448            makeFamiliesPanel();
449        } else {
450            initIconFamiliesPanel();
451            hideIcons();
452        }
453    }
454
455    /**
456     * Ask user to choose from 2 different names for the same icon map.
457     * @param key1 first name found for same map
458     * @param key2 second name found, default to delete
459     * @return the name and map to discard
460     */
461    private String queryWhichToDelete(String key1, String key2) {
462        int result = JmriJOptionPane.showOptionDialog(this, Bundle.getMessage("DuplicateMap", key1, key2),
463                Bundle.getMessage("QuestionTitle"), JmriJOptionPane.DEFAULT_OPTION, 
464                JmriJOptionPane.QUESTION_MESSAGE, null,
465                new Object[] {key1, key2}, key1);
466        if ( result == 0 ) { // position 0 in array, keep key1, return key2
467            return key2;
468        } else if ( result == 1 ) { // position 1 in array, keep key1, return key2
469            return key1;
470        }
471        return key2;
472    }
473
474    /**
475     * Resize frame to allow display/shrink after Icon map is dieplayed.
476     * @param isPalette selector for what to resize, true to resize parent tabbed frame
477     * @param oldDim old panel size
478     * @param frameDim old frame size
479     */
480    protected void reSizeDisplay(boolean isPalette, Dimension oldDim, Dimension frameDim) {
481        Dimension newDim = getPreferredSize();
482        Dimension deltaDim = shellDimension(this);
483        if (log.isDebugEnabled()) {
484            // Gather data for additional dimensions needed to display new panel in the total frame
485            Dimension frameDiffDim = new Dimension(frameDim.width - oldDim.width, frameDim.height - oldDim.height);
486            log.debug("resize {} {}. frameDiffDim= ({}, {}) deltaDim= ({}, {}) prefDim= ({}, {}))",
487                    (isPalette?"tabPane":"update"), _itemType,
488                    frameDiffDim.width, frameDiffDim.height,
489                    deltaDim.width, deltaDim.height, newDim.width, newDim.height);
490        }
491        if (isPalette && _initialized) {
492            _frame.reSize(ItemPalette._tabPane, deltaDim, newDim);
493        } else if (_update || _initialized) {
494            _frame.reSize(_frame, deltaDim, newDim);                            
495        }
496    }
497
498    public Dimension shellDimension(ItemPanel panel) {
499        if (panel instanceof FamilyItemPanel) {
500            return new Dimension(23, 122);
501        } else if (panel instanceof IconItemPanel) {
502            return new Dimension(23, 65);
503        }
504        return new Dimension(23, 48);
505    }
506
507    static public GridBagConstraints itemGridBagConstraint() {
508        GridBagConstraints c = new GridBagConstraints();
509        c.fill = GridBagConstraints.NONE;
510        c.anchor = GridBagConstraints.CENTER;
511        c.weightx = 1.0;
512        c.weighty = 1.0;
513        c.gridwidth = 1;
514        c.gridheight = 1;
515        c.gridx = 0;
516        c.gridy = 0;
517        return c;
518    }
519
520    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ItemPanel.class);
521
522}