001package jmri.jmrit.operations.automation;
002
003import java.awt.BorderLayout;
004import java.awt.FlowLayout;
005import java.awt.event.ActionEvent;
006import java.awt.event.ActionListener;
007import java.beans.PropertyChangeEvent;
008import java.beans.PropertyChangeListener;
009import java.util.ArrayList;
010import java.util.List;
011
012import javax.swing.*;
013import javax.swing.table.TableCellEditor;
014import javax.swing.table.TableColumnModel;
015
016import org.slf4j.Logger;
017import org.slf4j.LoggerFactory;
018
019import jmri.InstanceManager;
020import jmri.jmrit.operations.automation.actions.Action;
021import jmri.jmrit.operations.routes.RouteLocation;
022import jmri.jmrit.operations.setup.Control;
023import jmri.jmrit.operations.trains.Train;
024import jmri.jmrit.operations.trains.TrainManager;
025import jmri.util.table.ButtonEditor;
026import jmri.util.table.ButtonRenderer;
027
028/**
029 * Table Model for edit of a automation used by operations
030 *
031 * @author Daniel Boudreau Copyright (C) 2016
032 */
033public class AutomationTableModel extends javax.swing.table.AbstractTableModel implements PropertyChangeListener {
034
035    protected static final String POINTER = "    -->";
036    
037    // Defines the columns
038    private static final int ID_COLUMN = 0;
039    private static final int CURRENT_COLUMN = ID_COLUMN + 1;
040    private static final int ACTION_COLUMN = CURRENT_COLUMN + 1;
041    private static final int TRAIN_COLUMN = ACTION_COLUMN + 1;
042    private static final int ROUTE_COLUMN = TRAIN_COLUMN + 1;
043    private static final int AUTOMATION_COLUMN = ROUTE_COLUMN + 1;
044    private static final int STATUS_COLUMN = AUTOMATION_COLUMN + 1;
045    private static final int HIAF_COLUMN = STATUS_COLUMN + 1;
046    private static final int MESSAGE_COLUMN = HIAF_COLUMN + 1;
047    private static final int UP_COLUMN = MESSAGE_COLUMN + 1;
048    private static final int DOWN_COLUMN = UP_COLUMN + 1;
049    private static final int DELETE_COLUMN = DOWN_COLUMN + 1;
050
051    private static final int HIGHEST_COLUMN = DELETE_COLUMN + 1;
052
053    public AutomationTableModel() {
054        super();
055    }
056
057    Automation _automation;
058    JTable _table;
059    AutomationTableFrame _frame;
060    boolean _matchMode = false;
061
062    private void updateList() {
063        if (_automation == null) {
064            return;
065        }
066        // first, remove listeners from the individual objects
067        removePropertyChangeAutomationItems();
068        _list = _automation.getItemsBySequenceList();
069        // and add them back in
070        for (AutomationItem item : _list) {
071            item.addPropertyChangeListener(this);
072        }
073    }
074
075    List<AutomationItem> _list = new ArrayList<>();
076
077    protected void initTable(AutomationTableFrame frame, JTable table, Automation automation) {
078        _automation = automation;
079        _table = table;
080        _frame = frame;
081
082        // add property listeners
083        if (_automation != null) {
084            _automation.addPropertyChangeListener(this);
085        }
086        initTable(table);
087    }
088
089    private void initTable(JTable table) {
090        // Install the button handlers
091        TableColumnModel tcm = table.getColumnModel();
092        ButtonRenderer buttonRenderer = new ButtonRenderer();
093        TableCellEditor buttonEditor = new ButtonEditor(new javax.swing.JButton());
094        tcm.getColumn(MESSAGE_COLUMN).setCellRenderer(buttonRenderer);
095        tcm.getColumn(MESSAGE_COLUMN).setCellEditor(buttonEditor);
096        tcm.getColumn(UP_COLUMN).setCellRenderer(buttonRenderer);
097        tcm.getColumn(UP_COLUMN).setCellEditor(buttonEditor);
098        tcm.getColumn(DOWN_COLUMN).setCellRenderer(buttonRenderer);
099        tcm.getColumn(DOWN_COLUMN).setCellEditor(buttonEditor);
100        tcm.getColumn(DELETE_COLUMN).setCellRenderer(buttonRenderer);
101        tcm.getColumn(DELETE_COLUMN).setCellEditor(buttonEditor);
102        table.setDefaultRenderer(JComboBox.class, new jmri.jmrit.symbolicprog.ValueRenderer());
103        table.setDefaultEditor(JComboBox.class, new jmri.jmrit.symbolicprog.ValueEditor());
104
105        // set column preferred widths
106        table.getColumnModel().getColumn(ID_COLUMN).setPreferredWidth(35);
107        table.getColumnModel().getColumn(CURRENT_COLUMN).setPreferredWidth(60);
108        table.getColumnModel().getColumn(ACTION_COLUMN).setPreferredWidth(200);
109        table.getColumnModel().getColumn(TRAIN_COLUMN).setPreferredWidth(200);
110        table.getColumnModel().getColumn(ROUTE_COLUMN).setPreferredWidth(200);
111        table.getColumnModel().getColumn(AUTOMATION_COLUMN).setPreferredWidth(200);
112        table.getColumnModel().getColumn(STATUS_COLUMN).setPreferredWidth(70);
113        table.getColumnModel().getColumn(HIAF_COLUMN).setPreferredWidth(50);
114        table.getColumnModel().getColumn(MESSAGE_COLUMN).setPreferredWidth(70);
115        table.getColumnModel().getColumn(UP_COLUMN).setPreferredWidth(60);
116        table.getColumnModel().getColumn(DOWN_COLUMN).setPreferredWidth(70);
117        table.getColumnModel().getColumn(DELETE_COLUMN).setPreferredWidth(70);
118
119        _frame.loadTableDetails(table);
120        // does not use a table sorter
121        table.setRowSorter(null);
122
123        updateList();
124    }
125
126    @Override
127    public int getRowCount() {
128        return _list.size();
129    }
130
131    @Override
132    public int getColumnCount() {
133        return HIGHEST_COLUMN;
134    }
135
136    @Override
137    public String getColumnName(int col) {
138        switch (col) {
139            case ID_COLUMN:
140                return Bundle.getMessage("Id");
141            case CURRENT_COLUMN:
142                return Bundle.getMessage("Current");
143            case ACTION_COLUMN:
144                return Bundle.getMessage("Action");
145            case TRAIN_COLUMN:
146                return Bundle.getMessage("Train");
147            case ROUTE_COLUMN:
148                return Bundle.getMessage("RouteLocation");
149            case AUTOMATION_COLUMN:
150                return Bundle.getMessage("AutomationOther");
151            case STATUS_COLUMN:
152                return Bundle.getMessage("Status");
153            case MESSAGE_COLUMN:
154                return Bundle.getMessage("Message");
155            case HIAF_COLUMN:
156                return Bundle.getMessage("HaltIfActionFails");
157            case UP_COLUMN:
158                return Bundle.getMessage("Up");
159            case DOWN_COLUMN:
160                return Bundle.getMessage("Down");
161            case DELETE_COLUMN:
162                return Bundle.getMessage("ButtonDelete");
163            default:
164                return "unknown"; // NOI18N
165        }
166    }
167
168    @Override
169    public Class<?> getColumnClass(int col) {
170        switch (col) {
171            case ID_COLUMN:
172                return String.class;
173            case CURRENT_COLUMN:
174                return String.class;
175            case ACTION_COLUMN:
176                return JComboBox.class;
177            case TRAIN_COLUMN:
178                return JComboBox.class;
179            case ROUTE_COLUMN:
180                return JComboBox.class;
181            case AUTOMATION_COLUMN:
182                return JComboBox.class;
183            case STATUS_COLUMN:
184                return String.class;
185            case HIAF_COLUMN:
186                return Boolean.class;
187            case MESSAGE_COLUMN:
188                return JButton.class;
189            case UP_COLUMN:
190                return JButton.class;
191            case DOWN_COLUMN:
192                return JButton.class;
193            case DELETE_COLUMN:
194                return JButton.class;
195            default:
196                return null;
197        }
198    }
199
200    @Override
201    public boolean isCellEditable(int row, int col) {
202        switch (col) {
203            case CURRENT_COLUMN:
204            case ACTION_COLUMN:
205            case TRAIN_COLUMN:
206            case ROUTE_COLUMN:
207            case AUTOMATION_COLUMN:
208            case UP_COLUMN:
209            case DOWN_COLUMN:
210            case DELETE_COLUMN:
211                return true;
212            case HIAF_COLUMN: {
213                AutomationItem item = _list.get(row);
214                return item.getAction().isMessageFailEnabled();
215            }
216            case MESSAGE_COLUMN: {
217                AutomationItem item = _list.get(row);
218                JComboBox<Action> acb = getActionComboBox(item);
219                return ((Action) acb.getSelectedItem()).isMessageOkEnabled();
220            }
221            default:
222                return false;
223        }
224    }
225
226    // TODO adding synchronized to the following causes thread lock.
227    // See line in propertyChange below:
228    // _table.scrollRectToVisible(_table.getCellRect(row, 0, true));
229    // Stack trace:
230    //    owns: Component$AWTTreeLock  (id=127)   
231    //    waiting for: AutomationTableModel  (id=128) 
232    //    AutomationTableModel.getRowCount() line: 131    
233    //    JTable.getRowCount() line: 2662 
234    //    BasicTableUI.paint(Graphics, JComponent) line: 1766 
235    //    BasicTableUI(ComponentUI).update(Graphics, JComponent) line: 161    
236    //    JTable(JComponent).paintComponent(Graphics) line: 777   
237    //    JTable(JComponent).paint(Graphics) line: 1053   
238    //    JViewport(JComponent).paintChildren(Graphics) line: 886 
239    //    JViewport(JComponent).paint(Graphics) line: 1062    
240    //    JViewport.paint(Graphics) line: 692 
241    //    JViewport(JComponent).paintToOffscreen(Graphics, int, int, int, int, int, int) line: 5223   
242    //    RepaintManager$PaintManager.paintDoubleBuffered(JComponent, Image, Graphics, int, int, int, int) line: 1572 
243    //    RepaintManager$PaintManager.paint(JComponent, JComponent, Graphics, int, int, int, int) line: 1495  
244    //    RepaintManager.paint(JComponent, JComponent, Graphics, int, int, int, int) line: 1265   
245    //    JViewport(JComponent).paintForceDoubleBuffered(Graphics) line: 1089 
246    //    JViewport.paintView(Graphics) line: 1635    
247    //    JViewport.flushViewDirtyRegion(Graphics, Rectangle) line: 1508  
248    //    JViewport.setViewPosition(Point) line: 1093 
249    //    JViewport.scrollRectToVisible(Rectangle) line: 436  
250    //    JTable(JComponent).scrollRectToVisible(Rectangle) line: 3108    
251    //    AutomationTableModel.propertyChange(PropertyChangeEvent) line: 498  
252    //    PropertyChangeSupport.fire(PropertyChangeListener[], PropertyChangeEvent) line: 335 
253    //    PropertyChangeSupport.firePropertyChange(PropertyChangeEvent) line: 327 
254    //    PropertyChangeSupport.firePropertyChange(String, Object, Object) line: 263  
255    //    Automation.setDirtyAndFirePropertyChange(String, Object, Object) line: 666  
256    //    Automation.setCurrentAutomationItem(AutomationItem) line: 279   
257    //    Automation.setNextAutomationItem() line: 243    
258    //    Automation.CheckForActionPropertyChange(PropertyChangeEvent) line: 607  
259    //    Automation.propertyChange(PropertyChangeEvent) line: 646    
260    //    PropertyChangeSupport.fire(PropertyChangeListener[], PropertyChangeEvent) line: 335 
261    //    PropertyChangeSupport.firePropertyChange(PropertyChangeEvent) line: 327 
262    //    PropertyChangeSupport.firePropertyChange(String, Object, Object) line: 263  
263    //    ResetTrainAction(Action).firePropertyChange(String, Object, Object) line: 244   
264    //    ResetTrainAction(Action).finishAction(boolean, Object[]) line: 158  
265    //    ResetTrainAction(Action).finishAction(boolean) line: 128    
266    //    ResetTrainAction.doAction() line: 27    
267    //    Automation$1.run() line: 172    
268    //    Thread.run() line: 745  
269
270    @Override
271    public Object getValueAt(int row, int col) {
272        if (row >= getRowCount()) {
273            return "ERROR row " + row; // NOI18N
274        }
275        AutomationItem item = _list.get(row);
276        if (item == null) {
277            return "ERROR automation item unknown " + row; // NOI18N
278        }
279        switch (col) {
280            case ID_COLUMN:
281                return item.getId();
282            case CURRENT_COLUMN:
283                return getCurrentPointer(row, item);
284            case ACTION_COLUMN:
285                return getActionComboBox(item);
286            case TRAIN_COLUMN:
287                return getTrainComboBox(item);
288            case ROUTE_COLUMN:
289                return getRouteLocationComboBox(item);
290            case AUTOMATION_COLUMN:
291                return getAutomationComboBox(item);
292            case STATUS_COLUMN:
293                return getStatus(item);
294            case HIAF_COLUMN:
295                return item.isHaltFailureEnabled() & item.getAction().isMessageFailEnabled();
296            case MESSAGE_COLUMN:
297                if (item.getMessage().equals(AutomationItem.NONE) && item.getMessageFail().equals(AutomationItem.NONE))
298                    return Bundle.getMessage("Add");
299                else
300                    return Bundle.getMessage("ButtonEdit");
301            case UP_COLUMN:
302                return Bundle.getMessage("Up");
303            case DOWN_COLUMN:
304                return Bundle.getMessage("Down");
305            case DELETE_COLUMN:
306                return Bundle.getMessage("ButtonDelete");
307            default:
308                return "unknown " + col; // NOI18N
309        }
310    }
311
312    @Override
313    public void setValueAt(Object value, int row, int col) {
314        if (value == null) {
315            log.debug("Warning automation table row {} still in edit", row);
316            return;
317        }
318        AutomationItem item = _list.get(row);
319        switch (col) {
320            case CURRENT_COLUMN:
321                setCurrent(item);
322                break;
323            case ACTION_COLUMN:
324                setAction(value, item);
325                break;
326            case TRAIN_COLUMN:
327                setTrain(value, item);
328                break;
329            case ROUTE_COLUMN:
330                setRouteLocation(value, item);
331                break;
332            case AUTOMATION_COLUMN:
333                setAutomationColumn(value, item);
334                break;
335            case HIAF_COLUMN:
336                item.setHaltFailureEnabled(((Boolean) value).booleanValue());
337                break;
338            case MESSAGE_COLUMN:
339                setMessage(value, item);
340                break;
341            case UP_COLUMN:
342                moveUpAutomationItem(item);
343                break;
344            case DOWN_COLUMN:
345                moveDownAutomationItem(item);
346                break;
347            case DELETE_COLUMN:
348                deleteAutomationItem(item);
349                break;
350            default:
351                break;
352        }
353    }
354
355    private String getCurrentPointer(int row, AutomationItem item) {
356        if (_automation.getCurrentAutomationItem() == item) {
357            return POINTER;
358        } else {
359            return "";
360        }
361    }
362
363    private JComboBox<Action> getActionComboBox(AutomationItem item) {
364        JComboBox<Action> cb = AutomationItem.getActionComboBox();
365        //      cb.setSelectedItem(item.getAction()); TODO understand why this didn't work, class?
366        for (int index = 0; index < cb.getItemCount(); index++) {
367            // select the action based on its action code
368            if (item.getAction() != null && (cb.getItemAt(index)).getCode() == item.getAction().getCode()) {
369                cb.setSelectedIndex(index);
370                break;
371            }
372        }
373        return cb;
374    }
375
376    private JComboBox<Train> getTrainComboBox(AutomationItem item) {
377        JComboBox<Train> cb = InstanceManager.getDefault(TrainManager.class).getTrainComboBox();
378        cb.setSelectedItem(item.getTrain());
379        // determine if train combo box is enabled
380        cb.setEnabled(item.getAction() != null && item.getAction().isTrainMenuEnabled());
381        return cb;
382    }
383
384    private JComboBox<RouteLocation> getRouteLocationComboBox(AutomationItem item) {
385        JComboBox<RouteLocation> cb = new JComboBox<>();
386        if (item.getTrain() != null && item.getTrain().getRoute() != null) {
387            cb = item.getTrain().getRoute().getComboBox();
388            cb.setSelectedItem(item.getRouteLocation());
389        }
390        // determine if route combo box is enabled
391        cb.setEnabled(item.getAction() != null && item.getAction().isRouteMenuEnabled());
392        return cb;
393    }
394
395    /**
396     * Returns either a comboBox loaded with Automations, or a goto list of
397     * AutomationItems, or TrainSchedules.
398     *
399     * @return comboBox loaded with automations or a goto automationIem list
400     */
401    private JComboBox<?> getAutomationComboBox(AutomationItem item) {
402        if (item.getAction() != null) {
403            return item.getAction().getComboBox();
404        }
405        return null;
406    }
407
408    private String getStatus(AutomationItem item) {
409        return item.getStatus();
410    }
411    
412    private void setCurrent(AutomationItem item) {
413        _automation.setCurrentAutomationItem(item);
414        _automation.resetAutomationItems(item);
415    }
416
417    private void setAction(Object value, AutomationItem item) {
418        @SuppressWarnings("unchecked")
419        JComboBox<Action> cb = (JComboBox<Action>) value;
420        item.setAction((Action) cb.getSelectedItem());
421    }
422
423    private void setTrain(Object value, AutomationItem item) {
424        @SuppressWarnings("unchecked")
425        JComboBox<Train> cb = (JComboBox<Train>) value;
426        item.setTrain((Train) cb.getSelectedItem());
427    }
428
429    private void setRouteLocation(Object value, AutomationItem item) {
430        @SuppressWarnings("unchecked")
431        JComboBox<RouteLocation> cb = (JComboBox<RouteLocation>) value;
432        item.setRouteLocation((RouteLocation) cb.getSelectedItem());
433    }
434
435    private void setAutomationColumn(Object value, AutomationItem item) {
436        item.setOther(((JComboBox<?>) value).getSelectedItem());
437    }
438
439    private void setMessage(Object value, AutomationItem item) {
440        // Create comment panel
441        final JDialog dialog = new JDialog();
442        dialog.setLayout(new BorderLayout());
443        dialog.setTitle(Bundle.getMessage("Message"));
444
445        final JTextArea messageTextArea = new JTextArea(6, 100);
446        JScrollPane messageScroller = new JScrollPane(messageTextArea);
447        messageScroller.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("MessageOk")));
448        dialog.add(messageScroller, BorderLayout.NORTH);
449        messageTextArea.setText(item.getMessage());
450        messageTextArea.setToolTipText(Bundle.getMessage("TipMessage"));
451
452        JPanel buttonPane = new JPanel();
453        buttonPane.setLayout(new FlowLayout(FlowLayout.CENTER));
454        dialog.add(buttonPane, BorderLayout.SOUTH);
455
456        JCheckBox haltCheckBox = new JCheckBox(Bundle.getMessage("HaltIfFail"));
457        haltCheckBox.setSelected(item.isHaltFailureEnabled());
458
459        final JTextArea messageFailTextArea = new JTextArea(6, 100);
460        if (item.getAction() != null && item.getAction().isMessageFailEnabled()) {
461            JScrollPane messageFailScroller = new JScrollPane(messageFailTextArea);
462            messageFailScroller.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("MessageFail")));
463            dialog.add(messageFailScroller, BorderLayout.CENTER);
464            messageFailTextArea.setText(item.getMessageFail());
465            messageFailTextArea.setToolTipText(Bundle.getMessage("TipMessage"));
466
467            buttonPane.add(haltCheckBox);
468            buttonPane.add(new JLabel("      ")); // some padding
469        }
470
471        JButton okayButton = new JButton(Bundle.getMessage("ButtonOK"));
472        okayButton.addActionListener(new ActionListener() {
473            @Override
474            public void actionPerformed(ActionEvent arg0) {
475                item.setMessage(messageTextArea.getText());
476                item.setMessageFail(messageFailTextArea.getText());
477                item.setHaltFailureEnabled(haltCheckBox.isSelected());
478                dialog.dispose();
479                return;
480            }
481        });
482        buttonPane.add(okayButton);
483
484        JButton cancelButton = new JButton(Bundle.getMessage("ButtonCancel"));
485        cancelButton.addActionListener(new ActionListener() {
486            @Override
487            public void actionPerformed(ActionEvent arg0) {
488                dialog.dispose();
489                return;
490            }
491        });
492        buttonPane.add(cancelButton);
493
494        JButton defaultMessagesButton = new JButton(Bundle.getMessage("DefaultMessages"));
495        defaultMessagesButton.setToolTipText(Bundle.getMessage("TipDefaultButton"));
496        defaultMessagesButton.addActionListener(new ActionListener() {
497            @Override
498            public void actionPerformed(ActionEvent arg0) {
499                if (messageTextArea.getText().equals(AutomationItem.NONE)) {
500                    messageTextArea.setText(Bundle.getMessage("DefaultMessageOk"));
501                }
502                if (messageFailTextArea.getText().equals(AutomationItem.NONE)) {
503                    messageFailTextArea.setText(Bundle.getMessage("DefaultMessageFail"));
504                }
505                return;
506            }
507        });
508        buttonPane.add(defaultMessagesButton);
509
510        dialog.setModal(true);
511        dialog.pack();
512        dialog.setVisible(true);
513    }
514
515    private void moveUpAutomationItem(AutomationItem item) {
516        log.debug("move automation item up");
517        _automation.moveItemUp(item);
518    }
519
520    private void moveDownAutomationItem(AutomationItem item) {
521        log.debug("move automation item down");
522        _automation.moveItemDown(item);
523    }
524
525    private void deleteAutomationItem(AutomationItem item) {
526        log.debug("Delete automation item");
527        _automation.deleteItem(item);
528    }
529
530    // this table listens for changes to a automation and its car types
531    @Override
532    public void propertyChange(PropertyChangeEvent e) {
533        if (Control.SHOW_PROPERTY)
534            log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), e
535                    .getNewValue());
536
537        if (e.getPropertyName().equals(Automation.LISTCHANGE_CHANGED_PROPERTY)) {
538            updateList();
539            fireTableDataChanged();
540        }
541        if (e.getPropertyName().equals(Automation.CURRENT_ITEM_CHANGED_PROPERTY)) {
542            SwingUtilities.invokeLater(() -> {
543                int row = _list.indexOf(_automation.getCurrentAutomationItem());
544                int viewRow = _table.convertRowIndexToView(row);
545                // the following line can be responsible for a thread lock
546                _table.scrollRectToVisible(_table.getCellRect(viewRow, 0, true));
547                fireTableDataChanged();
548            });
549        }
550        // update automation item?
551        if (e.getSource().getClass().equals(AutomationItem.class)) {
552            AutomationItem item = (AutomationItem) e.getSource();
553            int row = _list.indexOf(item);
554            if (Control.SHOW_PROPERTY)
555                log.debug("Update automation item table row: {}", row);
556            if (row >= 0) {
557                fireTableRowsUpdated(row, row);
558            }
559        }
560    }
561
562    private void removePropertyChangeAutomationItems() {
563        for (AutomationItem item : _list) {
564            item.removePropertyChangeListener(this);
565        }
566    }
567
568    public void dispose() {
569        if (_automation != null) {
570            removePropertyChangeAutomationItems();
571            _automation.removePropertyChangeListener(this);
572        }
573    }
574
575    private final static Logger log = LoggerFactory.getLogger(AutomationTableModel.class);
576}