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