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