001package jmri.jmrit.beantable.oblock;
002
003import jmri.*;
004import jmri.jmrit.logix.OBlockManager;
005import jmri.jmrit.logix.Portal;
006import jmri.jmrit.logix.PortalManager;
007import jmri.swing.NamedBeanComboBox;
008import jmri.util.JmriJFrame;
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011
012import javax.annotation.CheckForNull;
013import javax.annotation.Nonnull;
014import javax.swing.*;
015import java.awt.*;
016import java.awt.event.ActionEvent;
017import java.util.Objects;
018
019/**
020 * Defines a GUI for editing OBlock - Signal objects in the tabbed Table interface.
021 * Adapted from AudioSourceFrame.
022 * Compare to CPE CircuitBuilder Signal Config frame {@link jmri.jmrit.display.controlPanelEditor.EditSignalFrame}
023 *
024 * @author Matthew Harris copyright (c) 2009
025 * @author Egbert Broerse (C) 2020
026 */
027public class SignalEditFrame extends JmriJFrame {
028
029    JPanel main = new JPanel();
030
031    SignalTableModel model;
032    NamedBean signal;
033    PortalManager pm;
034    OBlockManager obm;
035    private final SignalEditFrame frame = this;
036    private Portal _portal;
037    SignalTableModel.SignalRow _sr;
038    //private final Object lock = new Object();
039
040    // UI components for Add/Edit Signal (head or mast)
041    JLabel portalLabel = new JLabel(Bundle.getMessage("AtPortalLabel"), JLabel.TRAILING);
042
043    JLabel signalMastLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("BeanNameSignalMast")));
044    JLabel signalHeadLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("BeanNameSignalHead")));
045    JLabel fromBlockLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("FromBlockName")));
046    JLabel fromBlock = new JLabel();
047    JLabel toBlockLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("ToBlockName")));
048    JLabel toBlock = new JLabel();
049    JLabel mastName = new JLabel();
050    JLabel headName = new JLabel();
051    String[] p0 = {""};
052    private final JComboBox<String> portalComboBox = new JComboBox<>(p0);
053    private final NamedBeanComboBox<SignalMast> sigMastComboBox = new NamedBeanComboBox<>(InstanceManager.getDefault(SignalMastManager.class),
054            null, NamedBean.DisplayOptions.DISPLAYNAME);
055    private final NamedBeanComboBox<SignalHead> sigHeadComboBox = new NamedBeanComboBox<>(InstanceManager.getDefault(SignalHeadManager.class),
056            null, NamedBean.DisplayOptions.DISPLAYNAME);
057//    private final NamedBeanComboBox<OBlock> fromBlockComboBox = new NamedBeanComboBox<>(InstanceManager.getDefault(OBlockManager.class),
058//            null, NamedBean.DisplayOptions.DISPLAYNAME);
059//    private final NamedBeanComboBox<OBlock> toBlockComboBox = new NamedBeanComboBox<>(InstanceManager.getDefault(OBlockManager.class),
060//            null, NamedBean.DisplayOptions.DISPLAYNAME);
061    private final JButton flipButton = new JButton(Bundle.getMessage("ButtonFlipBlocks"));
062    // the following 3 items copied from beanedit, place in separate static method?
063    JSpinner lengthSpinner = new JSpinner(); // 2 digit decimal format field, initialized later as instance
064    JRadioButton inch = new JRadioButton(Bundle.getMessage("LengthInches"));
065    JRadioButton cm = new JRadioButton(Bundle.getMessage("LengthCentimeters"));
066    JLabel statusBar = new JLabel(Bundle.getMessage("AddXStatusInitial1",
067            (Bundle.getMessage("BeanNameSignalMast") + "/" + Bundle.getMessage("BeanNameSignalHead")),
068            Bundle.getMessage("ButtonOK")));
069
070    private boolean _newSignal;
071
072    @SuppressWarnings("OverridableMethodCallInConstructor")
073    public SignalEditFrame(@Nonnull String title,
074                           @CheckForNull NamedBean signal,
075                           @CheckForNull SignalTableModel.SignalRow sr,
076                           @CheckForNull SignalTableModel model) {
077        super(title, true, true);
078        this.model = model;
079        this.signal = signal;
080        if (signal == null) {
081            _newSignal = true;
082        }
083        log.debug("SR == {}", (sr == null ? "null" : "not null"));
084        obm = InstanceManager.getDefault(OBlockManager.class);
085        pm = InstanceManager.getDefault(PortalManager.class);
086        for (Portal pi : pm.getPortalSet()) {
087            portalComboBox.addItem(pi.getName());
088        }
089        layoutFrame();
090        if (sr != null) {
091            _sr = sr;
092            _portal = sr.getPortal();
093            populateFrame(_sr);
094        } else {
095            resetFrame();
096        }
097        addCloseListener(this);
098    }
099
100    public void layoutFrame() {
101        frame.addHelpMenu("package.jmri.jmrit.beantable.OBlockTable", true);
102        frame.getContentPane().setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.PAGE_AXIS));
103        frame.setSize(300, 200);
104        main.setLayout(new BoxLayout(main, BoxLayout.PAGE_AXIS));
105
106        JPanel configGrid = new JPanel();
107        GridLayout layout = new GridLayout(4, 2, 10, 0); // (int rows, int cols, int hgap, int vgap)
108        configGrid.setLayout(layout);
109
110        JPanel p = new JPanel();
111        p.setLayout(new BoxLayout(p, BoxLayout.PAGE_AXIS));
112
113        // row 1
114        JPanel p1 = new JPanel();
115        p1.add(signalMastLabel);
116        p1.add(sigMastComboBox);
117        sigMastComboBox.setAllowNull(true);
118        p1.add(mastName);
119        configGrid.add(p1);
120
121        p1 = new JPanel();
122        p1.add(signalHeadLabel);
123        p1.add(sigHeadComboBox);
124        sigHeadComboBox.setAllowNull(true);
125        p1.add(headName);
126        configGrid.add(p1);
127        sigMastComboBox.addActionListener(e -> {
128            if ((sigMastComboBox.getSelectedIndex() > 0) && (sigHeadComboBox.getItemCount() > 0)) {
129                sigHeadComboBox.setSelectedIndex(0); // either one
130                model.checkDuplicateSignal(sigMastComboBox.getSelectedItem());
131            }
132        });
133        sigHeadComboBox.addActionListener(e -> {
134            if ((sigHeadComboBox.getSelectedIndex() > 0) && (sigMastComboBox.getItemCount() > 0)) {
135                sigMastComboBox.setSelectedIndex(0); // either one
136                model.checkDuplicateSignal(sigHeadComboBox.getSelectedItem());
137            }
138        });
139
140        // row 2
141        p1 = new JPanel();
142        p1.add(portalLabel);
143        p1.add(portalComboBox); // combo has a blank first item
144        portalComboBox.addActionListener(e -> {
145            if (portalComboBox.getSelectedIndex() > 0) {
146                fromBlock.setText(pm.getPortal((String) portalComboBox.getSelectedItem()).getFromBlockName());
147                toBlock.setText(pm.getPortal((String) portalComboBox.getSelectedItem()).getToBlockName());
148            }
149        });
150        configGrid.add(p1);
151        flipButton.addActionListener(e -> {
152            String left = fromBlock.getText();
153            fromBlock.setText(toBlock.getText());
154            toBlock.setText(left);
155        });
156        p1 = new JPanel();
157        p1.add(flipButton);
158        flipButton.setToolTipText(Bundle.getMessage("FlipToolTip"));
159        configGrid.add(p1);
160
161        // row 3
162        p1 = new JPanel();
163        p1.add(fromBlockLabel);
164        p1.add(fromBlock);
165//        fromBlockComboBox.setAllowNull(true);
166//        fromBlockComboBox.addActionListener(e -> {
167//            if (fromBlockComboBox.getSelectedIndex() == toBlockComboBox.getSelectedIndex()) {
168//                toBlockComboBox.setSelectedIndex(0);
169//            }
170//        });
171        configGrid.add(p1);
172
173        p1 = new JPanel();
174        p1.add(toBlockLabel);
175        p1.add(toBlock);
176//        toBlockComboBox.setAllowNull(true);
177//        toBlockComboBox.addActionListener(e -> {
178//            if (fromBlockComboBox.getSelectedIndex() == toBlockComboBox.getSelectedIndex()) {
179//                fromBlockComboBox.setSelectedIndex(0);
180//            }
181//        });
182        configGrid.add(p1);
183
184        // row 4
185        // copied from beanedit, also in BlockPathEditFrame
186        p1 = new JPanel();
187        p1.add(new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("Offset"))));
188        lengthSpinner.setModel(
189                new SpinnerNumberModel(Float.valueOf(0f), Float.valueOf(-2000f), Float.valueOf(2000f), Float.valueOf(0.01f)));
190        lengthSpinner.setEditor(new JSpinner.NumberEditor(lengthSpinner, "###0.00"));
191        lengthSpinner.setPreferredSize(new JTextField(8).getPreferredSize());
192        lengthSpinner.setValue(0f); // reset from possible previous use
193        lengthSpinner.setToolTipText(Bundle.getMessage("OffsetToolTip"));
194        p1.add(lengthSpinner);
195        configGrid.add(p1);
196
197        ButtonGroup bg = new ButtonGroup();
198        bg.add(inch);
199        bg.add(cm);
200
201        p1 = new JPanel();
202        p1.add(inch);
203        p1.add(cm);
204        p1.setLayout(new BoxLayout(p1, BoxLayout.PAGE_AXIS));
205        inch.setSelected(true);
206        inch.addActionListener(e -> {
207            cm.setSelected(!inch.isSelected());
208            updateLength();
209        });
210        cm.addActionListener(e -> {
211            inch.setSelected(!cm.isSelected());
212            updateLength();
213        });
214        configGrid.add(p1);
215        p.add(configGrid);
216
217        p.add(Box.createHorizontalGlue());
218
219        JPanel p2 = new JPanel();
220        statusBar.setFont(statusBar.getFont().deriveFont(0.9f * signalMastLabel.getFont().getSize())); // a bit smaller
221        statusBar.setForeground(Color.gray);
222        p2.add(statusBar);
223        p.add(p2);
224
225        p2 = new JPanel();
226        p2.setLayout(new BoxLayout(p2, BoxLayout.LINE_AXIS));
227        JButton cancel;
228        p2.add(cancel = new JButton(Bundle.getMessage("ButtonCancel")));
229        cancel.addActionListener((ActionEvent e) -> closeFrame());
230        JButton ok;
231        p2.add(ok = new JButton(Bundle.getMessage("ButtonOK")));
232        ok.addActionListener(this::applyPressed);
233        p.add(p2);
234
235        //main.add(p);
236        frame.getContentPane().add(p);
237        
238        frame.setEscapeKeyClosesWindow(true);
239        frame.getRootPane().setDefaultButton(ok);
240        
241        //frame.add(scroll);
242        frame.pack();
243    }
244
245    /**
246     * Reset the Edit Signal frame with default values.
247     */
248    public void resetFrame() {
249        if (sigMastComboBox.getItemCount() > 0) {
250            sigMastComboBox.setSelectedIndex(0);
251        }
252        if (sigHeadComboBox.getItemCount() > 0) {
253            sigHeadComboBox.setSelectedIndex(0);
254        }
255        if (portalComboBox.getItemCount() > 0) {
256            portalComboBox.setSelectedIndex(0);
257        }
258        lengthSpinner.setValue(0f);
259        // reset statusBar text
260        if ((sigMastComboBox.getItemCount() == 0) && (sigHeadComboBox.getItemCount() == 0)) {
261            status(Bundle.getMessage("NoSignalWarning"), true);
262        } else if (portalComboBox.getItemCount() > 1) {
263            status(Bundle.getMessage("AddXStatusInitial1",
264                    (Bundle.getMessage("BeanNameSignalMast")+"/"+Bundle.getMessage("BeanNameSignalHead")),
265                    Bundle.getMessage("ButtonOK")), false); // I18N to include original button name in help string
266        } else {
267            status(Bundle.getMessage("NoSignalPortal"), true);
268        }
269        mastName.setVisible(false);
270        headName.setVisible(false);
271        sigMastComboBox.setVisible(true);
272        sigHeadComboBox.setVisible(true);
273        frame.pack();
274    }
275
276    /**
277     * Populate the Edit Signal frame with current values from a SignalRow in the SignalTable.
278     *
279     * @param sr existing SignalRow to copy the attributes from
280     */
281    public void populateFrame(SignalTableModel.SignalRow sr) {
282        if (sr == null) {
283            throw new IllegalArgumentException("Null Signal object");
284        }
285        status(Bundle.getMessage("AddXStatusInitial3", sr.getSignal().getDisplayName(),
286                Bundle.getMessage("ButtonOK")), false);
287        fromBlock.setText(sr.getFromBlock().getDisplayName());
288        toBlock.setText(sr.getToBlock().getDisplayName());
289        if (signal instanceof SignalMast) {
290            mastName.setText(sr.getSignal().getDisplayName());
291            headName.setText("-");
292            //sigMastComboBox.setSelectedItemByName(sr.getSignal().getDisplayName()); // combo hidden for Edits
293        } else if (signal instanceof SignalHead) {
294            mastName.setText("-");
295            headName.setText(sr.getSignal().getDisplayName());
296            //sigHeadComboBox.setSelectedItemByName(sr.getSignal().getDisplayName()); // combo hidden for Edits
297        }
298        portalComboBox.setSelectedItem(_portal.getName());
299        cm.setSelected(sr._isMetric); // before filling in value in spinner prevent recalc
300        if (sr.isMetric()) {
301            lengthSpinner.setValue(sr.getLength()/10);
302        } else {
303            lengthSpinner.setValue(sr.getLength()/25.4f);
304        }
305        mastName.setVisible(true);
306        headName.setVisible(true);
307        sigMastComboBox.setVisible(false);
308        sigHeadComboBox.setVisible(false);
309        frame.pack();
310        _newSignal = false;
311    }
312
313    private void applyPressed(ActionEvent e) {
314        if (_newSignal) { // can't change an existing mast, easy to delete and recreate
315            if (sigMastComboBox.getSelectedIndex() > 0) {
316                signal = sigMastComboBox.getSelectedItem();
317            } else if (sigHeadComboBox.getSelectedIndex() > 0) {
318                signal = sigHeadComboBox.getSelectedItem();
319            } else {
320                status(Bundle.getMessage("WarnNoSignal"), true);
321                return;
322            }
323            String msg = model.checkDuplicateSignal(signal);
324            if (msg != null) {
325                status(msg, true);
326                return;
327            }
328        }
329        _portal = pm.getPortal((String) portalComboBox.getSelectedItem());
330        if (_portal == null || portalComboBox.getSelectedIndex() < 1) {
331            status(Bundle.getMessage("WarnNoPortal"), true);
332            return;
333        }
334        if (!_newSignal) {
335            model.deleteSignal(_sr);    // delete old in Portal if it was set
336            _sr.setPortal(_portal);
337        }
338        // fetch physical details
339        float length;
340        if (cm.isSelected()) {
341            length = (float) lengthSpinner.getValue()*10.0f;
342        } else {
343            length = (float) lengthSpinner.getValue()*25.4f;
344        }
345
346        if (_portal.setProtectSignal(signal, length, obm.getOBlock(toBlock.getText()))) {
347            if ((fromBlock.getText() == null) && (toBlock.getText() != null)) { // could be read from old panels?
348                _portal.setFromBlock(_portal.getOpposingBlock(obm.getOBlock(Objects.requireNonNull(toBlock.getText()))), true);
349            }
350        }
351        // update Metric choice in ProtectedBlock
352        if (toBlock.getText() != null) {
353            Objects.requireNonNull(obm.getOBlock(toBlock.getText())).setMetricUnits(cm.isSelected());
354        }
355        // Notify changes
356        model.fireTableDataChanged();
357
358        closeFrame();
359    }
360
361    protected void closeFrame(){
362        // remind to save, if Turnout was created or edited
363        //        if (isDirty) {
364        //            showReminderMessage();
365        //            isDirty = false;
366        //        }
367        // hide frame
368        setVisible(false);
369
370        model.setEditMode(false);
371        log.debug("SignalEditFrame.closeFrame signalEdit=False");
372        frame.dispose();
373    }
374
375    // copied from beanedit, also in BlockPathEditFrame
376    private void updateLength() {
377        float len = (float) lengthSpinner.getValue();
378        if (inch.isSelected()) {
379            lengthSpinner.setValue(len/2.54f);
380        } else {
381            lengthSpinner.setValue(len*2.54f);
382        }
383    }
384
385    void status(String message, boolean warn){
386        statusBar.setText(message);
387        statusBar.setForeground(warn ? Color.red : Color.gray);
388    }
389
390    // listen for frame closing
391    void addCloseListener(JmriJFrame frame) {
392        frame.addWindowListener(new java.awt.event.WindowAdapter() {
393            @Override
394            public void windowClosing(java.awt.event.WindowEvent e) {
395                model.setEditMode(false);
396                log.debug("SignalEditFrame.closeFrame signalEdit=False");
397                frame.dispose();
398            }
399        });
400    }
401
402    private static final Logger log = LoggerFactory.getLogger(SignalEditFrame.class);
403
404}