001package jmri.jmrit.beantable;
002
003import java.util.*;
004import javax.annotation.Nonnull;
005import javax.swing.*;
006
007import jmri.*;
008
009/**
010 * Model for a SignalHeadTable.
011 * 
012 * Code originally located within SignalHeadTableAction.java
013 * 
014 * @author Bob Jacobsen Copyright (C) 2003,2006,2007, 2008, 2009
015 * @author Petr Koud'a Copyright (C) 2007
016 * @author Egbert Broerse Copyright (C) 2016
017 * @author Steve Young Copyright (C) 2023
018 */
019public class SignalHeadTableModel extends jmri.jmrit.beantable.BeanTableDataModel<SignalHead> {
020
021    static public final int LITCOL = NUMCOLUMN;
022    static public final int HELDCOL = LITCOL + 1;
023    static public final int EDITCOL = HELDCOL + 1;
024
025    public SignalHeadTableModel(){
026        super();
027    }
028
029    @Override
030    public int getColumnCount() {
031        return NUMCOLUMN + 3;
032    }
033
034    @Override
035    public String getColumnName(int col) {
036        switch (col) {
037            case VALUECOL:
038                return Bundle.getMessage("SignalMastAppearance");  // override default title, correct name SignalHeadAppearance i.e. "Red"
039            case LITCOL:
040                return Bundle.getMessage("ColumnHeadLit");
041            case HELDCOL:
042                return Bundle.getMessage("ColumnHeadHeld");
043            case EDITCOL:
044                return ""; // no heading on "Edit"
045            default:
046                return super.getColumnName(col);
047        }
048    }
049
050    @Override
051    public Class<?> getColumnClass(int col) {
052        switch (col) {
053            case VALUECOL:
054                return RowComboBoxPanel.class; // Use a JPanel containing a custom Appearance ComboBox
055            case LITCOL:
056            case HELDCOL:
057                return Boolean.class;
058            case EDITCOL:
059                return JButton.class;
060            default:
061                return super.getColumnClass(col);
062        }
063    }
064
065    @Override
066    public int getPreferredWidth(int col) {
067        switch (col) {
068            case LITCOL:
069            case HELDCOL:
070                return new JTextField(4).getPreferredSize().width;
071            case EDITCOL:
072                return new JTextField(7).getPreferredSize().width;
073            default:
074                return super.getPreferredWidth(col);
075        }
076    }
077
078    @Override
079    public boolean isCellEditable(int row, int col) {
080        switch (col) {
081            case LITCOL:
082            case HELDCOL:
083            case EDITCOL:
084                return true;
085            default:
086                return super.isCellEditable(row, col);
087        }
088    }
089
090    @Override
091    public Object getValueAt(int row, int col) {
092        // some error checking
093        if (row >= sysNameList.size()) {
094            log.debug("row is greater than name list");
095            return "error";
096        }
097        String name = sysNameList.get(row);
098        SignalHead s = InstanceManager.getDefault(SignalHeadManager.class).getBySystemName(name);
099        if (s == null) {
100            return Boolean.FALSE; // if due to race condition, the device is going away
101        }
102        switch (col) {
103            case LITCOL:
104                return s.getLit();
105            case HELDCOL:
106                return s.getHeld();
107            case EDITCOL:
108                return Bundle.getMessage("ButtonEdit");
109            case VALUECOL:
110                String appearance = s.getAppearanceName();
111                if ( !appearance.isEmpty()) {
112                    return appearance;
113                } else {
114                    //Appearance (head) not set
115                    log.debug("No Appearance returned for head in row {}", row);
116                    return Bundle.getMessage("BeanStateUnknown"); // use place holder string in table
117                }
118            default:
119                return super.getValueAt(row, col);
120        }
121    }
122
123    @Override
124    public void setValueAt(Object value, int row, int col) {
125        String name = sysNameList.get(row);
126        SignalHead s = InstanceManager.getDefault(SignalHeadManager.class).getBySystemName(name);
127        if (s == null) {
128            return;  // device is going away anyway
129        }
130        switch (col) {
131            case VALUECOL:
132                if (value != null) {
133                    //row = table.convertRowIndexToModel(row); // find the right row in model instead of table (not needed here)
134                    log.debug("SignalHead setValueAt (rowConverted={}; value={})", row, value);
135                    // convert from String (selected item) to int
136                    int newState = 99;
137                    String[] stateNameList = s.getValidStateNames(); // Array of valid appearance names
138                    int[] validStateList = s.getValidStates(); // Array of valid appearance numbers
139                    for (int i = 0; i < stateNameList.length; i++) {
140                        if (value.equals(stateNameList[i])) {
141                            newState = validStateList[i];
142                            break;
143                        }
144                    }
145                    if (newState == 99) {
146                        if (stateNameList.length == 0) {
147                            newState = SignalHead.DARK;
148                            log.warn("New signal state not found so setting to Dark {}", s.getDisplayName());
149                        } else {
150                            newState = validStateList[0];
151                            log.warn("New signal state not found so setting to the first available {}", s.getDisplayName());
152                        }
153                    }
154                    log.debug("Signal Head set from: {} to: {} [{}]", s.getAppearanceName(), value, newState);
155                    s.setAppearance(newState);
156                    fireTableRowsUpdated(row, row);
157                }   break;
158            case LITCOL:
159                    s.setLit((Boolean) value);
160                    break;
161            case HELDCOL:
162                    s.setHeld((Boolean) value);
163                    break;
164            case EDITCOL:
165                // button clicked - edit
166                editSignal(s);
167                break;
168            default:
169                super.setValueAt(value, row, col);
170                break;
171        }
172    }
173
174    @Override
175    public String getValue(String name) {
176        SignalHead s = InstanceManager.getDefault(SignalHeadManager.class).getBySystemName(name);
177        if (s == null) {
178            return "<lost>"; // if due to race condition, the device is going away
179        }
180        String val = null;
181        try {
182            val = s.getAppearanceName();
183        } catch (java.lang.ArrayIndexOutOfBoundsException e) {
184            log.error("Could not get Appearance Name for {}", s.getDisplayName(), e);
185        }
186        if (val != null) {
187            return val;
188        } else {
189            return "Unexpected null value";
190        }
191    }
192
193    @Override
194    public SignalHeadManager getManager() {
195        return InstanceManager.getDefault(SignalHeadManager.class);
196    }
197
198    @Override
199    public SignalHead getBySystemName(@Nonnull String name) {
200        return InstanceManager.getDefault(SignalHeadManager.class).getBySystemName(name);
201    }
202
203    @Override
204    public SignalHead getByUserName(@Nonnull String name) {
205        return InstanceManager.getDefault(SignalHeadManager.class).getByUserName(name);
206    }
207
208    @Override
209    protected String getMasterClassName() {
210        return SignalHeadTableAction.class.getName();
211    }
212
213    @Override
214        public void clickOn(SignalHead t) {
215    }
216
217    /**
218     * Set column width.
219     *
220     * @return a button to fit inside the VALUE column
221     */
222    @Override
223    public JButton configureButton() {
224        // pick a large size
225        JButton b = new JButton(Bundle.getMessage("SignalHeadStateYellow")); // about the longest Appearance string
226        b.putClientProperty("JComponent.sizeVariant", "small");
227        b.putClientProperty("JButton.buttonType", "square");
228        return b;
229    }
230
231    @Override
232    public boolean matchPropertyName(java.beans.PropertyChangeEvent e) {
233        if (e.getPropertyName().contains("Lit") || e.getPropertyName().contains("Held") || e.getPropertyName().contains("ValidStatesChanged")) {
234            return true;
235        } else {
236            return super.matchPropertyName(e);
237        }
238    }
239
240    @Override
241    protected String getBeanType() {
242        return Bundle.getMessage("BeanNameSignalHead");
243    }
244
245    /**
246     * Respond to change from bean. Prevent Appearance change when
247     * Signal Head is set to Hold or Unlit.
248     *
249     * @param e A property change of any bean
250     */
251    @Override
252    // Might be useful to show only a Dark option in the comboBox if head is Held
253    // At present, does not work/change when head Lit/Held checkboxes are (de)activated
254    public void propertyChange(java.beans.PropertyChangeEvent e) {
255        if (!e.getPropertyName().contains("Lit") || e.getPropertyName().contains("Held") || e.getPropertyName().contains("ValidStatesChanged")) {
256            if (e.getSource() instanceof NamedBean) {
257                String name = ((NamedBean) e.getSource()).getSystemName();
258                if (log.isDebugEnabled()) {
259                    log.debug("Update cell {}, {} for {}", sysNameList.indexOf(name), VALUECOL, name);
260                }
261                // since we can add columns, the entire row is marked as updated
262                int row = sysNameList.indexOf(name);
263                this.fireTableRowsUpdated(row, row);
264                clearAppearanceVector(row); // activate this method below
265            }
266        }
267        super.propertyChange(e);
268    }
269
270    /**
271     * Customize the SignalHead Value (Appearance) column to show an
272     * appropriate ComboBox of available Appearances when the
273     * TableDataModel is being called from ListedTableAction.
274     *
275     * @param table a JTable of Signal Head
276     */
277    @Override
278    protected void configValueColumn(JTable table) {
279        // have the value column hold a JPanel with a JComboBox for Appearances
280        setColumnToHoldButton(table, VALUECOL, configureButton());
281        // add extras, override BeanTableDataModel
282        log.debug("Head configValueColumn (I am {})", super.toString());
283        table.setDefaultEditor(RowComboBoxPanel.class, new AppearanceComboBoxPanel());
284        table.setDefaultRenderer(RowComboBoxPanel.class, new AppearanceComboBoxPanel()); // use same class for the renderer
285        // Set more things?
286    }
287
288    /**
289     * A row specific Appearance combobox cell editor/renderer.
290     */
291    class AppearanceComboBoxPanel extends RowComboBoxPanel {
292        @Override
293        protected final void eventEditorMousePressed() {
294            this.editor.add(getEditorBox(table.convertRowIndexToModel(this.currentRow))); // add editorBox to JPanel
295            this.editor.revalidate();
296            SwingUtilities.invokeLater(this.comboBoxFocusRequester);
297            log.debug("eventEditorMousePressed in row: {})", this.currentRow);
298        }
299
300        /**
301         * Call the method in the surrounding method for the
302         * SignalHeadTable.
303         *
304         * @param row the user clicked on in the table
305         * @return an appropriate combobox for this signal head
306         */
307        @Override
308        protected JComboBox<String> getEditorBox(int row) {
309            return getAppearanceEditorBox(row);
310        }
311    }
312
313    /**
314     * Clear the old appearance comboboxes and force them to be rebuilt.
315     * Used with the Single Output Signal Head to capture reconfiguration.
316     *
317     * @param row Index of the signal mast (in TableDataModel) to be
318     *            rebuilt in the Hashtables
319     */
320    public void clearAppearanceVector(int row) {
321        boxMap.remove(this.getValueAt(row, SYSNAMECOL));
322        editorMap.remove(this.getValueAt(row, SYSNAMECOL));
323    }
324
325    // Hashtables for Editors; not used for Renderer)
326    /**
327     * Provide a JComboBox element to display inside the JPanel
328     * CellEditor. When not yet present, create, store and return a new
329     * one.
330     *
331     * @param row Index number (in TableDataModel)
332     * @return A combobox containing the valid appearance names for this
333     *         mast
334     */
335    public JComboBox<String> getAppearanceEditorBox(int row) {
336        JComboBox<String> editCombo = editorMap.get(this.getValueAt(row, SYSNAMECOL));
337        if (editCombo == null) {
338            // create a new one with correct appearances
339            editCombo = new JComboBox<>(getRowVector(row));
340            editorMap.put(this.getValueAt(row, SYSNAMECOL), editCombo);
341        }
342        return editCombo;
343    }
344
345    final Hashtable<Object, JComboBox<String>> editorMap = new Hashtable<>();
346
347    /**
348     * Get a list of all the valid appearances that have not been
349     * disabled.
350     *
351     * @param head the name of the signal head
352     * @return List of valid signal head appearance names
353     */
354    public Vector<String> getValidAppearances(SignalHead head) {
355        // convert String[] validStateNames to Vector
356        String[] app = head.getValidStateNames();
357        Vector<String> v = new Vector<>();
358        Collections.addAll(v, app);
359        return v;
360    }
361
362    /**
363     * Holds a Hashtable of valid appearances per signal head, used by
364     * getEditorBox()
365     *
366     * @param row Index number (in TableDataModel)
367     * @return The Vector of valid appearance names for this mast to
368     *         show in the JComboBox
369     */
370    Vector<String> getRowVector(int row) {
371        Vector<String> comboappearances = boxMap.get(this.getValueAt(row, SYSNAMECOL));
372        if (comboappearances == null) {
373            // create a new one with right appearance
374            comboappearances = getValidAppearances((SignalHead) this.getValueAt(row, SYSNAMECOL));
375            boxMap.put(this.getValueAt(row, SYSNAMECOL), comboappearances);
376        }
377        return comboappearances;
378    }
379
380    final Hashtable<Object, Vector<String>> boxMap = new Hashtable<>();
381
382    // end of methods to display VALUECOL ComboBox
383
384    private SignalHeadAddEditFrame editFrame = null;
385
386    private void editSignal(@Nonnull final SignalHead head) {
387        // Signal Head was found, initialize for edit
388        log.debug("editPressed started for {}", head.getSystemName());
389        // create the Edit Signal Head Window
390        // Use separate Runnable so window is created on top
391        Runnable t = () -> makeEditSignalWindow(head);
392        javax.swing.SwingUtilities.invokeLater(t);
393    }
394
395    private void makeEditSignalWindow(@Nonnull final SignalHead head) {
396        if (editFrame == null) {
397            editFrame = new SignalHeadAddEditFrame(head){
398                @Override
399                public void dispose() {
400                    editFrame = null;
401                    super.dispose();
402                }
403            };
404            editFrame.initComponents();
405        } else {
406            if (head.equals(editFrame.getSignalHead())) {
407                editFrame.setVisible(true);
408            } else {
409                log.error("Attempt to edit two signal heads at the same time-{}-and-{}-", editFrame.getSignalHead(), head.getSystemName());
410                String msg = Bundle.getMessage("WarningEdit", editFrame.getSignalHead(), head.getSystemName());
411                jmri.util.swing.JmriJOptionPane.showMessageDialog(editFrame, msg,
412                        Bundle.getMessage("WarningTitle"), jmri.util.swing.JmriJOptionPane.ERROR_MESSAGE);
413                editFrame.setVisible(true);
414            }
415        }
416    }
417
418    @Override
419    public void dispose(){
420        if ( editFrame != null ) {
421            editFrame.dispose();
422            editFrame = null;
423        }
424        super.dispose();
425    }
426
427    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SignalHeadTableModel.class);
428}