001package jmri.jmrit.beantable.signalmast;
002
003import java.awt.*;
004import java.awt.event.*;
005import java.io.File;
006import java.net.URISyntaxException;
007import java.net.URL;
008import java.util.*;
009import java.util.List;
010
011import javax.swing.*;
012
013import jmri.*;
014import jmri.implementation.*;
015import jmri.jmrit.XmlFile;
016import jmri.util.*;
017import jmri.util.swing.JComboBoxUtil;
018import jmri.util.swing.JmriJOptionPane;
019
020import org.jdom2.Element;
021
022/**
023 * JPanel to create a new Signal Mast.
024 *
025 * "Driver" refers to a particular class of SignalMast implementation that's to be configured.
026 *
027 * @author Bob Jacobsen Copyright (C) 2009, 2010, 2016
028 * @author Egbert Broerse Copyright (C) 2016
029 */
030public class AddSignalMastPanel extends JPanel {
031
032    // head matter
033    JTextField userName = new JTextField(20);
034    JComboBox<String> sigSysBox = new JComboBox<>();  // the basic signal system
035    JComboBox<String> mastBox = new JComboBox<>(new String[]{Bundle.getMessage("MastEmpty")}); // the mast within the system NOI18N
036    boolean mastBoxPassive = false; // if true, mastBox doesn't process updates
037    JComboBox<String> signalMastDriver;   // the specific SignalMast class type
038
039    List<SignalMastAddPane> panes = new ArrayList<>();
040
041    // center pane, which holds the specific display
042    JPanel centerPanel = new JPanel();
043    CardLayout cl = new CardLayout();
044    SignalMastAddPane currentPane;
045
046    // rest of structure
047    JPanel signalHeadPanel = new JPanel();
048    JButton cancel = new JButton(Bundle.getMessage("ButtonCancel")); // NOI18N
049    JButton apply = new JButton(Bundle.getMessage("ButtonApply")); // NOI18N
050    JButton create = new JButton(Bundle.getMessage("ButtonCreate")); // NOI18N
051
052    // connection to preferences
053    private UserPreferencesManager prefs = InstanceManager.getDefault(UserPreferencesManager.class);
054    private String systemSelectionCombo = this.getClass().getName() + ".SignallingSystemSelected"; // NOI18N
055    private String mastSelectionCombo = this.getClass().getName() + ".SignallingMastSelected"; // NOI18N
056    private String driverSelectionCombo = this.getClass().getName() + ".SignallingDriverSelected"; // NOI18N
057
058    private SignalMast mast = null;
059    private String originalUserName = null;
060
061    /**
062     * Constructor providing a blank panel to configure a new signal mast after
063     * pressing 'Add...' on the Signal Mast Table.
064     * <p>
065     * Responds to choice of signal system, mast type and driver
066     * {@link #updateSelectedDriver()}
067     */
068    public AddSignalMastPanel() {
069        log.debug("AddSignalMastPanel()");
070        // get the list of possible signal types (as shown by panes)
071        SignalMastAddPane.SignalMastAddPaneProvider.getInstancesCollection().forEach(
072            (provider)-> {
073                if (provider.isAvailable()) {
074                    panes.add(provider.getNewPane());
075                }
076            }
077        );
078
079        // scoping for temporary variables
080        String[] tempMastNamesArray = new String[panes.size()];
081        int i = 0;
082        for (SignalMastAddPane pane : panes) {
083            tempMastNamesArray[i++] = pane.getPaneName();
084        }
085        signalMastDriver = new JComboBox<>(tempMastNamesArray);
086        init();
087    }
088
089    final void init() {
090
091        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
092
093        JPanel p;
094        p = new JPanel();
095        p.setLayout(new jmri.util.javaworld.GridLayout2(5, 2));
096
097        JLabel l = new JLabel(Bundle.getMessage("LabelUserName"));  // NOI18N
098        p.add(l);
099        p.add(userName);
100
101        l = new JLabel(Bundle.getMessage("SigSys") + ": "); // NOI18N
102        p.add(l);
103        p.add(sigSysBox);
104        JComboBoxUtil.setupComboBoxMaxRows(sigSysBox);
105
106        l = new JLabel(Bundle.getMessage("MastType") + ": "); // NOI18N
107        p.add(l);
108        p.add(mastBox);
109        JComboBoxUtil.setupComboBoxMaxRows(mastBox);
110
111        l = new JLabel(Bundle.getMessage("DriverType") + ": "); // NOI18N
112        p.add(l);
113        p.add(signalMastDriver);
114        JComboBoxUtil.setupComboBoxMaxRows(signalMastDriver);
115
116        add(p);
117
118        // central region
119        centerPanel.setLayout(cl);
120        for (SignalMastAddPane pane : panes) {
121            centerPanel.add(pane, pane.getPaneName()); // assumes names are systemwide-unique
122        }
123        add(centerPanel);
124        signalMastDriver.addItemListener((ItemEvent evt) -> {
125            log.trace("about to call selection() from signalMastDriver itemStateChanged");
126            selection((String)evt.getItem());
127        });
128
129        // button region
130        JPanel buttonHolder = new JPanel();
131        buttonHolder.setLayout(new FlowLayout(FlowLayout.TRAILING));
132        cancel.setVisible(true);
133        buttonHolder.add(cancel);
134        cancel.addActionListener((ActionEvent e) -> {
135            cancelPressed();
136        } // Cancel button
137        );
138        cancel.setVisible(true);
139        buttonHolder.add(create); // Create button on add new mast pane
140        create.addActionListener((ActionEvent e) -> {
141            okPressed();
142        });
143        create.setVisible(true);
144        buttonHolder.add(apply); // Apply button on Edit existing mast pane
145        apply.addActionListener((ActionEvent e) -> {
146            okPressed();
147        });
148        apply.setVisible(false);
149        add(buttonHolder); // add bottom row of buttons (to me)
150
151        // default to 1st pane
152        currentPane = panes.get(0);
153
154        // load the list of signal systems
155        SignalSystemManager man = InstanceManager.getDefault(SignalSystemManager.class);
156        SortedSet<SignalSystem> systems = man.getNamedBeanSet();
157        for (SignalSystem system : systems) {
158            sigSysBox.addItem(system.getUserName());
159        }
160
161        if (prefs.getComboBoxLastSelection(systemSelectionCombo) != null) {
162            sigSysBox.setSelectedItem(prefs.getComboBoxLastSelection(systemSelectionCombo));
163        }
164        log.trace("  preferences set {} into sigSysBox", sigSysBox.getSelectedItem());
165
166        loadMastDefinitions();
167
168        // select the 1st one
169        selection(panes.get(0).getPaneName());  // there has to be at least one, so we can do the update
170
171        // set a remembered signalmast type, if present
172        if (prefs.getComboBoxLastSelection(driverSelectionCombo) != null) {
173            signalMastDriver.setSelectedItem(prefs.getComboBoxLastSelection(driverSelectionCombo));
174        }
175
176        sigSysBox.addItemListener((ItemEvent e) -> {
177            loadMastDefinitions();
178            updateSelectedDriver();
179        });
180    }
181
182    /**
183     * Select a particular signal implementation to display.
184     * @param view The signal implementation pane name to display
185     */
186    final void selection(String view) {
187        log.trace(" selection({}) start", view);
188        // find the new pane
189        for (SignalMastAddPane pane : panes) {
190            if (pane.getPaneName().equals(view)) {
191                currentPane = pane;
192            }
193        }
194
195        // update that selected pane before display.
196        updateSelectedDriver();
197
198        // and show
199        cl.show(centerPanel, view);
200        log.trace(" selection({}) end", view);
201    }
202
203    /**
204     * Build a panel filled in for existing mast after pressing 'Edit' in the
205     * Signal Mast table.
206     *
207     * @param mast {@code NamedBeanHandle<SignalMast> } for the signal mast to
208     *             be retrieved
209     * @see #AddSignalMastPanel()
210     */
211    public AddSignalMastPanel(SignalMast mast) {
212        this(); // calls the above method to build the base for an edit panel
213        log.debug("AddSignalMastPanel({}) start", mast);
214        this.mast = mast;
215
216        // switch buttons
217        apply.setVisible(true);
218        create.setVisible(false);
219
220        // can't change some things from original settings
221        sigSysBox.setEnabled(false);
222        mastBox.setEnabled(false);
223        signalMastDriver.setEnabled(false);
224
225        //load prior content
226        userName.setText(mast.getUserName());
227        originalUserName = mast.getUserName();
228        log.trace("Prior content system name: {}  mast type: {}", mast.getSignalSystem().getUserName(), mast.getMastType());
229        if (mast.getMastType() == null) log.error("MastType was null, and never should be");
230        sigSysBox.setSelectedItem(mast.getSignalSystem().getUserName());  // signal system
231
232        // select and show
233        for (SignalMastAddPane pane : panes) {
234            if (pane.canHandleMast(mast)) {
235                currentPane = pane;
236                // set the driver combobox
237                signalMastDriver.setSelectedItem(pane.getPaneName());
238                log.trace("About to call selection() from SignalMastAddPane loop in AddSignalMastPanel(SignalMast mast)");
239                selection(pane.getPaneName());
240
241                // Ensure that the mast type is set
242                mastBoxPassive = false;
243                if (mapTypeToName.get(mast.getMastType()) == null ) {
244                    log.error("About to set mast to null, which shouldn't happen. mast.getMastType() is {}", mast.getMastType(),
245                            new Exception("Traceback Exception")); // NOI18N
246                }
247                log.trace("set mastBox to \"{}\" from \"{}\"", mapTypeToName.get(mast.getMastType()), mast.getMastType()); // NOI18N
248                mastBox.setSelectedItem(mapTypeToName.get(mast.getMastType()));
249
250                pane.setMast(mast);
251                break;
252            }
253        }
254
255        // set mast type, suppress notification
256        mastBoxPassive = true;
257        String newMastType = mapTypeToName.get(mast.getMastType());
258        log.debug("Setting type to {}", newMastType); // NOI18N
259        mastBox.setSelectedItem(newMastType);
260        mastBoxPassive = false;
261
262        log.debug("AddSignalMastPanel({}) end", mast);
263    }
264
265    // signal system definition variables
266    private String sigsysname;
267    private ArrayList<File> mastFiles = new ArrayList<>(); // signal system definition files
268    private LinkedHashMap<String, Integer> mapNameToShowSize = new LinkedHashMap<>();
269    private LinkedHashMap<String, String> mapTypeToName = new LinkedHashMap<>();
270
271    /**
272     * Load the mast definitions from the selected signal system.
273     */
274    void loadMastDefinitions() {
275        log.trace(" loadMastDefinitions() start");
276        // need to remove itemListener before addItem() or item event will occur
277        if (mastBox.getItemListeners().length > 0) { // should this be a while loop?
278            mastBox.removeItemListener(mastBox.getItemListeners()[0]);
279        }
280        mastBox.removeAllItems();
281        try {
282            mastFiles = new ArrayList<>();
283            SignalSystemManager man = InstanceManager.getDefault(SignalSystemManager.class);
284
285            // get the signals system name from the user name in combo box
286            String u = (String) sigSysBox.getSelectedItem();
287            SignalSystem sig = man.getByUserName(u);
288            if (sig==null){
289                log.error("Signal System Not found for Username {}",u);
290                return;
291            }
292            sigsysname = sig.getSystemName();
293            log.trace("     loadMastDefinitions with sigsysname {}", sigsysname); // NOI18N
294            mapNameToShowSize = new LinkedHashMap<>();
295            mapTypeToName = new LinkedHashMap<>();
296
297            // do file IO to get all the appearances
298            // gather all the appearance files
299            // Look for the default system defined ones first
300            File[] programDirArray = new File[0];
301            URL pathProgramDir = FileUtil.findURL("xml/signals/" + sigsysname, FileUtil.Location.INSTALLED); // NOI18N
302            if (pathProgramDir != null) programDirArray = new File(pathProgramDir.toURI()).listFiles();
303            if (programDirArray == null) programDirArray = new File[0];
304
305            File[] profileDirArray = new File[0];
306            URL pathProfileDir = FileUtil.findURL("resources/signals/" + sigsysname, FileUtil.Location.USER); // NOI18N
307            if (pathProfileDir != null) profileDirArray = new File(pathProfileDir.toURI()).listFiles();
308            if (profileDirArray == null) profileDirArray = new File[0];
309
310            // create a composite list of files
311            File[] apps = Arrays.copyOf(programDirArray, programDirArray.length + profileDirArray.length);
312            System.arraycopy(profileDirArray, 0, apps, programDirArray.length, profileDirArray.length);
313
314            if (apps !=null) {
315                for (File app : apps) {
316                    if (app.getName().startsWith("appearance") && app.getName().endsWith(".xml")) { // NOI18N
317                        log.debug("   found file: {}", app.getName()); // NOI18N
318                        // load it and get name
319                        mastFiles.add(app);
320                        XmlFile xf = new XmlFile() {
321                        };
322                        Element root = xf.rootFromFile(app);
323                        String name = root.getChild("name").getText();
324                        log.trace("mastNames adding \"{}\" mastBox adding \"{}\" ", app, name); // NOI18N
325                        mastBox.addItem(name);
326                        log.trace("mapTypeToName adding key \"{}\" value \"{}\"", app.getName().substring(11, app.getName().indexOf(".xml")), name); // NOI18N
327                        mapTypeToName.put(app.getName().substring(11, app.getName().indexOf(".xml")), name); // NOI18N
328                        mapNameToShowSize.put(name, root.getChild("appearances") // NOI18N
329                                .getChild("appearance") // NOI18N
330                                .getChildren("show") // NOI18N
331                                .size());
332
333                    }
334                }
335            } else {
336                log.error("Unexpected null list of signal definition files"); // NOI18N
337            }
338
339        } catch (org.jdom2.JDOMException e) {
340            mastBox.addItem(Bundle.getMessage("ErrorSignalMastBox1")); // NOI18N
341            log.warn("in loadMastDefinitions", e); // NOI18N
342        } catch (java.io.IOException | URISyntaxException e) {
343            mastBox.addItem(Bundle.getMessage("ErrorSignalMastBox2")); // NOI18N
344            log.warn("in loadMastDefinitions", e); // NOI18N
345        }
346
347        try {
348            URL path = FileUtil.findURL("signals/" + sigsysname, FileUtil.Location.USER, "xml", "resources"); // NOI18N
349            if (path != null) {
350                File[] apps = new File(path.toURI()).listFiles();
351                if (apps != null) {
352                    for (File app : apps) {
353                        if (app.getName().startsWith("appearance") && app.getName().endsWith(".xml")) { // NOI18N
354                            log.debug("   found file: {}", app.getName()); // NOI18N
355                            // load it and get name
356                            // If the mast file name already exists no point in re-adding it
357                            if (!mastFiles.contains(app)) {
358                                mastFiles.add(app);
359                                XmlFile xf = new XmlFile() {
360                                };
361                                Element root = xf.rootFromFile(app);
362                                String name = root.getChild("name").getText();
363                                //if the mast name already exist no point in readding it.
364                                if (!mapNameToShowSize.containsKey(name)) {
365                                    mastBox.addItem(name);
366                                    mapNameToShowSize.put(name, root.getChild("appearances") // NOI18N
367                                            .getChild("appearance") // NOI18N
368                                            .getChildren("show") // NOI18N
369                                            .size());
370                                }
371                            }
372                        }
373                    }
374                } else {
375                    log.warn("No mast definition files found");
376                }
377            }
378        } catch (org.jdom2.JDOMException | java.io.IOException | URISyntaxException e) {
379            log.warn("in loadMastDefinitions", e); // NOI18N
380        }
381        mastBox.addItemListener((ItemEvent e) -> {
382            if (!mastBoxPassive) updateSelectedDriver();
383        });
384        updateSelectedDriver();
385
386        if (prefs.getComboBoxLastSelection(mastSelectionCombo + ":" + ((String) sigSysBox.getSelectedItem())) != null) { // NOI18N
387            mastBox.setSelectedItem(prefs.getComboBoxLastSelection(mastSelectionCombo + ":" + ((String) sigSysBox.getSelectedItem())));
388        }
389        log.trace(" loadMastDefinitions() end");
390    }
391
392    /**
393     * Update contents of Add/Edit mast panel appropriate for chosen Driver
394     * type.
395     * <p>
396     * Invoked when selecting a Signal Mast Driver in {@link #loadMastDefinitions}
397     */
398    protected void updateSelectedDriver() {
399        log.trace(" updateSelectedDriver() start");
400
401        if (mastBox.getSelectedIndex() < 0) return; // no mast selected yet
402        String mastFile = mastFiles.get(mastBox.getSelectedIndex()).getName();
403        String mastType = mastFile.substring(11, mastFile.indexOf(".xml"));
404        DefaultSignalAppearanceMap sigMap = DefaultSignalAppearanceMap.getMap(sigsysname, mastType);
405        SignalSystem sigsys = InstanceManager.getDefault(SignalSystemManager.class).getSystem(sigsysname);
406        if (sigsys == null){
407            log.error("Signalling System for {} Not Found",sigsysname);
408        } else {
409            currentPane.setAspectNames(sigMap, sigsys);
410        }
411        // clear mast info
412        currentPane.setMast(null);
413
414        currentPane.revalidate();
415
416        java.awt.Container ancestor = getTopLevelAncestor();
417        if ((ancestor instanceof JmriJFrame)) {
418            ((JmriJFrame) ancestor).pack();
419        } else {
420            log.debug("Can't call pack() on {}", ancestor);
421        }
422        log.trace(" updateSelectedDriver() end");
423    }
424
425    /**
426     * Check of user name done when creating new SignalMast.
427     * In case of error, it looks a message and (if not headless) shows a dialog.
428     *
429     * @param nam User name to be checked
430     * @return true if OK to proceed
431     */
432    boolean checkUserName(String nam) {
433        if (!((nam == null) || (nam.isEmpty()))) {
434            // user name provided, check if that name already exists
435            NamedBean nB = InstanceManager.getDefault(SignalMastManager.class).getByUserName(nam);
436            if (nB != null) {
437                issueWarningUserName(nam);
438                return false;
439            }
440            // Check to ensure that the username doesn't exist as a systemname.
441            nB = InstanceManager.getDefault(SignalMastManager.class).getBySystemName(nam);
442            if (nB != null) {
443                issueWarningUserNameAsSystem(nam);
444                return false;
445            }
446        }
447        return true;
448    }
449
450    void issueWarningUserName(String nam) {
451        log.error("User Name \"{}\" is already in use", nam); // NOI18N
452        if (!GraphicsEnvironment.isHeadless()) {
453            String msg = Bundle.getMessage("WarningUserName", new Object[]{("" + nam)}); // NOI18N
454            JmriJOptionPane.showMessageDialog(this, msg,
455                    Bundle.getMessage("WarningTitle"), // NOI18N
456                    JmriJOptionPane.ERROR_MESSAGE);
457        }
458    }
459
460    void issueWarningUserNameAsSystem(String nam) {
461        log.error("User Name \"{}\" already exists as a System name", nam);
462        if (!GraphicsEnvironment.isHeadless()) {
463            String msg = Bundle.getMessage("WarningUserNameAsSystem", new Object[]{("" + nam)});
464            JmriJOptionPane.showMessageDialog(this, msg,
465                    Bundle.getMessage("WarningTitle"),
466                    JmriJOptionPane.ERROR_MESSAGE);
467        }
468    }
469
470    /**
471     * Store user input for a signal mast definition in new or existing mast
472     * object.
473     * <p>
474     * Invoked from Apply/Create button.
475     */
476    private void okPressed() {
477        log.trace(" okPressed() start");
478        boolean success;
479
480        // get and validate entered global information
481        if ( (mastBox.getSelectedIndex() < 0) || ( mastFiles.get(mastBox.getSelectedIndex()) == null) ) {
482            issueDialogFailMessage(new RuntimeException("There's something wrong with the mast type selection"));
483            return;
484        }
485        String mastname = mastFiles.get(mastBox.getSelectedIndex()).getName();
486        String tmpUserName = NamedBean.normalizeUserName(userName.getText());
487        String user = ( tmpUserName != null ? tmpUserName : ""); // NOI18N
488
489        // if we are editing, and the username has been changed, we need to do a rename.
490        if (mast != null && !user.equals(originalUserName)) {
491            try {
492                NamedBeanHandleManager nbMan = InstanceManager.getDefault(NamedBeanHandleManager.class);
493
494                // Case 1: New user name is not empty.
495                if (!user.isEmpty()) {
496                    // This is a rename or add-user-name operation. Validate the new user name.
497                    SignalMastManager manager = InstanceManager.getDefault(SignalMastManager.class);
498                    NamedBean conflict = manager.getByUserName(user);
499                    if (conflict != null && conflict != mast) {
500                        issueWarningUserName(user); // "User Name is already in use"
501                        return;
502                    }
503                    conflict = manager.getBySystemName(user);
504                    if (conflict != null && conflict != mast) {
505                        issueWarningUserNameAsSystem(user); // "User Name exists as a System Name"
506                        return;
507                    }
508
509                    // Validation passed. Now, perform the correct update.
510                    mast.setUserName(user);
511
512                    if (originalUserName == null || originalUserName.isEmpty()) {
513                        // Was empty, now has a name.
514                        nbMan.updateBeanFromSystemToUser(mast);
515                    } else {
516                        // Was not empty, is now a different non-empty name.
517                        nbMan.renameBean(originalUserName, user, mast);
518                    }
519                } else { // user.isEmpty() is true
520                    // This is a remove-user-name operation.
521                    mast.setUserName(null);
522                    nbMan.updateBeanFromUserToSystem(mast);
523                }
524            } catch (JmriException e) {
525                log.error("Error while renaming Signal Mast", e);
526                JmriJOptionPane.showMessageDialog(this, e.getMessage(),
527                        Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
528                return;
529            }
530        }
531
532        // ask top-most pane to make a signal
533        try {
534            // For a new mast, this creates it.
535            // For an existing mast, this updates its properties from the pane.
536            success = currentPane.createMast(sigsysname, mastname, user);
537        } catch (RuntimeException ex) {
538            issueDialogFailMessage(ex);
539            return; // without clearing the panel, so user can try again
540        }
541        if (!success) {
542            // should have already provided user feedback via dialog
543            return;
544        }
545
546        clearPanel();
547        log.trace(" okPressed() end");
548    }
549
550    int issueNoUserNameGiven() {
551        return JmriJOptionPane.showConfirmDialog(this, Bundle.getMessage("SignalMastEmptyUserNameDialog"),  // NOI18N
552                Bundle.getMessage("SignalMastEmptyUserNameDialogTitle"),  // NOI18N
553                JmriJOptionPane.YES_NO_OPTION);
554    }
555
556    void issueDialogFailMessage(RuntimeException ex) {
557        // This is intrinsically swing, so pop a dialog
558        log.error("Failed during createMast", ex); // NOI18N
559        JmriJOptionPane.showMessageDialog(this,
560            Bundle.getMessage("DialogFailMessage", ex.toString()), // NOI18N
561            Bundle.getMessage("DialogFailTitle"),  // title of box // NOI18N
562            JmriJOptionPane.ERROR_MESSAGE);
563    }
564
565    /**
566     * Called when an already-initialized AddSignalMastPanel is being
567     * displayed again, right before it's set visible.
568     */
569    public void refresh() {
570        log.trace(" refresh() start");
571        // add new cards (new panes)
572        centerPanel.removeAll();
573        for (SignalMastAddPane pane : panes) {
574            centerPanel.add(pane, pane.getPaneName()); // assumes names are systemwide-unique
575        }
576
577        // select pane to match current combobox
578        log.trace("about to call selection from refresh");
579        selection(signalMastDriver.getItemAt(signalMastDriver.getSelectedIndex()));
580        log.trace(" refresh() end");
581    }
582
583    /**
584     * Respond to the Cancel button.
585     */
586    private void cancelPressed() {
587        log.trace(" cancelPressed() start");
588        clearPanel();
589        log.trace(" cancelPressed() end");
590    }
591
592    /**
593     * Close and dispose() panel.
594     * <p>
595     * Called at end of okPressed() and from Cancel
596     */
597    private void clearPanel() {
598        log.trace(" clearPanel() start");
599        java.awt.Container ancestor = getTopLevelAncestor();
600        if ((ancestor instanceof JmriJFrame)) {
601            ((JmriJFrame) ancestor).dispose();
602        } else {
603            log.warn("Unexpected top level ancestor: {}", ancestor); // NOI18N
604        }
605        userName.setText(""); // clear user name
606        log.trace(" clearPanel() end");
607    }
608
609
610    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AddSignalMastPanel.class);
611
612}