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