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}