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}