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