001package jmri.jmrit.logixng.actions;
002
003import java.awt.FlowLayout;
004import java.awt.event.ActionEvent;
005import java.beans.*;
006import java.util.*;
007
008import javax.swing.*;
009
010import jmri.*;
011import jmri.jmrit.logixng.*;
012import jmri.jmrit.logixng.implementation.DefaultSymbolTable;
013import jmri.jmrit.logixng.util.ReferenceUtil;
014import jmri.jmrit.logixng.util.parser.*;
015import jmri.jmrit.logixng.util.parser.ExpressionNode;
016import jmri.jmrit.logixng.util.parser.RecursiveDescentParser;
017import jmri.util.ThreadingUtil;
018import jmri.util.TypeConversionUtil;
019
020/**
021 * This action show a dialog.
022 *
023 * @author Daniel Bergqvist Copyright 2021
024 */
025public class ShowDialog extends AbstractDigitalAction
026        implements FemaleSocketListener, PropertyChangeListener, VetoableChangeListener {
027
028    private static final ResourceBundle rbx =
029            ResourceBundle.getBundle("jmri.jmrit.logixng.implementation.ImplementationBundle");
030
031    private String _validateSocketSystemName;
032    private final FemaleDigitalExpressionSocket _validateSocket;
033    private String _executeSocketSystemName;
034    private final FemaleDigitalActionSocket _executeSocket;
035    private Set<Button> _enabledButtons = new HashSet<>();
036    private String _localVariableForSelectedButton = "";
037    private String _localVariableForInputString = "";
038    private boolean _modal = true;
039    private boolean _multiLine = false;
040    private FormatType _formatType = FormatType.OnlyText;
041    private String _format = "";
042    private final List<Data> _dataList = new ArrayList<>();
043    private final InternalFemaleSocket _internalSocket = new InternalFemaleSocket();
044    private JDialog _dialog;
045
046
047    public ShowDialog(String sys, String user)
048            throws BadUserNameException, BadSystemNameException {
049        super(sys, user);
050        _validateSocket = InstanceManager.getDefault(DigitalExpressionManager.class)
051                .createFemaleSocket(this, this, Bundle.getMessage("ShowDialog_SocketValidate"));
052        _executeSocket = InstanceManager.getDefault(DigitalActionManager.class)
053                .createFemaleSocket(this, this, Bundle.getMessage("ShowDialog_SocketExecute"));
054    }
055
056    @Override
057    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames)
058            throws ParserException, JmriException {
059        DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class);
060        String sysName = systemNames.get(getSystemName());
061        String userName = userNames.get(getSystemName());
062        if (sysName == null) sysName = manager.getAutoSystemName();
063        ShowDialog copy = new ShowDialog(sysName, userName);
064        copy.setComment(getComment());
065        for (Button button : _enabledButtons) {
066            copy.getEnabledButtons().add(button);
067        }
068        copy.setLocalVariableForSelectedButton(_localVariableForSelectedButton);
069        copy.setLocalVariableForInputString(_localVariableForInputString);
070        copy.setModal(_modal);
071        copy.setMultiLine(_multiLine);
072        copy.setFormat(_format);
073        copy.setFormatType(_formatType);
074        for (Data data : _dataList) {
075            copy.getDataList().add(new Data(data));
076        }
077        return manager.registerAction(copy).deepCopyChildren(this, systemNames, userNames);
078    }
079
080    /**
081     * Return the list of buttons.
082     * @return the list of buttons.
083     */
084    public Set<Button> getEnabledButtons() {
085        return this._enabledButtons;
086    }
087
088    public void setModal(boolean modal) {
089        _modal = modal;
090    }
091
092    public boolean getModal() {
093        return _modal;
094    }
095
096    public void setMultiLine(boolean multiLine) {
097        _multiLine = multiLine;
098    }
099
100    public boolean getMultiLine() {
101        return _multiLine;
102    }
103
104    public void setLocalVariableForSelectedButton(String localVariable) {
105        _localVariableForSelectedButton = localVariable;
106    }
107
108    public String getLocalVariableForSelectedButton() {
109        return _localVariableForSelectedButton;
110    }
111
112    public void setLocalVariableForInputString(String localVariableForInputString) {
113        _localVariableForInputString = localVariableForInputString;
114    }
115
116    public String getLocalVariableForInputString() {
117        return _localVariableForInputString;
118    }
119
120    public void setFormatType(FormatType formatType) {
121        _formatType = formatType;
122    }
123
124    public FormatType getFormatType() {
125        return _formatType;
126    }
127
128    public void setFormat(String format) {
129        _format = format;
130    }
131
132    public String getFormat() {
133        return _format;
134    }
135
136    public List<Data> getDataList() {
137        return _dataList;
138    }
139
140    @Override
141    public void vetoableChange(java.beans.PropertyChangeEvent evt) throws java.beans.PropertyVetoException {
142/*
143        if ("CanDelete".equals(evt.getPropertyName())) { // No I18N
144            if (evt.getOldValue() instanceof Memory) {
145                if (evt.getOldValue().equals(getMemory().getBean())) {
146                    throw new PropertyVetoException(getDisplayName(), evt);
147                }
148            }
149        } else if ("DoDelete".equals(evt.getPropertyName())) { // No I18N
150            if (evt.getOldValue() instanceof Memory) {
151                if (evt.getOldValue().equals(getMemory().getBean())) {
152                    setMemory((Memory)null);
153                }
154            }
155        }
156*/
157    }
158
159    /** {@inheritDoc} */
160    @Override
161    public Category getCategory() {
162        return Category.OTHER;
163    }
164
165    private List<Object> getDataValues() throws JmriException {
166        List<Object> values = new ArrayList<>();
167        for (Data _data : _dataList) {
168            switch (_data._dataType) {
169                case LocalVariable:
170                    values.add(getConditionalNG().getSymbolTable().getValue(_data._data));
171                    break;
172
173                case Memory:
174                    MemoryManager memoryManager = InstanceManager.getDefault(MemoryManager.class);
175                    Memory memory = memoryManager.getMemory(_data._data);
176                    if (memory == null) throw new IllegalArgumentException("Memory '" + _data._data + "' not found");
177                    values.add(memory.getValue());
178                    break;
179
180                case Reference:
181                    values.add(ReferenceUtil.getReference(
182                            getConditionalNG().getSymbolTable(), _data._data));
183                    break;
184
185                case Formula:
186                    if (_data._expressionNode != null) {
187                        values.add(_data._expressionNode.calculate(getConditionalNG().getSymbolTable()));
188                    }
189
190                    break;
191
192                default:
193                    throw new IllegalArgumentException("_formatType has invalid value: "+_formatType.name());
194            }
195        }
196        return values;
197    }
198
199    /** {@inheritDoc} */
200    @Override
201    public void execute() throws JmriException {
202
203        String str;
204        String strMultiLine;
205
206        switch (_formatType) {
207            case OnlyText:
208                str = _format;
209                break;
210
211            case CommaSeparatedList:
212                StringBuilder sb = new StringBuilder();
213                for (Object value : getDataValues()) {
214                    if (sb.length() > 0) sb.append(", ");
215                    sb.append(value != null ? value.toString() : "null");
216                }
217                str = sb.toString();
218                break;
219
220            case StringFormat:
221                str = String.format(_format, getDataValues().toArray());
222                break;
223
224            default:
225                throw new IllegalArgumentException("_formatType has invalid value: "+_formatType.name());
226        }
227
228        final ConditionalNG conditionalNG = getConditionalNG();
229        final DefaultSymbolTable newSymbolTable = new DefaultSymbolTable(conditionalNG.getSymbolTable());
230
231        if (_multiLine) strMultiLine = "<html>" + str + "</html>";
232        else strMultiLine = str;
233
234        Object value = null;
235        if (!_localVariableForInputString.isEmpty()) {
236           value = newSymbolTable.getValue(_localVariableForInputString);
237        }
238        final Object currentValue = value;
239
240        ThreadingUtil.runOnGUIEventually(() -> {
241
242            if (_dialog != null) _dialog.dispose();
243
244            _dialog = new JDialog(
245                    (JFrame)null,
246                    Bundle.getMessage("ShowDialog_Title"),
247                    _modal);
248
249            _dialog.addWindowListener(new java.awt.event.WindowAdapter() {
250                @Override
251                public void windowClosing(java.awt.event.WindowEvent e) {
252                    _dialog = null;
253                }
254            });
255
256            JPanel panel = new JPanel();
257            panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
258            panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
259            _dialog.getContentPane().add(panel);
260
261            panel.add(new JLabel(strMultiLine));
262
263            JTextField textField = new JTextField(20);
264            if (!_localVariableForInputString.isEmpty()) {
265                if (currentValue != null) {
266                    String strValue = TypeConversionUtil.convertToString(currentValue, false);
267                    textField.setText(strValue);
268                }
269                panel.add(textField);
270            }
271
272            JPanel buttonPanel = new JPanel();
273            buttonPanel.setLayout(new FlowLayout());
274
275            for (Button button : Button.values()) {
276                if (_enabledButtons.contains(button)) {
277                    JButton jbutton = new JButton(button._text);
278                    jbutton.addActionListener((ActionEvent e) -> {
279                        synchronized(ShowDialog.this) {
280                            _internalSocket.conditionalNG = conditionalNG;
281                            _internalSocket.newSymbolTable = newSymbolTable;
282                            _internalSocket.selectedButton = button._value;
283                            _internalSocket.inputValue = textField.getText();
284                            conditionalNG.execute(_internalSocket);
285                        }
286                    });
287                    buttonPanel.add(jbutton);
288                }
289            }
290            panel.add(buttonPanel);
291
292            _dialog.pack();
293            _dialog.setLocationRelativeTo(null);
294            _dialog.setVisible(true);
295        });
296    }
297
298    @Override
299    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
300        switch (index) {
301            case 0:
302                return _validateSocket;
303
304            case 1:
305                return _executeSocket;
306
307            default:
308                throw new IllegalArgumentException(
309                        String.format("index has invalid value: %d", index));
310        }
311    }
312
313    @Override
314    public int getChildCount() {
315        return 2;
316    }
317
318    @Override
319    public void connected(FemaleSocket socket) {
320        if (socket == _validateSocket) {
321            _validateSocketSystemName = socket.getConnectedSocket().getSystemName();
322        } else if (socket == _executeSocket) {
323            _executeSocketSystemName = socket.getConnectedSocket().getSystemName();
324        } else {
325            throw new IllegalArgumentException("unkown socket");
326        }
327    }
328
329    @Override
330    public void disconnected(FemaleSocket socket) {
331        if (socket == _validateSocket) {
332            _validateSocketSystemName = null;
333        } else if (socket == _executeSocket) {
334            _executeSocketSystemName = null;
335        } else {
336            throw new IllegalArgumentException("unkown socket");
337        }
338    }
339
340    @Override
341    public String getShortDescription(Locale locale) {
342        return Bundle.getMessage(locale, "ShowDialog_Short");
343    }
344
345    @Override
346    public String getLongDescription(Locale locale) {
347        String bundleKey;
348        switch (_formatType) {
349            case OnlyText:
350                bundleKey = "ShowDialog_Long_TextOnly";
351                break;
352            case CommaSeparatedList:
353                bundleKey = "ShowDialog_Long_CommaSeparatedList";
354                break;
355            case StringFormat:
356                bundleKey = "ShowDialog_Long_StringFormat";
357                break;
358            default:
359                throw new RuntimeException("_formatType has unknown value: "+_formatType.name());
360        }
361        return Bundle.getMessage(locale, bundleKey, _format);
362    }
363
364    public FemaleDigitalExpressionSocket getValidateSocket() {
365        return _validateSocket;
366    }
367
368    public String getValidateSocketSystemName() {
369        return _validateSocketSystemName;
370    }
371
372    public void setValidateSocketSystemName(String systemName) {
373        _validateSocketSystemName = systemName;
374    }
375
376    public FemaleDigitalActionSocket getExecuteSocket() {
377        return _executeSocket;
378    }
379
380    public String getExecuteSocketSystemName() {
381        return _executeSocketSystemName;
382    }
383
384    public void setExecuteSocketSystemName(String systemName) {
385        _executeSocketSystemName = systemName;
386    }
387
388    /** {@inheritDoc} */
389    @Override
390    public void setup() {
391        try {
392            if (!_validateSocket.isConnected()
393                    || !_validateSocket.getConnectedSocket().getSystemName()
394                            .equals(_validateSocketSystemName)) {
395
396                String socketSystemName = _validateSocketSystemName;
397
398                _validateSocket.disconnect();
399
400                if (socketSystemName != null) {
401                    MaleSocket maleSocket =
402                            InstanceManager.getDefault(DigitalExpressionManager.class)
403                                    .getBySystemName(socketSystemName);
404                    if (maleSocket != null) {
405                        _validateSocket.connect(maleSocket);
406                        maleSocket.setup();
407                    } else {
408                        log.error("cannot load digital expression {}", socketSystemName);
409                    }
410                }
411            } else {
412                _validateSocket.getConnectedSocket().setup();
413            }
414
415            if (!_executeSocket.isConnected()
416                    || !_executeSocket.getConnectedSocket().getSystemName()
417                            .equals(_executeSocketSystemName)) {
418
419                String socketSystemName = _executeSocketSystemName;
420
421                _executeSocket.disconnect();
422
423                if (socketSystemName != null) {
424                    MaleSocket maleSocket =
425                            InstanceManager.getDefault(DigitalActionManager.class)
426                                    .getBySystemName(socketSystemName);
427                    if (maleSocket != null) {
428                        _executeSocket.connect(maleSocket);
429                        maleSocket.setup();
430                    } else {
431                        log.error("cannot load digital action {}", socketSystemName);
432                    }
433                }
434            } else {
435                _executeSocket.getConnectedSocket().setup();
436            }
437        } catch (SocketAlreadyConnectedException ex) {
438            // This shouldn't happen and is a runtime error if it does.
439            throw new RuntimeException("socket is already connected");
440        }
441    }
442
443    /** {@inheritDoc} */
444    @Override
445    public void registerListenersForThisClass() {
446        // Do nothing
447    }
448
449    /** {@inheritDoc} */
450    @Override
451    public void unregisterListenersForThisClass() {
452        // Do nothing
453    }
454
455    /** {@inheritDoc} */
456    @Override
457    public void propertyChange(PropertyChangeEvent evt) {
458        getConditionalNG().execute();
459    }
460
461    /** {@inheritDoc} */
462    @Override
463    public void disposeMe() {
464    }
465
466
467    /** {@inheritDoc} */
468    @Override
469    public void getUsageDetail(int level, NamedBean bean, List<NamedBeanUsageReport> report, NamedBean cdl) {
470/*
471        log.debug("getUsageReport :: ShowDialog: bean = {}, report = {}", cdl, report);
472        for (NamedBeanReference namedBeanReference : _namedBeanReferences.values()) {
473            if (namedBeanReference._handle != null) {
474                if (bean.equals(namedBeanReference._handle.getBean())) {
475                    report.add(new NamedBeanUsageReport("LogixNGAction", cdl, getLongDescription()));
476                }
477            }
478        }
479*/
480    }
481
482
483    public enum FormatType {
484        OnlyText(Bundle.getMessage("ShowDialog_FormatType_TextOnly"), true, false),
485        CommaSeparatedList(Bundle.getMessage("ShowDialog_FormatType_CommaSeparatedList"), false, true),
486        StringFormat(Bundle.getMessage("ShowDialog_FormatType_StringFormat"), true, true);
487
488        private final String _text;
489        private final boolean _useFormat;
490        private final boolean _useData;
491
492        private FormatType(String text, boolean useFormat, boolean useData) {
493            this._text = text;
494            this._useFormat = useFormat;
495            this._useData = useData;
496        }
497
498        @Override
499        public String toString() {
500            return _text;
501        }
502
503        public boolean getUseFormat() {
504            return _useFormat;
505        }
506
507        public boolean getUseData() {
508            return _useData;
509        }
510
511    }
512
513
514    public enum Button {
515        Ok(1, Bundle.getMessage("ButtonOK")),
516        Cancel(2, Bundle.getMessage("ButtonCancel")),
517        Yes(3, Bundle.getMessage("ButtonYes")),
518        No(4, Bundle.getMessage("ButtonNo"));
519
520        private final int _value;
521        private final String _text;
522
523        private Button(int value, String text) {
524            this._value = value;
525            this._text = text;
526        }
527
528        public int getValue() {
529            return _value;
530        }
531
532        @Override
533        public String toString() {
534            return _text;
535        }
536
537    }
538
539
540    public enum DataType {
541        LocalVariable(Bundle.getMessage("ShowDialog_Operation_LocalVariable")),
542        Memory(Bundle.getMessage("ShowDialog_Operation_Memory")),
543        Reference(Bundle.getMessage("ShowDialog_Operation_Reference")),
544        Formula(Bundle.getMessage("ShowDialog_Operation_Formula"));
545
546        private final String _text;
547
548        private DataType(String text) {
549            this._text = text;
550        }
551
552        @Override
553        public String toString() {
554            return _text;
555        }
556
557    }
558
559
560    public static class Data {
561
562        private DataType _dataType = DataType.LocalVariable;
563        private String _data = "";
564        private ExpressionNode _expressionNode;
565
566        public Data(Data data) throws ParserException {
567            _dataType = data._dataType;
568            _data = data._data;
569            calculateFormula();
570        }
571
572        public Data(DataType dataType, String data) throws ParserException {
573            if (dataType != null) {
574                _dataType = dataType;
575            } else {
576                // Sometimes data entered in a JTable is not updated correctly
577                log.warn("dataType is null");
578            }
579            _data = data;
580            calculateFormula();
581        }
582
583        private void calculateFormula() throws ParserException {
584            if (_dataType == DataType.Formula) {
585                Map<String, Variable> variables = new HashMap<>();
586                RecursiveDescentParser parser = new RecursiveDescentParser(variables);
587                _expressionNode = parser.parseExpression(_data);
588            } else {
589                _expressionNode = null;
590            }
591        }
592
593        public void setDataType(DataType dataType) {
594            if (dataType != null) {
595                _dataType = dataType;
596            } else {
597                // Sometimes data entered in a JTable is not updated correctly
598                log.warn("dataType is null");
599            }
600        }
601
602        public DataType getDataType() { return _dataType; }
603
604        public void setData(String data) { _data = data; }
605        public String getData() { return _data; }
606
607    }
608
609
610    private class InternalFemaleSocket extends jmri.jmrit.logixng.implementation.DefaultFemaleDigitalActionSocket {
611
612        private ConditionalNG conditionalNG;
613        private SymbolTable newSymbolTable;
614        private int selectedButton;
615        private String inputValue;
616
617        public InternalFemaleSocket() {
618            super(null, new FemaleSocketListener(){
619                @Override
620                public void connected(FemaleSocket socket) {
621                    // Do nothing
622                }
623
624                @Override
625                public void disconnected(FemaleSocket socket) {
626                    // Do nothing
627                }
628            }, "A");
629        }
630
631        @Override
632        public void execute() throws JmriException {
633            if (_executeSocket != null) {
634                MaleSocket maleSocket = (MaleSocket)ShowDialog.this.getParent();
635                try {
636                    SymbolTable oldSymbolTable = conditionalNG.getSymbolTable();
637                    conditionalNG.setSymbolTable(newSymbolTable);
638                    if (!_localVariableForSelectedButton.isEmpty()) {
639                        newSymbolTable.setValue(_localVariableForSelectedButton, selectedButton);
640                    }
641                    if (!_localVariableForInputString.isEmpty()) {
642                        newSymbolTable.setValue(_localVariableForInputString, inputValue);
643                    }
644                    boolean result = true;
645                    if (_validateSocket.isConnected()) {
646                        result = _validateSocket.evaluate();
647                    }
648                    if (result) {
649                        _dialog.dispose();
650                        _executeSocket.execute();
651                    }
652                    conditionalNG.setSymbolTable(oldSymbolTable);
653                } catch (JmriException e) {
654                    if (e.getErrors() != null) {
655                        maleSocket.handleError(ShowDialog.this, rbx.getString("ExceptionExecuteMulti"), e.getErrors(), e, log);
656                    } else {
657                        maleSocket.handleError(ShowDialog.this, Bundle.formatMessage(rbx.getString("ExceptionExecuteAction"), e.getLocalizedMessage()), e, log);
658                    }
659                } catch (RuntimeException e) {
660                    maleSocket.handleError(ShowDialog.this, Bundle.formatMessage(rbx.getString("ExceptionExecuteAction"), e.getLocalizedMessage()), e, log);
661                }
662            }
663        }
664
665    }
666
667
668    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ShowDialog.class);
669
670}