001package jmri.jmrit.beantable;
002
003import java.awt.event.ActionEvent;
004import java.text.MessageFormat;
005import java.util.*;
006
007import javax.annotation.CheckForNull;
008import javax.annotation.Nonnull;
009import javax.swing.*;
010import javax.swing.event.*;
011import javax.swing.table.*;
012
013import jmri.InstanceManager;
014import jmri.Manager;
015import jmri.NamedBean;
016import jmri.ProxyManager;
017import jmri.UserPreferencesManager;
018import jmri.SystemConnectionMemo;
019import jmri.jmrix.SystemConnectionMemoManager;
020import jmri.swing.ManagerComboBox;
021import jmri.util.swing.TriStateJCheckBox;
022import jmri.util.swing.XTableColumnModel;
023
024/**
025 * Swing action to create and register a NamedBeanTable GUI.
026 *
027 * @param <E> type of NamedBean supported in this table
028 * @author Bob Jacobsen Copyright (C) 2003
029 */
030public abstract class AbstractTableAction<E extends NamedBean> extends AbstractAction {
031
032    public AbstractTableAction(String actionName) {
033        super(actionName);
034    }
035
036    public AbstractTableAction(String actionName, Object option) {
037        super(actionName);
038    }
039
040    protected BeanTableDataModel<E> m;
041
042    /**
043     * Create the JTable DataModel, along with the changes for the specific
044     * NamedBean type.
045     */
046    protected abstract void createModel();
047
048    /**
049     * Include the correct title.
050     */
051    protected abstract void setTitle();
052
053    protected BeanTableFrame<E> f;
054
055    @Override
056    public void actionPerformed(ActionEvent e) {
057        // create the JTable model, with changes for specific NamedBean
058        createModel();
059        TableRowSorter<BeanTableDataModel<E>> sorter = new TableRowSorter<>(m);
060        JTable dataTable = m.makeJTable(m.getMasterClassName(), m, sorter);
061
062        // allow reordering of the columns
063        dataTable.getTableHeader().setReorderingAllowed(true);
064
065        // create the frame
066        f = new BeanTableFrame<E>(m, helpTarget(), dataTable) {
067
068            /**
069             * Include an "Add..." button
070             */
071            @Override
072            void extras() {
073                
074                addBottomButtons(this, dataTable);
075            }
076        };
077        setMenuBar(f); // comes after the Help menu is added by f = new
078                       // BeanTableFrame(etc.) in stand alone application
079        configureTable(dataTable);
080        setTitle();
081        addToFrame(f);
082        f.pack();
083        f.setVisible(true);
084    }
085
086    @SuppressWarnings("unchecked") // revisit Java16+  if dm instanceof BeanTableDataModel<E>
087    protected void addBottomButtons(BeanTableFrame<E> ata, JTable dataTable ){
088
089        TableItem<E> ti = new TableItem<>(this);
090        ti.setTableFrame(ata);
091        ti.includeAddButton(includeAddButton);
092        ti.dataTable = dataTable;
093        TableModel dm = dataTable.getModel();
094
095        if ( dm instanceof BeanTableDataModel) {
096            ti.dataModel = (BeanTableDataModel<E>)dm;
097        }
098        ti.includePropertyCheckBox();
099
100    }
101
102    /**
103     * Notification that column visibility for the JTable has updated.
104     * <p>
105     * This is overridden by classes which have column visibility Checkboxes on bottom bar.
106     * <p>
107     *
108     * Called on table startup and whenever a column goes hidden / visible.
109     *
110     * @param colsVisible   array of ALL table columns and their visibility
111     *                      status in order of main Table Model, NOT XTableColumnModel.
112     */
113    protected void columnsVisibleUpdated(boolean[] colsVisible){
114        log.debug("columns updated {}",colsVisible);
115    }
116
117    public BeanTableDataModel<E> getTableDataModel() {
118        createModel();
119        return m;
120    }
121
122    public void setFrame(@Nonnull BeanTableFrame<E> frame) {
123        f = frame;
124    }
125
126    public BeanTableFrame<E> getFrame() {
127        return f;
128    }
129
130    /**
131     * Allow subclasses to add to the frame without having to actually subclass
132     * the BeanTableDataFrame.
133     *
134     * @param f the Frame to add to
135     */
136    public void addToFrame(@Nonnull BeanTableFrame<E> f) {
137    }
138
139    /**
140     * Allow subclasses to add to the frame without having to actually subclass
141     * the BeanTableDataFrame.
142     *
143     * @param tti the TabbedTableItem to add to
144     */
145    public void addToFrame(@Nonnull ListedTableFrame.TabbedTableItem<E> tti) {
146    }
147
148    /**
149     * If the subClass is being included in a greater tabbed frame, then this
150     * method is used to add the details to the tabbed frame.
151     *
152     * @param f AbstractTableTabAction for the containing frame containing these
153     *          and other tabs
154     */
155    public void addToPanel(AbstractTableTabAction<E> f) {
156    }
157
158    /**
159     * If the subClass is being included in a greater tabbed frame, then this is
160     * used to specify which manager the subclass should be using.
161     *
162     * @param man Manager for this table tab
163     */
164    protected void setManager(@Nonnull Manager<E> man) {
165    }
166
167    /**
168     * Get the Bean Manager in use by the TableAction.
169     * @return Bean Manager, could be Proxy or normal Manager, may be null.
170     */
171    @CheckForNull
172    protected Manager<E> getManager(){
173        return null;
174    }
175
176    /**
177     * Allow subclasses to alter the frame's Menubar without having to actually
178     * subclass the BeanTableDataFrame.
179     *
180     * @param f the Frame to attach the menubar to
181     */
182    public void setMenuBar(BeanTableFrame<E> f) {
183    }
184
185    public JComponent getPanel() {
186        return null;
187    }
188
189    /**
190     * Perform configuration of the JTable as required by a specific TableAction.
191     * @param table The table to configure.
192     */
193    protected void configureTable(JTable table){
194    }
195
196    /**
197     * Dispose of the BeanTableDataModel ( if present ),
198     * which removes the DataModel property change listeners from Beans.
199     */
200    public void dispose() {
201        if (m != null) {
202            m.dispose();
203        }
204        // should this also dispose of the frame f?
205    }
206
207    /**
208     * Increments trailing digits of a system/user name (string) I.E. "Geo7"
209     * returns "Geo8" Note: preserves leading zeros: "Geo007" returns "Geo008"
210     * Also, if no trailing digits, appends "1": "Geo" returns "Geo1"
211     *
212     * @param name the system or user name string
213     * @return the same name with trailing digits incremented by one
214     */
215    protected @Nonnull String nextName(@Nonnull String name) {
216        final String[] parts = name.split("(?=\\d+$)", 2);
217        String numString = "0";
218        if (parts.length == 2) {
219            numString = parts[1];
220        }
221        final int numStringLength = numString.length();
222        final int num = Integer.parseInt(numString) + 1;
223        return parts[0] + String.format("%0" + numStringLength + "d", num);
224    }
225
226    /**
227     * Specify the JavaHelp target for this specific panel.
228     *
229     * @return a fixed default string "index" pointing to to highest level in
230     *         JMRI Help
231     */
232    protected String helpTarget() {
233        return "index"; // by default, go to the top
234    }
235
236    public String getClassDescription() {
237        return "Abstract Table Action";
238    }
239
240    public void setMessagePreferencesDetails() {
241        HashMap<Integer, String> options = new HashMap<>(3);
242        options.put(0x00, Bundle.getMessage("DeleteAsk"));
243        options.put(0x01, Bundle.getMessage("DeleteNever"));
244        options.put(0x02, Bundle.getMessage("DeleteAlways"));
245        jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class).setMessageItemDetails(getClassName(),
246                "deleteInUse", Bundle.getMessage("DeleteItemInUse"), options, 0x00);
247    }
248
249    protected abstract String getClassName();
250
251    /**
252     * Test if to include an Add New Button.
253     * @return true to include, else false.
254     */
255    public boolean includeAddButton() {
256        return includeAddButton;
257    }
258
259    protected boolean includeAddButton = true;
260
261    /**
262     * Used with the Tabbed instances of table action, so that the print option
263     * is handled via that on the appropriate tab.
264     *
265     * @param mode         table print mode
266     * @param headerFormat messageFormat for header
267     * @param footerFormat messageFormat for footer
268     */
269    public void print(JTable.PrintMode mode, MessageFormat headerFormat, MessageFormat footerFormat) {
270        log.error("Printing not handled for {} tables.", m.getBeanType());
271    }
272
273    protected abstract void addPressed(ActionEvent e);
274
275    /**
276     * Configure the combo box listing managers.
277     * Can be placed on Add New pane to select a connection for the new item.
278     *
279     * @param comboBox     the combo box to configure
280     * @param manager      the current manager
281     * @param managerClass the implemented manager class for the current
282     *                     manager; this is the class used by
283     *                     {@link InstanceManager#getDefault(Class)} to get the
284     *                     default manager, which may or may not be the current
285     *                     manager
286     */
287    protected void configureManagerComboBox(ManagerComboBox<E> comboBox, Manager<E> manager,
288            Class<? extends Manager<E>> managerClass) {
289        Manager<E> defaultManager = InstanceManager.getDefault(managerClass);
290        // populate comboBox
291        if (defaultManager instanceof ProxyManager) {
292            comboBox.setManagers(defaultManager);
293        } else {
294            comboBox.setManagers(manager);
295        }
296        // set current selection
297        if (manager instanceof ProxyManager) {
298            UserPreferencesManager upm = InstanceManager.getDefault(UserPreferencesManager.class);
299            String systemSelectionCombo = this.getClass().getName() + ".SystemSelected";
300            String userPref = upm.getComboBoxLastSelection(systemSelectionCombo);
301            if ( userPref != null) {
302                SystemConnectionMemo memo = SystemConnectionMemoManager.getDefault()
303                        .getSystemConnectionMemoForUserName(userPref);
304                if (memo!=null) {
305                    comboBox.setSelectedItem(memo.get(managerClass));
306                } else {
307                    ProxyManager<E> proxy = (ProxyManager<E>) manager;
308                    comboBox.setSelectedItem(proxy.getDefaultManager());
309                }
310            } else {
311                ProxyManager<E> proxy = (ProxyManager<E>) manager;
312                comboBox.setSelectedItem(proxy.getDefaultManager());
313            }
314        } else {
315            comboBox.setSelectedItem(manager);
316        }
317    }
318
319    /**
320     * Remove the Add panel prefixBox listener before disposal.
321     * The listener is created when the Add panel is defined.  It persists after the
322     * the Add panel has been disposed.  When the next Add is created, AbstractTableAction
323     * sets the default connection as the current selection.  This triggers validation before
324     * the new Add panel is created.
325     * <p>
326     * The listener is removed by the controlling table action before disposing of the Add
327     * panel after Close or Create.
328     * @param prefixBox The prefix combobox that might contain the listener.
329     */
330    protected void removePrefixBoxListener(ManagerComboBox<E> prefixBox) {
331        Arrays.asList(prefixBox.getActionListeners()).forEach((l) -> {
332            prefixBox.removeActionListener(l);
333        });
334    }
335
336    /**
337     * Display a warning to user about invalid entry. Needed as entry validation
338     * does not disable the Create button when full system name eg "LT1" is entered.
339     *
340     * @param curAddress address as entered in Add new... pane address field
341     * @param ex the exception that occurred
342     */
343    protected void displayHwError(String curAddress, Exception ex) {
344        log.warn("Invalid Entry: {}",ex.getMessage());
345        jmri.InstanceManager.getDefault(jmri.UserPreferencesManager .class).
346                showErrorMessage(Bundle.getMessage("ErrorTitle"),
347                        Bundle.getMessage("ErrorConvertHW", curAddress),"" + ex,"",
348                        true,false);
349    }
350
351    protected static class TableItem<E extends NamedBean> implements TableColumnModelListener {  // E comes from the parent
352
353        BeanTableDataModel<E> dataModel;
354        JTable dataTable;
355        final AbstractTableAction<E> tableAction;
356        BeanTableFrame<E> beanTableFrame;
357
358        void setTableFrame(BeanTableFrame<E> frame){
359            beanTableFrame = frame;
360        }
361
362        final TriStateJCheckBox propertyVisible =
363            new TriStateJCheckBox(Bundle.getMessage("ShowSystemSpecificProperties"));
364
365        public TableItem(@Nonnull AbstractTableAction<E> tableAction) {
366            this.tableAction = tableAction;
367        }
368
369        @SuppressWarnings("unchecked")
370        public AbstractTableAction<E> getAAClass() {
371            return tableAction;
372        }
373
374        public JTable getDataTable() {
375            return dataTable;
376        }
377
378        void includePropertyCheckBox() {
379
380            if (dataModel==null) {
381                log.error("datamodel for dataTable {} should not be null", dataTable);
382                return;
383            }
384
385            if (dataModel.getPropertyColumnCount() > 0) {
386                propertyVisible.setToolTipText(Bundle.getMessage
387                        ("ShowSystemSpecificPropertiesToolTip"));
388                addToBottomBox(propertyVisible);
389                propertyVisible.addActionListener((ActionEvent e) ->
390                    dataModel.setPropertyColumnsVisible(dataTable, propertyVisible.isSelected()));
391            }
392            fireColumnsUpdated(); // init bottom buttons
393            dataTable.getColumnModel().addColumnModelListener(this);
394
395        }
396
397        void includeAddButton(boolean includeAddButton){
398
399            if (includeAddButton) {
400                JButton addButton = new JButton(Bundle.getMessage("ButtonAdd"));
401                addToBottomBox(addButton );
402                addButton.addActionListener(tableAction::addPressed);
403            }
404        }
405
406        protected void addToBottomBox(JComponent comp) {
407            if (beanTableFrame != null ) {
408                beanTableFrame.addToBottomBox(comp, this.getClass().getName());
409            }
410        }
411
412        /**
413         * Notify the subclasses that column visibility has been updated,
414         * or the table has finished loading.
415         *
416         * Sends notification to the tableAction with boolean array of column visibility.
417         *
418         */
419        private void fireColumnsUpdated(){
420            TableColumnModel model = dataTable.getColumnModel();
421            if (model instanceof XTableColumnModel) {
422                Enumeration<TableColumn> e = ((XTableColumnModel) model).getColumns(false);
423                int numCols = ((XTableColumnModel) model).getColumnCount(false);
424                // XTableColumnModel has been spotted to return a fleeting different
425                // column count to actual model, generally if manager is changed at startup
426                // so we do a sanity check to make sure the models are in synch.
427                if (numCols != dataModel.getColumnCount()){
428                    log.debug("Difference with Xtable cols: {} Model cols: {}",numCols,dataModel.getColumnCount());
429                    return;
430                }
431                boolean[] colsVisible = new boolean[numCols];
432                while (e.hasMoreElements()) {
433                    TableColumn column = e.nextElement();
434                    boolean visible = ((XTableColumnModel) model).isColumnVisible(column);
435                    colsVisible[column.getModelIndex()] = visible;
436                }
437                tableAction.columnsVisibleUpdated(colsVisible);
438                setPropertyVisibleCheckbox(colsVisible);
439            }
440        }
441
442        /**
443         * Updates the custom bean property columns checkbox.
444         * @param colsVisible array of column visibility
445         */
446        private void setPropertyVisibleCheckbox(boolean[] colsVisible){
447            int numberofCustomCols = dataModel.getPropertyColumnCount();
448            if (numberofCustomCols>0){
449                boolean[] customColVisibility = new boolean[numberofCustomCols];
450                for ( int i=0; i<numberofCustomCols; i++){
451                    customColVisibility[i]=colsVisible[colsVisible.length-i-1];
452                }
453                propertyVisible.setState(customColVisibility);
454            }
455        }
456
457        /**
458         * {@inheritDoc}
459         * A column is now visible.  fireColumnsUpdated()
460         */
461        @Override
462        public void columnAdded(TableColumnModelEvent e) {
463            fireColumnsUpdated();
464        }
465
466        /**
467         * {@inheritDoc}
468         * A column is now hidden.  fireColumnsUpdated()
469         */
470        @Override
471        public void columnRemoved(TableColumnModelEvent e) {
472            fireColumnsUpdated();
473        }
474
475        /**
476         * {@inheritDoc}
477         * Unused.
478         */
479        @Override
480        public void columnMoved(TableColumnModelEvent e) {}
481
482        /**
483         * {@inheritDoc}
484         * Unused.
485         */
486        @Override
487        public void columnSelectionChanged(ListSelectionEvent e) {}
488
489        /**
490         * {@inheritDoc}
491         * Unused.
492         */
493        @Override
494        public void columnMarginChanged(ChangeEvent e) {}
495        
496        protected void dispose() {
497            if (dataTable !=null ) {
498                dataTable.getColumnModel().removeColumnModelListener(this);
499            }
500            if (dataModel != null) {
501                dataModel.stopPersistingTable(dataTable);
502                dataModel.dispose();
503            }
504            dataModel = null;
505            dataTable = null;
506        }
507
508    }
509    
510    
511    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractTableAction.class);
512
513}