001package jmri.jmrit.beantable.light;
002
003import java.awt.*;
004import java.awt.event.MouseAdapter;
005import java.awt.event.MouseEvent;
006import java.awt.image.BufferedImage;
007import java.io.File;
008import java.io.IOException;
009
010import javax.annotation.Nonnull;
011import javax.imageio.ImageIO;
012import javax.swing.*;
013import javax.swing.table.TableCellEditor;
014import javax.swing.table.TableCellRenderer;
015
016import jmri.*;
017import jmri.jmrit.beantable.BeanTableDataModel;
018import static jmri.jmrit.beantable.LightTableAction.getDescriptionText;
019
020/**
021 * Data model for a Light Table.
022 * Code originally within LightTableAction.
023 * 
024 * @author Dave Duchamp Copyright (C) 2004
025 * @author Egbert Broerse Copyright (C) 2017
026 * @author Steve Young Copyright (C) 2021
027 */
028public class LightTableDataModel extends BeanTableDataModel<Light> {
029    
030    static public final int ENABLECOL = BeanTableDataModel.NUMCOLUMN;
031    static public final int INTENSITYCOL = ENABLECOL + 1;
032    static public final int EDITCOL = INTENSITYCOL + 1;
033    static public final int CONTROLCOL = EDITCOL + 1;
034
035    // for icon state col
036    protected boolean _graphicState = false; // updated from prefs
037    
038    public LightTableDataModel(){
039        super();
040        initTable();
041    }
042
043    public LightTableDataModel(Manager<Light> mgr){
044        super();
045        setManager(mgr);
046        initTable();
047    }
048    
049    private void initTable() {
050
051        _graphicState = InstanceManager.getDefault(jmri.util.gui.GuiLafPreferencesManager.class).isGraphicTableState();
052        
053    }
054    
055    private Manager<Light> lightManager;
056    
057    /**
058     * {@inheritDoc}
059     */
060    @Nonnull
061    @Override
062    public Manager<Light> getManager() {
063        if (lightManager == null) {
064            lightManager = InstanceManager.getDefault(LightManager.class);
065        }
066        return lightManager;
067    }
068
069    /**
070     * {@inheritDoc}
071     */
072    @Override
073    protected final void setManager(@Nonnull Manager<Light> manager) {
074        if (!(manager instanceof LightManager)) {
075            return;
076        }
077        getManager().removePropertyChangeListener(this);
078        if (sysNameList != null) {
079            for (int i = 0; i < sysNameList.size(); i++) {
080                // if object has been deleted, it's not here; ignore it
081                NamedBean b = getBySystemName(sysNameList.get(i));
082                if (b != null) {
083                    b.removePropertyChangeListener(this);
084                }
085            }
086        }
087        lightManager = manager;
088        getManager().addPropertyChangeListener(this);
089        updateNameList();
090    }
091    
092    /**
093     * {@inheritDoc}
094     */
095    @Override
096    public int getColumnCount() {
097        return CONTROLCOL + 1 + getPropertyColumnCount();
098    }
099
100    /**
101     * {@inheritDoc}
102     */
103    @Override
104    public String getColumnName(int col) {
105        switch (col) {
106            case EDITCOL:
107                return ""; // no heading on "Edit"
108            case INTENSITYCOL:
109                return Bundle.getMessage("ColumnHeadIntensity");
110            case ENABLECOL:
111                return Bundle.getMessage("ColumnHeadEnabled");
112            case CONTROLCOL:
113                return Bundle.getMessage("LightControllerTitlePlural");
114            default:
115                return super.getColumnName(col);
116        }
117    }
118
119    /**
120     * {@inheritDoc}
121     */
122    @Override
123    public Class<?> getColumnClass(int col) {
124        switch (col) {
125            case EDITCOL:
126                return JButton.class;
127            case INTENSITYCOL:
128                return Double.class;
129            case ENABLECOL:
130                return Boolean.class;
131            case CONTROLCOL:
132                return String.class;
133            case VALUECOL:  // may use an image to show light state
134                return ( _graphicState ? JLabel.class : JButton.class );
135            default:
136                return super.getColumnClass(col);
137        }
138    }
139
140    /**
141     * {@inheritDoc}
142     */
143    @Override
144    public int getPreferredWidth(int col) {
145        switch (col) {
146            case USERNAMECOL: // override default value for UserName column
147            case CONTROLCOL:
148                return new JTextField(16).getPreferredSize().width;
149            case EDITCOL:
150                return new JButton(Bundle.getMessage("ButtonEdit")).getPreferredSize().width+4;
151            case INTENSITYCOL:
152            case ENABLECOL:
153                return new JTextField(6).getPreferredSize().width;
154            default:
155                return super.getPreferredWidth(col);
156        }
157    }
158
159    /**
160     * {@inheritDoc}
161     */
162    @Override
163    public boolean isCellEditable(int row, int col) {
164        switch (col) {
165            case INTENSITYCOL:
166                return getValueAt(row, SYSNAMECOL) instanceof VariableLight;
167            case EDITCOL:
168            case ENABLECOL:
169                return true;
170            case CONTROLCOL:
171                return false;
172            default:
173                return super.isCellEditable(row, col);
174        }
175    }
176
177    /**
178     * {@inheritDoc}
179     */
180    @Override
181    public String getValue(String name) {
182        Light l = lightManager.getBySystemName(name);
183        return (l==null ? ("Failed to find " + name) : l.describeState(l.getState()));
184    }
185
186    /**
187     * {@inheritDoc}
188     */
189    @Override
190    public Object getValueAt(int row, int col) {
191        Light l = (Light) super.getValueAt(row, SYSNAMECOL);
192        if (l == null){
193            return null;
194        }
195        switch (col) {
196            case EDITCOL:
197                return Bundle.getMessage("ButtonEdit");
198            case INTENSITYCOL:
199                if (l instanceof VariableLight) {
200                    return ((VariableLight)l).getTargetIntensity();
201                } else {
202                    return 0.0;
203                }
204            case ENABLECOL:
205                return l.getEnabled();
206            case CONTROLCOL:
207                StringBuilder sb = new StringBuilder();
208                for (LightControl lc : l.getLightControlList()) {
209                    sb.append(getDescriptionText(lc, lc.getControlType()));
210                    sb.append(" ");
211                }
212                return sb.toString();
213            default:
214                return super.getValueAt(row, col);
215        }
216    }
217
218    /**
219     * {@inheritDoc}
220     */
221    @Override
222    public void setValueAt(Object value, int row, int col) {
223        Light l = (Light) getValueAt(row, SYSNAMECOL);
224        if (l == null){
225            return;
226        }
227        switch (col) {
228            case EDITCOL:
229                // Use separate Runnable so window is created on top
230                javax.swing.SwingUtilities.invokeLater(() -> {
231                    editButton(l);
232                });
233                break;
234            case INTENSITYCOL:
235                // alternate
236                try {
237                    if (l instanceof VariableLight) {
238                        double intensity = Math.max(0, Math.min(1.0, (Double) value));
239                        ((VariableLight)l).setTargetIntensity(intensity);
240                    } else {
241                        double intensity = ((Double) value);
242                        l.setCommandedState( intensity > 0.5 ? Light.ON : Light.OFF);
243                    }
244                } catch (IllegalArgumentException e1) {
245                    jmri.util.swing.JmriJOptionPane.showMessageDialog(null,  e1.getMessage());
246                }
247                break;
248            case ENABLECOL:
249                l.setEnabled(!l.getEnabled());
250                break;
251            case VALUECOL:
252                clickOn(l);
253                fireTableRowsUpdated(row, row);
254                break;
255            default:
256                super.setValueAt(value, row, col);
257                break;
258        }
259    }
260    
261    private void editButton(Light bean){
262        jmri.jmrit.beantable.beanedit.LightEditAction beanEdit = new jmri.jmrit.beantable.beanedit.LightEditAction();
263        beanEdit.setBean(bean);
264        beanEdit.actionPerformed(null);
265    }
266
267    /**
268     * Delete the bean after all the checking has been done.
269     * <p>
270     * Deactivate the light, then use the superclass to delete it.
271     */
272    @Override
273    protected void doDelete(Light bean) {
274        bean.deactivateLight();
275        super.doDelete(bean);
276    }
277
278    // all properties update for now
279    @Override
280    protected boolean matchPropertyName(java.beans.PropertyChangeEvent e) {
281        return true;
282    }
283
284    /**
285     * {@inheritDoc}
286     */
287    @Override
288    public Light getBySystemName(@Nonnull String name) {
289        return lightManager.getBySystemName(name);
290    }
291
292    /**
293     * {@inheritDoc}
294     */
295    @Override
296    public Light getByUserName(@Nonnull String name) {
297        return InstanceManager.getDefault(LightManager.class).getByUserName(name);
298    }
299
300    /**
301     * {@inheritDoc}
302     */
303    @Override
304    protected String getMasterClassName() {
305        return jmri.jmrit.beantable.LightTableAction.class.getName();
306    }
307
308    /**
309     * {@inheritDoc}
310     */
311    @Override
312    public void clickOn(Light t) {
313        t.setState( t.getState()==Light.OFF ? Light.ON : Light.OFF );
314    }
315
316    /**
317     * {@inheritDoc}
318     */
319    @Override
320    public JButton configureButton() {
321        return new JButton(" " + Bundle.getMessage("StateOff") + " ");
322    }
323
324    /**
325     * Customize the light table Value (State) column to show an
326     * appropriate graphic for the light state if _graphicState = true,
327     * or (default) just show the localized state text when the
328     * TableDataModel is being called from ListedTableAction.
329     *
330     * @param table a JTable of Lights
331     */
332    @Override
333    protected void configValueColumn(JTable table) {
334        // have the value column hold a JPanel (icon)
335        //setColumnToHoldButton(table, VALUECOL, new JLabel("123456")); // for small round icon, but cannot be converted to JButton
336        // add extras, override BeanTableDataModel
337        log.debug("Light configValueColumn (I am {})", this);
338        if (_graphicState) { // load icons, only once
339            table.setDefaultEditor(JLabel.class, new ImageIconRenderer()); // editor
340            table.setDefaultRenderer(JLabel.class, new ImageIconRenderer()); // item class copied from SwitchboardEditor panel
341        } else {
342            super.configValueColumn(table); // classic text style state indication
343        }
344    }
345
346    /**
347     * Visualize state in table as a graphic, customized for Lights (2
348     * states + ... for transitioning). Renderer and Editor are
349     * identical, as the cell contents are not actually edited, only
350     * used to toggle state using {@link #clickOn(Light)}.
351     *
352     */
353    static class ImageIconRenderer extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {
354
355        protected JLabel label;
356        protected String rootPath = "resources/icons/misc/switchboard/"; // also used in display.switchboardEditor
357        protected char beanTypeChar = 'L'; // for Light
358        protected String onIconPath = rootPath + beanTypeChar + "-on-s.png";
359        protected String offIconPath = rootPath + beanTypeChar + "-off-s.png";
360        protected BufferedImage onImage;
361        protected BufferedImage offImage;
362        protected ImageIcon onIcon;
363        protected ImageIcon offIcon;
364        protected int iconHeight = -1;
365
366        @Override
367        public Component getTableCellRendererComponent(
368                JTable table, Object value, boolean isSelected,
369                boolean hasFocus, int row, int column) {
370            log.debug("Renderer Item = {}, State = {}", row, value);
371            if (iconHeight < 0) { // load resources only first time, either for renderer or editor
372                loadIcons();
373                log.debug("icons loaded");
374            }
375            return updateLabel((String) value, row);
376        }
377
378        @Override
379        public Component getTableCellEditorComponent(
380                JTable table, Object value, boolean isSelected,
381                int row, int column) {
382            log.debug("Renderer Item = {}, State = {}", row, value);
383            if (iconHeight < 0) { // load resources only first time, either for renderer or editor
384                loadIcons();
385                log.debug("icons loaded");
386            }
387            return updateLabel((String) value, row);
388        }
389
390        public JLabel updateLabel(String value, int row) {
391            if (iconHeight > 0) { // if necessary, increase row height;
392                log.debug("TODO adjust table row height for Lights?");
393                //table.setRowHeight(row, Math.max(table.getRowHeight(), iconHeight - 5));
394            }
395            if (value.equals(Bundle.getMessage("StateOff")) && offIcon != null) {
396                label = new JLabel(offIcon);
397                label.setVerticalAlignment(JLabel.BOTTOM);
398                log.debug("offIcon set");
399            } else if (value.equals(Bundle.getMessage("StateOn")) && onIcon != null) {
400                label = new JLabel(onIcon);
401                label.setVerticalAlignment(JLabel.BOTTOM);
402                log.debug("onIcon set");
403            } else if (value.equals(Bundle.getMessage("BeanStateInconsistent"))) {
404                label = new JLabel("X", JLabel.CENTER); // centered text alignment
405                label.setForeground(Color.red);
406                log.debug("Light state inconsistent");
407                iconHeight = 0;
408            } else if (value.equals(Bundle.getMessage("LightStateIntermediate"))) {
409                label = new JLabel("...", JLabel.CENTER); // centered text alignment
410                log.debug("Light state in transition");
411                iconHeight = 0;
412            } else { // failed to load icon
413                label = new JLabel(value, JLabel.CENTER); // centered text alignment
414                log.warn("Error reading icons for LightTable");
415                iconHeight = 0;
416            }
417            label.setToolTipText(value);
418            label.addMouseListener(new MouseAdapter() {
419                @Override
420                public final void mousePressed(MouseEvent evt) {
421                    log.debug("Clicked on icon in row {}", row);
422                    stopCellEditing();
423                }
424            });
425            return label;
426        }
427
428        @Override
429        public Object getCellEditorValue() {
430            log.debug("getCellEditorValue, me = {})", this);
431            return this.toString();
432        }
433
434        /**
435         * Read and buffer graphics. Only called once for this table.
436         *
437         * @see #getTableCellEditorComponent(JTable, Object, boolean,
438         * int, int)
439         */
440        protected void loadIcons() {
441            try {
442                onImage = ImageIO.read(new File(onIconPath));
443                offImage = ImageIO.read(new File(offIconPath));
444            } catch (IOException ex) {
445                log.error("error reading image from {} or {}", onIconPath, offIconPath, ex);
446            }
447            log.debug("Success reading images");
448            int imageWidth = onImage.getWidth();
449            int imageHeight = onImage.getHeight();
450            // scale icons 50% to fit in table rows
451            Image smallOnImage = onImage.getScaledInstance(imageWidth / 2, imageHeight / 2, Image.SCALE_DEFAULT);
452            Image smallOffImage = offImage.getScaledInstance(imageWidth / 2, imageHeight / 2, Image.SCALE_DEFAULT);
453            onIcon = new ImageIcon(smallOnImage);
454            offIcon = new ImageIcon(smallOffImage);
455            iconHeight = onIcon.getIconHeight();
456        }
457
458    }
459    
460    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LightTableDataModel.class);
461            
462}