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