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        log.trace("configureManagerComboBox called with manager {}", manager);
302        Manager<E> defaultManager = InstanceManager.getDefault(managerClass);
303        log.trace("default manager is {}", defaultManager);
304        // populate comboBox
305        if (defaultManager instanceof ProxyManager) {
306            comboBox.setManagers(defaultManager);
307        } else {
308            comboBox.setManagers(manager);
309        }
310        // set current selection
311        if (manager instanceof ProxyManager) {
312            UserPreferencesManager upm = InstanceManager.getDefault(UserPreferencesManager.class);
313            String systemSelectionCombo = this.getClass().getName() + ".SystemSelected";
314            String userPref = upm.getComboBoxLastSelection(systemSelectionCombo);
315            if ( userPref != null) {
316                SystemConnectionMemo memo = SystemConnectionMemoManager.getDefault()
317                        .getSystemConnectionMemoForUserName(userPref);
318                if (memo!=null) {
319                    log.trace("managerClass is {}, memo is {}", managerClass, memo);
320                    comboBox.setSelectedItem(memo.get(managerClass));
321                } else {
322                    ProxyManager<E> proxy = (ProxyManager<E>) manager;
323                    comboBox.setSelectedItem(proxy.getDefaultManager());
324                }
325            } else {
326                ProxyManager<E> proxy = (ProxyManager<E>) manager;
327                comboBox.setSelectedItem(proxy.getDefaultManager());
328            }
329        } else {
330            comboBox.setSelectedItem(manager);
331        }
332    }
333
334    /**
335     * Remove the Add panel prefixBox listener before disposal.
336     * The listener is created when the Add panel is defined.  It persists after the
337     * the Add panel has been disposed.  When the next Add is created, AbstractTableAction
338     * sets the default connection as the current selection.  This triggers validation before
339     * the new Add panel is created.
340     * <p>
341     * The listener is removed by the controlling table action before disposing of the Add
342     * panel after Close or Create.
343     * @param prefixBox The prefix combobox that might contain the listener.
344     */
345    protected void removePrefixBoxListener(ManagerComboBox<E> prefixBox) {
346        Arrays.asList(prefixBox.getActionListeners()).forEach((l) -> {
347            prefixBox.removeActionListener(l);
348        });
349    }
350
351    /**
352     * Display a warning to user about invalid entry. Needed as entry validation
353     * does not disable the Create button when full system name eg "LT1" is entered.
354     *
355     * @param curAddress address as entered in Add new... pane address field
356     * @param ex the exception that occurred
357     */
358    protected void displayHwError(String curAddress, Exception ex) {
359        log.warn("Invalid Entry: {}",ex.getMessage());
360        jmri.InstanceManager.getDefault(jmri.UserPreferencesManager .class).
361                showErrorMessage(Bundle.getMessage("ErrorTitle"),
362                        Bundle.getMessage("ErrorConvertHW", curAddress),"" + ex,"",
363                        true,false);
364    }
365
366    protected static class TableItem<E extends NamedBean> implements TableColumnModelListener {  // E comes from the parent
367
368        BeanTableDataModel<E> dataModel;
369        JTable dataTable;
370        final AbstractTableAction<E> tableAction;
371        BeanTableFrame<E> beanTableFrame;
372
373        void setTableFrame(BeanTableFrame<E> frame){
374            beanTableFrame = frame;
375        }
376
377        final TriStateJCheckBox propertyVisible =
378            new TriStateJCheckBox(Bundle.getMessage("ShowSystemSpecificProperties"));
379
380        public TableItem(@Nonnull AbstractTableAction<E> tableAction) {
381            this.tableAction = tableAction;
382        }
383
384        @SuppressWarnings("unchecked")
385        public AbstractTableAction<E> getAAClass() {
386            return tableAction;
387        }
388
389        public JTable getDataTable() {
390            return dataTable;
391        }
392
393        void includePropertyCheckBox() {
394
395            if (dataModel==null) {
396                log.error("datamodel for dataTable {} should not be null", dataTable);
397                return;
398            }
399
400            if (dataModel.getPropertyColumnCount() > 0) {
401                propertyVisible.setToolTipText(Bundle.getMessage
402                        ("ShowSystemSpecificPropertiesToolTip"));
403                addToBottomBox(propertyVisible);
404                propertyVisible.addActionListener((ActionEvent e) ->
405                    dataModel.setPropertyColumnsVisible(dataTable, propertyVisible.isSelected()));
406            }
407            fireColumnsUpdated(); // init bottom buttons
408            dataTable.getColumnModel().addColumnModelListener(this);
409
410        }
411
412        void includeAddButton(boolean includeAddButton){
413
414            if (includeAddButton) {
415                JButton addButton = new JButton(Bundle.getMessage("ButtonAdd"));
416                addToBottomBox(addButton );
417                addButton.addActionListener(tableAction::addPressed);
418            }
419        }
420
421        protected void addToBottomBox(JComponent comp) {
422            if (beanTableFrame != null ) {
423                beanTableFrame.addToBottomBox(comp, this.getClass().getName());
424            }
425        }
426
427        /**
428         * Notify the subclasses that column visibility has been updated,
429         * or the table has finished loading.
430         *
431         * Sends notification to the tableAction with boolean array of column visibility.
432         *
433         */
434        private void fireColumnsUpdated(){
435            TableColumnModel model = dataTable.getColumnModel();
436            if (model instanceof XTableColumnModel) {
437                Enumeration<TableColumn> e = ((XTableColumnModel) model).getColumns(false);
438                int numCols = ((XTableColumnModel) model).getColumnCount(false);
439                // XTableColumnModel has been spotted to return a fleeting different
440                // column count to actual model, generally if manager is changed at startup
441                // so we do a sanity check to make sure the models are in synch.
442                if (numCols != dataModel.getColumnCount()){
443                    log.debug("Difference with Xtable cols: {} Model cols: {}",numCols,dataModel.getColumnCount());
444                    return;
445                }
446                boolean[] colsVisible = new boolean[numCols];
447                while (e.hasMoreElements()) {
448                    TableColumn column = e.nextElement();
449                    boolean visible = ((XTableColumnModel) model).isColumnVisible(column);
450                    colsVisible[column.getModelIndex()] = visible;
451                }
452                tableAction.columnsVisibleUpdated(colsVisible);
453                setPropertyVisibleCheckbox(colsVisible);
454            }
455        }
456
457        /**
458         * Updates the custom bean property columns checkbox.
459         * @param colsVisible array of column visibility
460         */
461        private void setPropertyVisibleCheckbox(boolean[] colsVisible){
462            int numberofCustomCols = dataModel.getPropertyColumnCount();
463            if (numberofCustomCols>0){
464                boolean[] customColVisibility = new boolean[numberofCustomCols];
465                for ( int i=0; i<numberofCustomCols; i++){
466                    customColVisibility[i]=colsVisible[colsVisible.length-i-1];
467                }
468                propertyVisible.setState(customColVisibility);
469            }
470        }
471
472        /**
473         * {@inheritDoc}
474         * A column is now visible.  fireColumnsUpdated()
475         */
476        @Override
477        public void columnAdded(TableColumnModelEvent e) {
478            fireColumnsUpdated();
479        }
480
481        /**
482         * {@inheritDoc}
483         * A column is now hidden.  fireColumnsUpdated()
484         */
485        @Override
486        public void columnRemoved(TableColumnModelEvent e) {
487            fireColumnsUpdated();
488        }
489
490        /**
491         * {@inheritDoc}
492         * Unused.
493         */
494        @Override
495        public void columnMoved(TableColumnModelEvent e) {}
496
497        /**
498         * {@inheritDoc}
499         * Unused.
500         */
501        @Override
502        public void columnSelectionChanged(ListSelectionEvent e) {}
503
504        /**
505         * {@inheritDoc}
506         * Unused.
507         */
508        @Override
509        public void columnMarginChanged(ChangeEvent e) {}
510
511        protected void dispose() {
512            if (dataTable !=null ) {
513                dataTable.getColumnModel().removeColumnModelListener(this);
514            }
515            if (dataModel != null) {
516                dataModel.stopPersistingTable(dataTable);
517                dataModel.dispose();
518            }
519            dataModel = null;
520            dataTable = null;
521        }
522
523    }
524
525    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractTableAction.class);
526
527}