001package jmri.jmrit.beantable.beanedit;
002
003import java.awt.BorderLayout;
004import java.awt.Color;
005import java.awt.Component;
006import java.awt.Dimension;
007import java.awt.GridBagConstraints;
008import java.awt.GridBagLayout;
009import java.awt.Insets;
010import java.awt.event.ActionEvent;
011
012import java.util.ArrayList;
013import java.util.Iterator;
014import java.util.List;
015import java.util.Vector;
016
017import javax.annotation.OverridingMethodsMustInvokeSuper;
018import javax.swing.*;
019import javax.swing.event.ChangeEvent;
020import javax.swing.table.AbstractTableModel;
021
022import jmri.*;
023import jmri.NamedBean.DisplayOptions;
024import jmri.jmrit.display.layoutEditor.LayoutBlock;
025import jmri.jmrit.display.layoutEditor.LayoutBlockManager;
026import jmri.util.JmriJFrame;
027import jmri.util.swing.JmriJOptionPane;
028
029/**
030 * Provides the basic information and structure for for a editing the details of
031 * a bean object.
032 * 
033 * @param <B> the type of supported NamedBean
034 *
035 * @author Kevin Dickerson Copyright (C) 2011
036 */
037public abstract class BeanEditAction<B extends NamedBean> extends AbstractAction {
038
039    public BeanEditAction(String s) {
040        super(s);
041    }
042
043    public BeanEditAction() {
044        super("Bean Edit");
045    }
046
047    B bean;
048
049    public void setBean(B bean) {
050        this.bean = bean;
051    }
052
053    /**
054     * Call to create all the different tabs that will be added to the frame.
055     */
056    protected void initPanels() {
057        basicDetails();
058    }
059
060    /**
061     * Initialise panels to be at start of Tabbed Panel menu.
062     * Default empty.
063     */
064    protected void initPanelsFirst() {
065    }
066
067    /**
068     * Initialise panels to be at end of Tabbed Panel menu.
069     * Startup usage details and Properties.
070     */
071    protected void initPanelsLast() {
072        usageDetails();
073        propertiesDetails();
074    }
075
076    JTextField userNameField = new JTextField(20);
077    JTextArea commentField = new JTextArea(3, 30);
078    JScrollPane commentFieldScroller = new JScrollPane(commentField);
079    private JLabel statusBar = new JLabel(Bundle.getMessage("ItemEditStatusInfo", Bundle.getMessage("ButtonApply")));
080
081    /**
082     * Create a generic panel that holds the basic bean information System Name,
083     * User Name, and Comment.
084     *
085     * @return a new panel
086     */
087    BeanItemPanel basicDetails() {
088        BeanItemPanel basic = new BeanItemPanel();
089
090        basic.setName(Bundle.getMessage("Basic"));
091        basic.setLayout(new BoxLayout(basic, BoxLayout.Y_AXIS));
092
093        basic.addItem(new BeanEditItem(new JLabel(bean.getSystemName()), Bundle.getMessage("ColumnSystemName"), null));
094        //Bundle.getMessage("ConnectionHint", "N/A"))); // TODO get connection name from nbMan.getSystemPrefix()
095
096        basic.addItem(new BeanEditItem(userNameField, Bundle.getMessage("ColumnUserName"), null));
097
098        basic.addItem(new BeanEditItem(commentFieldScroller, Bundle.getMessage("ColumnComment"), null));
099
100        basic.setSaveItem(new AbstractAction() {
101            @Override
102            public void actionPerformed(ActionEvent e) {
103                saveBasicItems(e);
104            }
105        });
106        basic.setResetItem(new AbstractAction() {
107            @Override
108            public void actionPerformed(ActionEvent e) {
109                resetBasicItems(e);
110            }
111        });
112        bei.add(basic);
113        return basic;
114    }
115
116    /**
117     * Create a generic panel that holds Bean usage details.
118     *
119     * @return a new panel
120     */
121    BeanItemPanel usageDetails() {
122        BeanItemPanel usage = new BeanItemPanel();
123
124        usage.setName(Bundle.getMessage("Usage"));
125        usage.setLayout(new BoxLayout(usage, BoxLayout.Y_AXIS));
126
127        usage.addItem(new BeanEditItem(null, null, Bundle.getMessage("UsageText", bean.getDisplayName())));
128
129        ArrayList<String> listeners = new ArrayList<>();
130        for (String ref : bean.getListenerRefs()) {
131            if (!listeners.contains(ref)) {
132                listeners.add(ref);
133            }
134        }
135
136        Object[] strArray = new Object[listeners.size()];
137        listeners.toArray(strArray);
138        JList<Object> list = new JList<>(strArray);
139        list.setLayoutOrientation(JList.VERTICAL);
140        list.setVisibleRowCount(-1);
141        list.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
142        JScrollPane listScroller = new JScrollPane(list);
143        listScroller.setPreferredSize(new Dimension(250, 80));
144        listScroller.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black)));
145        usage.addItem(new BeanEditItem(listScroller, Bundle.getMessage("ColumnLocation"), null));
146
147        bei.add(usage);
148        return usage;
149    }
150    private BeanPropertiesTableModel<B> propertiesModel;
151
152    /**
153     * Create a generic panel that holds Bean Property details.
154     *
155     * @return a new panel
156     */
157    BeanItemPanel propertiesDetails() {
158        BeanItemPanel properties = new BeanItemPanel();
159        properties.setName(Bundle.getMessage("Properties"));
160        properties.addItem(new BeanEditItem(null, null, Bundle.getMessage("NamedBeanPropertiesTableDescription")));
161        properties.setLayout(new BoxLayout(properties, BoxLayout.Y_AXIS));
162        propertiesModel = new BeanPropertiesTableModel<>();
163        JTable jtAttributes = new JTable();
164        jtAttributes.setModel(propertiesModel);
165        JScrollPane jsp = new JScrollPane(jtAttributes);
166        Dimension tableDim = new Dimension(400, 200);
167        jsp.setMinimumSize(tableDim);
168        jsp.setMaximumSize(tableDim);
169        jsp.setPreferredSize(tableDim);
170        properties.addItem(new BeanEditItem(jsp, "", null));
171        properties.setSaveItem(new AbstractAction() {
172            @Override
173            public void actionPerformed(ActionEvent e) {
174                propertiesModel.updateModel(bean);
175            }
176        });
177        properties.setResetItem(new AbstractAction() {
178            @Override
179            public void actionPerformed(ActionEvent e) {
180                propertiesModel.setModel(bean);
181            }
182        });
183
184        bei.add(properties);
185        return properties;
186    }
187
188    @OverridingMethodsMustInvokeSuper
189    protected void saveBasicItems(ActionEvent e) {
190        String uname = bean.getUserName();
191        if (uname == null && !userNameField.getText().isEmpty()) {
192            renameBean(userNameField.getText());
193        } else if (uname != null && !uname.equals(userNameField.getText())) {
194            if (userNameField.getText().isEmpty()) {
195                removeName();
196            } else {
197                renameBean(userNameField.getText());
198            }
199        }
200        bean.setComment(commentField.getText());
201    }
202
203    @OverridingMethodsMustInvokeSuper
204    protected void resetBasicItems(ActionEvent e) {
205        userNameField.setText(bean.getUserName());
206        commentField.setText(bean.getComment());
207    }
208
209    abstract protected String helpTarget();
210
211    protected ArrayList<BeanItemPanel> bei = new ArrayList<>(5);
212    JmriJFrame f;
213
214    protected Component selectedTab = null;
215    private final JTabbedPane detailsTab = new JTabbedPane();
216
217    /**
218     * Apply Button.
219     * Accessible so Edit Actions can set custom tool tip.
220     */
221    protected JButton applyBut;
222    
223    public void setSelectedComponent(Component c) {
224        selectedTab = c;
225    }
226
227    @Override
228    public void actionPerformed(ActionEvent e) {
229        if (bean == null) {
230            // display message in status bar TODO
231            log.error("No bean set so unable to edit a null bean");  // NOI18N
232            return;
233        }
234        if (f == null) {
235            f = new JmriJFrame(Bundle.getMessage("EditBean", bean.getBeanType(), bean.getDisplayName()), false, false);
236            f.addHelpMenu(helpTarget(), true);
237            applyBut = new JButton(Bundle.getMessage("ButtonApply")); // create before initPanels()
238            java.awt.Container containerPanel = f.getContentPane();
239            initPanelsFirst();
240            initPanels();
241            initPanelsLast();
242
243            int i=0;
244            for (BeanItemPanel bi : bei) {
245                addToPanel(bi, bi.getListOfItems());
246                detailsTab.add(bi, bi.getName(), i);
247                detailsTab.setEnabledAt(i, bi.isEnabled());
248                detailsTab.setToolTipTextAt(i, bi.getToolTipText());
249                i++;
250            }
251            containerPanel.add(detailsTab, BorderLayout.CENTER);
252
253            // shared bottom panel part
254            JPanel bottom = new JPanel();
255            bottom.setLayout(new BoxLayout(bottom, BoxLayout.PAGE_AXIS));
256            // shared status bar above buttons
257            JPanel panelStatus = new JPanel();
258            statusBar.setFont(statusBar.getFont().deriveFont(0.9f * userNameField.getFont().getSize())); // a bit smaller
259            statusBar.setForeground(Color.gray);
260            panelStatus.add(statusBar);
261            bottom.add(panelStatus);
262
263            // shared buttons
264            JPanel buttons = new JPanel();
265            applyBut.addActionListener(this::applyButtonAction);
266            JButton okBut = new JButton(Bundle.getMessage("ButtonOK"));
267            okBut.addActionListener((ActionEvent e1) -> {
268                applyButtonAction(e1);
269                f.dispose();
270            });
271            JButton cancelBut = new JButton(Bundle.getMessage("ButtonCancel"));
272            cancelBut.addActionListener(this::cancelButtonAction);
273            buttons.add(applyBut);
274            buttons.add(okBut);
275            buttons.add(cancelBut);
276            bottom.add(buttons);
277            containerPanel.add(bottom, BorderLayout.SOUTH);
278        }
279        for (BeanItemPanel bi : bei) {
280            bi.resetField();
281        }
282        persistSelectedTab(); // use persistence unless specified by overriding class
283        if (selectedTab != null) {
284            detailsTab.setSelectedComponent(selectedTab);
285        }
286        f.addWindowListener(new java.awt.event.WindowAdapter() {
287            @Override
288            public void windowClosing(java.awt.event.WindowEvent e) {
289                cancelButtonAction(null);
290            }
291        });
292        f.pack();
293        f.setVisible(true);
294    }
295
296    /**
297     * Selects previously selected Tab Index for override class name.
298     * Adds listener when Tab changed update UI preference.
299     */
300    private void persistSelectedTab(){
301        String TAB_SELECT_STRING = "selectedTabIndex"; // NOI18N
302        Object obj = InstanceManager.getDefault(UserPreferencesManager.class)
303            .getProperty(getClass().getName(), TAB_SELECT_STRING);
304        int previoustab = (obj!=null ? (Integer) obj : 0);
305        // make sure that valid index selected in case a tab is removed in future.
306        detailsTab.setSelectedIndex(Math.max(Math.min(detailsTab.getTabCount()-1, previoustab),0));
307        // add listener
308        detailsTab.getModel().addChangeListener((ChangeEvent evt) -> {
309            InstanceManager.getDefault(UserPreferencesManager.class)
310                .setProperty(getClass().getName(), TAB_SELECT_STRING, detailsTab.getSelectedIndex());
311        });
312    
313    }
314
315    protected void applyButtonAction(ActionEvent e) {
316        save();
317    }
318
319    protected void cancelButtonAction(ActionEvent e) {
320        f.dispose();
321    }
322
323    /**
324     * Set out the panel based upon the items passed in via the ArrayList.
325     *
326     * @param panel JPanel to add stuff to
327     * @param items a {@link BeanEditItem} list of key-value pairs for the items
328     *              to add
329     */
330    protected void addToPanel(JPanel panel, List<BeanEditItem> items) {
331        GridBagLayout gbLayout = new GridBagLayout();
332        GridBagConstraints cL = new GridBagConstraints();
333        GridBagConstraints cD = new GridBagConstraints();
334        GridBagConstraints cR = new GridBagConstraints();
335        cL.fill = GridBagConstraints.HORIZONTAL;
336        cL.insets = new Insets(4, 0, 0, 15);   // inset for left hand column (description)
337        cR.insets = new Insets(4, 10, 13, 15); // inset for help (right hand column, multi line text area)
338        cD.insets = new Insets(4, 0, 0, 0);    // top inset 4, up from 2 to align JLabel with JTextField
339        cD.anchor = GridBagConstraints.NORTHWEST;
340        cL.anchor = GridBagConstraints.NORTHWEST;
341
342        int y = 0;
343        JPanel p = new JPanel();
344
345        for (BeanEditItem it : items) {
346            // add the 3 elements on a JPanel to the parent panel grid layout
347            if (it.getDescription() != null && it.getComponent() != null) {
348                JLabel descript = new JLabel(it.getDescription() + ":", JLabel.LEFT);
349                if (it.getDescription().isEmpty()) {
350                    descript.setText("");
351                }
352                cL.gridx = 0;
353                cL.gridy = y;
354                cL.ipadx = 3;
355
356                gbLayout.setConstraints(descript, cL);
357                p.setLayout(gbLayout);
358                p.add(descript, cL);
359
360                cD.gridx = 1;
361                cD.gridy = y;
362
363                Component thing = it.getComponent();
364                //log.debug("descript: '" + it.getDescription() + "', thing: " + thing.getClass().getName());
365                if (thing instanceof JComboBox
366                        || thing instanceof JTextField
367                        || thing instanceof JCheckBox
368                        || thing instanceof JRadioButton) {
369                    cD.insets = new Insets(0, 0, 0, 0); // put a little higher than a JLabel
370                } else if (thing instanceof JColorChooser) {
371                    cD.insets = new Insets(-6, 0, 0, 0); // move it up
372                } else {
373                    cD.insets = new Insets(4, 0, 0, 0); // reset
374                }
375                gbLayout.setConstraints(thing, cD);
376                p.add(thing, cD);
377
378                cR.gridx = 2;
379                cR.gridwidth = 1;
380                cR.anchor = GridBagConstraints.WEST;
381
382            } else {
383                cR.anchor = GridBagConstraints.CENTER;
384                cR.gridx = 0;
385                cR.gridwidth = 3;
386            }
387            cR.gridy = y;
388            if (it.getHelp() != null) {
389                JTextPane help = new JTextPane();
390                help.setText(it.getHelp());
391                gbLayout.setConstraints(help, cR);
392                formatTextAreaAsLabel(help);
393                p.add(help, cR);
394            }
395            y++;
396        }
397        panel.add(p);
398    }
399
400    void formatTextAreaAsLabel(JTextPane pane) {
401        pane.setOpaque(false);
402        pane.setEditable(false);
403        pane.setBorder(null);
404    }
405
406    public void save() {
407        String feedback = Bundle.getMessage("ItemUpdateFeedback", bean.getBeanType())
408                + " " + bean.getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME);
409        // provide feedback to user, can be overwritten by save action error handler
410        statusBar.setText(feedback);
411        statusBar.setForeground(Color.gray);
412        for (BeanItemPanel bi : bei) {
413            bi.saveItem();
414        }
415    }
416
417    static boolean validateNumericalInput(String text) {
418        if (text.length() != 0) {
419            try {
420                Integer.parseInt(text);
421            } catch (java.lang.NumberFormatException ex) {
422                return false;
423            }
424        }
425        return true;
426    }
427
428    NamedBeanHandleManager nbMan = InstanceManager.getDefault(NamedBeanHandleManager.class);
429
430    abstract protected B getByUserName(String name);
431
432    /**
433     * Generic method to change the user name of a Bean.
434     *
435     * @param _newName string to use as the new user name
436     */
437    public void renameBean(String _newName) {
438        if (!allowBlockNameChange("Rename", _newName)) return;  // NOI18N
439        B nBean = bean;
440        String oldName = nBean.getUserName();
441
442        String value = _newName;
443
444        if (value.equals(oldName)) {
445            //name not changed.
446            return;
447        } else {
448            B nB = getByUserName(value);
449            if (nB != null) {
450                log.error("User name is not unique {}", value); // NOI18N
451                String msg;
452                msg = java.text.MessageFormat.format(Bundle.getMessage("WarningUserName"),
453                        new Object[]{("" + value)});
454                JmriJOptionPane.showMessageDialog(f, msg,
455                        Bundle.getMessage("WarningTitle"),
456                        JmriJOptionPane.ERROR_MESSAGE);
457                return;
458            }
459        }
460
461        nBean.setUserName(value);
462        if (!value.isEmpty()) {
463            if (oldName == null || oldName.isEmpty()) {
464                if (!nbMan.inUse(nBean.getSystemName(), nBean)) {
465                    return;
466                }
467                String msg = Bundle.getMessage("UpdateToUserName",
468                        new Object[]{nBean.getBeanType(), value, nBean.getSystemName()});
469                int optionPane = JmriJOptionPane.showConfirmDialog(f,
470                        msg, Bundle.getMessage("UpdateToUserNameTitle"),
471                        JmriJOptionPane.YES_NO_OPTION);
472                if (optionPane == JmriJOptionPane.YES_OPTION) {
473                    //This will update the bean reference from the systemName to the userName
474                    try {
475                        nbMan.updateBeanFromSystemToUser(nBean);
476                    } catch (jmri.JmriException ex) {
477                        //We should never get an exception here as we already check that the username is not valid
478                    }
479                }
480
481            } else {
482                nbMan.renameBean(oldName, value, nBean);
483            }
484
485        } else {
486            //This will update the bean reference from the old userName to the SystemName
487            nbMan.updateBeanFromUserToSystem(nBean);
488        }
489    }
490
491    /**
492     * Generic method to remove the user name from a bean.
493     */
494    public void removeName() {
495        if (!allowBlockNameChange("Remove", "")) return;  // NOI18N
496        String msg = java.text.MessageFormat.format(Bundle.getMessage("UpdateToSystemName"),
497                new Object[]{bean.getBeanType()});
498        int optionPane = JmriJOptionPane.showConfirmDialog(f,
499                msg, Bundle.getMessage("UpdateToSystemNameTitle"),
500                JmriJOptionPane.YES_NO_OPTION);
501        if (optionPane == JmriJOptionPane.YES_OPTION) {
502            nbMan.updateBeanFromUserToSystem(bean);
503        }
504        bean.setUserName(null);
505    }
506
507    /**
508     * Determine whether it is safe to rename/remove a Block user name.
509     * <p>The user name is used by the LayoutBlock to link to the block and
510     * by Layout Editor track components to link to the layout block.
511     * @param changeType This will be Remove or Rename.
512     * @param newName For Remove this will be empty, for Rename it will be the new user name.
513     * @return true to continue with the user name change.
514     */
515    boolean allowBlockNameChange(String changeType, String newName) {
516        if (!bean.getBeanType().equals("Block")) return true;  // NOI18N
517
518        // If there is no layout block or the block has no user name, Block rename and remove are ok without notification.
519        String oldName = bean.getUserName();
520        if (oldName == null) return true;
521        LayoutBlock layoutBlock = jmri.InstanceManager.getDefault(LayoutBlockManager.class).getByUserName(oldName);
522        if (layoutBlock == null) return true;
523
524        // Remove is not allowed if there is a layout block
525        if (changeType.equals("Remove")) {
526            log.warn("Cannot remove user name for block {}", oldName);  // NOI18N
527                JmriJOptionPane.showMessageDialog(f,
528                        Bundle.getMessage("BlockRemoveUserNameWarning", oldName),  // NOI18N
529                        Bundle.getMessage("WarningTitle"),  // NOI18N
530                        JmriJOptionPane.WARNING_MESSAGE);
531            return false;
532        }
533
534        // Confirmation dialog
535        int optionPane = JmriJOptionPane.showConfirmDialog(f,
536                Bundle.getMessage("BlockChangeUserName", oldName, newName),  // NOI18N
537                Bundle.getMessage("QuestionTitle"),  // NOI18N
538                JmriJOptionPane.YES_NO_OPTION);
539        return optionPane == JmriJOptionPane.YES_OPTION;
540    }
541
542    /**
543     * TableModel for edit of Bean properties.
544     * <p>
545     * At this stage we purely use this to allow the user to delete properties,
546     * not to add them. Changing properties is possible but only for strings.
547     * Based upon the code from the RosterMediaPane
548     */
549    private static class BeanPropertiesTableModel<B extends NamedBean> extends AbstractTableModel {
550
551        Vector<KeyValueModel> attributes;
552        String titles[];
553        boolean wasModified;
554
555        private static class KeyValueModel {
556
557            public KeyValueModel(String k, Object v) {
558                key = k;
559                value = v;
560            }
561            public String key;
562            public Object value;
563        }
564
565        public BeanPropertiesTableModel() {
566            titles = new String[2];
567            titles[0] = Bundle.getMessage("NamedBeanPropertyName");
568            titles[1] = Bundle.getMessage("NamedBeanPropertyValue");
569        }
570
571        public void setModel(B nb) {
572            attributes = new Vector<>(nb.getPropertyKeys().size());
573            Iterator<String> ite = nb.getPropertyKeys().iterator();
574            while (ite.hasNext()) {
575                String key = ite.next();
576                KeyValueModel kv = new KeyValueModel(key, nb.getProperty(key));
577                attributes.add(kv);
578            }
579            wasModified = false;
580        }
581
582        public void updateModel(B nb) {
583            if (!wasModified()) {
584                return; //No changed made
585            }   // add and update keys
586            for (int i = 0; i < attributes.size(); i++) {
587                KeyValueModel kv = attributes.get(i);
588                if ((kv.key != null)
589                        && // only update if key value defined, will do the remove too
590                        ((nb.getProperty(kv.key) == null) || (!kv.value.equals(nb.getProperty(kv.key))))) {
591                    nb.setProperty(kv.key, kv.value);
592                }
593            }
594            //remove undefined keys
595
596            Iterator<String> ite = nb.getPropertyKeys().iterator();
597            while (ite.hasNext()) {
598                if (!keyExist(ite.next())) // not a very efficient algorithm!
599                {
600                    ite.remove();
601                }
602            }
603            wasModified = false;
604        }
605
606        private boolean keyExist(Object k) {
607            if (k == null) {
608                return false;
609            }
610            for (int i = 0; i < attributes.size(); i++) {
611                if (k.equals(attributes.get(i).key)) {
612                    return true;
613                }
614            }
615            return false;
616        }
617
618        @Override
619        public int getColumnCount() {
620            return 2;
621        }
622
623        @Override
624        public int getRowCount() {
625            return attributes.size();
626        }
627
628        @Override
629        public String getColumnName(int col) {
630            return titles[col];
631        }
632
633        @Override
634        public Object getValueAt(int row, int col) {
635            if (row < attributes.size()) {
636                if (col == 0) {
637                    return attributes.get(row).key;
638                }
639                if (col == 1) {
640                    return attributes.get(row).value;
641                }
642            }
643            return "...";
644        }
645
646        @Override
647        public void setValueAt(Object value, int row, int col) {
648            KeyValueModel kv;
649
650            if (row < attributes.size()) // already exist?
651            {
652                kv = attributes.get(row);
653            } else {
654                kv = new KeyValueModel("", "");
655            }
656
657            if (col == 0) // update key
658            //Force keys to be save as a single string with no spaces
659            {
660                if (!keyExist(((String) value).replaceAll("\\s", ""))) // if not exist
661                {
662                    kv.key = ((String) value).replaceAll("\\s", "");
663                } else {
664                    setValueAt(value + "-1", row, col); // else change key name
665                    return;
666                }
667            }
668
669            if (col == 1) // update value
670            {
671                kv.value = value;
672            }
673            if (row < attributes.size()) // existing one
674            {
675                attributes.set(row, kv);
676            } else {
677                attributes.add(row, kv); // new one
678            }
679            if ((col == 0) && (kv.key.isEmpty())) {
680                attributes.remove(row); // actually maybe remove
681            }
682            wasModified = true;
683            fireTableCellUpdated(row, col);
684        }
685
686        @Override
687        public boolean isCellEditable(int row, int col) {
688            return true;
689        }
690
691        public boolean wasModified() {
692            return wasModified;
693        }
694    }
695
696    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BeanEditAction.class);
697
698}