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}