001package jmri.jmrit.beantable;
002
003import java.awt.BorderLayout;
004import java.awt.Component;
005import java.awt.Point;
006import java.awt.Rectangle;
007import java.awt.event.ActionEvent;
008import java.awt.event.MouseAdapter;
009import java.awt.event.MouseEvent;
010import java.util.EventObject;
011import javax.annotation.Nonnull;
012import javax.swing.DefaultCellEditor;
013import javax.swing.JComboBox;
014import javax.swing.JPanel;
015import javax.swing.JTable;
016import javax.swing.ListCellRenderer;
017import javax.swing.SwingUtilities;
018import javax.swing.event.ListSelectionEvent;
019import javax.swing.table.TableCellRenderer;
020import org.slf4j.Logger;
021import org.slf4j.LoggerFactory;
022
023/**
024 * Table cell editor abstract class with a custom ComboBox per row as the editing component.
025 * <p>
026 * Used as TableCellRenderer in SignalMast JTable, declared in ConfigValueColumn()
027 * Based on: http://alvinalexander.com/java/jwarehouse/netbeans-src/monitor/src/org/netbeans/modules/web/monitor/client/ComboBoxTableCellEditor.java.shtml
028 * @author Egbert Broerse 2016
029 * @since 4.7.1
030 */
031public abstract class RowComboBoxPanel
032        extends    DefaultCellEditor
033        implements TableCellRenderer {
034
035    /**
036     * The surrounding panel for the combobox.
037     */
038    protected JPanel editor;
039
040    /**
041     * The surrounding panel for the combobox.
042     */
043    protected JPanel renderer;
044
045    /**
046     * Listeners for the table added?
047     */
048    protected boolean tableListenerAdded = false;
049
050    /**
051     * The table.
052     */
053    protected JTable table;
054
055    /**
056     *  To request the focus for the combobox (with SwingUtilities.invokeLater())
057     */
058    protected Runnable comboBoxFocusRequester;
059
060    /**
061     *  The current row.
062     */
063    protected int currentRow = -1;
064
065    /**
066     *  The previously selected value in the editor.
067     */
068    protected Object prevItem;
069
070    /**
071     *  React on action events on the combobox?
072     */
073    protected boolean consumeComboBoxActionEvent = true;
074
075    /**
076     *  The event that causes the editing to start. We need it to know
077     *  if we should open the popup automatically.
078     */
079    protected EventObject startEditingEvent = null;
080
081    /**
082     *  Create a new CellEditor and CellRenderer.
083     *  @param values array (list) of options to display
084     *  @param customRenderer renderer to display things
085     */
086    public RowComboBoxPanel(Object [] values,
087                            ListCellRenderer<?> customRenderer) {
088        super (new JComboBox<>());
089        // is being filled from HashMap
090        this.editor = new JPanel(new BorderLayout ());
091        if (values != null) {
092            setItems(values); // in 4.5.7 this is not yet called using values, but might be useful in a more general application
093        }
094        this.renderer = new JPanel(new BorderLayout ());
095        super.setClickCountToStart(1); // value for a DefaultCellEditor: immediately start editing
096        //show the combobox if the mouse clicks at the panel
097        this.editor.addMouseListener (new MouseAdapter ()
098        {
099            @Override
100            public final void mousePressed (MouseEvent evt)
101            {
102                eventEditorMousePressed();
103            }
104        });
105    }
106
107    public RowComboBoxPanel(Object [] values) {
108        this(values, null);
109    }
110
111    public RowComboBoxPanel() {
112        this(new Object [0]);
113    } // as it is defined in configValueColumn()
114
115    public RowComboBoxPanel(ListCellRenderer<?> customRenderer) {
116        this(new Object [0], customRenderer);
117    }
118
119    /**
120     * Create the editor component for the cell and add a listener for changes in the table.
121     *
122     * @param table parent JTable of NamedBean
123     * @param value current value for cell to be rendered.
124     * @param isSelected tells if this row is selected in the table.
125     * @param row the row in table.
126     * @param col the column in table, in this case Value (Aspect or Appearance).
127     * @return A JPanel containing a JComboBox with valid options as the CellEditor for the Value.
128     */
129    @Override
130    public final Component getTableCellEditorComponent (JTable  table,
131                                                        Object  value,
132                                                        boolean isSelected,
133                                                        int     row,
134                                                        int     col)
135    {
136        //add a listener to the table
137        if  ( ! this.tableListenerAdded) {
138            this.tableListenerAdded = true;
139            this.table = table;
140            this.table.getSelectionModel().addListSelectionListener((ListSelectionEvent evt) -> {
141                eventTableSelectionChanged ();
142            });
143        }
144        this.currentRow = row;
145        updateData(row, true, table);
146        return getEditorComponent(table, value, isSelected, row, col);
147    }
148
149    /**
150     * (Re)build combobox with all allowed state values, select current and add action listener.
151     *
152     * @param table parent JTable of NamedBean
153     * @param value current value for cell to be rendered.
154     * @param isSelected tells if this row is selected in the table.
155     * @param row the row in table.
156     * @param col the column in table, in this case Value (Aspect or Appearance).
157     * @return a JPanel containing a JComboBox
158     * @see #getTableCellEditorComponent(JTable, Object, boolean, int, int)
159     * @see #getEditorBox(int)
160     */
161    protected Component getEditorComponent(JTable  table,
162                                           Object  value,
163                                           boolean isSelected,
164                                           int     row,
165                                           int     col)
166    {
167        //new or old row? > should be cleaned up, leave our isSelected argument?
168        //isSelected = table.isRowSelected(row);
169        if  (isSelected) {
170            //old row
171            log.debug("getEditorComponent>isSelected (value={})", value);
172        }
173        //the user selected another row (or initially no row was selected)
174        this.editor.removeAll();  // remove the combobox from the panel
175        JComboBox<?> editorbox = getEditorBox(table.convertRowIndexToModel(row));
176        editorbox.putClientProperty("JComponent.sizeVariant", "small");
177        editorbox.putClientProperty("JComboBox.buttonType", "square");
178        log.debug("getEditorComponent>notSelected (row={}, value={}; me = {}))", row, value, this.toString());
179        if (value != null) {
180            editorbox.setSelectedItem(value); // display current Value
181        }
182        editorbox.addActionListener((ActionEvent evt) -> {
183            Object choice = editorbox.getSelectedItem();
184            log.debug("actionPerformed (event={}, choice={}", evt.toString(), choice.toString());
185            eventRowComboBoxActionPerformed(choice); // signal the changed row
186        });
187        this.editor.add(editorbox);
188        return this.editor;
189    }
190
191    /**
192     * Create the renderer component for the cell and add a listener for changes in the table.
193     *
194     * @param table the parent Table.
195     * @param value current value for cell to be rendered.
196     * @param isSelected tells if this row is selected in the table.
197     * @param hasFocus true if the row has focus.
198     * @param row the row in table.
199     * @param col the column in table, in this case Value (Aspect/Appearance).
200     * @return A JPanel containing a JComboBox with only the current Value as the CellRenderer.
201     */
202    @Override
203    public final Component getTableCellRendererComponent (JTable  table,
204                                                          Object  value,
205                                                          boolean isSelected,
206                                                          boolean hasFocus,
207                                                          int     row,
208                                                          int     col)
209    {
210        //add a listener to the table
211        if  ( ! this.tableListenerAdded) {
212            this.tableListenerAdded = true;
213            this.table = table;
214            this.table.getSelectionModel().addListSelectionListener((ListSelectionEvent evt) -> {
215                eventTableSelectionChanged ();
216            });
217        }
218
219        this.currentRow = row;
220        return getRendererComponent(table, value, isSelected, hasFocus, row, col); // OK to call getEditorComponent() instead?
221    }
222
223    /**
224     * (Re)build combobox with only the active state value.
225     *
226     * @param table the parent Table.
227     * @param value current value for cell to be rendered.
228     * @param isSelected tells if this row is selected in the table.
229     * @param hasFocus true if the row has focus.
230     * @param row the row in table.
231     * @param col the column in table, in this case Value (Aspect/Appearance).
232     * @return a JPanel containing a JComboBox
233     * @see #getTableCellRendererComponent(JTable, Object, boolean, boolean, int, int)
234     */
235    protected Component getRendererComponent(JTable  table,
236                                             Object  value,
237                                             boolean isSelected,
238                                             boolean hasFocus,
239                                             int     row,
240                                             int     col)
241    {
242        this.renderer.removeAll();  //remove the combobox from the panel
243        JComboBox<String> renderbox = new JComboBox<>(); // create a fake comboBox with the current Value (Aspect of mast/Appearance of the Head) in this row
244        log.debug("RCBP getRendererComponent (row={}, value={})", row, value);
245        renderbox.putClientProperty("JComponent.sizeVariant", "small");
246        renderbox.putClientProperty("JComboBox.buttonType", "square");
247        if (value != null) {
248            renderbox.addItem(value.toString()); // display (only) the current Value
249        } else {
250            renderbox.addItem(""); // blank item
251        }
252        renderer.add(renderbox);
253        return this.renderer;
254    }
255
256    /**
257     * Refresh contents of editor.
258     *
259     * @param row the row in table.
260     * @param isSelected tells if this row is selected in the table.
261     * @param table the parent Table.
262     * @see #getTableCellEditorComponent(JTable, Object, boolean, int, int)
263     */
264    protected void updateData(int row, boolean isSelected, JTable table) {
265        // get valid Value options for ComboBox
266        log.debug("RCBP updateData (row:{}; me = {}))", row, this.toString());
267        JComboBox<?> editorbox = getEditorBox(table.convertRowIndexToModel(row));
268        this.editor.add(editorbox);
269        if (isSelected) {
270            editor.setBackground(table.getSelectionBackground());
271        } else {
272            editor.setBackground(table.getBackground());
273        }
274    }
275
276    /**
277     *  Is the cell editable? If the mouse was pressed at a margin
278     *  we don't want the cell to be editable.
279     *
280     *  @param evt The event-object
281     *  @return true when user clicked inside cell, not on cell border
282     */
283    @Override
284    public boolean isCellEditable(EventObject evt) {
285        this.startEditingEvent = evt;
286        if  (evt instanceof MouseEvent  &&  evt.getSource () instanceof JTable) {
287            MouseEvent me = (MouseEvent) evt;
288            JTable thisTable = (JTable) me.getSource ();
289            Point pt = new Point (me.getX (), me.getY ());
290            int row = thisTable.rowAtPoint (pt);
291            int col = thisTable.columnAtPoint (pt);
292            Rectangle rec = thisTable.getCellRect (row, col, false);
293            if  (me.getY () >= rec.y + rec.height  ||  me.getX () >= rec.x + rec.width)
294            {
295                return false;
296            }
297        }
298        return super.isCellEditable(evt);
299    }
300
301    /**
302     * Get current contents (value) in cell.
303     *
304     * @return value (String in 4.6 applications)
305     */
306    @Override
307    public Object getCellEditorValue() {
308        log.debug("getCellEditorValue, prevItem: {}; me = {})", prevItem, this.toString());
309        return prevItem;
310    }
311
312    /**
313     *  Put contents into the combobox.
314     *  @param items array (strings) of options to display
315     */
316    public final void setItems(@Nonnull Object [] items) {
317        JComboBox<String> editorbox = new JComboBox<> ();
318        final int n = items.length;
319        for  (int i = 0; i < n; i++)
320        {
321            if (items [i] != null) {
322                editorbox.addItem (items[i].toString());
323            }
324        }
325        this.editor.add(editorbox);
326    }
327
328    /**
329     * Open combobox (Editor) when clicked.
330     */
331    protected void eventEditorMousePressed() {
332        this.editor.add(getEditorBox(table.convertRowIndexToModel(this.currentRow))); // add editorBox to JPanel
333        this.editor.revalidate();
334        SwingUtilities.invokeLater(this.comboBoxFocusRequester);
335        log.debug("eventEditorMousePressed in row {}; me = {})", this.currentRow, this.toString());
336    }
337
338    /**
339     * Stop editing if a new row is selected.
340     */
341    protected void eventTableSelectionChanged() {
342        log.debug("eventTableSelectionChanged");
343        if  ( ! this.table.isRowSelected(this.currentRow))
344        {
345            stopCellEditing ();
346        }
347    }
348
349    /**
350     * Method for our own VALUECOL row specific JComboBox.
351     * @param choice the selected item (Aspect/Appearance) in the combobox list
352     */
353    protected void eventRowComboBoxActionPerformed(@Nonnull Object choice) {
354        Object item = choice;
355        log.debug("eventRowComboBoxActionPerformed; selected item: {}, me = {})", item, this.toString());
356        prevItem = choice; // passed as cell value
357        if (consumeComboBoxActionEvent) stopCellEditing();
358    }
359
360    protected int getCurrentRow() {
361        return this.currentRow;
362    }
363
364    /*
365     * Placeholder method; contents are overridden in application.
366     */
367    protected JComboBox<String> getEditorBox(int row) {
368        String [] list = {"Error", "Not Valid"};
369        return new JComboBox<>(list);
370    }
371
372    private final static Logger log = LoggerFactory.getLogger(RowComboBoxPanel.class);
373
374}