001package jmri.jmrit.roster.swing.functiontable;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.util.ArrayList;
006import java.util.List;
007
008import javax.swing.table.AbstractTableModel;
009
010import jmri.jmrit.roster.Roster;
011import jmri.jmrit.roster.RosterEntry;
012import jmri.jmrit.roster.RosterFunctionEntry;
013import jmri.jmrit.throttle.preferences.ThrottlesPreferences;
014import jmri.util.swing.EditableResizableImagePanel;
015
016import org.slf4j.Logger;
017import org.slf4j.LoggerFactory;
018
019import com.fasterxml.jackson.core.JsonProcessingException;
020import com.fasterxml.jackson.databind.ObjectMapper;
021
022/**
023 * A TableModel that provides a way for the user to edit details of functions of a decoder in the roster. 
024 * It is used in the FunctionLabelPane.
025 * 
026 * <hr>
027 * This file is part of JMRI.
028 * <p>
029 * JMRI is free software; you can redistribute it and/or modify it under the
030 * terms of version 2 of the GNU General Public License as published by the Free
031 * Software Foundation. See the "COPYING" file for a copy of this license.
032 * <p>
033 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
034 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
035 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
036 *
037 * @author Lionel Jeanson 2009-2026
038 */
039
040public class FunctionTableModel extends AbstractTableModel implements PropertyChangeListener {
041    private RosterEntry rosterEntry;
042
043    public static final String FB_ORDER_ATTR="FnDisplayOrder";
044    // to allow easy GUI update
045    public static final int COL_FN = 0; // Function number
046    public static final int COL_LK = 1; // Lockable
047    public static final int COL_OF = 2; // icon off
048    public static final int COL_ON = 3; // icon on
049    public static final int COL_VI = 4; // Visibility
050    public static final int COL_SH = 5; // isShuntingFunction
051    public static final int COL_LA = 6; // label
052    public static final int NBCOL = 8;
053    public static final int NBVISCOL = 7;   
054    public static final int COL_DO = 7; // Display Order, not diplayed, but used to manage the drag'n drop order of the rows
055
056    private static final String[] columnNames = new String[NBCOL];
057    private static final Class<?>[] columnClass = new Class<?>[NBCOL];
058
059     // row to fn mapping, to allow easy GUI update when the user reorders the rows in the table
060    private int[] rowFn;
061    // caches for ImagePanels to avoid creating new ones each time the table is refreshed
062    private EditableResizableImagePanel[] imageOnPanels;
063    private EditableResizableImagePanel[] imageOffPanels; 
064    
065    /**
066     * Create a new FunctionTableModel for a COPY of the given RosterEntry,
067     * 
068     * @param re the RosterEntry on which to base this model. A copy of the RosterEntry is made so that changes to the model do not affect the original RosterEntry.
069     */
070    public FunctionTableModel(RosterEntry re) {
071        columnNames[COL_FN]=Bundle.getMessage("FunctionButtonN");
072        columnNames[COL_DO]=Bundle.getMessage("FunctionButtonDisplayOrder");
073        columnNames[COL_VI]=Bundle.getMessage("FunctionButtonVisible");
074        columnNames[COL_LK]=Bundle.getMessage("FunctionButtonLockable");
075        columnNames[COL_OF]=Bundle.getMessage("FunctionButtonImageOff");
076        columnNames[COL_ON]=Bundle.getMessage("FunctionButtonImageOn");
077        columnNames[COL_SH]=Bundle.getMessage("FunctionButtonShunterFn");
078        columnNames[COL_LA]=Bundle.getMessage("FunctionButtonLabel");
079
080        columnClass[COL_FN]=Integer.class;
081        columnClass[COL_DO]=Integer.class;
082        columnClass[COL_VI]=Boolean.class;
083        columnClass[COL_LK]=Boolean.class;
084        columnClass[COL_OF]=String.class; 
085        columnClass[COL_ON]=String.class; 
086        columnClass[COL_SH]=Boolean.class;
087        columnClass[COL_LA]=String.class;                
088    
089        rosterEntry = new RosterEntry(re, re.getId()+"FunctionTableModelWIP");
090        initRowFn();
091        initImagePanels();        
092    }
093
094    /**
095     * Update the FunctionTableModel with a COPY of the given RosterEntry,
096     * 
097     * @param re the RosterEntry on which to base this model. A copy of the RosterEntry is made so that changes to the model do not affect the original RosterEntry.
098     */
099    public void setRosterEntry(RosterEntry re) {
100        rosterEntry = new RosterEntry(re, re.getId()+"FunctionTableModelWIP"); // we work on a copy of the RosterEntry to avoid changing the original RosterEntry until the user clicks on "Apply" in the FunctionLabelPane
101        initRowFn();
102        initImagePanels();
103        fireTableDataChanged();
104    }
105
106    /**
107     * Reset the FunctionTableModel from the given inputs
108     * 
109     * @param fnEntries an array of RosterFunctionEntry
110     * @param shuntFn the shunting function string, if any, or null
111     * @param fnDisplayOrder the function display order JSON array, if any, or null
112     * 
113     */
114    public void reset(RosterFunctionEntry[] fnEntries, String shuntFn, String fnDisplayOrder) {        
115        rosterEntry.deleteAttribute(FB_ORDER_ATTR);
116        initRowFn();
117        rosterEntry.setShuntingFunction(shuntFn);        
118        for (int i=0; i<rosterEntry.getMaxFnNumAsInt()+1;i++) {
119            if (fnEntries != null && i<fnEntries.length) {
120                rosterEntry.setFunctionEntry(new RosterFunctionEntry(fnEntries[i]));
121            } else {
122                rosterEntry.setFunctionEntry(new RosterFunctionEntry(i));
123            }
124        }
125        if (fnDisplayOrder != null) {
126            rosterEntry.putAttribute(FB_ORDER_ATTR, fnDisplayOrder);
127            initRowFn();
128        }
129        initImagePanels();
130        fireTableDataChanged();
131    }
132
133    public RosterFunctionEntry getRosterFunctionEntryAtRow(int row) {
134        int fn = getFnForRow(row);
135        if (fn == -1) {
136            return null;
137        }
138        return rosterEntry.getFunctionEntries()[fn];
139    }
140
141    public void setRosterFunctionEntryAtRow(RosterFunctionEntry rfe, int rowIndex) {
142        int fn = getFnForRow(rowIndex);
143        RosterFunctionEntry newrfe = new RosterFunctionEntry(fn, rfe); // create a copy, keep original function number
144        rosterEntry.setFunctionEntry(newrfe);
145        // force reload of image panels
146        getFunctionImagePanel(fn).setImagePath(newrfe.getImage());
147        getFunctionSelectedImagePanel(fn).setImagePath(newrfe.getImage());
148        // refresh rendering
149        fireTableDataChanged();
150    }
151
152    private void initRowFn() {
153        String fnDOAtt = rosterEntry.getAttribute(FB_ORDER_ATTR);
154        if (fnDOAtt != null) {
155            try {
156                rowFn = new ObjectMapper().readValue(fnDOAtt, int[].class );
157                return;
158            } catch (JsonProcessingException e) {
159                log.warn("Couldn't parse FnDisplayOrder attribute ",e);
160            } 
161        } 
162        rowFn = new int[rosterEntry.getMaxFnNumAsInt()+1];
163        for (int i=0;i < rowFn.length; i++) {
164            rowFn[i] = i; // initial mapping
165        }
166    }
167
168    private void initImagePanels() {
169        imageOnPanels = new EditableResizableImagePanel[rosterEntry.getMaxFnNumAsInt()+1];
170        imageOffPanels = new EditableResizableImagePanel[rosterEntry.getMaxFnNumAsInt()+1];
171    }
172
173    /**
174     * Return the EditableResizableImagePanel for the function image (off) for a function number.
175     * 
176     * @param fn the function number
177     * @return the EditableResizableImagePanel
178     */
179    public EditableResizableImagePanel getFunctionImagePanel(int fn) {
180        if (imageOffPanels[fn] == null) {
181            imageOffPanels[fn] = new EditableResizableImagePanel(rosterEntry.getFunctionImage(fn), FunctionTableCellRenderer.CELL_HEIGHT, FunctionTableCellRenderer.CELL_HEIGHT);
182            imageOffPanels[fn].setDropFolder(Roster.getDefault().getRosterFilesLocation());
183            imageOffPanels[fn].setBackground(new java.awt.Color(0, 0, 0, 0));
184            imageOffPanels[fn].setBorder(javax.swing.BorderFactory.createLineBorder(java.awt.Color.blue));
185            imageOffPanels[fn].addMenuItemBrowseFolder(Bundle.getMessage("MediaRosterOpenSystemFileBrowserOnJMRIfnButtonsRessources"), jmri.util.FileUtil.getExternalFilename("resources/icons/functionicons"));
186            imageOffPanels[fn].addPropertyChangeListener(EditableResizableImagePanel.IMAGE_PATH, evt -> {
187                    // Update the model when the image path changes
188                    setValueAt(evt.getNewValue(), getRowForFn(fn), FunctionTableModel.COL_OF);
189                });
190        }
191        return imageOffPanels[fn];
192    }
193
194    /**
195     * Return the EditableResizableImagePanel for the function image (on) for a function number.
196     * 
197     * @param fn the function number
198     * @return the EditableResizableImagePanel
199     */    
200    public EditableResizableImagePanel getFunctionSelectedImagePanel(int fn) {
201        if (imageOnPanels[fn] == null) {
202            imageOnPanels[fn] = new EditableResizableImagePanel(rosterEntry.getFunctionSelectedImage(fn), FunctionTableCellRenderer.CELL_HEIGHT, FunctionTableCellRenderer.CELL_HEIGHT);
203            imageOnPanels[fn].setDropFolder(Roster.getDefault().getRosterFilesLocation());
204            imageOnPanels[fn].setBackground(new java.awt.Color(0, 0, 0, 0));
205            imageOnPanels[fn].setBorder(javax.swing.BorderFactory.createLineBorder(java.awt.Color.blue));
206            imageOnPanels[fn].addMenuItemBrowseFolder(Bundle.getMessage("MediaRosterOpenSystemFileBrowserOnJMRIfnButtonsRessources"), jmri.util.FileUtil.getExternalFilename("resources/icons/functionicons"));
207            imageOnPanels[fn].addPropertyChangeListener(EditableResizableImagePanel.IMAGE_PATH, evt -> {
208                    // Update the model when the image path changes
209                    setValueAt(evt.getNewValue(), getRowForFn(fn), FunctionTableModel.COL_ON);
210                });
211        }
212        return imageOnPanels[fn];
213    }
214
215    /**
216     *  Return the function number for a row index. 
217     * 
218     * @param rowIndex the row index
219     * @return the function number, -1 if out of bounds
220     */
221    public int getFnForRow(int rowIndex) {
222        if (rowIndex < rowFn.length) {
223            return rowFn[rowIndex];
224        }
225        return -1;
226    }
227
228    /**
229     *  Return the row index for a function number. 
230     * 
231     * @param fn the function number
232     * @return the row index, -1 if out of bounds
233     */
234    public int getRowForFn(int fn) {
235        int i = 0;
236        while (rowFn[i] != fn) {
237            i++;
238        }
239        if (rowFn[i] == fn) {
240            return i;
241        }
242        return -1;
243    }
244
245    /** 
246     * Move one row from first ndex to second index.
247     * Lines between the two indexes are shifted up or down to make room for the moved line.
248     * 
249     * @param from moving from this index
250     * @param to moving to this index
251     */
252
253    public void moveRow(int from, int to) {
254        log.debug("moveRow() from {} to {}", from, to);
255        // reorder table display²
256        int fnToMove = rowFn[from];
257        if (from > to) {            
258            for (int i=from; i>to; i--) {
259                rowFn[i] = rowFn[i-1];
260            }            
261        } else {
262            for (int i=from; i<to; i++) {
263                rowFn[i] = rowFn[i+1];
264            }
265        }
266        rowFn[to] = fnToMove;
267        fireTableDataChanged();
268        for (int i=Math.min(to, from);i<=Math.max(to, from);i++) {
269            setValueAt(i, i, COL_DO );
270        }        
271    }
272
273    /**
274     *  Return the function visibility order attribute string to be stored in the RosterEntry.
275     *  This is a JSON string representing the rowFn array, or null if the order is the default order (i.e. rowFn[i] == i for all i).
276     *  At index i is the function number of the function to be shown at that position in the GUI (Throttle for instance)..
277     *  The function number is the index in the RosterEntry function list. 
278     * 
279     * @return the attriute string to be stored in the RosterEntry, or null if not needed
280     */
281    public String getVisibilityOrderAttributeString() {
282        // Really needed?
283        boolean needed = false;
284        for (int i=0; i<rowFn.length; i++) {
285            if (i != rowFn[i]) {
286                needed = true;
287                break;
288            }
289        }
290        if (!needed) {
291            return null;
292        }
293        try {
294            return new ObjectMapper().writeValueAsString(rowFn);
295        } catch (JsonProcessingException e) {
296            log.debug("Couldn't generate JSON string",e);
297        }
298        return null;
299    }
300    
301    @Override
302    public int getRowCount() {
303        return rosterEntry.getMaxFnNumAsInt()+1;
304    }
305
306    @Override
307    public int getColumnCount() {
308        return NBVISCOL;
309    }
310
311    @Override
312    public Class<?> getColumnClass(int columnIndex) {
313        return columnClass[columnIndex];
314    }
315
316    @Override
317    public Object getValueAt(int rowIndex, int columnIndex) {
318        int fn = getFnForRow(rowIndex);
319        switch (columnIndex) {
320            case COL_FN:
321                return fn;
322            case COL_DO:
323                return rowIndex; // because the table is sorted by visibility number
324            case COL_VI:
325                return rosterEntry.getFunctionVisible(fn);
326            case COL_LK:
327                return rosterEntry.getFunctionLockable(fn);
328            case COL_OF:
329                return rosterEntry.getFunctionImage(fn) != null ? rosterEntry.getFunctionImage(fn) : "";
330            case COL_ON:
331                return rosterEntry.getFunctionSelectedImage(fn) != null ? rosterEntry.getFunctionSelectedImage(fn) : "";
332            case COL_SH:
333                return ("F" + fn).equals(rosterEntry.getShuntingFunction());
334            case COL_LA:
335                return rosterEntry.getFunctionLabel(fn);                
336            default:
337                log.error("getValueAt(): Invalid column index: {}", columnIndex);
338        }
339        return null;
340    }
341
342    @Override
343    public boolean isCellEditable(int rowIndex, int columnIndex)
344    {
345         // FN Number is not editable, display order managed by drag'n drop, 
346         // images cells are not editable because they are handled by EditableResizableImagePanel
347        if (columnIndex == COL_FN || columnIndex == COL_DO || columnIndex == COL_ON || columnIndex == COL_OF) {
348            return false;
349        }
350        return true;
351    }
352
353    @Override
354    public void setValueAt(Object value, int rowIndex, int columnIndex) {
355        int fn = getFnForRow(rowIndex);
356        switch (columnIndex) {
357            case COL_FN:
358                // FN Number is not editable
359                break;
360            case COL_DO:
361                // Fn Dsplay Order not editable
362                break;
363            case COL_VI:
364                rosterEntry.setFunctionVisible(fn, (Boolean) value);
365                break;
366            case COL_LK:
367                rosterEntry.setFunctionLockable(fn, (Boolean) value);
368                break;
369            case COL_OF:
370                rosterEntry.setFunctionImage(fn, (String) value);
371                break;
372            case COL_ON:
373                rosterEntry.setFunctionSelectedImage(fn, (String) value);
374                break;
375            case COL_SH:                
376                if ((Boolean)value) {
377                    rosterEntry.setShuntingFunction("F" + fn);
378                } else {
379                    if (rosterEntry.getShuntingFunction() != null && rosterEntry.getShuntingFunction().equals("F" + fn)) {
380                        rosterEntry.setShuntingFunction(null);
381                    }
382                }
383                fireTableDataChanged(); // force refresh all lines                                
384                break;
385            case COL_LA:
386                rosterEntry.setFunctionLabel(fn, (String) value);
387                break;                
388            default:
389                log.error("setValueAt(): Invalid column index: {}", columnIndex);
390        }
391        fireTableCellUpdated(rowIndex, columnIndex); // Notify that the cell has changed
392    }
393
394    @Override
395    public String getColumnName(int columnIndex) {
396        return columnNames[columnIndex];
397    }
398
399    /**
400     *  Return list of all function label ordered by function number 
401     * 
402     * @return a list of String
403     */
404    public List<String> getLabels() {
405        List<String> labels = new ArrayList<>();
406        for (int fn = 0; fn < getRowCount(); fn++) {
407            labels.add(getLabel(fn));
408        }
409        return labels;
410    }
411
412    /**
413     *  Return the function label for a function number 
414     * 
415     * @param fn the function number
416     * 
417     * @return a list of String
418     */    
419    public String getLabel(int fn) {
420        int idx = getRowForFn(fn);
421        if (idx != -1) {
422            return rosterEntry.getFunctionLabel(fn);
423        }
424        log.error("getLabel(): no row index for fn {}", fn);
425        return null;
426    }
427
428    /**
429     *  Set the function label for a function number 
430     * 
431     * @param fn the function number
432     * @param label the label to set
433     * 
434     */
435    public void setLabel(int fn, String label) {
436        rosterEntry.setFunctionLabel(fn, label);
437        int idx = getRowForFn(fn);
438        if (idx != -1) {
439            fireTableCellUpdated(idx, COL_LA); // Notify that the label column has changed for row n
440            return;
441        }
442        log.warn("setLabel(): no row index for fn {}", fn);
443        fireTableDataChanged();
444    }
445
446    /**
447     *  Return the function lockable flag for a function number 
448     * 
449     * @param fn the function number
450     * 
451     * @return the lockable flag
452     */
453    public boolean getLockable(int fn) {
454        return rosterEntry.getFunctionLockable(fn);
455    }
456
457    /**
458     *  Set a function lockable flag for a function number 
459     * 
460     * @param fn the function number
461     * @param lockable the lockable flag to set
462     * 
463     */
464    public void setLockable(int fn, boolean lockable) {
465        rosterEntry.setFunctionLockable(fn, lockable);
466        int idx = getRowForFn(fn);
467        if (idx != -1) {
468        fireTableCellUpdated(idx, COL_LK); // Notify that the lockable column has changed for row n
469            return;
470        }
471        log.warn("setLockable(): no row index for fn {}", fn);
472        fireTableDataChanged();        
473    }
474
475    /** 
476     * Returns the RosterEntry being edited by that table model
477     * 
478     * @return the RosterEntry being edited by that table model
479     */
480    public RosterEntry getRosterEntry() {
481        return rosterEntry;
482    }
483
484    @Override
485    public void propertyChange(PropertyChangeEvent evt) {
486        if (evt == null) {
487            return;
488        }
489        log.debug("Property change event received {} / {}", evt.getPropertyName(), evt.getNewValue());        
490        if (ThrottlesPreferences.prefPopertyName.equals(evt.getPropertyName())) {
491            fireTableDataChanged();
492        }   
493    }
494
495    private static final Logger log = LoggerFactory.getLogger(FunctionTableModel.class);
496}