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