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}