001package jmri.jmrit.logixng.tools.swing;
002
003import java.awt.Color;
004import java.awt.*;
005import java.beans.PropertyChangeEvent;
006import java.beans.PropertyChangeListener;
007import java.util.*;
008import java.util.List;
009
010import javax.swing.*;
011import javax.swing.event.TreeModelEvent;
012import javax.swing.event.TreeModelListener;
013import javax.swing.border.EmptyBorder;
014import javax.swing.tree.*;
015
016import jmri.InstanceManager;
017import jmri.jmrit.logixng.*;
018import jmri.jmrit.logixng.SymbolTable.VariableData;
019import jmri.util.FileUtil;
020import jmri.util.ThreadingUtil;
021
022/**
023 * Show the action/expression tree.
024 * <P>
025 * Base class for ConditionalNG editors
026 *
027 * @author Daniel Bergqvist 2018
028 */
029public class TreePane extends JPanel implements PropertyChangeListener {
030
031    private boolean _rootVisible = true;
032
033    private static final Map<String, Color> FEMALE_SOCKET_COLORS = new HashMap<>();
034
035    JTree _tree;
036
037    protected final FemaleSocket _femaleRootSocket;
038    protected FemaleSocketTreeModel femaleSocketTreeModel;
039
040
041    /**
042     * Construct a ConditionalEditor.
043     *
044     * @param femaleRootSocket the root of the tree
045     */
046    public TreePane(FemaleSocket femaleRootSocket) {
047        _femaleRootSocket = femaleRootSocket;
048        // Note!! This must be made dynamic, so that new socket types are recognized automaticly and added to the list
049        // and the list must be saved between runs.
050        FEMALE_SOCKET_COLORS.put("jmri.jmrit.logixng.implementation.DefaultFemaleDigitalActionSocket", Color.RED);
051        FEMALE_SOCKET_COLORS.put("jmri.jmrit.logixng.implementation.DefaultFemaleDigitalExpressionSocket", Color.BLUE);
052
053        _femaleRootSocket.forEntireTree((Base b) -> {
054            b.addPropertyChangeListener(TreePane.this);
055        });
056    }
057
058    public void initComponents() {
059        initComponents((FemaleSocket femaleSocket, JPanel panel) -> panel);
060    }
061
062    public void initComponents(FemaleSocketDecorator decorator) {
063
064        femaleSocketTreeModel = new FemaleSocketTreeModel(_femaleRootSocket);
065
066        // Create a JTree and tell it to display our model
067        _tree = new JTree();
068        _tree.setRowHeight(0);
069        ToolTipManager.sharedInstance().registerComponent(_tree);
070        _tree.setModel(femaleSocketTreeModel);
071        _tree.setCellRenderer(new FemaleSocketTreeRenderer(decorator));
072
073        _tree.setRootVisible(_rootVisible);
074        _tree.setShowsRootHandles(true);
075
076        // Expand the entire tree
077        for (int i = 0; i < _tree.getRowCount(); i++) {
078            FemaleSocket femaleSocket = (FemaleSocket) _tree.getPathForRow(i).getLastPathComponent();
079            if (femaleSocket.isConnected() && femaleSocket.getConnectedSocket().isEnabled()) {
080                _tree.expandRow(i);
081            }
082        }
083
084        // The JTree can get big, so allow it to scroll
085        JScrollPane scrollpane = new JScrollPane(_tree);
086
087        // create panel
088        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
089
090        // Display it all in a window and make the window appear
091        add(scrollpane, "Center");
092    }
093
094    public boolean getRootVisible() {
095        return _rootVisible;
096    }
097
098    public void setRootVisible(boolean rootVisible) {
099        _rootVisible = rootVisible;
100    }
101
102    /**
103     * Get the path for the item base.
104     *
105     * @param base the item to look for
106     * @param list a list of the female sockets that makes up the path
107     */
108    protected void getPath(Base base, List<FemaleSocket> list) {
109        for (Base b = base; b != null; b = b.getParent()) {
110            if (b instanceof FemaleSocket) list.add(0, (FemaleSocket)b);
111        }
112    }
113
114    @SuppressWarnings("unchecked")
115    @Override
116    public void propertyChange(PropertyChangeEvent evt) {
117
118        if (Base.PROPERTY_CHILD_COUNT.equals(evt.getPropertyName())) {
119            // Remove myself as listener from sockets that has been removed
120            if (evt.getOldValue() != null) {
121                if (! (evt.getOldValue() instanceof List)) throw new RuntimeException("Old value is not a list");
122                for (FemaleSocket socket : (List<FemaleSocket>)evt.getOldValue()) {
123                    socket.removePropertyChangeListener(this);
124                }
125            }
126
127            // Add myself as listener to sockets that has been added
128            if (evt.getNewValue() != null) {
129                if (! (evt.getNewValue() instanceof List)) throw new RuntimeException("New value is not a list");
130                for (FemaleSocket socket : (List<FemaleSocket>)evt.getNewValue()) {
131                    socket.addPropertyChangeListener(this);
132                }
133            }
134
135            // Update the tree
136            Base b = (Base)evt.getSource();
137
138            List<FemaleSocket> list = new ArrayList<>();
139            getPath(b, list);
140
141            ThreadingUtil.runOnGUIEventually(() -> {
142                FemaleSocket femaleSocket = list.get(list.size()-1);
143                updateTree(femaleSocket, list.toArray());
144            });
145        }
146
147
148        if (Base.PROPERTY_CHILD_REORDER.equals(evt.getPropertyName())) {
149
150            if (! (evt.getNewValue() instanceof List)) throw new RuntimeException("New value is not a list");
151            for (FemaleSocket socket : (List<FemaleSocket>)evt.getNewValue()) {
152                // Update the tree
153                List<FemaleSocket> list = new ArrayList<>();
154                getPath(socket, list);
155                ThreadingUtil.runOnGUIEventually(() -> {
156                    updateTree(socket, list.toArray());
157                });
158            }
159        }
160
161
162        if (Base.PROPERTY_SOCKET_CONNECTED.equals(evt.getPropertyName())
163                || Base.PROPERTY_SOCKET_DISCONNECTED.equals(evt.getPropertyName())) {
164
165            FemaleSocket femaleSocket = ((FemaleSocket)evt.getSource());
166            List<FemaleSocket> list = new ArrayList<>();
167            getPath(femaleSocket, list);
168            ThreadingUtil.runOnGUIEventually(() -> {
169                updateTree(femaleSocket, list.toArray());
170            });
171        }
172    }
173
174    protected void updateTree(FemaleSocket currentFemaleSocket, Object[] currentPath) {
175        TreeModelEvent tme = new TreeModelEvent(currentFemaleSocket,currentPath);
176        for (TreeModelListener l : femaleSocketTreeModel.listeners) {
177            l.treeNodesChanged(tme);
178        }
179        _tree.updateUI();
180        FemaleSocket femaleSocket = (FemaleSocket) _tree.getLastSelectedPathComponent();
181        if (femaleSocket != null && !femaleSocket.existsInTree()) {
182            _tree.getSelectionModel().clearSelection();
183        }
184    }
185
186    public void updateTree(Base item) {
187        List<FemaleSocket> list = new ArrayList<>();
188        getPath(item, list);
189
190        FemaleSocket femaleSocket = list.get(list.size()-1);
191        updateTree(femaleSocket, list.toArray());
192    }
193
194    public void dispose() {
195        _femaleRootSocket.forEntireTree((Base b) -> {
196            b.removePropertyChangeListener(TreePane.this);
197        });
198    }
199
200
201    /**
202     * The methods in this class allow the JTree component to traverse the
203     * female sockets of the ConditionalNG tree.
204     */
205    public static class FemaleSocketTreeModel implements TreeModel {
206
207        private final FemaleSocket _root;
208        protected final List<TreeModelListener> listeners = new ArrayList<>();
209
210
211        public FemaleSocketTreeModel(FemaleSocket root) {
212            this._root = root;
213        }
214
215        @Override
216        public Object getRoot() {
217            return _root;
218        }
219
220        @Override
221        public boolean isLeaf(Object node) {
222            FemaleSocket socket = (FemaleSocket) node;
223            if (!socket.isConnected()) {
224                return true;
225            }
226            return socket.getConnectedSocket().getChildCount() == 0;
227        }
228
229        @Override
230        public int getChildCount(Object parent) {
231            FemaleSocket socket = (FemaleSocket) parent;
232            if (!socket.isConnected()) {
233                return 0;
234            }
235            return socket.getConnectedSocket().getChildCount();
236        }
237
238        @Override
239        public Object getChild(Object parent, int index) {
240            FemaleSocket socket = (FemaleSocket) parent;
241            if (!socket.isConnected()) {
242                return null;
243            }
244            return socket.getConnectedSocket().getChild(index);
245        }
246
247        @Override
248        public int getIndexOfChild(Object parent, Object child) {
249            FemaleSocket socket = (FemaleSocket) parent;
250            if (!socket.isConnected()) {
251                return -1;
252            }
253
254            MaleSocket connectedSocket = socket.getConnectedSocket();
255            for (int i = 0; i < connectedSocket.getChildCount(); i++) {
256                if (child == connectedSocket.getChild(i)) {
257                    return i;
258                }
259            }
260            return -1;
261        }
262
263        // This method is invoked by the JTree only for editable trees.
264        // This TreeModel does not allow editing, so we do not implement
265        // this method.  The JTree editable property is false by default.
266        @Override
267        public void valueForPathChanged(TreePath path, Object newvalue) {
268        }
269
270        @Override
271        public void addTreeModelListener(TreeModelListener l) {
272            listeners.add(l);
273        }
274
275        @Override
276        public void removeTreeModelListener(TreeModelListener l) {
277            listeners.remove(l);
278        }
279
280    }
281
282
283    private static final class FemaleSocketTreeRenderer implements TreeCellRenderer {
284
285        private final FemaleSocketDecorator _decorator;
286        private static ImageIcon _lockIcon;
287
288
289        public FemaleSocketTreeRenderer(FemaleSocketDecorator decorator) {
290            this._decorator = decorator;
291        }
292
293        @Override
294        public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
295
296            UIDefaults uiDefaults = javax.swing.UIManager.getDefaults();
297
298            FemaleSocket socket = (FemaleSocket)value;
299
300            JPanel mainPanel = new JPanel();
301
302            mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
303            mainPanel.setOpaque(false);
304            if (selected && InstanceManager.getDefault(LogixNGPreferences.class).getTreeEditorHighlightRow()) {
305                mainPanel.setOpaque(true);
306                mainPanel.setBackground(uiDefaults.getColor("Tree.selectionBackground"));
307            }
308
309            JPanel commentPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
310            mainPanel.add(commentPanel);
311
312            JPanel panel = new JPanel();
313            panel.setAlignmentX(LEFT_ALIGNMENT);
314            mainPanel.add(panel);
315            panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
316            panel.setOpaque(false);
317
318            JLabel socketLabel = new JLabel(socket.getShortDescription());
319            Font font = socketLabel.getFont();
320            socketLabel.setFont(font.deriveFont((float)(font.getSize2D()*1.7)));
321            socketLabel.setForeground(FEMALE_SOCKET_COLORS.get(socket.getClass().getName()));
322//            socketLabel.setForeground(Color.red);
323            panel.add(socketLabel);
324
325            panel.add(javax.swing.Box.createRigidArea(new Dimension(5,0)));
326
327            JLabel socketNameLabel = new JLabel(socket.getName());
328            socketNameLabel.setForeground(FEMALE_SOCKET_COLORS.get(socket.getClass().getName()));
329//            socketNameLabel.setForeground(Color.red);
330            panel.add(socketNameLabel);
331
332            panel.add(javax.swing.Box.createRigidArea(new Dimension(5,0)));
333
334            JLabel connectedItemLabel = new JLabel();
335            if (socket.isConnected()) {
336
337                if (InstanceManager.getDefault(LogixNGPreferences.class).getTreeEditorHighlightRow()) {
338                    connectedItemLabel.setFont(uiDefaults.getFont("Tree.font"));
339                    if (selected) {
340                        connectedItemLabel.setForeground(uiDefaults.getColor("Tree.selectionForeground"));
341                    }
342                }
343
344                MaleSocket connectedSocket = socket.getConnectedSocket();
345
346                if (connectedSocket.isSystem()) {
347                    JLabel systemLabel = new JLabel(" "+Bundle.getMessage("TreePane_System")+" ", JLabel.CENTER);
348                    systemLabel.setForeground(Color.YELLOW);
349                    systemLabel.setBackground(Color.RED);
350                    systemLabel.setOpaque(true);
351                    panel.add(systemLabel);
352                    panel.add(javax.swing.Box.createRigidArea(new Dimension(5,0)));
353                }
354
355                if (connectedSocket.isLocked()) {
356                    if (_lockIcon == null) {
357                        _lockIcon = new ImageIcon(FileUtil.findURL("program:resources/icons/logixng/lock.png", FileUtil.Location.INSTALLED));
358                    }
359                    JLabel icLabel = new JLabel(_lockIcon, JLabel.CENTER);
360                    panel.add(icLabel);
361                }
362
363                String comment = connectedSocket.getComment();
364                if (comment != null) {
365                    JLabel commentLabel = new JLabel();
366                    commentLabel.setText("<html><pre>"+comment+"</pre></html>");
367                    commentLabel.setForeground(Color.GRAY);
368                    Font font2 = commentLabel.getFont();
369                    commentLabel.setFont(font2.deriveFont(Font.ITALIC));
370                    commentPanel.setOpaque(false);
371                    commentPanel.add(commentLabel);
372                    commentPanel.setAlignmentX(LEFT_ALIGNMENT);
373                    commentPanel.setBorder(new EmptyBorder(10, 0, 0, 0));
374                }
375
376                String label = connectedSocket.getLongDescription();
377                if (connectedSocket.getUserName() != null) {
378                    label += " ::: " + connectedSocket.getUserName();
379                }
380                if (!connectedSocket.isEnabled()) {
381                    label = "<html><strike>" + label + "</strike></html>";
382                }
383                connectedItemLabel.setText(label);
384
385                mainPanel.setToolTipText(connectedSocket.getShortDescription());
386
387                for (VariableData variableData : connectedSocket.getLocalVariables()) {
388                    JLabel variableLabel = new JLabel(Bundle.getMessage(
389                            "PrintLocalVariable",
390                            variableData._name,
391                            variableData._initialValueType,
392                            variableData._initialValueData));
393                    variableLabel.setAlignmentX(LEFT_ALIGNMENT);
394                    if (InstanceManager.getDefault(LogixNGPreferences.class).getTreeEditorHighlightRow()) {
395                        variableLabel.setFont(uiDefaults.getFont("Tree.font"));
396                        if (selected) {
397                            variableLabel.setForeground(uiDefaults.getColor("Tree.selectionForeground"));
398                        }
399                    }
400                    mainPanel.add(variableLabel);
401                }
402            }
403
404            panel.add(connectedItemLabel);
405
406            return _decorator.decorate(socket, mainPanel);
407        }
408
409    }
410
411
412    public interface FemaleSocketDecorator {
413        public JPanel decorate(FemaleSocket femaleSocket, JPanel panel);
414    }
415
416//    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TreeViewer.class);
417
418}