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