001package jmri.jmrit.roster.swing; 002 003import com.fasterxml.jackson.databind.util.StdDateFormat; 004 005import java.awt.Component; 006import java.awt.Rectangle; 007import java.awt.event.ActionEvent; 008import java.awt.event.ActionListener; 009import java.awt.event.MouseEvent; 010import java.text.DateFormat; 011import java.text.SimpleDateFormat; 012import java.text.ParseException; 013import java.util.ArrayList; 014import java.util.Arrays; 015import java.util.Date; 016import java.util.Enumeration; 017import java.util.List; 018 019import javax.swing.BoxLayout; 020import javax.swing.DefaultCellEditor; 021import javax.swing.JCheckBoxMenuItem; 022import javax.swing.JPopupMenu; 023import javax.swing.JScrollPane; 024import javax.swing.JTable; 025import javax.swing.JTextField; 026import javax.swing.ListSelectionModel; 027import javax.swing.RowSorter; 028import javax.swing.SortOrder; 029import javax.swing.border.Border; 030import javax.swing.event.ListSelectionEvent; 031import javax.swing.event.ListSelectionListener; 032import javax.swing.event.RowSorterEvent; 033import javax.swing.table.DefaultTableCellRenderer; 034import javax.swing.table.TableColumn; 035import javax.swing.table.TableRowSorter; 036 037import jmri.InstanceManager; 038import jmri.jmrit.roster.Roster; 039import jmri.jmrit.roster.RosterEntry; 040import jmri.jmrit.roster.RosterEntrySelector; 041import jmri.jmrit.roster.rostergroup.RosterGroupSelector; 042import jmri.util.gui.GuiLafPreferencesManager; 043import jmri.util.swing.JmriPanel; 044import jmri.util.swing.JmriMouseAdapter; 045import jmri.util.swing.JmriMouseEvent; 046import jmri.util.swing.JmriMouseListener; 047import jmri.util.swing.XTableColumnModel; 048 049/** 050 * Provide a table of roster entries as a JmriJPanel. 051 * 052 * @author Bob Jacobsen Copyright (C) 2003, 2010 053 * @author Randall Wood Copyright (C) 2013 054 */ 055public class RosterTable extends JmriPanel implements RosterEntrySelector, RosterGroupSelector { 056 057 RosterTableModel dataModel; 058 TableRowSorter<RosterTableModel> sorter; 059 JTable dataTable; 060 JScrollPane dataScroll; 061 XTableColumnModel columnModel = new XTableColumnModel(); 062 private RosterGroupSelector rosterGroupSource = null; 063 protected transient ListSelectionListener tableSelectionListener; 064 private RosterEntry[] selectedRosterEntries = null; 065 private RosterEntry[] sortedRosterEntries = null; 066 private RosterEntry re = null; 067 068 public RosterTable() { 069 this(false); 070 } 071 072 public RosterTable(boolean editable) { 073 // set to single selection 074 this(editable, ListSelectionModel.SINGLE_SELECTION); 075 } 076 077 public RosterTable(boolean editable, int selectionMode) { 078 super(); 079 dataModel = new RosterTableModel(editable); 080 sorter = new TableRowSorter<>(dataModel); 081 sorter.addRowSorterListener(rowSorterEvent -> { 082 if (rowSorterEvent.getType() == RowSorterEvent.Type.SORTED) { 083 // clear sorted cache 084 sortedRosterEntries = null; 085 } 086 }); 087 dataTable = new JTable(dataModel); 088 dataTable.setRowSorter(sorter); 089 dataScroll = new JScrollPane(dataTable); 090 dataTable.setRowHeight(InstanceManager.getDefault(GuiLafPreferencesManager.class).getFontSize() + 4); 091 092 sorter.setComparator(RosterTableModel.IDCOL, new jmri.util.AlphanumComparator()); 093 094 // set initial sort 095 List<RowSorter.SortKey> sortKeys = new ArrayList<>(); 096 sortKeys.add(new RowSorter.SortKey(RosterTableModel.ADDRESSCOL, SortOrder.ASCENDING)); 097 sorter.setSortKeys(sortKeys); 098 099 // allow reordering of the columns 100 dataTable.getTableHeader().setReorderingAllowed(true); 101 102 // have to shut off autoResizeMode to get horizontal scroll to work (JavaSwing p 541) 103 dataTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); 104 105 dataTable.setColumnModel(columnModel); 106 // dataModel.setColumnModel(columnModel); 107 dataTable.createDefaultColumnsFromModel(); 108 dataTable.setAutoCreateColumnsFromModel(false); 109 110 // format the last updated date time, last operated date time. 111 dataTable.setDefaultRenderer(Date.class, new DateTimeCellRenderer()); 112 113 TableColumn tc = columnModel.getColumnByModelIndex(RosterTableModel.PROTOCOL); 114 columnModel.setColumnVisible(tc, false); 115 116 // resize columns as requested 117 resetColumnWidths(); 118 119 // general GUI config 120 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); 121 122 // install items in GUI 123 add(dataScroll); 124 125 // set Viewport preferred size from size of table 126 java.awt.Dimension dataTableSize = dataTable.getPreferredSize(); 127 // width is right, but if table is empty, it's not high 128 // enough to reserve much space. 129 dataTableSize.height = Math.max(dataTableSize.height, 400); 130 dataTableSize.width = Math.max(dataTableSize.width, 400); 131 dataScroll.getViewport().setPreferredSize(dataTableSize); 132 133 dataTable.setSelectionMode(selectionMode); 134 JmriMouseListener mouseHeaderListener = new TableHeaderListener(); 135 dataTable.getTableHeader().addMouseListener(JmriMouseListener.adapt(mouseHeaderListener)); 136 137 dataTable.setDefaultEditor(Object.class, new RosterCellEditor()); 138 dataTable.setDefaultEditor(Date.class, new DateTimeCellEditor()); 139 140 tableSelectionListener = (ListSelectionEvent e) -> { 141 if (!e.getValueIsAdjusting()) { 142 selectedRosterEntries = null; // clear cached list of selections 143 if (dataTable.getSelectedRowCount() == 1) { 144 re = Roster.getDefault().getEntryForId(dataModel.getValueAt(sorter.convertRowIndexToModel(dataTable.getSelectedRow()), RosterTableModel.IDCOL).toString()); 145 } else if (dataTable.getSelectedRowCount() > 1) { 146 re = null; 147 } // leave last selected item visible if no selection 148 } else if (e.getFirstIndex() == -1) { 149 //A reorder of the table might of occurred therefore we are going to make sure that the selected item is still in view 150 moveTableViewToSelected(); 151 } 152 }; 153 dataTable.getSelectionModel().addListSelectionListener(tableSelectionListener); 154 } 155 156 public JTable getTable() { 157 return dataTable; 158 } 159 160 public RosterTableModel getModel() { 161 return dataModel; 162 } 163 164 public final void resetColumnWidths() { 165 Enumeration<TableColumn> en = columnModel.getColumns(false); 166 while (en.hasMoreElements()) { 167 TableColumn tc = en.nextElement(); 168 int width = dataModel.getPreferredWidth(tc.getModelIndex()); 169 tc.setPreferredWidth(width); 170 } 171 dataTable.sizeColumnsToFit(-1); 172 } 173 174 @Override 175 public void dispose() { 176 this.setRosterGroupSource(null); 177 if (dataModel != null) { 178 dataModel.dispose(); 179 } 180 dataModel = null; 181 dataTable.getSelectionModel().removeListSelectionListener(tableSelectionListener); 182 dataTable = null; 183 super.dispose(); 184 } 185 186 public void setRosterGroup(String rosterGroup) { 187 this.dataModel.setRosterGroup(rosterGroup); 188 } 189 190 public String getRosterGroup() { 191 return this.dataModel.getRosterGroup(); 192 } 193 194 /** 195 * @return the rosterGroupSource 196 */ 197 public RosterGroupSelector getRosterGroupSource() { 198 return this.rosterGroupSource; 199 } 200 201 /** 202 * @param rosterGroupSource the rosterGroupSource to set 203 */ 204 public void setRosterGroupSource(RosterGroupSelector rosterGroupSource) { 205 if (this.rosterGroupSource != null) { 206 this.rosterGroupSource.removePropertyChangeListener(SELECTED_ROSTER_GROUP, dataModel); 207 } 208 this.rosterGroupSource = rosterGroupSource; 209 if (this.rosterGroupSource != null) { 210 this.rosterGroupSource.addPropertyChangeListener(SELECTED_ROSTER_GROUP, dataModel); 211 } 212 } 213 214 protected void showTableHeaderPopup(JmriMouseEvent e) { 215 JPopupMenu popupMenu = new JPopupMenu(); 216 for (int i = 0; i < columnModel.getColumnCount(false); i++) { 217 TableColumn tc = columnModel.getColumnByModelIndex(i); 218 JCheckBoxMenuItem menuItem = new JCheckBoxMenuItem(dataTable.getModel().getColumnName(i), columnModel.isColumnVisible(tc)); 219 menuItem.addActionListener(new HeaderActionListener(tc)); 220 popupMenu.add(menuItem); 221 222 } 223 popupMenu.show(e.getComponent(), e.getX(), e.getY()); 224 } 225 226 protected void moveTableViewToSelected() { 227 if (re == null) { 228 return; 229 } 230 //Remove the listener as this change will re-activate it and we end up in a loop! 231 dataTable.getSelectionModel().removeListSelectionListener(tableSelectionListener); 232 dataTable.clearSelection(); 233 int entires = dataTable.getRowCount(); 234 for (int i = 0; i < entires; i++) { 235 if (dataModel.getValueAt(sorter.convertRowIndexToModel(i), RosterTableModel.IDCOL).equals(re.getId())) { 236 dataTable.addRowSelectionInterval(i, i); 237 dataTable.scrollRectToVisible(new Rectangle(dataTable.getCellRect(i, 0, true))); 238 } 239 } 240 dataTable.getSelectionModel().addListSelectionListener(tableSelectionListener); 241 } 242 243 @Override 244 public String getSelectedRosterGroup() { 245 return dataModel.getRosterGroup(); 246 } 247 248 // cache selectedRosterEntries so that multiple calls to this 249 // between selection changes will not require the creation of a new array 250 @Override 251 public RosterEntry[] getSelectedRosterEntries() { 252 if (selectedRosterEntries == null) { 253 int[] rows = dataTable.getSelectedRows(); 254 selectedRosterEntries = new RosterEntry[rows.length]; 255 for (int idx = 0; idx < rows.length; idx++) { 256 selectedRosterEntries[idx] = Roster.getDefault().getEntryForId(dataModel.getValueAt(sorter.convertRowIndexToModel(rows[idx]), RosterTableModel.IDCOL).toString()); 257 } 258 } 259 return Arrays.copyOf(selectedRosterEntries, selectedRosterEntries.length); 260 } 261 262 // cache getSortedRosterEntries so that multiple calls to this 263 // between selection changes will not require the creation of a new array 264 public RosterEntry[] getSortedRosterEntries() { 265 if (sortedRosterEntries == null) { 266 sortedRosterEntries = new RosterEntry[sorter.getModelRowCount()]; 267 for (int idx = 0; idx < sorter.getModelRowCount(); idx++) { 268 sortedRosterEntries[idx] = Roster.getDefault().getEntryForId(dataModel.getValueAt(sorter.convertRowIndexToModel(idx), RosterTableModel.IDCOL).toString()); 269 } 270 } 271 return Arrays.copyOf(sortedRosterEntries, sortedRosterEntries.length); 272 } 273 274 public void setEditable(boolean editable) { 275 this.dataModel.editable = editable; 276 } 277 278 public boolean getEditable() { 279 return this.dataModel.editable; 280 } 281 282 public void setSelectionMode(int selectionMode) { 283 dataTable.setSelectionMode(selectionMode); 284 } 285 286 public int getSelectionMode() { 287 return dataTable.getSelectionModel().getSelectionMode(); 288 } 289 290 public boolean setSelection(RosterEntry... selection) { 291 //Remove the listener as this change will re-activate it and we end up in a loop! 292 dataTable.getSelectionModel().removeListSelectionListener(tableSelectionListener); 293 dataTable.clearSelection(); 294 boolean foundIt = false; 295 if (selection != null) { 296 for (RosterEntry entry : selection) { 297 re = entry; 298 int entries = dataTable.getRowCount(); 299 for (int i = 0; i < entries; i++) { 300 if (dataModel.getValueAt(sorter.convertRowIndexToModel(i), RosterTableModel.IDCOL).equals(re.getId())) { 301 dataTable.addRowSelectionInterval(i, i); 302 foundIt = true; 303 } 304 } 305 } 306 if (selection.length > 1 || !foundIt) { 307 re = null; 308 } else { 309 this.moveTableViewToSelected(); 310 } 311 } else { 312 re = null; 313 } 314 dataTable.getSelectionModel().addListSelectionListener(tableSelectionListener); 315 return foundIt; 316 } 317 318 class HeaderActionListener implements ActionListener { 319 320 TableColumn tc; 321 322 HeaderActionListener(TableColumn tc) { 323 this.tc = tc; 324 } 325 326 @Override 327 public void actionPerformed(ActionEvent e) { 328 JCheckBoxMenuItem check = (JCheckBoxMenuItem) e.getSource(); 329 //Do not allow the last column to be hidden 330 if (!check.isSelected() && columnModel.getColumnCount(true) == 1) { 331 return; 332 } 333 columnModel.setColumnVisible(tc, check.isSelected()); 334 } 335 } 336 337 class TableHeaderListener extends JmriMouseAdapter { 338 339 @Override 340 public void mousePressed(JmriMouseEvent e) { 341 if (e.isPopupTrigger()) { 342 showTableHeaderPopup(e); 343 } 344 } 345 346 @Override 347 public void mouseReleased(JmriMouseEvent e) { 348 if (e.isPopupTrigger()) { 349 showTableHeaderPopup(e); 350 } 351 } 352 353 @Override 354 public void mouseClicked(JmriMouseEvent e) { 355 if (e.isPopupTrigger()) { 356 showTableHeaderPopup(e); 357 } 358 } 359 } 360 361 public class RosterCellEditor extends DefaultCellEditor { 362 363 public RosterCellEditor() { 364 super(new JTextField() { 365 366 @Override 367 public void setBorder(Border border) { 368 //No border required 369 } 370 }); 371 } 372 373 //This allows the cell to be edited using a single click if the row was previously selected, this allows a double on an unselected row to launch the programmer 374 @Override 375 public boolean isCellEditable(java.util.EventObject e) { 376 if (re == null) { 377 //No previous roster entry selected so will take this as a select so no return false to prevent editing 378 return false; 379 } 380 381 if (e instanceof MouseEvent) { 382 MouseEvent me = (MouseEvent) e; 383 //If the click count is not equal to 1 then return false. 384 if (me.getClickCount() != 1) { 385 return false; 386 } 387 } 388 return re.getId().equals(dataModel.getValueAt(sorter.convertRowIndexToModel(dataTable.getSelectedRow()), RosterTableModel.IDCOL)); 389 } 390 } 391 392 private static class DateTimeCellRenderer extends DefaultTableCellRenderer { 393 @Override 394 protected void setValue(Object value) { 395 if ( value instanceof Date) { 396 super.setValue(DateFormat.getDateTimeInstance().format((Date) value)); 397 } else { 398 super.setValue(value); 399 } 400 } 401 } 402 403 private class DateTimeCellEditor extends RosterCellEditor { 404 405 public DateTimeCellEditor() { 406 super(); 407 } 408 409 private final static String EDITOR_DATE_FORMAT = "yyyy-MM-dd hh:mm"; 410 private Date startDate = new Date(); 411 412 @Override 413 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int col) { 414 if (!(value instanceof Date) ) { 415 value = new Date(); // field pre-populated if currently empty to show entry format 416 } 417 startDate = (Date)value; 418 String formatted = new SimpleDateFormat(EDITOR_DATE_FORMAT).format((Date)value); 419 ((JTextField)editorComponent).setText(formatted); 420 editorComponent.setToolTipText("e.g. 2022-12-25 12:34"); 421 return editorComponent; 422 } 423 424 @Override 425 public Object getCellEditorValue() { 426 String o = (String)super.getCellEditorValue(); 427 if ( o.isBlank() ) { // user cancels the date / time 428 return null; 429 } 430 SimpleDateFormat fm = new SimpleDateFormat(EDITOR_DATE_FORMAT); 431 try { 432 // get Date in local time before passing to StdDateFormat 433 startDate = fm.parse(o.trim()); 434 } catch (ParseException e) { 435 } // return value unchanged in case of user mis-type 436 return new StdDateFormat().format(startDate); 437 } 438 439 } 440 441}