001package jmri.jmrit.beantable.block;
002
003import java.awt.*;
004import java.awt.event.MouseAdapter;
005import java.awt.event.MouseEvent;
006import java.awt.image.BufferedImage;
007
008import java.beans.PropertyChangeEvent;
009
010import java.io.File;
011import java.io.IOException;
012
013import java.util.*;
014
015import javax.annotation.CheckForNull;
016import javax.annotation.Nonnull;
017import javax.imageio.ImageIO;
018import javax.swing.*;
019import javax.swing.table.TableCellEditor;
020import javax.swing.table.TableCellRenderer;
021import javax.swing.table.TableModel;
022
023import jmri.*;
024import jmri.implementation.SignalSpeedMap;
025import jmri.jmrit.beantable.*;
026import jmri.jmrit.beantable.beanedit.BlockEditAction;
027import jmri.util.gui.GuiLafPreferencesManager;
028import jmri.util.swing.JComboBoxUtil;
029import jmri.util.swing.JmriJOptionPane;
030
031/**
032 * Data model for a Block Table.
033 * Code originally within BlockTableAction.
034 *
035 * @author Bob Jacobsen Copyright (C) 2003, 2008
036 * @author Egbert Broerse Copyright (C) 2017
037 * @author Steve Young Copyright (C) 2021
038 */
039public class BlockTableDataModel extends BeanTableDataModel<Block> {
040
041    public static final int EDITCOL = BeanTableDataModel.NUMCOLUMN;
042    public static final int DIRECTIONCOL = EDITCOL + 1;
043    public static final int LENGTHCOL = DIRECTIONCOL + 1;
044    public static final int CURVECOL = LENGTHCOL + 1;
045    public static final int STATECOL = CURVECOL + 1;
046    public static final int SENSORCOL = STATECOL + 1;
047    public static final int REPORTERCOL = SENSORCOL + 1;
048    public static final int CURRENTREPCOL = REPORTERCOL + 1;
049    public static final int PERMISCOL = CURRENTREPCOL + 1;
050    public static final int SPEEDCOL = PERMISCOL + 1;
051    public static final int GHOSTCOL = SPEEDCOL + 1;
052    public static final int COLUMNCOUNT = GHOSTCOL + 1;
053
054    private final boolean _graphicState =
055        InstanceManager.getDefault(GuiLafPreferencesManager.class).isGraphicTableState();
056
057    private Vector<String> speedList = new Vector<>();
058
059    private String defaultBlockSpeedText;
060
061    public BlockTableDataModel(Manager<Block> mgr){
062        super();
063        setManager(mgr); // for consistency with other BeanTableModels, default BlockManager always used.
064
065        defaultBlockSpeedText = Bundle.getMessage("UseGlobal", "Global") + " " +
066            InstanceManager.getDefault(BlockManager.class).getDefaultSpeed(); // first entry in drop down list
067        speedList.add(defaultBlockSpeedText);
068        Vector<String> speedMap = InstanceManager.getDefault(SignalSpeedMap.class).getValidSpeedNames();
069        for (int i = 0; i < speedMap.size(); i++) {
070            if (!speedList.contains(speedMap.get(i))) {
071                speedList.add(speedMap.get(i));
072            }
073        }
074
075    }
076
077    @Override
078    public String getValue(String name) {
079        if (name == null) {
080            log.warn("requested getValue(null)");
081            return "(no name)";
082        }
083        Block b = InstanceManager.getDefault(BlockManager.class).getBySystemName(name);
084        if (b == null) {
085            log.debug("requested getValue(\"{}\"), Block doesn't exist", name);
086            return "(no Block)";
087        }
088        Object m = b.getValue();
089        if (m != null) {
090            if ( m instanceof Reportable) {
091                return ((Reportable) m).toReportString();
092            }
093            else {
094                return m.toString();
095            }
096        } else {
097            return "";
098        }
099    }
100
101    @Override
102    public Manager<Block> getManager() {
103        return InstanceManager.getDefault(BlockManager.class);
104    }
105
106    @Override
107    public Block getBySystemName(@Nonnull String name) {
108        return InstanceManager.getDefault(BlockManager.class).getBySystemName(name);
109    }
110
111    @Override
112    public Block getByUserName(@Nonnull String name) {
113        return InstanceManager.getDefault(BlockManager.class).getByUserName(name);
114    }
115
116    @Override
117    protected String getMasterClassName() {
118        return BlockTableAction.class.getName();
119    }
120
121    @Override
122    public void clickOn(Block t) {
123        // don't do anything on click; not used in this class, because
124        // we override setValueAt
125    }
126
127    @Override
128    public int getColumnCount() {
129        return COLUMNCOUNT;
130    }
131
132    @Override
133    public Object getValueAt(int row, int col) {
134        // some error checking
135        if (row >= sysNameList.size()) {
136            log.error("requested getValueAt(\"{}\"), row outside of range", row);
137            return "Error table size";
138        }
139        Block b = getBySystemName(sysNameList.get(row));
140        if (b == null) {
141            log.error("requested getValueAt(\"{}\"), Block doesn't exist", row);
142            return "(no Block)";
143        }
144        switch (col) {
145            case DIRECTIONCOL:
146                return Path.decodeDirection(b.getDirection());
147            case CURVECOL:
148                BlockCurvatureJComboBox box = new BlockCurvatureJComboBox(b.getCurvature());
149                box.setJTableCellClientProperties();
150                return box;
151            case LENGTHCOL:
152                return metricUi ?  b.getLengthCm() : b.getLengthIn();
153            case PERMISCOL:
154                return b.getPermissiveWorking();
155            case GHOSTCOL:
156                return b.getIsGhost();
157            case SPEEDCOL:
158                String speed = b.getBlockSpeed();
159                if (!speedList.contains(speed)) {
160                    speedList.add(speed);
161                }
162                JComboBox<String> c = new JComboBox<>(speedList);
163                c.setEditable(true);
164                c.setSelectedItem(speed);
165                JComboBoxUtil.setupComboBoxMaxRows(c);
166                return c;
167            case STATECOL:
168                return b.describeState(b.getState());
169            case SENSORCOL:
170                return b.getSensor();
171            case REPORTERCOL:
172                return b.getReporter();
173            case CURRENTREPCOL:
174                return b.isReportingCurrent();
175            case EDITCOL:
176                return Bundle.getMessage("ButtonEdit");
177            default:
178                return super.getValueAt(row, col);
179        }
180    }
181
182    @Override
183    public void setValueAt(Object value, int row, int col) {
184        // no setting of block state from table
185        Block b = getBySystemName(sysNameList.get(row));
186        switch (col) {
187            case VALUECOL:
188                b.setValue(value);
189                break;
190            case LENGTHCOL:
191                float len = 0.0f;
192                try {
193                    len = jmri.util.IntlUtilities.floatValue(value.toString());
194                } catch (java.text.ParseException ex2) {
195                    log.error("Error parsing length value of \"{}\"", value);
196                }   // block setLength() expecting value in mm, TODO: unit testing around this.
197                b.setLength( metricUi ? len * 10.0f : len * 25.4f);
198                break;
199            case CURVECOL:
200                b.setCurvature(BlockCurvatureJComboBox.getCurvatureFromObject(value));
201                break;
202            case PERMISCOL:
203                b.setPermissiveWorking((Boolean) value);
204                break;
205            case GHOSTCOL:
206                b.setIsGhost((boolean) value);
207                break;
208            case SPEEDCOL:
209                @SuppressWarnings("unchecked")
210                String speed = (String) ((JComboBox<String>) value).getSelectedItem();
211                try {
212                    b.setBlockSpeed(speed);
213                } catch (JmriException ex) {
214                    JmriJOptionPane.showMessageDialog(null, ex.getMessage() + "\n" + speed);
215                    return;
216                }
217                if (!speedList.contains(speed) && !speed.contains("Global")) { // NOI18N
218                    speedList.add(speed);
219                }
220                break;
221            case REPORTERCOL:
222                if ( value==null || value instanceof Reporter) {
223                    b.setReporter((Reporter)value);
224                }
225                break;
226            case SENSORCOL:
227                b.setSensor(value instanceof Sensor ? ((Sensor)value).getDisplayName() : "");
228                break;
229            case CURRENTREPCOL:
230                b.setReportingCurrent((Boolean) value);
231                break;
232            case EDITCOL:
233                javax.swing.SwingUtilities.invokeLater( () -> editButton(b) );
234                break;
235            default:
236                super.setValueAt(value, row, col);
237                break;
238        }
239    }
240
241    @Override
242    public String getColumnName(int col) {
243        switch (col) {
244            case DIRECTIONCOL:
245                return Bundle.getMessage("BlockDirection");
246            case VALUECOL:
247                return Bundle.getMessage("BlockValue");
248            case CURVECOL:
249                return Bundle.getMessage("BlockCurveColName");
250            case LENGTHCOL:
251                return Bundle.getMessage("BlockLengthColName");
252            case PERMISCOL:
253                return Bundle.getMessage("BlockPermColName");
254            case GHOSTCOL:
255                return Bundle.getMessage("BlockGhostColName");
256            case SPEEDCOL:
257                return Bundle.getMessage("BlockSpeedColName");
258            case STATECOL:
259                return Bundle.getMessage("BlockState");
260            case REPORTERCOL:
261                return Bundle.getMessage("BlockReporter");
262            case SENSORCOL:
263                return Bundle.getMessage("BlockSensor");
264            case CURRENTREPCOL:
265                return Bundle.getMessage("BlockReporterCurrent");
266            case EDITCOL:
267                return Bundle.getMessage("ButtonEdit");
268            default:
269                return super.getColumnName(col);
270        }
271    }
272
273    @Override
274    public Class<?> getColumnClass(int col) {
275        switch (col) {
276            case DIRECTIONCOL:
277            case VALUECOL: // not a button
278                return String.class;
279            case LENGTHCOL:
280                return Float.class;
281            case STATECOL: // may use an image to show block state
282                if (_graphicState) {
283                    return JLabel.class;
284                } else {
285                    return String.class;
286                }
287            case SPEEDCOL:
288            case CURVECOL:
289                return JComboBox.class;
290            case REPORTERCOL:
291                return Reporter.class;
292            case SENSORCOL:
293                return Sensor.class;
294            case CURRENTREPCOL:
295            case PERMISCOL:
296            case GHOSTCOL:
297                return Boolean.class;
298            case EDITCOL:
299                return JButton.class;
300            default:
301                return super.getColumnClass(col);
302        }
303    }
304
305    @Override
306    public int getPreferredWidth(int col) {
307        switch (col) {
308            case DIRECTIONCOL:
309            case LENGTHCOL:
310            case PERMISCOL:
311            case GHOSTCOL:
312            case SPEEDCOL:
313            case CURRENTREPCOL:
314            case EDITCOL:
315                return new JTextField(7).getPreferredSize().width;
316            case CURVECOL:
317            case STATECOL:
318            case REPORTERCOL:
319            case SENSORCOL:
320                return new JTextField(8).getPreferredSize().width;
321            default:
322                return super.getPreferredWidth(col);
323        }
324    }
325
326    @Override
327    public void configValueColumn(JTable table) {
328        // value column isn't button, so config is null
329    }
330
331    @Override
332    public boolean isCellEditable(int row, int col) {
333        switch (col) {
334            case CURVECOL:
335            case LENGTHCOL:
336            case PERMISCOL:
337            case GHOSTCOL:
338            case SPEEDCOL:
339            case REPORTERCOL:
340            case SENSORCOL:
341            case CURRENTREPCOL:
342            case EDITCOL:
343                return true;
344            case STATECOL:
345                return false;
346            default:
347                return super.isCellEditable(row, col);
348        }
349    }
350
351    @Override
352    public JTable makeJTable(@Nonnull String name, @Nonnull TableModel model,
353        @CheckForNull RowSorter<? extends TableModel> sorter) {
354        if (!(model instanceof BlockTableDataModel)){
355            throw new IllegalArgumentException("Model is not a BlockTableDataModel");
356        }
357        return configureJTable(name, new BlockTableJTable((BlockTableDataModel)model), sorter);
358    }
359
360    @Override
361    public void configureTable(JTable table) {
362        configStateColumn(table);
363        super.configureTable(table);
364    }
365
366    void editButton(Block b) {
367        BlockEditAction beanEdit = new BlockEditAction();
368        beanEdit.setBean(b);
369        beanEdit.actionPerformed(null);
370    }
371
372    /**
373     * returns true for all Block properties.
374     * @param e property event that has changed.
375     * @return true as all matched.
376     */
377    @Override
378    protected boolean matchPropertyName(PropertyChangeEvent e) {
379        return true;
380    }
381
382    @Override
383    public JButton configureButton() {
384        log.error("configureButton should not have been called");
385        return null;
386    }
387
388    @Override
389    public void propertyChange(PropertyChangeEvent e) {
390        if ( "DefaultBlockSpeedChange".equals(e.getPropertyName()) ) { // NOI18N
391            updateSpeedList();
392        } else {
393            super.propertyChange(e);
394        }
395    }
396
397    private boolean metricUi = InstanceManager.getDefault(UserPreferencesManager.class)
398        .getSimplePreferenceState(BlockTableAction.BLOCK_METRIC_PREF);
399
400    /**
401     * Set and refresh the UI to use Metric or Imperial values.
402     * @param boo true if metric, false for Imperial.
403     */
404    public void setMetric(boolean boo){
405        metricUi = boo;
406        fireTableDataChanged();
407    }
408
409    private void updateSpeedList() {
410        speedList.remove(defaultBlockSpeedText);
411        defaultBlockSpeedText = (Bundle.getMessage("UseGlobal", "Global")
412            + " " + InstanceManager.getDefault(BlockManager.class).getDefaultSpeed());
413        speedList.add(0, defaultBlockSpeedText);
414        fireTableDataChanged();
415    }
416
417    public void setDefaultSpeeds(JFrame frame) {
418        JComboBox<String> blockSpeedCombo = new JComboBox<>(speedList);
419        JComboBoxUtil.setupComboBoxMaxRows(blockSpeedCombo);
420
421        blockSpeedCombo.setEditable(true);
422
423        JPanel block = new JPanel();
424        block.add(new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("BlockSpeedLabel"))));
425        block.add(blockSpeedCombo);
426
427        blockSpeedCombo.removeItem(defaultBlockSpeedText);
428
429        blockSpeedCombo.setSelectedItem(InstanceManager.getDefault(BlockManager.class).getDefaultSpeed());
430
431        // block of options above row of buttons; gleaned from Maintenance.makeDialog()
432        // can be accessed by Jemmy in GUI test
433        String title = Bundle.getMessage("BlockSpeedLabel");
434        // build JPanel for comboboxes
435        JPanel speedspanel = new JPanel();
436        speedspanel.setLayout(new BoxLayout(speedspanel, BoxLayout.PAGE_AXIS));
437        speedspanel.add(new JLabel(Bundle.getMessage("BlockSpeedSelectDialog")));
438        //default LEFT_ALIGNMENT
439        block.setAlignmentX(Component.LEFT_ALIGNMENT);
440        speedspanel.add(block);
441
442        int retval = JmriJOptionPane.showConfirmDialog(
443                frame,
444                speedspanel,
445                title,
446                JmriJOptionPane.OK_CANCEL_OPTION,
447                JmriJOptionPane.INFORMATION_MESSAGE);
448        log.debug("Retval = {}", retval);
449        if (retval != JmriJOptionPane.OK_OPTION) { // OK button not clicked
450            return;
451        }
452
453        String speedValue = (String) blockSpeedCombo.getSelectedItem();
454        //We will allow the turnout manager to handle checking if the values have changed
455        try {
456            InstanceManager.getDefault(BlockManager.class).setDefaultSpeed(speedValue);
457        } catch (IllegalArgumentException ex) {
458            JmriJOptionPane.showMessageDialog(frame, ex.getLocalizedMessage()+ "\n" + speedValue);
459        }
460    }
461
462    /**
463     * Customize the block table State column to show an appropriate
464     * graphic for the block occupancy state if _graphicState = true, or
465     * (default) just show the localized state text when the
466     * TableDataModel is being called from ListedTableAction.
467     *
468     * @param table a JTable of Blocks
469     */
470    protected void configStateColumn(JTable table) {
471        log.debug("Block configStateColumn (I am {})", this);
472        if (_graphicState) {
473            // have the state column hold a JPanel (icon)
474            table.setDefaultRenderer(JLabel.class, new ImageIconRenderer());
475        } // else, classic text style state indication, do nothing extra
476    }
477
478    // state column may be image so have the tooltip as text version of Block state.
479    // length column tooltip confirms inches or cm.
480    @Override
481    public String getCellToolTip(JTable table, int modelRow, int modelCol) {
482        switch (modelCol) {
483            case BlockTableDataModel.STATECOL:
484                Block b = (Block) getValueAt(modelRow, 0);
485                return b.describeState(b.getState());
486            case BlockTableDataModel.LENGTHCOL:
487                return ( metricUi ? Bundle.getMessage("LengthCentimeters"): Bundle.getMessage("LengthInches"));
488            default:
489                return super.getCellToolTip(table, modelRow, modelCol);
490        }
491    }
492
493    /**
494     * Visualize state in table as a graphic, customized for Blocks (2
495     * states). Renderer and Editor are identical, as the cell contents
496     * are not actually edited.
497     *
498     */
499    static class ImageIconRenderer extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {
500
501        protected JLabel label;
502        protected String rootPath = "resources/icons/misc/switchboard/"; // also used in display.switchboardEditor
503        protected char beanTypeChar = 'S'; // reuse Sensor icon for block state
504        protected String onIconPath = rootPath + beanTypeChar + "-on-s.png";
505        protected String offIconPath = rootPath + beanTypeChar + "-off-s.png";
506        protected BufferedImage onImage;
507        protected BufferedImage offImage;
508        protected ImageIcon onIcon;
509        protected ImageIcon offIcon;
510        protected int iconHeight = -1;
511
512        @Override
513        public Component getTableCellRendererComponent(
514                JTable table, Object value, boolean isSelected,
515                boolean hasFocus, int row, int column) {
516            log.debug("Renderer Item = {}, State = {}", row, value);
517            if (iconHeight < 0) { // load resources only first time, either for renderer or editor
518                loadIcons();
519                log.debug("icons loaded");
520            }
521            Block b = (Block) table.getModel().getValueAt(row, 0);
522            return updateLabel(b);
523        }
524
525        @Override
526        public Component getTableCellEditorComponent(
527                JTable table, Object value, boolean isSelected,
528                int row, int column) {
529            log.debug("Renderer Item = {}, State = {}", row, value);
530            if (iconHeight < 0) { // load resources only first time, either for renderer or editor
531                loadIcons();
532                log.debug("icons loaded");
533            }
534            Block b = (Block) table.getModel().getValueAt(row, 0);
535            return updateLabel(b);
536        }
537
538        public JLabel updateLabel(Block b) {
539            // TODO Undetected ?
540            // TODO adjust table row height for Block icons
541            //  if (iconHeight > 0) { // if necessary, increase row height;
542            //table.setRowHeight(row, Math.max(table.getRowHeight(), iconHeight - 5));
543            //                     }
544            if (b.getState()==Block.UNOCCUPIED && offIcon != null) {
545                label = new JLabel(offIcon);
546                label.setVerticalAlignment(SwingConstants.BOTTOM);
547            } else if (b.getState()==Block.OCCUPIED && onIcon != null) {
548                label = new JLabel(onIcon);
549                label.setVerticalAlignment(SwingConstants.BOTTOM);
550            } else if (b.getState()==Block.INCONSISTENT) {
551                label = new JLabel("X", SwingConstants.CENTER); // centered text alignment
552                label.setForeground(Color.red);
553                iconHeight = 0;
554            } else { // Unknown Undetected Other
555                label = new JLabel("?", SwingConstants.CENTER); // centered text alignment
556                iconHeight = 0;
557            }
558            label.addMouseListener(new MouseAdapter() {
559                @Override
560                public final void mousePressed(MouseEvent evt) {
561                    log.debug("Clicked on icon for block {}",b);
562                    stopCellEditing();
563                }
564            });
565            return label;
566        }
567
568        @Override
569        public Object getCellEditorValue() {
570            log.debug("getCellEditorValue, me = {})", this.toString());
571            return this.toString();
572        }
573
574        /**
575         * Read and buffer graphics. Only called once for this table.
576         *
577         * @see #getTableCellEditorComponent(JTable, Object, boolean,
578         * int, int)
579         */
580        protected void loadIcons() {
581            try {
582                onImage = ImageIO.read(new File(onIconPath));
583                offImage = ImageIO.read(new File(offIconPath));
584            } catch (IOException ex) {
585                log.error("error reading image from {} or {}", onIconPath, offIconPath, ex);
586            }
587            log.debug("Success reading images");
588            int imageWidth = onImage.getWidth();
589            int imageHeight = onImage.getHeight();
590            // scale icons 50% to fit in table rows
591            Image smallOnImage = onImage.getScaledInstance(imageWidth / 2, imageHeight / 2, Image.SCALE_DEFAULT);
592            Image smallOffImage = offImage.getScaledInstance(imageWidth / 2, imageHeight / 2, Image.SCALE_DEFAULT);
593            onIcon = new ImageIcon(smallOnImage);
594            offIcon = new ImageIcon(smallOffImage);
595            iconHeight = onIcon.getIconHeight();
596        }
597
598    } // end of ImageIconRenderer class
599
600    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BlockTableDataModel.class);
601
602}