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}