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}