001package jmri.jmrit.beantable.oblock;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.text.ParseException;
006
007import java.util.*;
008//import java.util.concurrent.CopyOnWriteArrayList;
009
010import javax.swing.*;
011import javax.swing.table.AbstractTableModel;
012import jmri.InstanceManager;
013import jmri.NamedBean;
014import jmri.jmrit.logix.OBlock;
015import jmri.jmrit.logix.OBlockManager;
016import jmri.jmrit.logix.Portal;
017import jmri.jmrit.logix.PortalManager;
018import jmri.util.IntlUtilities;
019import jmri.util.gui.GuiLafPreferencesManager;
020import jmri.util.swing.JmriJOptionPane;
021
022/**
023 * GUI to define the Signals within an OBlock.
024 * <p>
025 * Can be used with two interfaces:
026 * <ul>
027 *     <li>original "desktop" InternalFrames (parent class TableFrames, an extended JmriJFrame)
028 *     <li>JMRI standard Tabbed tables (parent class JPanel)
029 * </ul>
030 * <hr>
031 * This file is part of JMRI.
032 * <p>
033 * JMRI is free software; you can redistribute it and/or modify it under the
034 * terms of version 2 of the GNU General Public License as published by the Free
035 * Software Foundation. See the "COPYING" file for a copy of this license.
036 * <p>
037 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
038 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
039 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
040 *
041 * @author Pete Cressman (C) 2010
042 * @author Egbert Broerse (C) 2020
043 */
044public class SignalTableModel extends AbstractTableModel implements PropertyChangeListener {
045
046    public static final int NAME_COLUMN = 0;
047    public static final int FROM_BLOCK_COLUMN = 1;
048    public static final int PORTAL_COLUMN = 2;
049    public static final int TO_BLOCK_COLUMN = 3;
050    public static final int LENGTHCOL = 4;
051    public static final int UNITSCOL = 5;
052    public static final int DELETE_COL = 6;
053    static public final int EDIT_COL = 7; // only used on _tabbed UI
054    public static final int NUMCOLS = 7;  // returns 7+1 for _tabbed
055    int _lastIdx; // for debug
056
057    PortalManager _portalMgr;
058    TableFrames _parent;
059    private SignalArray _signalList = new SignalArray();
060    private final boolean _tabbed; // updated from prefs (restart required)
061    private float _tempLen = 0.0f; // mm for length col of tempRow
062    private String[] tempRow;
063    boolean inEditMode = false;
064    java.text.DecimalFormat twoDigit = new java.text.DecimalFormat("0.00");
065
066    protected static class SignalRow {
067
068        NamedBean _signal;
069        OBlock _fromBlock;
070        Portal _portal;
071        OBlock _toBlock;
072        float _length;  // offset from signal to speed change point, stored in mm
073        boolean _isMetric;
074
075        SignalRow(NamedBean signal, OBlock fromBlock, Portal portal, OBlock toBlock, float length, boolean isMetric) {
076            _signal = signal;
077            _fromBlock = fromBlock;
078            _portal = portal;
079            _toBlock = toBlock;
080            _length = length;
081            _isMetric = isMetric;
082        }
083
084        void setSignal(NamedBean signal) {
085            _signal = signal;
086        }
087        NamedBean getSignal() {
088            return _signal;
089        }
090        void setFromBlock(OBlock fromBlock) {
091            _fromBlock = fromBlock;
092        }
093        OBlock getFromBlock() {
094            return _fromBlock;
095        }
096        void setPortal(Portal portal) {
097            _portal = portal;
098        }
099        Portal getPortal() {
100            return _portal;
101        }
102        void setToBlock(OBlock toBlock) {
103            _toBlock = toBlock;
104        }
105        OBlock getToBlock() {
106            return _toBlock;
107        }
108        void setLength(float length) {
109            _length = length;
110        }
111        float getLength() {
112            return _length;
113        }
114        void setMetric(boolean isMetric) {
115            _isMetric = isMetric;
116        }
117        boolean isMetric() {
118            return _isMetric;
119        }
120
121    }
122
123    static class SignalArray extends ArrayList<SignalRow> {
124
125        public int numberOfSignals() {
126            return size();
127        }
128
129    }
130
131    public SignalTableModel(TableFrames parent) {
132        super();
133        _parent = parent;
134        _portalMgr = InstanceManager.getDefault(PortalManager.class);
135        _tabbed = InstanceManager.getDefault(GuiLafPreferencesManager.class).isOblockEditTabbed();
136    }
137
138    public void init() {
139        makeList();
140        initTempRow();
141    }
142
143    void initTempRow() {
144        if (!_tabbed) {
145            tempRow = new String[NUMCOLS];
146            tempRow[LENGTHCOL] = twoDigit.format(0.0);
147            tempRow[UNITSCOL] = Bundle.getMessage("in");
148            tempRow[DELETE_COL] = Bundle.getMessage("ButtonClear");
149        }
150    }
151
152    // Rebuild _signalList CopyOnWriteArrayList<SignalRow>, copying Signals from Portal table
153    private void makeList() {
154        //CopyOnWriteArrayList<SignalRow> tempList = new CopyOnWriteArrayList<>();
155        SignalArray tempList = new SignalArray();
156        Collection<Portal> portals = _portalMgr.getPortalSet();
157        for (Portal portal : portals) {
158            // check portal is well formed
159            OBlock fromBlock = portal.getFromBlock();
160            OBlock toBlock = portal.getToBlock();
161            if (fromBlock != null && toBlock != null) {
162                SignalRow sr;
163                NamedBean signal = portal.getFromSignal();
164                if (signal != null) {
165                    sr = new SignalRow(signal, fromBlock, portal, toBlock, portal.getFromSignalOffset(), toBlock.isMetric());
166                    //_signalList.add(sr);
167                    addToList(tempList, sr);
168                    //log.debug("1 SR added to tempList, new size = {}", tempList.numberOfSignals());
169                }
170                signal = portal.getToSignal();
171                if (signal != null) {
172                    sr = new SignalRow(signal, toBlock, portal, fromBlock, portal.getToSignalOffset(), fromBlock.isMetric());
173                    //_signalList.add(sr);
174                    addToList(tempList, sr);
175                    //log.debug("1 SR added to tempList, new size = {}", tempList.numberOfSignals());
176                }
177            } else {
178                // Can't get jmri.util.JUnitAppender.assertErrorMessage recognized in TableFramesTest! OK just warn then
179                log.warn("Portal {} needs an OBlock on each side", portal.getName());
180            }
181        }
182        //_signalList = tempList;
183        _signalList = (SignalArray) tempList.clone();
184        _lastIdx = tempList.numberOfSignals();
185        //log.debug("TempList copied, size = {}", tempList.numberOfSignals());
186        _signalList.sort(new NameSorter());
187        //log.debug("makeList exit: _signalList size {} items.", _signalList.numberOfSignals());
188    }
189
190    private static void addToList(SignalArray array, SignalRow sr) {
191        // not in array, for the sort, insert at correct position // TODO add + sort instead?
192        boolean add = true;
193        for (int j = 0; j < array.numberOfSignals(); j++) {
194            if (sr.getSignal().getDisplayName().compareTo(array.get(j).getSignal().getDisplayName()) < 0) {
195                array.add(j, sr); // added first time
196                add = false;
197                //log.debug("comparing list item {} name {}", j, sr.getSignal().getDisplayName());
198                break;
199            }
200        }
201        if (add) {
202            array.add(sr);
203            //log.debug("comparing list item at last pos {} name {}", array.numberOfSignals() , sr.getSignal().getDisplayName());
204        }
205    }
206
207    public static class NameSorter implements Comparator<SignalRow>
208    {
209        @Override
210        public int compare(SignalRow o1, SignalRow o2) {
211            return o2.getSignal().compareTo(o1.getSignal());
212        }
213    }
214
215    private String checkSignalRow(SignalRow sr) {
216        Portal portal = sr.getPortal();
217        OBlock fromBlock = sr.getFromBlock();
218        OBlock toBlock = sr.getToBlock();
219        String msg = null;
220        if (portal != null) {
221            if (toBlock == null && fromBlock == null) {
222                msg = Bundle.getMessage("SignalDirection",
223                        portal.getName(),
224                        portal.getFromBlock().getDisplayName(),
225                        portal.getToBlock().getDisplayName());
226                return msg;
227            }
228            OBlock pToBlk = portal.getToBlock();
229            OBlock pFromBlk = portal.getFromBlock();
230            if (pToBlk.equals(toBlock)) {
231                if (fromBlock == null) {
232                    sr.setFromBlock(pFromBlk);
233                }
234            } else if (pFromBlk.equals(toBlock)) {
235                if (fromBlock == null) {
236                    sr.setFromBlock(pToBlk);
237                }
238            } else if (pToBlk.equals(fromBlock)) {
239                if (toBlock == null) {
240                    sr.setToBlock(pFromBlk);
241                }
242            } else if (pFromBlk.equals(fromBlock)) {
243                if (toBlock == null) {
244                    sr.setToBlock(pToBlk);
245                }
246            } else {
247                msg = Bundle.getMessage("PortalBlockConflict", portal.getName(),
248                        (toBlock != null ? toBlock.getDisplayName() : "(null to-block reference)"));
249            }
250        } else if (fromBlock != null && toBlock != null) {
251            Portal p = getPortalWithBlocks(fromBlock, toBlock);
252            if (p == null) {
253                msg = Bundle.getMessage("NoSuchPortal", fromBlock.getDisplayName(), toBlock.getDisplayName());
254            } else {
255                sr.setPortal(p);
256            }
257        }
258        if (msg == null && fromBlock != null && fromBlock.equals(toBlock)) {
259            msg = Bundle.getMessage("SametoFromBlock", fromBlock.getDisplayName());
260        }
261        return msg;
262    }
263
264    // From the PortalSet get the single portal using the given To and From OBlock.
265    private Portal getPortalWithBlocks(OBlock fromBlock, OBlock toBlock) {
266        Collection<Portal> portals = _portalMgr.getPortalSet();
267        for (Portal portal : portals) {
268            OBlock fromBlk = portal.getFromBlock();
269            OBlock toBlk = portal.getToBlock();
270            if ((fromBlk.equals(fromBlock) &&  toBlk.equals(toBlock)) ||
271                    (fromBlk.equals(toBlock) && toBlk.equals(fromBlock))) {
272                return portal;
273            }
274        }
275        return null;
276    }
277
278    protected String checkDuplicateSignal(NamedBean signal) {
279        //log.debug("checkDuplSig checking for duplicate Signal in list by the same name");
280        if (signal == null) {
281            return null;
282        }
283        for (SignalRow srow : _signalList) {
284            if (signal.equals(srow.getSignal())) {
285                return Bundle.getMessage("DuplSignalName", signal.getDisplayName(),
286                        srow.getToBlock().getDisplayName(), srow.getPortal().getName(),
287                        srow.getFromBlock().getDisplayName());
288            }
289        }
290        return null;
291    }
292
293    private String checkDuplicateSignal(SignalRow row) {
294        //log.debug("checkDuplSig checking for duplicate Signal in list using new entry row");
295        NamedBean signal = row.getSignal();
296        if (signal == null) {
297            return null;
298        }
299        for (SignalRow srow : _signalList) {
300            if (srow.equals(row)) {
301                continue;
302            }
303            if (signal.equals(srow.getSignal())) {
304                return Bundle.getMessage("DuplSignalName", signal.getDisplayName(), srow.getToBlock().getDisplayName(), srow.getPortal().getName(), srow.getFromBlock().getDisplayName());
305
306            }
307        }
308        return null;
309    }
310
311    private String checkDuplicateProtection(SignalRow row) {
312        Portal portal = row.getPortal();
313        OBlock block = row.getToBlock();
314        if (block == null || portal == null) {
315            return null;
316        }
317        for (SignalRow srow : _signalList) {
318            if (srow.equals(row)) {
319                continue;
320            }
321            if (block.equals(srow.getToBlock()) && portal.equals(srow.getPortal())) {
322                return Bundle.getMessage("DuplProtection", block.getDisplayName(), portal.getName(), srow.getFromBlock().getDisplayName(), srow.getSignal().getDisplayName());
323            }
324        }
325        return null;
326    }
327
328    @Override
329    public int getColumnCount() {
330        return NUMCOLS + (_tabbed ? 1 : 0); // add Edit column on _tabbed
331    }
332
333    @Override
334    public int getRowCount() {
335        return _signalList.numberOfSignals() + (_tabbed ? 0 : 1); // + 1 row in _desktop to create entry row
336        // +1 adds the extra empty row at the bottom of the table display, causes IOB when called externally when _tabbed
337    }
338
339    @Override
340    public String getColumnName(int col) {
341        switch (col) {
342            case NAME_COLUMN:
343                return Bundle.getMessage("SignalName");
344            case FROM_BLOCK_COLUMN:
345                return Bundle.getMessage("FromBlockName");
346            case PORTAL_COLUMN:
347                return Bundle.getMessage("ThroughPortal");
348            case TO_BLOCK_COLUMN:
349                return Bundle.getMessage("ToBlockName");
350            case LENGTHCOL:
351                return Bundle.getMessage("Offset");
352            case UNITSCOL:
353            case EDIT_COL:
354                return "  ";
355            default:
356                // fall through
357                break;
358        }
359        return "";
360    }
361
362    @Override
363    public Object getValueAt(int rowIndex, int columnIndex) {
364        if (!_tabbed && (rowIndex == _signalList.numberOfSignals())) { // this must be tempRow, a new entry, read values from tempRow
365            if (columnIndex == LENGTHCOL) {
366                //log.debug("GetValue SignalTable length entered {} =============== in row {}", _tempLen, rowIndex);
367                if (tempRow[UNITSCOL].equals(Bundle.getMessage("cm"))) {
368                    return (twoDigit.format(_tempLen/10));
369                }
370                return (twoDigit.format(_tempLen/25.4f));
371            }
372            if (columnIndex == UNITSCOL) {
373                return tempRow[UNITSCOL].equals(Bundle.getMessage("cm")); // TODO renderer/special class
374            }
375            return tempRow[columnIndex];
376        }
377        if (rowIndex >= _signalList.numberOfSignals() || rowIndex >= _lastIdx) {
378            //log.error("SignalTable requested ROW {}, SIZE is {}, expected {}", rowIndex, _signalList.numberOfSignals(), _lastIdx);
379            //log.debug("items in list: {}", _signalList.numberOfSignals()); // debug
380            return columnIndex + "" + rowIndex + "?";
381        }
382
383        SignalRow signalRow = _signalList.get(rowIndex); // edit an existing array entry
384        switch (columnIndex) {
385            case NAME_COLUMN:
386                if (signalRow.getSignal() != null) {
387                    return signalRow.getSignal().getDisplayName();
388                }
389                break;
390            case FROM_BLOCK_COLUMN:
391                if (signalRow.getFromBlock() != null) {
392                    return signalRow.getFromBlock().getDisplayName();
393                }
394                break;
395            case PORTAL_COLUMN:
396                if (signalRow.getPortal() != null) {
397                    return signalRow.getPortal().getName();
398                }
399                break;
400            case TO_BLOCK_COLUMN:
401                if (signalRow.getToBlock() != null) {
402                    return signalRow.getToBlock().getDisplayName();
403                }
404                break;
405            case LENGTHCOL:
406                if (signalRow.isMetric()) {
407                    return (twoDigit.format(signalRow.getLength()/10));
408                }
409                return (twoDigit.format(signalRow.getLength()/25.4f));
410            case UNITSCOL:
411                return signalRow.isMetric();
412            case DELETE_COL:
413                return Bundle.getMessage("ButtonDelete");
414            case EDIT_COL:
415                return Bundle.getMessage("ButtonEdit");
416            default:
417                // fall through
418                break;
419        }
420        return "";
421    }
422
423    @Override
424    public void setValueAt(Object value, int row, int col) {
425        String msg = null;
426        if (_signalList.numberOfSignals() == row) { // this is the new entry in tempRow, not yet in _signalList
427            if (col == DELETE_COL) { // labeled "Clear" in tempRow
428                initTempRow();
429                fireTableRowsUpdated(row, row);
430                return;
431            } else if (col == UNITSCOL) {
432                if (value.equals(true)) {
433                    tempRow[UNITSCOL] = Bundle.getMessage("cm");
434                } else {
435                    tempRow[UNITSCOL] = Bundle.getMessage("in");
436                }
437                fireTableRowsUpdated(row, row);
438                return;               
439            } else if (col == LENGTHCOL) {
440                //log.debug("SetValue SignalTable length set {} in row {}", value.toString(), row);
441                try {
442                    _tempLen = IntlUtilities.floatValue(value.toString());
443                    //log.debug("setValue _tempLen = {} {}", _tempLen, tempRow[UNITSCOL]);
444                    if (tempRow[UNITSCOL].equals(Bundle.getMessage("cm"))) {
445                        _tempLen *= 10f;
446                    } else {
447                        _tempLen *= 25.4f;                            
448                    }
449                } catch (ParseException e) {
450                    JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("BadNumber", tempRow[LENGTHCOL]),
451                            Bundle.getMessage("ErrorTitle"), JmriJOptionPane.WARNING_MESSAGE);                    
452                }
453                return;
454            }
455            String str = (String) value;
456            if (str == null || str.trim().length() == 0) {
457                tempRow[col] = null;
458                return;
459            }
460            tempRow[col] = str.trim();
461            // try to add new value into new row in SignalTable
462            OBlock fromBlock = null;
463            OBlock toBlock = null;
464            Portal portal = null;
465            NamedBean signal;
466            OBlockManager OBlockMgr = InstanceManager.getDefault(OBlockManager.class);
467            if (tempRow[FROM_BLOCK_COLUMN] != null) {
468                fromBlock = OBlockMgr.getOBlock(tempRow[FROM_BLOCK_COLUMN]);
469                if (fromBlock == null) {
470                    msg = Bundle.getMessage("NoSuchBlock", tempRow[FROM_BLOCK_COLUMN]);
471                }
472            }
473            if (msg == null && tempRow[TO_BLOCK_COLUMN] != null) {
474                toBlock = OBlockMgr.getOBlock(tempRow[TO_BLOCK_COLUMN]);
475                if (toBlock == null) {
476                    msg = Bundle.getMessage("NoSuchBlock", tempRow[TO_BLOCK_COLUMN]);
477                }
478            }
479            if (msg == null) {
480                if (tempRow[PORTAL_COLUMN] != null) {
481                    portal = _portalMgr.getPortal(tempRow[PORTAL_COLUMN]);
482                    if (portal == null) {
483                        msg = Bundle.getMessage("NoSuchPortalName", tempRow[PORTAL_COLUMN]);
484                    }                    
485                } else {
486                    if (fromBlock != null && toBlock != null) {
487                        portal = getPortalWithBlocks(fromBlock, toBlock);
488                        if (portal == null) {
489                            msg = Bundle.getMessage("NoSuchPortal", tempRow[FROM_BLOCK_COLUMN], tempRow[TO_BLOCK_COLUMN]);
490                        } else {
491                            tempRow[PORTAL_COLUMN] = portal.getName();
492                        }
493                    }                    
494                }
495            }
496            if (msg == null && tempRow[NAME_COLUMN] != null) {
497                signal = Portal.getSignal(tempRow[NAME_COLUMN]);
498                if (signal == null) {
499                    msg = Bundle.getMessage("NoSuchSignal", tempRow[NAME_COLUMN]);
500                } else {
501                    msg = checkDuplicateSignal(signal);
502                }
503                if (msg == null) {
504                    if (fromBlock != null && toBlock != null) {
505                        portal = getPortalWithBlocks(fromBlock, toBlock);
506                        if (portal == null) {
507                            msg = Bundle.getMessage("NoSuchPortal", tempRow[FROM_BLOCK_COLUMN], tempRow[TO_BLOCK_COLUMN]);
508                        } else {
509                            tempRow[PORTAL_COLUMN] = portal.getName();
510                        }
511                    } else {
512                        return;
513                    }
514                }
515                if (msg == null) {
516                    float length = 0.0f;
517                    boolean isMetric = tempRow[UNITSCOL].equals(Bundle.getMessage("cm"));
518                    try {
519                        length = IntlUtilities.floatValue(tempRow[LENGTHCOL]);
520                        if (isMetric) {
521                            length *= 10f;
522                        } else {
523                            length *= 25.4f;                            
524                        }
525                    } catch (ParseException e) {
526                        msg = Bundle.getMessage("BadNumber", tempRow[LENGTHCOL]);                    
527                    }
528                    if (isMetric) {
529                        tempRow[UNITSCOL] = Bundle.getMessage("cm");
530                    } else {
531                        tempRow[UNITSCOL] = Bundle.getMessage("in");
532                    }
533                    if (msg == null) {
534                        // all checks passed, create new SignalRow to add to _signalList
535                        SignalRow signalRow = new SignalRow(signal, fromBlock, portal, toBlock, length, isMetric);
536                        msg = setSignal(signalRow, false);
537                        //if (msg == null) {
538                            //if (signalRow.getLength() == 0) {
539                                //log.error("#544 empty tempRow added to SignalList (now {})", _signalList.numberOfSignals());
540                            //}
541                            //_signalList.add(signalRow); // BUG no need to do this, as the table will be updated from the OBlock settings
542                            // it caused the ghost row, which is squasehed out when the list is rebuilt
543                        //}
544                        initTempRow();
545                        fireTableDataChanged();
546                    }
547                }
548            }
549        } else { // Editing an existing signal configuration row
550            SignalRow signalRow;
551            try {
552                signalRow = _signalList.get(row);
553                //log.debug("SetValue fetched SignalRow {}", row);
554            } catch (IndexOutOfBoundsException e) {
555                // ignore, happened in 4.21.2 for some reason, showed as a duplicate row after new entry, now fixed
556                log.warn("setValue out of range");
557                return;
558            }
559            OBlockManager OBlockMgr = InstanceManager.getDefault(OBlockManager.class);
560            switch (col) {
561                case NAME_COLUMN:
562                    NamedBean signal = Portal.getSignal((String) value);
563                    if (signal == null) {
564                        msg = Bundle.getMessage("NoSuchSignal", value);
565                        // signalRow.setSignal(null);
566                        break;
567                    }
568                    Portal portal = signalRow.getPortal();
569                    if (portal != null && signalRow.getToBlock() != null) {
570                        NamedBean oldSignal = signalRow.getSignal();
571                        signalRow.setSignal(signal);
572                        msg = checkDuplicateSignal(signalRow);
573                        if (msg == null) {
574                            deleteSignal(signalRow);    // delete old
575                            msg = setSignal(signalRow, false);
576                            fireTableRowsUpdated(row, row);
577                        } else {
578                            signalRow.setSignal(oldSignal);
579                        }
580                    }
581                    break;
582                case FROM_BLOCK_COLUMN:
583                    OBlock block = OBlockMgr.getOBlock((String) value);
584                    if (block == null) {
585                        msg = Bundle.getMessage("NoSuchBlock", value);
586                        break;
587                    }
588                    if (block.equals(signalRow.getFromBlock())) {
589                        break;      // no change
590                    }
591                    deleteSignal(signalRow);    // delete old
592                    signalRow.setFromBlock(block);
593                    portal = signalRow.getPortal();
594                    if (checkPortalBlock(portal, block)) {
595                        signalRow.setToBlock(null);
596                    } else {
597                        // get new portal
598                        portal = getPortalWithBlocks(block, signalRow.getToBlock());
599                        signalRow.setPortal(portal);
600                    }
601                    msg = checkSignalRow(signalRow);
602                    if (msg == null) {
603                        msg = checkDuplicateProtection(signalRow);
604                    } else {
605                        signalRow.setPortal(null);
606                        break;
607                    }
608                    if (msg == null && signalRow.getPortal() != null) {
609                        msg = setSignal(signalRow, true);
610                    } else {
611                        signalRow.setPortal(null);
612                    }
613                    fireTableRowsUpdated(row, row);
614                    break;
615                case PORTAL_COLUMN:
616                    portal = _portalMgr.getPortal((String) value);
617                    if (portal == null) {
618                        msg = Bundle.getMessage("NoSuchPortalName", value);
619                        break;
620                    }
621                    deleteSignal(signalRow);    // delete old in Portal
622                    signalRow.setPortal(portal);
623                    block = signalRow.getToBlock();
624                    if (checkPortalBlock(portal, block)) {
625                        signalRow.setFromBlock(null);
626                    } else {
627                        block = signalRow.getFromBlock();
628                        if (checkPortalBlock(portal, block)) {
629                            signalRow.setToBlock(null);
630                        }
631                    }
632                    msg = checkSignalRow(signalRow);
633                    if (msg == null) {
634                        msg = checkDuplicateProtection(signalRow);
635                    } else {
636                        signalRow.setToBlock(null);
637                        break;
638                    }
639                    if (msg == null) {
640                        signalRow.setPortal(portal);
641                        msg = setSignal(signalRow, false);
642                        fireTableRowsUpdated(row, row);
643                    }
644                    break;
645                case TO_BLOCK_COLUMN:
646                    block = OBlockMgr.getOBlock((String) value);
647                    if (block == null) {
648                        msg = Bundle.getMessage("NoSuchBlock", value);
649                        break;
650                    }
651                    if (block.equals(signalRow.getToBlock())) {
652                        break;      // no change
653                    }
654                    deleteSignal(signalRow);    // delete old in Portal
655                    signalRow.setToBlock(block);
656                    portal = signalRow.getPortal();
657                    if (checkPortalBlock(portal, block)) {
658                        signalRow.setFromBlock(null);
659                    } else {
660                        // get new portal
661                        portal = getPortalWithBlocks(signalRow.getFromBlock(), block);
662                        signalRow.setPortal(portal);
663                    }
664                    msg = checkSignalRow(signalRow);
665                    if (msg == null) {
666                        msg = checkDuplicateProtection(signalRow);
667                    } else {
668                        signalRow.setPortal(null);
669                        break;
670                    }
671                    if (msg == null && signalRow.getPortal() != null) {
672                        msg = setSignal(signalRow, true);
673                    } else {
674                        signalRow.setPortal(null);
675                    }
676                    fireTableRowsUpdated(row, row);
677                    break;
678                case LENGTHCOL: // named "Offset" in table header, will be stored on ToBlock
679                    //log.debug("SetValue SignalTable length set {} in row {}", value.toString(), row);
680                    try {
681                        float len = IntlUtilities.floatValue(value.toString());
682                        //log.debug("SetValue Offset copied to: {} in row {}", len, row);
683                        if (signalRow.isMetric()) {
684                            signalRow.setLength(len * 10.0f);
685                        } else {
686                            signalRow.setLength(len * 25.4f);
687                        }
688                        //log.debug("Length stored in SR as {}", signalRow.getLength());
689                        //fireTableRowsUpdated(row, row); // reads (GetValue) from portal signal as configured? ignores the new entry
690                    } catch (ParseException e) {
691                        msg = Bundle.getMessage("BadNumber", value);
692                        //log.error("SetValue BadNumber {}", value);
693                    }
694                    if (msg == null && signalRow.getPortal() != null) {
695                        msg = setSignal(signalRow, false); // configures Portal & OBlock
696                    } else {
697                        signalRow.setPortal(null);
698                    }
699                    //fireTableRowsUpdated(row, row); // not needed, change will be picked up from the OBlockTable PropertyChange
700                    break;
701                case UNITSCOL:
702                    signalRow.setMetric((Boolean)value);
703                    fireTableRowsUpdated(row, row);
704                    break;
705                case DELETE_COL:
706                    deleteSignal(signalRow);
707                    _signalList.remove(signalRow);
708                    fireTableDataChanged();
709                    break;
710                case EDIT_COL:
711                    editSignal(Portal.getSignal(signalRow.getSignal().getDisplayName()), signalRow);
712                    break;
713                default:
714                    // fall through
715                    break;
716            }
717        }
718
719        if (msg != null) {
720            JmriJOptionPane.showMessageDialog(null, msg,
721                    Bundle.getMessage("WarningTitle"), JmriJOptionPane.WARNING_MESSAGE);
722            // doesn't close by clicking OK after DnD as focus lost, only Esc in JMRI 4.21.2 on macOS
723        }
724    }
725
726    // also used in _tabbed EditSignalPane
727    protected void deleteSignal(SignalRow signalRow) {
728        Portal portal = signalRow.getPortal();
729        if (portal == null) {
730            portal = getPortalWithBlocks(signalRow.getFromBlock(), signalRow.getToBlock());
731        }
732        if (portal != null) {
733            // remove signal from previous portal
734            portal.deleteSignal(signalRow.getSignal());
735        }
736    }
737
738    private void editSignal(NamedBean signal, SignalRow sr) {
739        if (_tabbed && signal != null && !inEditMode) {
740            inEditMode = true;
741            // open SignalEditFrame
742            SignalEditFrame sef = new SignalEditFrame(Bundle.getMessage("TitleSignalEditor", sr.getSignal().getDisplayName()),
743                    signal, sr, this);
744            // TODO run on separate thread?
745            sef.setVisible(true);
746        }
747    }
748
749    static private String setSignal(SignalRow signalRow, boolean deletePortal) {
750        Portal portal = signalRow.getPortal();
751        float length = signalRow.getLength();
752        if (portal.setProtectSignal(signalRow.getSignal(), length, signalRow.getToBlock())) {
753            if (signalRow.getFromBlock() == null) {
754                signalRow.setFromBlock(portal.getOpposingBlock(signalRow.getToBlock()));
755            }
756        } else {
757            if (deletePortal) {
758                signalRow.setPortal(null);
759            } else {
760                signalRow.setToBlock(null);
761            }
762            return Bundle.getMessage("PortalBlockConflict", portal.getName(),
763                    signalRow.getToBlock().getDisplayName());
764        }
765        return null;
766    }
767
768    static private boolean checkPortalBlock(Portal portal, OBlock block) {
769        if (block == null) {
770            return false;
771        }
772        return (block.equals(portal.getToBlock()) || block.equals(portal.getFromBlock()));
773    }
774
775    @Override
776    public boolean isCellEditable(int row, int col) {
777        return true;
778    }
779
780    @Override
781    public Class<?> getColumnClass(int col) {
782        switch (col) {
783            case DELETE_COL:
784            case EDIT_COL:
785                return JButton.class;
786            case UNITSCOL:
787                return JToggleButton.class;
788            case NAME_COLUMN:
789            default:
790                return String.class;
791        }
792    }
793
794    public static int getPreferredWidth(int col) {
795        switch (col) {
796            case NAME_COLUMN:
797            case FROM_BLOCK_COLUMN:
798            case PORTAL_COLUMN:
799            case TO_BLOCK_COLUMN:
800                return new JTextField(12).getPreferredSize().width;
801            case LENGTHCOL:
802                return new JTextField(6).getPreferredSize().width;
803            case UNITSCOL:
804                return new JTextField(5).getPreferredSize().width;
805            case DELETE_COL:
806                return new JButton("DELETE").getPreferredSize().width; // NOI18N
807            case EDIT_COL:
808                return new JButton("EDIT").getPreferredSize().width; // NOI18N
809            default:
810                // fall through
811                break;
812        }
813        return 5;
814    }
815
816    public boolean editMode() {
817        return inEditMode;
818    }
819
820    public void setEditMode(boolean editing) {
821        inEditMode = editing;
822    }
823
824    @Override
825    public void propertyChange(PropertyChangeEvent e) {
826        String property = e.getPropertyName();
827        if (property.equals("length") || property.equals("portalCount")
828                || property.equals("UserName") || property.equals("signalChange")) {
829            makeList();
830            fireTableDataChanged();
831        }
832    }
833
834    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SignalTableModel.class);
835
836}