001package jmri.jmrit.display.layoutEditor.LayoutEditorDialogs;
002
003import java.awt.*;
004import java.awt.event.*;
005import java.util.ArrayList;
006import java.util.List;
007
008import javax.annotation.Nonnull;
009import javax.swing.*;
010import javax.swing.border.EtchedBorder;
011import javax.swing.border.TitledBorder;
012
013import jmri.NamedBean.DisplayOptions;
014import jmri.*;
015import jmri.jmrit.display.layoutEditor.*;
016import jmri.jmrit.display.Positionable;
017import jmri.jmrit.display.SignalMastIcon;
018import jmri.swing.NamedBeanComboBox;
019import jmri.util.JmriJFrame;
020import jmri.util.swing.JmriJOptionPane;
021
022/**
023 * MVC Editor component for LayoutTraverser objects.
024 *
025 * @author Bob Jacobsen  Copyright (c) 2020
026 * @author Dave Sand Copyright (c) 2024
027 */
028public class LayoutTraverserEditor extends LayoutTrackEditor {
029
030    /**
031     * constructor method.
032     * @param layoutEditor main layout editor.
033     */
034    public LayoutTraverserEditor(@Nonnull LayoutEditor layoutEditor) {
035        super(layoutEditor);
036    }
037
038    /*==============*\
039    | Edit Traverser |
040    \*==============*/
041    // variables for Edit Traverser pane
042    private LayoutTraverser layoutTraverser = null;
043    private LayoutTraverserView layoutTraverserView = null;
044
045    private JmriJFrame editLayoutTraverserFrame = null;
046    private final JTextField deckLengthTextField = new JTextField(8);
047    private final JTextField deckWidthTextField = new JTextField(8);
048    private final JComboBox<String> orientationComboBox = new JComboBox<>();
049    private final JTextField slotOffsetTextField = new JTextField(8);
050
051    private final NamedBeanComboBox<Block> editLayoutTraverserBlockNameComboBox = new NamedBeanComboBox<>(
052             InstanceManager.getDefault(BlockManager.class), null, DisplayOptions.DISPLAYNAME);
053    private JButton editLayoutTraverserSegmentEditBlockButton;
054    private final JComboBox<String> editLayoutTraverserMainlineComboBox = new JComboBox<>();
055
056    private JPanel editLayoutTraverserSlotPanel;
057    private JButton editLayoutTraverserAddSlotButton;
058    private JCheckBox editLayoutTraverserDccControlledCheckBox;
059    private JCheckBox editLayoutTraverserUseSignalMastsCheckBox;
060    private JPanel signalMastAssignmentsPanel;
061    private NamedBeanComboBox<SignalMast> exitMastComboBox;
062    private NamedBeanComboBox<SignalMast> bufferMastComboBox;
063
064    private final List<NamedBeanComboBox<SignalMast>> approachMastComboBoxes = new ArrayList<>();
065    private boolean editLayoutTraverserOpen = false;
066    private boolean editLayoutTraverserNeedsRedraw = false;
067
068    private JRadioButton doNotPlaceIcons;
069    private JRadioButton placeIconsLeft;
070    private JRadioButton placeIconsRight;
071
072    private final List<Turnout> traverserTurnouts = new ArrayList<>();
073
074    /**
075     * Edit a Traverser.
076     */
077    @Override
078    public void editLayoutTrack(@Nonnull LayoutTrackView layoutTrackView) {
079        if ( layoutTrackView instanceof LayoutTraverserView ) {
080            this.layoutTraverserView = (LayoutTraverserView) layoutTrackView;
081            this.layoutTraverser = this.layoutTraverserView.getTraverser();
082        } else {
083            log.error("editLayoutTrack called with wrong type {}", layoutTrackView, new Exception("traceback"));
084        }
085        sensorList.clear();
086
087        if (editLayoutTraverserOpen) {
088            editLayoutTraverserFrame.setVisible(true);
089            return;
090        }
091        editLayoutTraverserFrame = new JmriJFrame(Bundle.getMessage("EditTraverser"), false, true);  // NOI18N
092        editLayoutTraverserFrame.addHelpMenu("package.jmri.jmrit.display.EditTraverser", true);  // NOI18N
093        editLayoutTraverserFrame.setLocation(50, 30);
094
095        Container contentPane = editLayoutTraverserFrame.getContentPane();
096        JPanel headerPane = new JPanel();
097        JPanel footerPane = new JPanel();
098        headerPane.setLayout(new BoxLayout(headerPane, BoxLayout.Y_AXIS));
099        footerPane.setLayout(new BoxLayout(footerPane, BoxLayout.Y_AXIS));
100        contentPane.setLayout(new BorderLayout());
101        contentPane.add(headerPane, BorderLayout.NORTH);
102        contentPane.add(footerPane, BorderLayout.SOUTH);
103
104        // Geometry Panel
105        JPanel geometryPanel = new JPanel();
106        geometryPanel.setLayout(new FlowLayout());
107        geometryPanel.add(new JLabel(Bundle.getMessage("Length")));
108        deckLengthTextField.setEnabled(false);
109        geometryPanel.add(deckLengthTextField);
110        geometryPanel.add(new JLabel(Bundle.getMessage("Width")));
111        geometryPanel.add(deckWidthTextField);
112        geometryPanel.add(new JLabel(Bundle.getMessage("Orientation")));
113        orientationComboBox.removeAllItems();
114        orientationComboBox.addItem(Bundle.getMessage("Horizontal"));
115        orientationComboBox.addItem(Bundle.getMessage("Vertical"));
116        geometryPanel.add(orientationComboBox);
117        headerPane.add(geometryPanel);
118
119        // Slot Panel
120        JPanel slotPanel = new JPanel();
121        slotPanel.setLayout(new FlowLayout());
122        slotPanel.add(new JLabel(Bundle.getMessage("SlotOffset")));
123        slotPanel.add(slotOffsetTextField);
124        editLayoutTraverserAddSlotButton = new JButton(Bundle.getMessage("AddSlotPair"));
125        slotPanel.add(editLayoutTraverserAddSlotButton);
126        headerPane.add(slotPanel);
127
128        // Block Name Panel
129        JPanel blockPanel = new JPanel();
130        blockPanel.setLayout(new FlowLayout());
131        blockPanel.add(new JLabel(Bundle.getMessage("BlockID")));
132        LayoutEditor.setupComboBox(editLayoutTraverserBlockNameComboBox, false, true, true);
133        blockPanel.add(editLayoutTraverserBlockNameComboBox);
134        editLayoutTraverserSegmentEditBlockButton = new JButton(Bundle.getMessage("EditBlock", ""));
135        blockPanel.add(editLayoutTraverserSegmentEditBlockButton);
136
137        boolean mainlineSaved = layoutTraverser.isMainline();
138        editLayoutTraverserMainlineComboBox.removeAllItems();
139        editLayoutTraverserMainlineComboBox.addItem(Bundle.getMessage("Mainline"));
140        editLayoutTraverserMainlineComboBox.addItem(Bundle.getMessage("NotMainline"));
141        layoutTraverser.setMainline(mainlineSaved);  // restore the current state
142
143        blockPanel.add(editLayoutTraverserMainlineComboBox);
144        headerPane.add(blockPanel);
145
146        // Controls Panel
147        JPanel controlsPanel = new JPanel();
148        controlsPanel.setLayout(new FlowLayout());
149        editLayoutTraverserDccControlledCheckBox = new JCheckBox(Bundle.getMessage("TraverserDCCControlled"));
150        controlsPanel.add(editLayoutTraverserDccControlledCheckBox);
151        editLayoutTraverserUseSignalMastsCheckBox = new JCheckBox(Bundle.getMessage("TraverserUseSignalMasts"));
152        controlsPanel.add(editLayoutTraverserUseSignalMastsCheckBox);
153        headerPane.add(controlsPanel);
154
155        // set up Done and Cancel buttons
156        JPanel donePanel = new JPanel();
157        donePanel.setLayout(new FlowLayout());
158        addDoneCancelButtons(donePanel, editLayoutTraverserFrame.getRootPane(),
159                this::editLayoutTraverserDonePressed, this::traverserEditCancelPressed);
160        footerPane.add(donePanel);
161
162        editLayoutTraverserSlotPanel = new JPanel();
163        editLayoutTraverserSlotPanel.setLayout(new BoxLayout(editLayoutTraverserSlotPanel, BoxLayout.Y_AXIS));
164        JScrollPane slotScrollPane = new JScrollPane(editLayoutTraverserSlotPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
165        contentPane.add(slotScrollPane, BorderLayout.CENTER);
166
167        // Set initial values
168        deckLengthTextField.setText(String.valueOf(layoutTraverser.getDeckLength()));
169        deckWidthTextField.setText(String.valueOf(layoutTraverser.getDeckWidth()));
170        orientationComboBox.setSelectedIndex(layoutTraverser.getOrientation());
171        slotOffsetTextField.setText(String.valueOf(layoutTraverser.getSlotOffset()));
172
173        editLayoutTraverserBlockNameComboBox.setSelectedItem(layoutTraverser.getLayoutBlock() != null ? layoutTraverser.getLayoutBlock().getBlock() : null);
174        editLayoutTraverserDccControlledCheckBox.setSelected(layoutTraverser.isTurnoutControlled());
175        editLayoutTraverserUseSignalMastsCheckBox.setSelected(layoutTraverser.isDispatcherManaged());
176        if (layoutTraverser.isMainline()) {
177            editLayoutTraverserMainlineComboBox.setSelectedIndex(0);
178        } else {
179            editLayoutTraverserMainlineComboBox.setSelectedIndex(1);
180        }
181        for (ActionListener al : editLayoutTraverserMainlineComboBox.getActionListeners()) {
182            editLayoutTraverserMainlineComboBox.removeActionListener(al);
183        }
184        editLayoutTraverserMainlineComboBox.addActionListener((java.awt.event.ActionEvent e) -> {
185            if (layoutTraverser != null) {
186                layoutTraverser.setMainline(editLayoutTraverserMainlineComboBox.getSelectedIndex() == 0);
187            }
188        });
189
190        // Add listeners
191        editLayoutTraverserAddSlotButton.addActionListener(this::addTrackPairPressed);
192        editLayoutTraverserSegmentEditBlockButton.addActionListener(this::editLayoutTraverserEditBlockPressed);
193        slotOffsetTextField.addFocusListener(new FocusAdapter() {
194            @Override
195            public void focusLost(FocusEvent e) {
196                try {
197                    double offset = Double.parseDouble(slotOffsetTextField.getText());
198                    if (!jmri.util.MathUtil.equals(layoutTraverser.getSlotOffset(), offset)) {
199                        layoutTraverser.setSlotOffset(offset);
200                        updateSlotPanel();
201                        layoutEditor.redrawPanel();
202                        layoutEditor.setDirty();
203                    }
204                } catch (NumberFormatException ex) {
205                    // ignore invalid input
206                }
207            }
208        });
209        for (ActionListener al : orientationComboBox.getActionListeners()) {
210            orientationComboBox.removeActionListener(al);
211        }
212        orientationComboBox.addActionListener(e -> {
213            layoutTraverser.setOrientation(orientationComboBox.getSelectedIndex());
214            updateSlotPanel();
215            layoutEditor.redrawPanel();
216            layoutEditor.setDirty();
217        });
218        editLayoutTraverserDccControlledCheckBox.addActionListener(e -> {
219            layoutTraverser.setTurnoutControlled(editLayoutTraverserDccControlledCheckBox.isSelected());
220            updateSlotPanel();
221        });
222        editLayoutTraverserUseSignalMastsCheckBox.addActionListener(e -> {
223            layoutTraverser.setDispatcherManaged(editLayoutTraverserUseSignalMastsCheckBox.isSelected());
224            updateSlotPanel();
225        });
226
227        updateSlotPanel();
228        editLayoutTraverserFrame.addWindowListener(new java.awt.event.WindowAdapter() {
229            @Override
230            public void windowClosing(java.awt.event.WindowEvent e) {
231                traverserEditCancelPressed(null);
232            }
233        });
234        editLayoutTraverserFrame.pack();
235        editLayoutTraverserFrame.setVisible(true);
236        editLayoutTraverserOpen = true;
237    }
238
239    private void addTrackPairPressed(ActionEvent e) {
240        layoutTraverser.addSlotPair();
241        updateSlotPanel();
242        layoutEditor.redrawPanel();
243        layoutEditor.setDirty();
244    }
245
246    @InvokeOnGuiThread
247    private void editLayoutTraverserEditBlockPressed(ActionEvent a) {
248         String newName = editLayoutTraverserBlockNameComboBox.getSelectedItemDisplayName();
249         if (newName == null) {
250             newName = "";
251         }
252         if ((layoutTraverser.getBlockName().isEmpty())
253                 || !layoutTraverser.getBlockName().equals(newName)) {
254             layoutTraverser.setLayoutBlock(layoutEditor.provideLayoutBlock(newName));
255             editLayoutTraverserNeedsRedraw = true;
256         }
257         LayoutBlock blockToEdit = layoutTraverser.getLayoutBlock();
258         if (blockToEdit == null) {
259             JmriJOptionPane.showMessageDialog(editLayoutTraverserFrame,
260                     Bundle.getMessage("Error1"), // NOI18N
261                     Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
262             return;
263         }
264         blockToEdit.editLayoutBlock(editLayoutTraverserFrame);
265         layoutEditor.setDirty();
266         editLayoutTraverserNeedsRedraw = true;
267     }
268
269    private void updateSlotPanel() {
270        deckLengthTextField.setText(String.valueOf(layoutTraverser.getDeckLength()));
271        deckWidthTextField.setText(String.valueOf(layoutTraverser.getDeckWidth()));
272
273        editLayoutTraverserSlotPanel.removeAll();
274
275        JPanel turnoutAssignmentsPanel = new JPanel();
276        turnoutAssignmentsPanel.setLayout(new BoxLayout(turnoutAssignmentsPanel, BoxLayout.Y_AXIS));
277        turnoutAssignmentsPanel.setBorder(new TitledBorder(new EtchedBorder(), Bundle.getMessage("TurnoutAssignments")));
278        traverserTurnouts.clear();
279        layoutTraverser.getSlotList().forEach(rt -> traverserTurnouts.add(rt.getTurnout()));
280        for (int i = 0; i < layoutTraverser.getNumberSlots() / 2; i++) {
281            turnoutAssignmentsPanel.add(new TraverserPairPanel(i));
282        }
283        editLayoutTraverserSlotPanel.add(turnoutAssignmentsPanel);
284
285        signalMastAssignmentsPanel = new JPanel();
286        approachMastComboBoxes.clear();
287
288        if (layoutTraverser.isDispatcherManaged()) {
289            signalMastAssignmentsPanel.setLayout(new BoxLayout(signalMastAssignmentsPanel, BoxLayout.Y_AXIS));
290            signalMastAssignmentsPanel.setBorder(new TitledBorder(new EtchedBorder(), Bundle.getMessage("TraverserSignalMastAssignmentsTitle")));
291
292            for (int i = 0; i < layoutTraverser.getNumberSlots() / 2; i++) {
293                JPanel p = new JPanel(new GridBagLayout());
294                p.setBorder(new TitledBorder(new EtchedBorder(), Bundle.getMessage("SlotPair") + " " + (i + 1)));
295                GridBagConstraints c = new GridBagConstraints();
296                c.gridx = 0;
297                c.gridy = 0;
298                c.anchor = GridBagConstraints.LINE_START;
299                c.insets = new Insets(2, 2, 2, 2);
300
301                JLabel labelA = new JLabel();
302                JLabel labelB = new JLabel();
303                if (layoutTraverser.getOrientation() == LayoutTraverser.HORIZONTAL) {
304                    labelA.setText(Bundle.getMessage("MakeLabel", Bundle.getMessage("ApproachMastSlotLeft")));
305                    labelB.setText(Bundle.getMessage("MakeLabel", Bundle.getMessage("ApproachMastSlotRight")));
306                } else {
307                    labelA.setText(Bundle.getMessage("MakeLabel", Bundle.getMessage("ApproachMastSlotUp")));
308                    labelB.setText(Bundle.getMessage("MakeLabel", Bundle.getMessage("ApproachMastSlotDown")));
309                }
310                p.add(labelA, c);
311
312                c.gridy = 1;
313                p.add(labelB, c);
314
315                c.gridx = 1;
316                c.gridy = 0;
317                c.insets = new Insets(2, 5, 2, 2);
318                NamedBeanComboBox<SignalMast> comboA = new NamedBeanComboBox<>(InstanceManager.getDefault(SignalMastManager.class), layoutTraverser.getSlotList().get(i * 2).getApproachMast(), DisplayOptions.DISPLAYNAME);
319                LayoutEditor.setupComboBox(comboA, false, true, true);
320                comboA.setEditable(false);
321                comboA.setAllowNull(true);
322                p.add(comboA, c);
323                approachMastComboBoxes.add(comboA);
324
325                c.gridy = 1;
326                NamedBeanComboBox<SignalMast> comboB = new NamedBeanComboBox<>(InstanceManager.getDefault(SignalMastManager.class), layoutTraverser.getSlotList().get(i * 2 + 1).getApproachMast(), DisplayOptions.DISPLAYNAME);
327                LayoutEditor.setupComboBox(comboB, false, true, true);
328                comboB.setEditable(false);
329                comboB.setAllowNull(true);
330                p.add(comboB, c);
331                approachMastComboBoxes.add(comboB);
332
333                signalMastAssignmentsPanel.add(p);
334            }
335
336            if (!approachMastComboBoxes.isEmpty()) {
337                signalMastAssignmentsPanel.add(new JSeparator());
338            }
339
340            JPanel placementPanel = new JPanel();
341            placementPanel.setLayout(new BoxLayout(placementPanel, BoxLayout.Y_AXIS)); // NOI18N
342            placementPanel.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("TraverserAddMastIconsTitle")));
343
344            doNotPlaceIcons = new JRadioButton(Bundle.getMessage("DoNotPlace")); // NOI18N
345            placeIconsLeft = new JRadioButton(Bundle.getMessage("LeftHandSide")); // NOI18N
346            placeIconsRight = new JRadioButton(Bundle.getMessage("RightHandSide")); // NOI18N
347            ButtonGroup bg = new ButtonGroup();
348            bg.add(doNotPlaceIcons);
349            bg.add(placeIconsLeft);
350            bg.add(placeIconsRight);
351            switch (layoutTraverser.getSignalIconPlacement()) {
352                case 1:
353                    placeIconsLeft.setSelected(true);
354                    break;
355                case 2:
356                    placeIconsRight.setSelected(true);
357                    break;
358                default: doNotPlaceIcons.setSelected(true);
359            }
360
361            JPanel radioPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
362            radioPanel.add(doNotPlaceIcons);
363            radioPanel.add(placeIconsLeft);
364            radioPanel.add(placeIconsRight);
365            placementPanel.add(radioPanel);
366            signalMastAssignmentsPanel.add(placementPanel);
367
368            signalMastAssignmentsPanel.add(new JSeparator());
369
370            JPanel mastPanel = new JPanel(new GridBagLayout());
371            GridBagConstraints c = new GridBagConstraints();
372            c.gridx = 0;
373            c.gridy = 0;
374            c.anchor = GridBagConstraints.LINE_START;
375            c.insets = new Insets(2, 2, 2, 2); // Default insets
376            mastPanel.add(new JLabel(Bundle.getMessage("TraverserExitMastLabel")), c);
377            c.gridx = 1;
378            c.insets = new Insets(2, 5, 2, 2); // Add left padding
379            exitMastComboBox = new NamedBeanComboBox<>(InstanceManager.getDefault(SignalMastManager.class), layoutTraverser.getExitSignalMast(), DisplayOptions.DISPLAYNAME);
380            exitMastComboBox.setAllowNull(true);
381            mastPanel.add(exitMastComboBox, c);
382
383            c.gridx = 0;
384            c.gridy = 1;
385            c.insets = new Insets(2, 2, 2, 2); // Reset for label
386            mastPanel.add(new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("TraverserBufferMastLabel"))), c);
387            c.gridx = 1;
388            c.insets = new Insets(2, 5, 2, 2); // Add left padding
389            bufferMastComboBox = new NamedBeanComboBox<>(InstanceManager.getDefault(SignalMastManager.class), layoutTraverser.getBufferMast(), DisplayOptions.DISPLAYNAME);
390            bufferMastComboBox.setAllowNull(true);
391            mastPanel.add(bufferMastComboBox, c);
392
393            signalMastAssignmentsPanel.add(mastPanel);
394            editLayoutTraverserSlotPanel.add(signalMastAssignmentsPanel);
395        }
396
397        editLayoutTraverserSlotPanel.revalidate();
398        editLayoutTraverserSlotPanel.repaint();
399        editLayoutTraverserFrame.pack();
400    }
401
402    private void saveSlotPanelDetail() {
403        for (Component mainComp : editLayoutTraverserSlotPanel.getComponents()) {
404            if (mainComp instanceof JPanel && ((JPanel)mainComp).getBorder() instanceof TitledBorder) {
405                TitledBorder border = (TitledBorder)((JPanel)mainComp).getBorder();
406                if (Bundle.getMessage("TurnoutAssignments").equals(border.getTitle())) {
407                    JPanel turnoutAssignmentsPanel = (JPanel) mainComp;
408                    for (Component tppComp : turnoutAssignmentsPanel.getComponents()) {
409                        if (tppComp instanceof TraverserPairPanel) {
410                            TraverserPairPanel tpp = (TraverserPairPanel) tppComp;
411                            tpp.updateDetails();
412                        }
413                    }
414                }
415            }
416        }
417        if (layoutTraverser.isDispatcherManaged()) {
418            layoutTraverser.setExitSignalMast(exitMastComboBox.getSelectedItemDisplayName());
419            layoutTraverser.setBufferSignalMast(bufferMastComboBox.getSelectedItemDisplayName());
420            int placement = 0;
421            if (placeIconsLeft.isSelected()) {
422                placement = 1;
423            } else if (placeIconsRight.isSelected()) {
424                placement = 2;
425            }
426            layoutTraverser.setSignalIconPlacement(placement);
427
428            for (int i = 0; i < approachMastComboBoxes.size(); i++) {
429                SignalMast newMast = approachMastComboBoxes.get(i).getSelectedItem();
430                layoutTraverser.getSlotList().get(i).setApproachMast( (newMast != null) ? newMast.getSystemName() : null );
431            }
432        }
433    }
434
435    private void editLayoutTraverserDonePressed(ActionEvent a) {
436        layoutTraverser.setOrientation(orientationComboBox.getSelectedIndex());
437        try {
438            double width = Double.parseDouble(deckWidthTextField.getText());
439            if (!jmri.util.MathUtil.equals(layoutTraverser.getDeckWidth(), width)) {
440                layoutTraverser.setDeckWidth(width);
441                editLayoutTraverserNeedsRedraw = true;
442            }
443        } catch (NumberFormatException ex) {
444            JmriJOptionPane.showMessageDialog(editLayoutTraverserFrame, Bundle.getMessage("EntryError") + ": "
445                    + ex, Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
446            return;
447        }
448        try {
449            double offset = Double.parseDouble(slotOffsetTextField.getText());
450            if (!jmri.util.MathUtil.equals(layoutTraverser.getSlotOffset(), offset)) {
451                layoutTraverser.setSlotOffset(offset);
452                editLayoutTraverserNeedsRedraw = true;
453            }
454        } catch (NumberFormatException ex) {
455            JmriJOptionPane.showMessageDialog(editLayoutTraverserFrame, Bundle.getMessage("EntryError") + ": "
456                    + ex, Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
457            return;
458        }
459
460        String newName = editLayoutTraverserBlockNameComboBox.getSelectedItemDisplayName();
461        if (newName == null) {
462            newName = "";
463        }
464        if ((layoutTraverser.getBlockName().isEmpty()) || !layoutTraverser.getBlockName().equals(newName)) {
465            layoutTraverser.setLayoutBlock(layoutEditor.provideLayoutBlock(newName));
466            editLayoutTraverserNeedsRedraw = true;
467        }
468        layoutTraverser.setMainline(editLayoutTraverserMainlineComboBox.getSelectedIndex() == 0);
469
470        if (layoutTraverser.isDispatcherManaged()) {
471            // Always remove any existing icons for this traverser's approach masts first.
472            // This ensures that moving icons from right-to-left works correctly.
473            List<SignalMastIcon> iconsToRemove = new ArrayList<>();
474            for (Positionable p : layoutEditor.getContents()) {
475                if (p instanceof SignalMastIcon) {
476                    SignalMastIcon icon = (SignalMastIcon) p;
477                    if (layoutTraverser.isApproachMast(icon.getSignalMast())) {
478                        iconsToRemove.add(icon);
479                    }
480                }
481            }
482            for (SignalMastIcon icon : iconsToRemove) {
483                icon.remove();
484                editLayoutTraverserNeedsRedraw = true;
485            }
486
487            // Now, if requested, place the new icons.
488            if (!doNotPlaceIcons.isSelected()) { // placeIconsLeft or placeIconsRight is selected
489                for (int i = 0; i < approachMastComboBoxes.size(); i++) {
490                    LayoutTraverser.SlotTrack slot = layoutTraverser.getSlotList().get(i);
491                    SignalMast mast = approachMastComboBoxes.get(i).getSelectedItem();
492                    if (mast != null) {
493                        if (slot.getConnect() != null) {
494                            SignalMastIcon icon = new SignalMastIcon(layoutEditor);
495                            icon.setSignalMast(mast.getDisplayName());
496                            log.debug("Placing mast for traverser slot, connected to track segment: {}", slot.getConnect().getName()); // NOI18N
497                            // Note: Using the turntable placement logic as it is suitable for this purpose.
498                            layoutEditor.getLETools().placingBlockForTurntable(icon, placeIconsRight.isSelected(),
499                                    0.0,
500                                    slot.getConnect(), layoutTraverserView.getSlotCoordsOrdered(i));
501                            editLayoutTraverserNeedsRedraw = true;
502                        }
503                    }
504                }
505            }
506        }
507
508        saveSlotPanelDetail();
509        editLayoutTraverserOpen = false;
510        editLayoutTraverserFrame.setVisible(false);
511        editLayoutTraverserFrame.dispose();
512        editLayoutTraverserFrame = null;
513        layoutEditor.redrawPanel();
514        layoutEditor.setDirty();
515    }
516
517    private void traverserEditCancelPressed(ActionEvent a) {
518        editLayoutTraverserOpen = false;
519        editLayoutTraverserFrame.setVisible(false);
520        editLayoutTraverserFrame.dispose();
521        editLayoutTraverserFrame = null;
522        if (editLayoutTraverserNeedsRedraw) {
523            layoutEditor.redrawPanel();
524            layoutEditor.setDirty();
525            editLayoutTraverserNeedsRedraw = false;
526        }
527    }
528
529    public class TraverserPairPanel extends JPanel {
530
531        private final LayoutTraverser.SlotTrack slotA;
532        private final LayoutTraverser.SlotTrack slotB;
533        private final int pairIndex;
534
535        private final JPanel turnoutDetailsPanel;
536        private final NamedBeanComboBox<Turnout> turnoutNameComboBoxA;
537        private final NamedBeanComboBox<Turnout> turnoutNameComboBoxB;
538        private final TitledBorder slotTitledBorder;
539        private final JComboBox<String> slotTurnoutStateComboBoxA;
540        private final JComboBox<String> slotTurnoutStateComboBoxB;
541        private final JLabel slotTurnoutLabelA;
542        private final JLabel slotTurnoutLabelB;
543        private final JCheckBox disabledCheckBoxA;
544        private final JCheckBox disabledCheckBoxB;
545        private final int[] slotTurnoutStateValues = new int[]{Turnout.CLOSED, Turnout.THROWN};
546
547        public TraverserPairPanel(int pairIndex) {
548            this.pairIndex = pairIndex;
549            this.slotA = layoutTraverser.getSlotList().get(pairIndex * 2);
550            this.slotB = layoutTraverser.getSlotList().get(pairIndex * 2 + 1);
551
552            JPanel top = new JPanel();
553            this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
554            this.add(top);
555
556            String turnoutStateThrown = InstanceManager.turnoutManagerInstance().getThrownText();
557            String turnoutStateClosed = InstanceManager.turnoutManagerInstance().getClosedText();
558            String[] turnoutStates = new String[]{turnoutStateClosed, turnoutStateThrown};
559
560            turnoutDetailsPanel = new JPanel(new GridBagLayout());
561            turnoutDetailsPanel.setBorder(new EtchedBorder());
562            GridBagConstraints c = new GridBagConstraints();
563            c.insets = new Insets(2, 5, 2, 5);
564            c.anchor = GridBagConstraints.LINE_START;
565
566            // Side A
567            turnoutNameComboBoxA = new NamedBeanComboBox<>(InstanceManager.getDefault(TurnoutManager.class));
568            LayoutEditor.setupComboBox(turnoutNameComboBoxA, false, true, false);
569            turnoutNameComboBoxA.setSelectedItem(slotA.getTurnout());
570            turnoutNameComboBoxA.addPopupMenuListener(
571                    layoutEditor.newTurnoutComboBoxPopupMenuListener(turnoutNameComboBoxA, traverserTurnouts));
572            slotTurnoutStateComboBoxA = new JComboBox<>(turnoutStates);
573            slotTurnoutLabelA = new JLabel();
574            disabledCheckBoxA = new JCheckBox(Bundle.getMessage("Disabled"));
575            disabledCheckBoxA.setSelected(slotA.isDisabled());
576            disabledCheckBoxA.addActionListener((ActionEvent e) -> {
577                if (disabledCheckBoxA.isSelected()) {
578                    var msg = layoutTraverser.isSlotConnectionClear(slotA);
579                    if (msg.length() > 0) {
580                        msg.insert(0, Bundle.getMessage("TV_Message_Header"));
581                        msg.append(Bundle.getMessage("TV_Message_Slot_Disable"));
582                        JmriJOptionPane.showMessageDialog(editLayoutTraverserFrame,
583                                msg.toString(),
584                                Bundle.getMessage("ErrorTitle"),
585                                JmriJOptionPane.ERROR_MESSAGE);
586                        disabledCheckBoxA.setSelected(false);
587                    }
588                }
589                slotA.setDisabled(disabledCheckBoxA.isSelected());
590                layoutEditor.redrawPanel();
591                layoutEditor.setDirty();
592            });
593
594            c.gridy = 0;
595            c.gridx = 0;
596            turnoutDetailsPanel.add(slotTurnoutLabelA, c);
597            c.gridx = 1;
598            turnoutDetailsPanel.add(turnoutNameComboBoxA, c);
599            c.gridx = 2;
600            turnoutDetailsPanel.add(new JLabel(Bundle.getMessage("TurnoutState")), c);
601            c.gridx = 3;
602            turnoutDetailsPanel.add(slotTurnoutStateComboBoxA, c);
603            c.gridx = 4;
604            turnoutDetailsPanel.add(disabledCheckBoxA, c);
605
606            if (slotA.getTurnoutState() == Turnout.CLOSED) {
607                slotTurnoutStateComboBoxA.setSelectedItem(turnoutStateClosed);
608            } else {
609                slotTurnoutStateComboBoxA.setSelectedItem(turnoutStateThrown);
610            }
611
612            // Side B
613            turnoutNameComboBoxB = new NamedBeanComboBox<>(InstanceManager.getDefault(TurnoutManager.class));
614            LayoutEditor.setupComboBox(turnoutNameComboBoxB, false, true, false);
615            turnoutNameComboBoxB.setSelectedItem(slotB.getTurnout());
616            turnoutNameComboBoxB.addPopupMenuListener(
617                    layoutEditor.newTurnoutComboBoxPopupMenuListener(turnoutNameComboBoxB, traverserTurnouts));
618            slotTurnoutStateComboBoxB = new JComboBox<>(turnoutStates);
619            slotTurnoutLabelB = new JLabel();
620            disabledCheckBoxB = new JCheckBox(Bundle.getMessage("Disabled"));
621            disabledCheckBoxB.setSelected(slotB.isDisabled());
622            disabledCheckBoxB.addActionListener((ActionEvent e) -> {
623                if (disabledCheckBoxB.isSelected()) {
624                    var msg = layoutTraverser.isSlotConnectionClear(slotB);
625                    if (msg.length() > 0) {
626                        msg.insert(0, Bundle.getMessage("TV_Message_Header"));
627                        JmriJOptionPane.showMessageDialog(editLayoutTraverserFrame,
628                                msg.toString(),
629                                Bundle.getMessage("ErrorTitle"),
630                                JmriJOptionPane.ERROR_MESSAGE);
631                        disabledCheckBoxB.setSelected(false);
632                    }
633                }
634                slotB.setDisabled(disabledCheckBoxB.isSelected());
635                layoutEditor.redrawPanel();
636                layoutEditor.setDirty();
637            });
638
639            c.gridy = 1;
640            c.gridx = 0;
641            turnoutDetailsPanel.add(slotTurnoutLabelB, c);
642            c.gridx = 1;
643            turnoutDetailsPanel.add(turnoutNameComboBoxB, c);
644            c.gridx = 2;
645            turnoutDetailsPanel.add(new JLabel(Bundle.getMessage("TurnoutState")), c);
646            c.gridx = 3;
647            turnoutDetailsPanel.add(slotTurnoutStateComboBoxB, c);
648            c.gridx = 4;
649            turnoutDetailsPanel.add(disabledCheckBoxB, c);
650
651            if (slotB.getTurnoutState() == Turnout.CLOSED) {
652                slotTurnoutStateComboBoxB.setSelectedItem(turnoutStateClosed);
653            } else {
654                slotTurnoutStateComboBoxB.setSelectedItem(turnoutStateThrown);
655            }
656            this.add(turnoutDetailsPanel);
657
658            setTurnoutLabels();
659
660            JButton deleteButton = new JButton(Bundle.getMessage("Delete"));
661            top.add(deleteButton);
662            deleteButton.addActionListener((ActionEvent e) -> {
663                delete();
664                updateSlotPanel();
665            });
666
667            JButton moveUpButton = new JButton(Bundle.getMessage("MoveUp"));
668            top.add(moveUpButton);
669            moveUpButton.addActionListener((ActionEvent e) -> {
670                layoutTraverser.moveSlotPairUp(pairIndex);
671                updateSlotPanel();
672            });
673            moveUpButton.setVisible(layoutTraverser.isTurnoutControlled() && pairIndex > 0);
674
675            JButton moveDownButton = new JButton(Bundle.getMessage("MoveDown"));
676            top.add(moveDownButton);
677            moveDownButton.addActionListener((ActionEvent e) -> {
678                layoutTraverser.moveSlotPairDown(pairIndex);
679                updateSlotPanel();
680            });
681            moveDownButton.setVisible(layoutTraverser.isTurnoutControlled() && pairIndex < (layoutTraverser.getNumberSlots() / 2) - 1);
682
683            slotTitledBorder = BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black));
684            this.setBorder(slotTitledBorder);
685
686            showTurnoutDetails();
687
688            slotTitledBorder.setTitle(Bundle.getMessage("SlotPair") + " : " + (pairIndex + 1));
689        }
690
691        private void setTurnoutLabels() {
692            if (layoutTraverser.getOrientation() == LayoutTraverser.HORIZONTAL) {
693                slotTurnoutLabelA.setText(Bundle.getMessage("MakeLabel", Bundle.getMessage("TurnoutLeft")));
694                slotTurnoutLabelB.setText(Bundle.getMessage("MakeLabel", Bundle.getMessage("TurnoutRight")));
695            } else {
696                slotTurnoutLabelA.setText(Bundle.getMessage("MakeLabel", Bundle.getMessage("TurnoutUp")));
697                slotTurnoutLabelB.setText(Bundle.getMessage("MakeLabel", Bundle.getMessage("TurnoutDown")));
698            }
699        }
700
701        private void delete() {
702            int n = JmriJOptionPane.showConfirmDialog(null,
703                    Bundle.getMessage("Question8s"),
704                    Bundle.getMessage("WarningTitle"),
705                    JmriJOptionPane.YES_NO_OPTION);
706            if (n == JmriJOptionPane.YES_OPTION) {
707                if (layoutTraverser.isSlotDeleteAllowed(pairIndex)) {
708                    layoutTraverser.deleteTrackPair(pairIndex);
709                }
710            }
711        }
712
713        private void updateDetails() {
714            if (layoutTraverser.isTurnoutControlled()) {
715                String turnoutNameA = turnoutNameComboBoxA.getSelectedItemDisplayName();
716                if (turnoutNameA == null) turnoutNameA = "";
717                slotA.setTurnout(turnoutNameA, slotTurnoutStateValues[slotTurnoutStateComboBoxA.getSelectedIndex()]);
718
719                String turnoutNameB = turnoutNameComboBoxB.getSelectedItemDisplayName();
720                if (turnoutNameB == null) turnoutNameB = "";
721                slotB.setTurnout(turnoutNameB, slotTurnoutStateValues[slotTurnoutStateComboBoxB.getSelectedIndex()]);
722            }
723            slotA.setDisabled(disabledCheckBoxA.isSelected());
724            slotB.setDisabled(disabledCheckBoxB.isSelected());
725        }
726
727        private void showTurnoutDetails() {
728            boolean visible = layoutTraverser.isTurnoutControlled();
729            turnoutDetailsPanel.setVisible(visible);
730        }
731    }
732
733    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LayoutTraverserEditor.class);
734}