001package jmri.jmrit.logixng.tools.swing;
002
003import java.awt.Component;
004import java.awt.event.ActionEvent;
005import java.awt.event.ActionListener;
006import java.beans.PropertyVetoException;
007import java.util.*;
008import java.util.function.Predicate;
009import java.util.stream.Stream;
010
011import javax.swing.*;
012import javax.swing.table.*;
013
014import jmri.*;
015import jmri.jmrit.logixng.*;
016import jmri.util.swing.JComboBoxUtil;
017import jmri.util.swing.JmriJOptionPane;
018
019/**
020 * Table model for inline LogixNGs.
021 *
022 * @author Daniel Bergqvist Copyright (C) 2022
023 */
024public class InlineLogixNGsTableModel extends AbstractTableModel {
025
026    public static final int COLUMN_SYSTEM_NAME = 0;
027    public static final int COLUMN_USER_NAME = COLUMN_SYSTEM_NAME + 1;
028    public static final int COLUMN_PANEL_NAME = COLUMN_USER_NAME + 1;
029    public static final int COLUMN_POSITIONABLE_NAME = COLUMN_PANEL_NAME + 1;
030    public static final int COLUMN_NAMED_BEAN = COLUMN_POSITIONABLE_NAME + 1;
031    public static final int COLUMN_POS_X = COLUMN_NAMED_BEAN + 1;
032    public static final int COLUMN_POS_Y = COLUMN_POS_X + 1;
033    public static final int COLUMN_MENU = COLUMN_POS_Y + 1;
034    public static final int NUM_COLUMNS = COLUMN_MENU + 1;
035
036    private final List<LogixNG> _logixNGs = new ArrayList<>();
037    private boolean _inEditLogixNGMode = false;
038    private LogixNGEditor _logixNGEditor;
039    private Predicate<LogixNG> _filter;
040
041
042    public void init() {
043        updateList();
044        InstanceManager.getDefault(LogixNG_Manager.class)
045                .addPropertyChangeListener("length", (evt) -> { updateList(); });
046    }
047
048    public List<LogixNG> getLogixNGList() {
049        return InstanceManager.getDefault(LogixNG_Manager.class)
050                .getNamedBeanSet().stream().filter((LogixNG t) -> t.isInline())
051                .collect(java.util.stream.Collectors.toList());
052    }
053
054    private void updateList() {
055        Stream<LogixNG> stream = InstanceManager.getDefault(LogixNG_Manager.class)
056                .getNamedBeanSet().stream().filter((LogixNG t) -> t.isInline());
057        _logixNGs.clear();
058        if (_filter != null) stream = stream.filter(_filter);
059        _logixNGs.addAll(stream.collect(java.util.stream.Collectors.toList()));
060        fireTableDataChanged();
061    }
062
063    /**
064     * Set the filter to select which beans to include in the table.
065     * @param filter the filter
066     */
067    public void setFilter(Predicate<LogixNG> filter) {
068        this._filter = filter;
069        updateList();
070    }
071
072    /**
073     * Get the filter to select which beans to include in the table.
074     * @return the filter
075     */
076    public Predicate<LogixNG> getFilter() {
077        return _filter;
078    }
079
080    /** {@inheritDoc} */
081    @Override
082    public int getRowCount() {
083        return _logixNGs.size();
084    }
085
086    /** {@inheritDoc} */
087    @Override
088    public int getColumnCount() {
089        return NUM_COLUMNS;
090    }
091
092    /** {@inheritDoc} */
093    @Override
094    public String getColumnName(int col) {
095        switch (col) {
096            case COLUMN_SYSTEM_NAME:
097                return Bundle.getMessage("ColumnSystemName");
098            case COLUMN_USER_NAME:
099                return Bundle.getMessage("ColumnUserName");
100            case COLUMN_PANEL_NAME:
101                return Bundle.getMessage("InlineLogixNGsTableModel_ColumnPanelName");
102            case COLUMN_POSITIONABLE_NAME:
103                return Bundle.getMessage("InlineLogixNGsTableModel_ColumnPositionableName");
104            case COLUMN_NAMED_BEAN:
105                return Bundle.getMessage("InlineLogixNGsTableModel_ColumnNamedBean");
106            case COLUMN_POS_X:
107                return Bundle.getMessage("InlineLogixNGsTableModel_ColumnPosX");
108            case COLUMN_POS_Y:
109                return Bundle.getMessage("InlineLogixNGsTableModel_ColumnPosY");
110            case COLUMN_MENU:
111                return Bundle.getMessage("InlineLogixNGsTableModel_ColumnMenu");
112            default:
113                throw new IllegalArgumentException("Invalid column");
114        }
115    }
116
117    /** {@inheritDoc} */
118    @Override
119    public Class<?> getColumnClass(int col) {
120        switch (col) {
121            case COLUMN_SYSTEM_NAME:
122            case COLUMN_USER_NAME:
123            case COLUMN_PANEL_NAME:
124            case COLUMN_POSITIONABLE_NAME:
125            case COLUMN_NAMED_BEAN:
126                return String.class;
127            case COLUMN_POS_X:
128            case COLUMN_POS_Y:
129                return Integer.class;
130            case COLUMN_MENU:
131                return Menu.class;
132            default:
133                throw new IllegalArgumentException("Invalid column");
134        }
135    }
136
137    /** {@inheritDoc} */
138    @Override
139    public boolean isCellEditable(int row, int col) {
140        return col == COLUMN_USER_NAME || col == COLUMN_MENU;
141    }
142
143    /** {@inheritDoc} */
144    @Override
145    public void setValueAt(Object value, int rowIndex, int columnIndex) {
146        if (columnIndex == COLUMN_USER_NAME) {
147            if (value.equals("")) value = null;
148
149            LogixNG logixNG = _logixNGs.get(rowIndex);
150            if (value == null && logixNG.getUserName() == null) return;
151            if (value != null && value.equals(logixNG.getUserName())) return;
152
153            LogixNG_Manager logixNG_Manager = InstanceManager.getDefault(LogixNG_Manager.class);
154            LogixNG otherLogixNG = value != null
155                    ? logixNG_Manager.getByUserName((String) value) : null;
156            if (otherLogixNG != null) {
157                log.error("User name is not unique {}", value);
158                String msg = Bundle.getMessage("WarningUserName", "" + value);
159                JmriJOptionPane.showMessageDialog(null, msg,
160                        Bundle.getMessage("WarningTitle"),
161                        JmriJOptionPane.ERROR_MESSAGE);
162                return;
163            }
164
165            NamedBeanHandleManager nbMan = InstanceManager.getDefault(NamedBeanHandleManager.class);
166
167            logixNG.setUserName((String) value);
168            if (nbMan.inUse(logixNG.getSystemName(), logixNG)) {
169                String msg = Bundle.getMessage("UpdateToUserName", logixNG.getBeanType(), value, logixNG.getSystemName());
170                int optionPane = JmriJOptionPane.showConfirmDialog(null,
171                        msg, Bundle.getMessage("UpdateToUserNameTitle"),
172                        JmriJOptionPane.YES_NO_OPTION);
173                if (optionPane == JmriJOptionPane.YES_OPTION) {
174                    //This will update the bean reference from the systemName to the userName
175                    try {
176                        nbMan.updateBeanFromSystemToUser(logixNG);
177                    } catch (JmriException ex) {
178                        //We should never get an exception here as we already check that the username is not valid
179                        log.error("Impossible exception setting user name", ex);
180                    }
181                }
182            }
183        }
184    }
185
186    /** {@inheritDoc} */
187    @Override
188    public Object getValueAt(int rowIndex, int columnIndex) {
189        if (rowIndex >= _logixNGs.size()) throw new IllegalArgumentException(
190                String.format("Invalid row index: %s. Num rows: %s", rowIndex, _logixNGs.size()));
191
192        LogixNG logixNG = _logixNGs.get(rowIndex);
193
194        switch (columnIndex) {
195            case COLUMN_SYSTEM_NAME:
196                return logixNG.getSystemName();
197            case COLUMN_USER_NAME:
198                return logixNG.getUserName();
199            case COLUMN_PANEL_NAME:
200                return getEditorName(logixNG.getInlineLogixNG());
201            case COLUMN_POSITIONABLE_NAME:
202                return getPositionableName(logixNG.getInlineLogixNG());
203            case COLUMN_NAMED_BEAN:
204                String typeName = getTypeName(logixNG.getInlineLogixNG());
205                return typeName != null ? typeName : "";
206            case COLUMN_POS_X:
207                return getX(logixNG.getInlineLogixNG());
208            case COLUMN_POS_Y:
209                return getY(logixNG.getInlineLogixNG());
210            case COLUMN_MENU:
211                return Menu.Edit;
212            default:
213                throw new IllegalArgumentException("Invalid column");
214        }
215    }
216
217    public static String getEditorName(InlineLogixNG inlineLogixNG) {
218        return inlineLogixNG != null
219                ? inlineLogixNG.getEditorName()
220                : Bundle.getMessage("InlineLogixNGsTableModel_Error");
221    }
222
223    public static String getTypeName(InlineLogixNG inlineLogixNG) {
224        return inlineLogixNG != null
225                ? inlineLogixNG.getTypeName()
226                : Bundle.getMessage("InlineLogixNGsTableModel_Error");
227    }
228
229    public static String getPositionableName(InlineLogixNG inlineLogixNG) {
230        return inlineLogixNG != null
231                ? inlineLogixNG.getNameString()
232                : Bundle.getMessage("InlineLogixNGsTableModel_Error");
233    }
234
235    public static int getX(InlineLogixNG inlineLogixNG) {
236        return inlineLogixNG != null ? inlineLogixNG.getX() : 0;
237    }
238
239    public static int getY(InlineLogixNG inlineLogixNG) {
240        return inlineLogixNG != null ? inlineLogixNG.getY() : 0;
241    }
242
243    public void setColumnForMenu(JTable table) {
244        JComboBox<Menu> comboBox = new JComboBox<>();
245        table.setRowHeight(comboBox.getPreferredSize().height);
246        table.getColumnModel().getColumn(COLUMN_MENU)
247                .setPreferredWidth((comboBox.getPreferredSize().width) + 4);
248    }
249
250
251    public static enum Menu {
252        Edit(Bundle.getMessage("InlineLogixNGsTableModel_TableMenuEdit")),
253        Delete(Bundle.getMessage("InlineLogixNGsTableModel_TableMenuDelete"));
254
255        private final String _descr;
256
257        private Menu(String descr) {
258            _descr = descr;
259        }
260
261        @Override
262        public String toString() {
263            return _descr;
264        }
265    }
266
267
268    public static class MenuCellRenderer extends DefaultTableCellRenderer {
269
270        @Override
271        public Component getTableCellRendererComponent(JTable table, Object value,
272                boolean isSelected, boolean hasFocus, int row, int column) {
273
274            if (value == null) value = Menu.Edit;
275
276            if (! (value instanceof Menu)) {
277                throw new IllegalArgumentException("value is not an Menu: " + value.getClass().getName());
278            }
279            setText(((Menu) value).toString());
280            return this;
281        }
282    }
283
284
285    public static class MenuCellEditor extends AbstractCellEditor
286            implements TableCellEditor, ActionListener {
287
288        JTable _table;
289        InlineLogixNGsTableModel _tableModel;
290
291        public MenuCellEditor(JTable table, InlineLogixNGsTableModel tableModel) {
292            _table = table;
293            _tableModel = tableModel;
294        }
295
296        @Override
297        public Object getCellEditorValue() {
298            return Menu.Edit;
299        }
300
301        @Override
302        public Component getTableCellEditorComponent(JTable table, Object value,
303                boolean isSelected, int row, int column) {
304
305            if (value == null) value = Menu.Edit;
306
307            if (! (value instanceof Menu)) {
308                throw new IllegalArgumentException("value is not an Menu: " + value.getClass().getName());
309            }
310
311            JComboBox<Menu> menuComboBox = new JComboBox<>();
312
313            for (Menu menu : Menu.values()) {
314                menuComboBox.addItem(menu);
315            }
316            JComboBoxUtil.setupComboBoxMaxRows(menuComboBox);
317
318            menuComboBox.setSelectedItem(value);
319            menuComboBox.addActionListener(this);
320
321            return menuComboBox;
322        }
323
324        @Override
325        @SuppressWarnings("unchecked")  // Not possible to check that event.getSource() is instanceof JComboBox<Menu>
326        public void actionPerformed(ActionEvent event) {
327            if (! (event.getSource() instanceof JComboBox)) {
328                throw new IllegalArgumentException("value is not an InitialValueType: " + event.getSource().getClass().getName());
329            }
330            JComboBox<Menu> menuComboBox = (JComboBox<Menu>) event.getSource();
331
332            int row = _table.getRowSorter().convertRowIndexToModel(_table.getSelectedRow());
333            Menu menu = menuComboBox.getItemAt(menuComboBox.getSelectedIndex());
334
335            // Cancel editing before doing the change to the table.
336            _table.editingCanceled(null);
337
338            switch (menu) {
339                case Edit:
340                    edit(row);
341                    break;
342                case Delete:
343                    delete(row);
344                    break;
345                default:
346                    // Do nothing
347            }
348        }
349
350        /**
351         * Check if edit of a conditional is in progress.
352         *
353         * @return true if this is the case, after showing dialog to user
354         */
355        private boolean checkEditConditionalNG() {
356            if (_tableModel._inEditLogixNGMode) {
357                // Already editing a LogixNG, ask for completion of that edit
358                JmriJOptionPane.showMessageDialog(null,
359                        Bundle.getMessage("Error_InlineLogixNGInEditMode"), // NOI18N
360                        Bundle.getMessage("ErrorTitle"), // NOI18N
361                        JmriJOptionPane.ERROR_MESSAGE);
362                _tableModel._logixNGEditor.bringToFront();
363                return true;
364            }
365            return false;
366        }
367
368        private void edit(int row) {
369            if (checkEditConditionalNG()) return;
370
371            LogixNG logixNG = _tableModel._logixNGs.get(row);
372            LogixNGEditor logixNGEditor =
373                    new LogixNGEditor(null, logixNG.getSystemName());
374            logixNGEditor.addEditorEventListener((HashMap<String, String> data) -> {
375                data.forEach((key, value) -> {
376                    if (key.equals("Finish")) {                  // NOI18N
377                        _tableModel._inEditLogixNGMode = false;
378                    } else if (key.equals("Delete")) {           // NOI18N
379                        _tableModel._inEditLogixNGMode = false;
380                        delete(row);
381                    } else if (key.equals("chgUname")) {         // NOI18N
382                        logixNG.setUserName(value);
383                        _tableModel.fireTableDataChanged();
384                    }
385                });
386                if (logixNG.getNumConditionalNGs() == 0) {
387                    deleteBean(logixNG);
388                }
389            });
390            logixNGEditor.bringToFront();
391            _tableModel._inEditLogixNGMode = true;
392            _tableModel._logixNGEditor = logixNGEditor;
393        }
394
395        private void delete(int row) {
396            LogixNG logixNG = _tableModel._logixNGs.get(row);
397
398            if (_tableModel._inEditLogixNGMode) {
399                // Already editing a bean, ask for completion of that edit
400                JmriJOptionPane.showMessageDialog(null,
401                        Bundle.getMessage("Error_InlineLogixNGInEditMode",
402                                logixNG.getSystemName()),
403                        Bundle.getMessage("ErrorTitle"),
404                        JmriJOptionPane.ERROR_MESSAGE);
405                if (_tableModel._logixNGEditor != null) {
406                    _tableModel._logixNGEditor.bringToFront();
407                }
408                return;
409            }
410
411            DeleteBean<LogixNG> deleteBean = new DeleteBean<>(
412                    InstanceManager.getDefault(LogixNG_Manager.class));
413
414            boolean hasChildren = logixNG.getNumConditionalNGs() > 0;
415
416            deleteBean.delete(logixNG, hasChildren, (t)->{deleteBean(t);},
417                    (t,list)->{logixNG.getListenerRefsIncludingChildren(list);},
418                    jmri.jmrit.logixng.LogixNG_UserPreferences.class.getName());
419        }
420
421        private void deleteBean(LogixNG logixNG) {
422            logixNG.setEnabled(false);
423            try {
424                InstanceManager.getDefault(LogixNG_Manager.class).deleteBean(logixNG, "DoDelete");
425                if (logixNG.getInlineLogixNG() != null) {
426                    logixNG.getInlineLogixNG().setLogixNG(null);
427                }
428            } catch (PropertyVetoException e) {
429                //At this stage the DoDelete shouldn't fail, as we have already done a can delete, which would trigger a veto
430                log.error("{} : Could not Delete.", e.getMessage());
431            }
432        }
433    }
434
435
436    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(InlineLogixNGsTableModel.class);
437}