001package jmri.jmrit.roster; 002 003import java.awt.BorderLayout; 004import java.awt.Color; 005import java.awt.Dimension; 006import java.awt.GridBagConstraints; 007import java.awt.GridBagLayout; 008import java.awt.Insets; 009import java.beans.PropertyChangeEvent; 010import java.beans.PropertyChangeListener; 011import java.util.Vector; 012 013import javax.swing.BorderFactory; 014import javax.swing.JLabel; 015import javax.swing.JPanel; 016import javax.swing.JScrollPane; 017import javax.swing.JTable; 018import javax.swing.JTextField; 019import javax.swing.table.AbstractTableModel; 020import javax.swing.table.TableCellEditor; 021import javax.swing.table.TableCellRenderer; 022 023import jmri.BooleanPermission; 024import jmri.InstanceManager; 025import jmri.PermissionManager; 026import jmri.PermissionsProgrammer; 027import jmri.util.gui.GuiLafPreferencesManager; 028import jmri.util.swing.EditableResizableImagePanel; 029import jmri.util.swing.MultiLineCellEditor; 030import jmri.util.swing.MultiLineCellRenderer; 031import jmri.util.swing.ResizableRowDataModel; 032 033/** 034 * A media pane for roster configuration tool. It contains:<ul> 035 * <li>a selector for roster image (a large image for throttle 036 * background...)</li> 037 * <li>a selector for roster icon (a small image for list displays...)</li> 038 * <li>a selector for roster URL (link to wikipedia page about 039 * prototype...)</li> 040 * <li>a table displaying user attributes for that locomotive</li> 041 * </ul> 042 * <hr> 043 * This file is part of JMRI. 044 * <p> 045 * JMRI is free software; you can redistribute it and/or modify it under the 046 * terms of version 2 of the GNU General Public License as published by the Free 047 * Software Foundation. See the "COPYING" file for a copy of this license. 048 * <p> 049 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY 050 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 051 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 052 * 053 * @author Lionel Jeanson Copyright (C) 2009 054 * @author Randall Wood Copyright (C) 2014 055 */ 056public class RosterMediaPane extends JPanel { 057 058 JLabel _imageFPlabel = new JLabel(); 059 EditableResizableImagePanel _imageFilePath; 060 JLabel _iconFPlabel = new JLabel(); 061 EditableResizableImagePanel _iconFilePath; 062 JLabel _URLlabel = new JLabel(); 063 JTextField _URL = new JTextField(30); 064 RosterAttributesTableModel rosterAttributesModel; 065 066 private static final PermissionManager permissionManager = InstanceManager.getDefault(PermissionManager.class); 067 068 /** 069 * This constructor allows the panel to be used in visual bean editors, but 070 * should not be used in code. 071 */ 072 public RosterMediaPane() { 073 super(); 074 } 075 076 public RosterMediaPane(RosterEntry r) { 077 super(); 078 _imageFilePath = new EditableResizableImagePanel(r.getImagePath(), 320, 240); 079 _imageFilePath.setDropFolder(Roster.getDefault().getRosterFilesLocation()); 080 _imageFilePath.setToolTipText(Bundle.getMessage("MediaRosterImageToolTip")); 081 _imageFilePath.setBorder(BorderFactory.createLineBorder(Color.blue)); 082 _imageFPlabel.setText(Bundle.getMessage("MediaRosterImageLabel")); 083 084 _iconFilePath = new EditableResizableImagePanel(r.getIconPath(), 160, 120); 085 _iconFilePath.setDropFolder(Roster.getDefault().getRosterFilesLocation()); 086 _iconFilePath.setToolTipText(Bundle.getMessage("MediaRosterIconToolTip")); 087 _iconFilePath.setBorder(BorderFactory.createLineBorder(Color.blue)); 088 _iconFPlabel.setText(Bundle.getMessage("MediaRosterIconLabel")); 089 090 _URL.setText(r.getURL()); 091 _URL.setToolTipText(Bundle.getMessage("MediaRosterURLToolTip")); 092 _URLlabel.setText(Bundle.getMessage("MediaRosterURLLabel")); 093 094 rosterAttributesModel = new RosterAttributesTableModel(r); //t, columnNames); 095 JTable jtAttributes = new JTable(){ 096 // MultiLineRenderer and MultiLineCellEditor for value column 097 @Override 098 public TableCellRenderer getCellRenderer(int row, int column) { 099 // no remapping of model vs view columns 100 if (column == 1) { 101 return new MultiLineCellRenderer(); 102 } 103 return super.getCellRenderer(row, column); 104 } 105 @Override 106 public TableCellEditor getCellEditor(int row, int column) { 107 // no remapping of model vs view columns 108 if (column == 1) { 109 return new MultiLineCellEditor(); 110 } 111 return super.getCellEditor(row, column); 112 } 113 }; 114 rosterAttributesModel.associatedTable = jtAttributes; 115 jtAttributes.setModel(rosterAttributesModel); 116 JScrollPane jsp = new JScrollPane(jtAttributes); 117 jtAttributes.setFillsViewportHeight(true); 118 119 JPanel mediap = new JPanel(); 120 GridBagLayout gbLayout = new GridBagLayout(); 121 GridBagConstraints gbc = new GridBagConstraints(); 122 Dimension textFieldDim = new Dimension(320, 20); 123 Dimension imageFieldDim = new Dimension(320, 200); 124 Dimension iconFieldDim = new Dimension(160, 100); 125 Dimension tableDim = new Dimension(400, 200); 126 mediap.setLayout(gbLayout); 127 128 gbc.insets = new Insets(0, 8, 0, 8); 129 gbc.gridx = 0; 130 gbc.gridy = 0; 131 gbLayout.setConstraints(_imageFPlabel, gbc); 132 mediap.add(_imageFPlabel); 133 134 gbc.gridx = 1; 135 gbc.gridy = 0; 136 _imageFilePath.setMinimumSize(imageFieldDim); 137 _imageFilePath.setMaximumSize(imageFieldDim); 138 _imageFilePath.setPreferredSize(imageFieldDim); 139 gbLayout.setConstraints(_imageFilePath, gbc); 140 mediap.add(_imageFilePath); 141 142 gbc.gridx = 0; 143 gbc.gridy = 2; 144 gbLayout.setConstraints(_iconFPlabel, gbc); 145 mediap.add(_iconFPlabel); 146 147 gbc.gridx = 1; 148 gbc.gridy = 2; 149 _iconFilePath.setMinimumSize(iconFieldDim); 150 _iconFilePath.setMaximumSize(iconFieldDim); 151 _iconFilePath.setPreferredSize(iconFieldDim); 152 gbLayout.setConstraints(_iconFilePath, gbc); 153 mediap.add(_iconFilePath); 154 155 gbc.gridx = 0; 156 gbc.gridy = 4; 157 gbLayout.setConstraints(_URLlabel, gbc); 158 mediap.add(_URLlabel); 159 160 gbc.gridx = 1; 161 gbc.gridy = 4; 162 _URL.setMinimumSize(textFieldDim); 163 _URL.setPreferredSize(textFieldDim); 164 gbLayout.setConstraints(_URL, gbc); 165 mediap.add(_URL); 166 167 this.setLayout(new BorderLayout()); 168 add(mediap, BorderLayout.NORTH); 169 add(new JLabel(Bundle.getMessage("MediaRosterAttributeTableDescription")), BorderLayout.CENTER); // some nothing in the middle 170 jsp.setMinimumSize(tableDim); 171 jsp.setMaximumSize(tableDim); 172 jsp.setPreferredSize(tableDim); 173 add(jsp, BorderLayout.SOUTH); 174 } 175 176 public boolean guiChanged(RosterEntry r) { 177 if (!r.getURL().equals(_URL.getText())) { 178 return true; 179 } 180 if ((r.getImagePath() != null && !r.getImagePath().equals(_imageFilePath.getImagePath())) 181 || (r.getImagePath() == null && _imageFilePath.getImagePath() != null)) { 182 return true; 183 } 184 if ((r.getIconPath() != null && !r.getIconPath().equals(_iconFilePath.getImagePath())) 185 || (r.getIconPath() == null && _iconFilePath.getImagePath() != null)) { 186 return true; 187 } 188 return rosterAttributesModel.wasModified(); 189 } 190 191 public void update(RosterEntry r) { 192 r.setURL(_URL.getText()); 193 r.setImagePath(_imageFilePath.getImagePath()); 194 r.setIconPath(_iconFilePath.getImagePath()); 195 rosterAttributesModel.updateModel(r); 196 } 197 198 public void dispose() { 199 if (log.isDebugEnabled()) { 200 log.debug("dispose"); 201 } 202 if (_imageFilePath != null) { 203 _imageFilePath.removeDnd(); 204 } 205 if (_iconFilePath != null) { 206 _iconFilePath.removeDnd(); 207 } 208 } 209 210 private static class RosterAttributesTableModel extends AbstractTableModel implements ResizableRowDataModel, PropertyChangeListener { 211 212 Vector<KeyValueModel> attributes; 213 String[] titles; 214 boolean wasModified; 215 boolean isUpdatingModel = false; 216 JTable associatedTable; 217 private static final String EMPTY_FIELD = "..."; 218 219 private static class KeyValueModel { 220 public String key, value; 221 222 public KeyValueModel(String k, String v) { 223 key = k; 224 value = v; 225 } 226 } 227 228 public RosterAttributesTableModel(RosterEntry r) { 229 setModel(r); 230 titles = new String[2]; 231 titles[0] = Bundle.getMessage("MediaRosterAttributeName"); 232 titles[1] = Bundle.getMessage("MediaRosterAttributeValue"); 233 } 234 235 public void setModel(RosterEntry r) { 236 attributes = new Vector<>(r.getAttributes().size()); 237 for (String key : r.getAttributes()) { 238 attributes.add(new KeyValueModel(key, r.getAttribute(key))); 239 } 240 wasModified = false; 241 r.addPropertyChangeListener(this); 242 } 243 244 @Override 245 public void propertyChange(PropertyChangeEvent evt) { 246 if (isUpdatingModel) { 247 return; 248 } 249 if (evt.getPropertyName().startsWith(RosterEntry.ATTRIBUTE_UPDATED) && (evt.getNewValue() instanceof String)) { 250 String key = evt.getPropertyName().substring(RosterEntry.ATTRIBUTE_UPDATED.length()); 251 keyDelete(key); 252 attributes.add(new KeyValueModel(key, (String) evt.getNewValue())); 253 wasModified = true; 254 } else if (evt.getPropertyName().startsWith(RosterEntry.ATTRIBUTE_DELETED) && (evt.getOldValue() instanceof String)) { 255 keyDelete((String) evt.getOldValue()); 256 wasModified = true; 257 } 258 if (wasModified) { 259 fireTableDataChanged(); 260 } 261 } 262 263 public void updateModel(RosterEntry r) { 264 for (KeyValueModel kv : attributes) { 265 if ((kv.key.length() > 0) && // only update if key value defined, will do the remove to 266 ((r.getAttribute(kv.key) == null) || (kv.value.compareTo(r.getAttribute(kv.key)) != 0))) { 267 r.putAttribute(kv.key, kv.value); 268 } 269 } 270 //remove undefined keys 271 // not very efficient algorithm! 272 r.getAttributes().removeIf(s -> !keyExist(s)); 273 wasModified = false; 274 } 275 276 private boolean keyExist(String k) { 277 if (k == null) { 278 return false; 279 } 280 for (KeyValueModel attribute : attributes) { 281 if (k.compareTo(attribute.key) == 0) { 282 return true; 283 } 284 } 285 return false; 286 } 287 288 private boolean keyDelete(String k) { 289 if (k == null) { 290 return false; 291 } 292 for (int i = 0; i < attributes.size(); i++) { 293 if (k.compareTo(attributes.get(i).key) == 0) { 294 attributes.remove(i); 295 return true; 296 } 297 } 298 return false; 299 } 300 301 @Override 302 public int getColumnCount() { 303 return 2; 304 } 305 306 @Override 307 public int getRowCount() { 308 return attributes.size() + 1; 309 } 310 311 @Override 312 public String getColumnName(int col) { 313 return titles[col]; 314 } 315 316 @Override 317 public Object getValueAt(int row, int col) { 318 if (row < attributes.size()) { 319 if (col == 0) { 320 return attributes.get(row).key; 321 } 322 if (col == 1) { 323 String content = attributes.get(row).value; 324 int lines = content.split("\n").length; 325 resizeRowToText(row, lines); 326 return content; 327 } 328 } 329 return EMPTY_FIELD; 330 } 331 332 @Override 333 public void resizeRowToText(int modelRow, int heightInLines) { 334 if (heightInLines < 1 ) heightInLines = 1; 335 int height = heightInLines * (InstanceManager.getDefault(GuiLafPreferencesManager.class).getFontSize() + 4); // same line height as in RosterTable 336 if (height != associatedTable.getRowHeight(modelRow)) { 337 associatedTable.setRowHeight(modelRow, height); 338 } 339 } 340 341 @Override 342 public void setValueAt(Object value, int row, int col) { 343 KeyValueModel kv; 344 345 if ((value == null) || (value.toString().equals(EMPTY_FIELD))) { 346 return; // don't allow empty key or value 347 } 348 349 if (row < attributes.size()) // already exist? 350 { 351 kv = attributes.get(row); 352 } else { 353 kv = new KeyValueModel("", ""); 354 } 355 356 if (col == 0) // update key 357 //Force keys to be save as a single string with no spaces 358 { 359 if (!keyExist(((String) value).replaceAll("\\s", ""))) // if not exist 360 { 361 kv.key = ((String) value).replaceAll("\\s", ""); 362 } else { 363 setValueAt(value + "-1", row, col); // else change key name 364 return; 365 } 366 } 367 368 if (col == 1) // update value 369 { 370 kv.value = (String) value; 371 } 372 if (row < attributes.size()) // existing one 373 { 374 attributes.set(row, kv); 375 } else { 376 attributes.add(row, kv); // new one 377 } 378 if ((col == 0) && (kv.key.compareTo("") == 0)) { 379 attributes.remove(row); // actually maybe remove 380 } 381 wasModified = true; 382 fireTableCellUpdated(row, col); 383 } 384 385 @Override 386 public boolean isCellEditable(int row, int col) { 387 // permission to edit optional columns? 388 switch (col) { 389 case 0: 390 return permissionManager.hasAtLeastPermission( 391 PermissionsProgrammer.PERMISSION_ROSTER_ADD_EDIT_REMOVE_ADDITIONAL_COLUMNS, 392 BooleanPermission.BooleanValue.TRUE); 393 case 1: 394 if (row >= attributes.size()) { // doesn't already exist? 395 return permissionManager.hasAtLeastPermission( 396 PermissionsProgrammer.PERMISSION_ROSTER_ADD_EDIT_REMOVE_ADDITIONAL_COLUMNS, 397 BooleanPermission.BooleanValue.TRUE); 398 } 399 return permissionManager.hasAtLeastPermission( 400 PermissionsProgrammer.PERMISSION_ROSTER_ADDED_COLUMNS, 401 BooleanPermission.BooleanValue.TRUE); 402 default: 403 log.error("Unknown column: {}", col, new IllegalArgumentException("Unknown column")); 404 return false; 405 } 406 } 407 408 public boolean wasModified() { 409 return wasModified; 410 } 411 } 412 413 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RosterMediaPane.class); 414}