001package jmri.jmrit.beantable.oblock;
002
003import javax.annotation.CheckForNull;
004import javax.annotation.Nonnull;
005import javax.swing.*;
006import java.awt.*;
007import java.awt.event.ActionEvent;
008
009import jmri.InstanceManager;
010import jmri.UserPreferencesManager;
011import jmri.jmrit.logix.*;
012import jmri.util.JmriJFrame;
013import jmri.util.swing.JmriJOptionPane;
014
015/**
016 * Defines a GUI for editing OBlock - OPath objects in the _tabbed OBlock Table interface.
017 * Based on {@link jmri.jmrit.audio.swing.AudioSourceFrame} and
018 * {@link jmri.jmrit.beantable.routetable.AbstractRouteAddEditFrame}
019 *
020 * @author Matthew Harris copyright (c) 2009
021 * @author Egbert Broerse (C) 2020
022 */
023public class BlockPathEditFrame extends JmriJFrame {
024
025    // UI components for Add/Edit Path
026    JLabel blockLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("BeanNameOBlock")), JLabel.TRAILING);
027    JLabel blockName = new JLabel();
028    JLabel pathLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("PathName")), JLabel.TRAILING);
029    protected JTextField pathUserName = new JTextField(15);
030    JLabel fromPortalLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("FromPortal")), JLabel.TRAILING);
031    JLabel toPortalLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("ToPortal")), JLabel.TRAILING);
032    String[] p0 = {""};
033    protected final JComboBox<String> fromPortalComboBox = new JComboBox<>(p0);
034    protected final JComboBox<String> toPortalComboBox = new JComboBox<>(p0);
035    JLabel statusBar = new JLabel(Bundle.getMessage("AddXStatusInitial1", Bundle.getMessage("Path"), Bundle.getMessage("ButtonOK")), JLabel.LEADING);
036    // the following 3 items copied from beanedit, place in separate static method?
037    private final JSpinner lengthSpinner = new JSpinner(); // 2 digit decimal format field, initialized later as instance
038    private final JRadioButton inch = new JRadioButton(Bundle.getMessage("LengthInches"));
039    private final JRadioButton cm = new JRadioButton(Bundle.getMessage("LengthCentimeters"));
040
041    private final BlockPathEditFrame frame = this;
042    private boolean _newPath = false;
043    //protected final OBlockManager obm = InstanceManager.getDefault(OBlockManager.class);
044    PortalManager pm;
045    private final OBlock _block;
046    private OPath _path;
047    TableFrames _core;
048    BlockPathTableModel _pathmodel;
049    PathTurnoutTableModel _tomodel;
050    TableFrames.PathTurnoutJPanel _turnoutTablePane;
051
052    protected UserPreferencesManager pref;
053    protected boolean isDirty = false;  // true to fire reminder to save work
054    private boolean checkEnabled = jmri.InstanceManager.getDefault(jmri.configurexml.ShutdownPreferences.class).isStoreCheckEnabled();
055
056//    @SuppressWarnings("OverridableMethodCallInConstructor")
057    public BlockPathEditFrame(String title, @Nonnull OBlock block, @CheckForNull OPath path,
058                              @CheckForNull TableFrames.PathTurnoutJPanel turnouttable, BlockPathTableModel pathmodel, TableFrames parent) {
059        super(title, true, true);
060        _block = block;
061        _turnoutTablePane = turnouttable;
062        _pathmodel = pathmodel;
063        _core = parent;
064        if (path == null || turnouttable == null) {
065            _newPath = true;
066        } else {
067            if ((path.getBlock() != null) && (path.getBlock() != block)) {
068                // somehow we received a path that part of another block
069                log.error("BlockPathEditFrame for OPath {}, but it is not part of OBlock {}", path.getName(), block.getDisplayName());
070                JmriJOptionPane.showMessageDialog(BlockPathEditFrame.this,
071                        Bundle.getMessage("OBlockEditWrongPath", path.getName(), block.getDisplayName()),
072                        Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
073                // cancel edit
074                closeFrame();
075                return;
076            } else {
077                _path = path;
078                _tomodel = turnouttable.getModel();
079                if (_tomodel != null) { // test uses a plain JTable without getRowCount()
080                    log.debug("TurnoutModel.size = {}", _tomodel.getRowCount());
081                }
082            }
083        }
084        // fill Portals combo
085        pm = InstanceManager.getDefault(PortalManager.class);
086        for (Portal pi : pm.getPortalSet()) {
087            if (pi.getFromBlock() == _block || pi.getToBlock() == _block) { // show only relevant Portals
088                fromPortalComboBox.addItem(pi.getName()); // in both combos
089                toPortalComboBox.addItem(pi.getName());
090            }
091        }
092        layoutFrame();
093        blockName.setText(_block.getDisplayName());
094        if (_newPath) {
095            resetFrame();
096        } else {
097            populateFrame(path);
098        }
099        addCloseListener(this);
100    }
101
102    public void layoutFrame() {
103        frame.addHelpMenu("package.jmri.jmrit.beantable.OBlockTable", true);
104        frame.getContentPane().setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS));
105        frame.setSize(400, 500);
106
107        JPanel p = new JPanel();
108        p.setLayout(new BoxLayout(p, BoxLayout.PAGE_AXIS));
109
110        JPanel configGrid = new JPanel();
111        GridLayout layout = new GridLayout(4, 2, 10, 0); // (int rows, int cols, int hgap, int vgap)
112        configGrid.setLayout(layout);
113
114        // row 1
115        configGrid.add(blockLabel);
116        configGrid.add(blockName);
117
118        // row 2
119        configGrid.add(pathLabel);
120        JPanel p1 = new JPanel();
121        p1.add(pathUserName);
122        configGrid.add(p1);
123
124        // row 3
125        configGrid.add(fromPortalLabel);
126        fromPortalComboBox.addActionListener(e -> {
127            if ((fromPortalComboBox.getItemCount() > 0) && (fromPortalComboBox.getSelectedItem() != null) &&
128                    (toPortalComboBox.getSelectedItem() != null)
129                    && (fromPortalComboBox.getSelectedItem().equals(toPortalComboBox.getSelectedItem()))) {
130                log.debug("resetting ToPortal");
131                toPortalComboBox.setSelectedIndex(0); // clear the other one
132            }
133        });
134        configGrid.add(fromPortalComboBox);
135
136        // row 4
137        configGrid.add(toPortalLabel);
138        toPortalComboBox.addActionListener(e -> {
139            if ((fromPortalComboBox.getItemCount() > 0) && (fromPortalComboBox.getSelectedItem() != null) &&
140                    (toPortalComboBox.getSelectedItem() != null)
141                    && (fromPortalComboBox.getSelectedItem().equals(toPortalComboBox.getSelectedItem()))) {
142                log.debug("resetting FromPortal");
143                fromPortalComboBox.setSelectedIndex(0); // clear the other one
144            }
145        });
146        configGrid.add(toPortalComboBox);
147
148        p.add(configGrid);
149
150        // Length
151        JPanel physical = new JPanel();
152        // copied from beanedit, also in BlockPathEditFrame
153        lengthSpinner.setModel(
154                new SpinnerNumberModel(Float.valueOf(0f), Float.valueOf(0f), Float.valueOf(1000f), Float.valueOf(0.01f)));
155        lengthSpinner.setEditor(new JSpinner.NumberEditor(lengthSpinner, "###0.00"));
156        lengthSpinner.setPreferredSize(new JTextField(8).getPreferredSize());
157        lengthSpinner.setValue(0f); // reset from possible previous use
158
159        ButtonGroup bg = new ButtonGroup();
160        bg.add(inch);
161        bg.add(cm);
162
163        p1 = new JPanel();
164        p1.add(inch);
165        p1.add(cm);
166        p1.setLayout(new BoxLayout(p1, BoxLayout.PAGE_AXIS));
167        inch.setSelected(true);
168        inch.addActionListener(e -> {
169            cm.setSelected(!inch.isSelected());
170            updateLength();
171        });
172        cm.addActionListener(e -> {
173            inch.setSelected(!cm.isSelected());
174            updateLength();
175        });
176        physical.add(p1);
177
178        JPanel p2 = new JPanel();
179        p2.add(lengthSpinner);
180        lengthSpinner.setToolTipText(Bundle.getMessage("LengthToolTip", Bundle.getMessage("Path")));
181        physical.add(p2);
182
183        p.add(physical);
184
185        JPanel totbl = new JPanel();
186        totbl.setLayout(new BorderLayout(10, 10));
187        totbl.setBorder(BorderFactory.createLineBorder(Color.BLACK));
188        totbl.add(_turnoutTablePane, BorderLayout.CENTER);
189        p.add(totbl);
190
191        p2 = new JPanel();
192        statusBar.setFont(statusBar.getFont().deriveFont(0.9f * blockName.getFont().getSize())); // a bit smaller
193        statusBar.setForeground(Color.gray);
194        p2.add(statusBar);
195        p.add(p2);
196
197        p.add(Box.createVerticalGlue());
198
199        p2 = new JPanel();
200        p2.setLayout(new BoxLayout(p2, BoxLayout.LINE_AXIS));
201        JButton cancel;
202        p2.add(cancel = new JButton(Bundle.getMessage("ButtonCancel")));
203        cancel.addActionListener((ActionEvent e) -> closeFrame());
204        JButton ok;
205        p2.add(ok = new JButton(Bundle.getMessage("ButtonOK")));
206        ok.addActionListener(this::okPressed);
207        p.add(p2, BorderLayout.SOUTH);
208
209        frame.getContentPane().add(p);
210    }
211
212    /**
213     * Populate the Edit OBlock frame with default values.
214     */
215    public void resetFrame() {
216        pathUserName.setText(null);
217        if (toPortalComboBox.getItemCount() < 2) {
218            status(Bundle.getMessage("NotEnoughPortals"), true);
219        } else {
220            status(Bundle.getMessage("AddXStatusInitial1", Bundle.getMessage("Path"), Bundle.getMessage("ButtonCreate")),
221                    false); // I18N to include original button name in help string
222        }
223        lengthSpinner.setValue(0f);
224        _newPath = true;
225    }
226
227    /**
228     * Populate the Edit Path frame with current values.
229     *
230     * @param p existing OPath to copy the attributes from
231     */
232    public void populateFrame(OPath p) {
233        if (p == null) {
234            throw new IllegalArgumentException("Null OPath object");
235        }
236        pathUserName.setText(p.getName());
237        if (p.getFromPortal() != null) {
238            log.debug("BPEF FROMPORTAL name = {}", p.getFromPortal().getName());
239            fromPortalComboBox.setSelectedItem(p.getFromPortal().getName());
240        }
241        if (p.getToPortal() != null) {
242            log.debug("BPEF TOPORTAL name = {}", p.getToPortal().getName());
243            toPortalComboBox.setSelectedItem(p.getToPortal().getName());
244        }
245        if (_block.isMetric()) {
246            cm.setSelected(true);
247            lengthSpinner.setValue(_path.getLengthCm());
248        } else {
249            inch.setSelected(true); // set first while length = 0 to prevent recalc
250            lengthSpinner.setValue(_path.getLengthIn());
251        }
252        status(Bundle.getMessage("AddXStatusInitial3", Bundle.getMessage("Path"), Bundle.getMessage("ButtonOK")), false);
253        _newPath = false;
254    }
255
256    protected void okPressed(ActionEvent e) {
257        String user = pathUserName.getText().trim();
258        if (user.equals("") || (_newPath && _block.getPathByName(user) != null)) { // check existing names before creating
259            status(user.equals("") ? Bundle.getMessage("WarningSysNameEmpty") : Bundle.getMessage("DuplPathName", user), true);
260            pathUserName.setBackground(Color.red);
261            log.debug("username empty");
262            return;
263        }
264        if (_newPath) {
265            Portal fromPortal = _block.getPortalByName((String) fromPortalComboBox.getSelectedItem());
266            Portal toPortal = _block.getPortalByName((String) toPortalComboBox.getSelectedItem());
267            if (fromPortal != null || toPortal != null) {
268                _path = new OPath(user, _block, fromPortal, toPortal, null);
269                if (!_block.addPath(_path)) {
270                    status(Bundle.getMessage("AddPathFailed", user), true);
271                } else {
272                    _pathmodel.initTempRow();
273                    _core.updateOBlockTablesMenu();
274                    _pathmodel.fireTableDataChanged();
275                    closeFrame(); // success
276                }
277            } else {
278                log.debug("_newPath - could not get from/to Portal from this OBlock");
279            }
280        } else if (!_path.getName().equals(user)) {
281            _path.setName(user); // name change on existing path
282        }
283        try { // adapted from BlockPathTableModel setValue
284            // set fromPortal
285            if (!setPortal(fromPortalComboBox, toPortalComboBox, true)) {
286                return;
287            }
288            // set toPortal
289            if (!setPortal(toPortalComboBox, fromPortalComboBox, false)) {
290                return;
291            }
292            _path.setLength((float) lengthSpinner.getValue() * (cm.isSelected() ? 10.0f : 25.4f)); // stored in mm
293            _block.setMetricUnits(cm.isSelected());
294        } catch (IllegalArgumentException ex) {
295            JmriJOptionPane.showMessageDialog(this, ex.getMessage(),
296                    Bundle.getMessage("PathCreateErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
297            status(Bundle.getMessage("AddPathFailed", user), true);
298            return;
299        }
300        // Notify changes
301        if (_pathmodel != null) {
302            _pathmodel.fireTableDataChanged();
303        }
304        _core.setPathEdit(false);
305        log.debug("BlockPathEditFrame.okPressed complete. pathEdit = false");
306        closeFrame();
307    }
308
309    private boolean setPortal(JComboBox<String> portalBox, JComboBox<String> compareBox, boolean isFrom) {
310        if (portalBox.getSelectedIndex() <= 0) {
311            // 0 = empty choice, need at least 1 Portal in an OBlockPath
312            log.debug("fromPortal no selection, require 1 so check toPortal is not empty");
313            if (compareBox.getSelectedIndex() > 0) {
314                if (isFrom) {
315                    _path.setFromPortal(null); // portal can be removed from path by setting to null but we require at least 1
316                } else {
317                    _path.setToPortal(null); // at least 1
318                }
319                log.debug("removed {}Portal", (isFrom ? "From" : "To"));
320            } else {
321                status(Bundle.getMessage("WarnPortalOnPath"), true);
322                return false;
323            }
324        } else {
325            String portalName = (String) portalBox.getSelectedItem();
326            log.debug("looking for {}Portal {} (combo item {})", (isFrom ? "From" : "To"), portalName, portalBox.getSelectedIndex());
327            Portal portal = _block.getPortalByName(portalName);
328            if (portal == null || pm.getPortal(portalName) == null) {
329                int val = _core.verifyWarning(Bundle.getMessage("BlockPortalConflict", portalName, _block.getDisplayName()));
330                if (val == 2) {
331                    return false; // abort
332                }
333                portal = pm.providePortal(portalName);
334                if (isFrom) {
335                    if (!portal.setFromBlock(_block, false)) {
336                        val = _core.verifyWarning(Bundle.getMessage("BlockPathsConflict", portalName, portal.getFromBlockName()));
337                    }
338                } else {
339                    if (!portal.setToBlock(_block, false)) {
340                        val = _core.verifyWarning(Bundle.getMessage("BlockPathsConflict", portalName, portal.getToBlockName()));
341                    }
342                }
343                if (val == 2) {
344                    return false;
345                }
346                log.debug("fromPortal == null");
347                portal.setFromBlock(_block, true);
348            }
349            if (isFrom) {
350                _path.setFromPortal(portal); // portal can be removed from path by setting to null but we require at least 1
351            } else {
352                _path.setToPortal(portal); // at least 1
353            }
354            if (!portal.addPath(_path)) {
355                status(Bundle.getMessage("AddPathFailed", portalName), true);
356                return false ;
357            }
358        }
359        return true;
360    }
361
362    protected void closeFrame() {
363        // remind to save, if Path was created or edited
364        if (isDirty) {
365            showReminderMessage();
366            isDirty = false;
367        }
368        // hide addFrame
369        setVisible(false);
370
371        if (_tomodel != null) {
372            _tomodel.dispose();
373        }
374        _core.setPathEdit(false);
375        log.debug("BlockPathEditFrame.closeFrame pathEdit=False");
376        this.dispose();
377    }
378
379    protected void showReminderMessage() {
380        if (checkEnabled) return;
381        InstanceManager.getDefault(UserPreferencesManager.class).
382                showInfoMessage(Bundle.getMessage("ReminderTitle"),  // NOI18N
383                        Bundle.getMessage("ReminderSaveString", Bundle.getMessage("MenuItemOBlockTable")),  // NOI18N
384                        "BlockPathEditFrame", "remindSaveOBlock"); // NOI18N
385    }
386
387    // copied from beanedit, also used in BlockPathEditFrame
388    private void updateLength() {
389        float len = (float) lengthSpinner.getValue();
390        if (inch.isSelected()) {
391            lengthSpinner.setValue(len/2.54f);
392        } else {
393            lengthSpinner.setValue(len*2.54f);
394        }
395    }
396
397    void status(String message, boolean warn){
398        statusBar.setText(message);
399        statusBar.setForeground(warn ? Color.red : Color.gray);
400    }
401
402    // listen for frame closing
403    void addCloseListener(JmriJFrame frame) {
404        frame.addWindowListener(new java.awt.event.WindowAdapter() {
405            @Override
406            public void windowClosing(java.awt.event.WindowEvent e) {
407                _core.setPathEdit(false);
408                log.debug("BlockPathEditFrame.closeFrame pathEdit=False");
409                frame.dispose();
410            }
411        });
412    }
413
414    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BlockPathEditFrame.class);
415
416}