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