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