View Javadoc
1   // ******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2024.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
36  //
37  // ******************************************************************************
38  package ffx.ui;
39  
40  import ffx.potential.bonded.MSNode;
41  import ffx.potential.bonded.MSRoot;
42  import ffx.potential.bonded.ROLSP;
43  import ffx.potential.bonded.RendererCache;
44  import java.awt.Color;
45  import java.awt.GraphicsEnvironment;
46  import java.io.Serial;
47  import java.util.ArrayList;
48  import java.util.Enumeration;
49  import java.util.logging.Logger;
50  import javax.swing.JLabel;
51  import javax.swing.JTree;
52  import javax.swing.SwingUtilities;
53  import javax.swing.event.TreeModelEvent;
54  import javax.swing.event.TreeModelListener;
55  import javax.swing.event.TreeSelectionEvent;
56  import javax.swing.event.TreeSelectionListener;
57  import javax.swing.tree.DefaultTreeCellRenderer;
58  import javax.swing.tree.DefaultTreeModel;
59  import javax.swing.tree.DefaultTreeSelectionModel;
60  import javax.swing.tree.RowMapper;
61  import javax.swing.tree.TreeNode;
62  import javax.swing.tree.TreePath;
63  
64  /**
65   * The Hierarchy Class creates and manages a JTree view of the data structure. It is used for
66   * synchronization, handles the selection mechanism, and sets the active system and nodes.
67   *
68   * @author Michael J. Schnieders
69   */
70  public final class Hierarchy extends JTree implements TreeSelectionListener, TreeModelListener {
71  
72    @Serial
73    private static final long serialVersionUID = 1L;
74  
75    private static final Logger logger = Logger.getLogger(Hierarchy.class.getName());
76    private final MSRoot root;
77    private final MainPanel mainPanel;
78    private final ArrayList<MSNode> activeNodes = new ArrayList<>();
79    private DefaultTreeModel hierarchyModel;
80    private DefaultTreeCellRenderer treeCellRenderer;
81    private DefaultTreeSelectionModel treeSelectionModel;
82    // Reference to the active FFXSystem
83    private FFXSystem activeSystem = null;
84    // Reference to the last Selected Node
85    private MSNode activeNode = null;
86    private JLabel status = null;
87    private JLabel step = null;
88    private JLabel energy = null;
89    private ArrayList<TreePath> newPaths = new ArrayList<>();
90    private ArrayList<TreePath> previousPaths = new ArrayList<>();
91    private final ArrayList<TreePath> removedPaths = new ArrayList<>();
92    private final TreePath nullPath = null;
93  
94    /**
95     * The Default Constructor initializes JTree properties, then updates is representation based on
96     * the Structure of the Tree that extends from the Root argument.
97     *
98     * @param mainPanel a {@link ffx.ui.MainPanel} object.
99     */
100   Hierarchy(MainPanel mainPanel) {
101     super(mainPanel.getDataRoot());
102     this.mainPanel = mainPanel;
103     root = this.mainPanel.getDataRoot();
104     initTree();
105   }
106 
107   /**
108    * addSelections
109    *
110    * @param a a {@link java.util.ArrayList} object.
111    */
112   public void addSelections(ArrayList<MSNode> a) {
113     synchronized (this) {
114       for (MSNode f : a) {
115         addSelection(f);
116       }
117     }
118   }
119 
120   /**
121    * Returns the active FSystem.
122    *
123    * @return a {@link ffx.ui.FFXSystem} object.
124    */
125   public FFXSystem getActive() {
126     return activeSystem;
127   }
128 
129   /**
130    * Sets the FFXSystem s to be active.
131    *
132    * @param ffxSystem a {@link ffx.ui.FFXSystem} object.
133    */
134   public void setActive(FFXSystem ffxSystem) {
135     synchronized (this) {
136       if (ffxSystem == activeSystem) {
137         return;
138       }
139       if (ffxSystem != null) {
140         // A closed (closing) system cannot be set active.
141         if (ffxSystem.isClosing()) {
142           // Set the most recently opened system to be active.
143           for (int i = root.getChildCount() - 1; i >= 0; i--) {
144             FFXSystem child = (FFXSystem) root.getChildAt(i);
145             if (!child.isClosing()) {
146               setActive(child);
147               return;
148             }
149           }
150           setActive(null);
151           return;
152         }
153       }
154       activeSystem = ffxSystem;
155       updateStatus();
156       if (mainPanel.getKeywordPanel() != null) {
157         mainPanel.getKeywordPanel().loadActive(activeSystem);
158       }
159       if (mainPanel.getModelingPanel() != null) {
160         mainPanel.getModelingPanel().loadActive(activeSystem);
161       }
162       if (mainPanel.getModelingShell() != null) {
163         mainPanel.getModelingShell().sync();
164       }
165     }
166   }
167 
168   /**
169    * setActive
170    *
171    * @param i a int.
172    */
173   public void setActive(int i) {
174     synchronized (this) {
175       if (i < root.getChildCount()) {
176         setActive((FFXSystem) root.getChildAt(i));
177       } else if (root.getChildCount() == 0) {
178         setActive(null);
179       }
180     }
181   }
182 
183   /**
184    * Getter for the field <code>activeNode</code>.
185    *
186    * @return a {@link ffx.potential.bonded.MSNode} object.
187    */
188   public MSNode getActiveNode() {
189     return activeNode;
190   }
191 
192   /**
193    * getSystems
194    *
195    * @return an array of {@link ffx.ui.FFXSystem} objects.
196    */
197   public FFXSystem[] getSystems() {
198     synchronized (this) {
199       int childCount = root.getChildCount();
200       if (childCount == 0) {
201         return null;
202       }
203       FFXSystem[] systems = new FFXSystem[childCount];
204       int index = 0;
205       for (Enumeration<TreeNode> e = root.children(); e.hasMoreElements(); ) {
206         systems[index++] = (FFXSystem) e.nextElement();
207       }
208       return systems;
209     }
210   }
211 
212   /**
213    * groupSelection
214    *
215    * @param f1 a {@link ffx.potential.bonded.MSNode} object.
216    * @param f2 a {@link ffx.potential.bonded.MSNode} object.
217    */
218   public void groupSelection(MSNode f1, MSNode f2) {
219     if (f1 == null || f2 == null) {
220       return;
221     }
222     synchronized (this) {
223       TreePath[] paths = new TreePath[2];
224       paths[0] = new TreePath(f1.getPath());
225       paths[1] = new TreePath(f2.getPath());
226       RowMapper rm = treeSelectionModel.getRowMapper();
227       int[] rows = rm.getRowsForPaths(paths);
228       setSelectionInterval(rows[0], rows[1]);
229     }
230   }
231 
232   /**
233    * removeSelections
234    *
235    * @param a a {@link java.util.ArrayList} object.
236    */
237   public void removeSelections(ArrayList<MSNode> a) {
238     synchronized (this) {
239       for (MSNode f : a) {
240         removeSelection(f);
241       }
242     }
243   }
244 
245   /** {@inheritDoc} */
246   @Override
247   public String toString() {
248     return "Structural Hierarchy";
249   }
250 
251   /**
252    * toggleSelections
253    *
254    * @param a a {@link java.util.ArrayList} object.
255    */
256   public void toggleSelections(ArrayList<MSNode> a) {
257     synchronized (this) {
258       for (MSNode f : a) {
259         toggleSelection(f);
260       }
261     }
262   }
263 
264   /** {@inheritDoc} */
265   @Override
266   public void valueChanged(TreeSelectionEvent e) {
267     synchronized (this) {
268 
269       // Determine the Active System
270       MSNode lastNode = (MSNode) getLastSelectedPathComponent();
271       if (lastNode != null) {
272         activeNode = lastNode;
273         FFXSystem s = activeNode.getMSNode(FFXSystem.class);
274         if (s != null) {
275           setActive(s);
276         }
277       }
278       TreePath[] paths = e.getPaths();
279       if (paths == null) {
280         return;
281       }
282 
283       // Reuse the same ArrayLists
284       ArrayList<TreePath> temp = previousPaths;
285       previousPaths = newPaths;
286       newPaths = temp;
287       // Determine new and removed paths
288       newPaths.clear();
289       removedPaths.clear();
290       for (int i = 0; i < paths.length; i++) {
291         if (e.isAddedPath(i)) {
292           newPaths.add(paths[i]);
293         } else {
294           removedPaths.add(paths[i]);
295         }
296       }
297       // Create a non-redundant set of new/removed paths
298       TreePath pathi, pathj;
299       for (int i = 0; i < newPaths.size(); i++) {
300         pathi = newPaths.get(i);
301         if (pathi == nullPath) {
302           continue;
303         }
304         for (int j = i + 1; j < newPaths.size(); j++) {
305           pathj = newPaths.get(j);
306           if (pathi == nullPath || pathj == nullPath) {
307             continue;
308           }
309           if (pathi.isDescendant(pathj)) {
310             newPaths.set(j, nullPath);
311           } else if (pathj.isDescendant(pathi)) {
312             newPaths.set(i, nullPath);
313           }
314         }
315       }
316       boolean check = true;
317       while (check) {
318         check = newPaths.remove(nullPath);
319       }
320       for (int i = 0; i < removedPaths.size(); i++) {
321         pathi = removedPaths.get(i);
322         if (pathi == nullPath) {
323           continue;
324         }
325         for (int j = i + 1; j < removedPaths.size(); j++) {
326           pathj = removedPaths.get(j);
327           if (pathi == nullPath || pathj == nullPath) {
328             continue;
329           }
330           if (pathi.isDescendant(pathj)) {
331             removedPaths.set(j, nullPath);
332           } else if (pathj.isDescendant(pathi)) {
333             removedPaths.set(i, nullPath);
334           }
335         }
336       }
337       check = true;
338       while (check) {
339         check = removedPaths.remove(nullPath);
340       }
341       // Remove the RemovedPaths from the Existing List
342       for (int i = 0; i < previousPaths.size(); i++) {
343         pathi = previousPaths.get(i);
344         for (TreePath removedPath : removedPaths) {
345           if (removedPath.isDescendant(pathi)) {
346             previousPaths.set(i, nullPath);
347             break;
348           }
349         }
350       }
351       check = true;
352       while (check) {
353         check = previousPaths.remove(nullPath);
354       }
355       // Combine new Paths and Existing Paths non-redundantly
356       for (int i = 0; i < newPaths.size(); i++) {
357         pathi = newPaths.get(i);
358         if (pathi == nullPath) {
359           continue;
360         }
361         for (int j = 0; j < previousPaths.size(); j++) {
362           pathj = previousPaths.get(j);
363           if (pathj == nullPath) {
364             continue;
365           }
366           if (pathi == nullPath) {
367             continue;
368           }
369           if (pathi.isDescendant(pathj)) {
370             previousPaths.set(j, nullPath);
371           } else if (pathj.isDescendant(pathi)) {
372             newPaths.set(i, nullPath);
373           }
374         }
375       }
376       check = true;
377       while (check) {
378         check = newPaths.remove(nullPath);
379       }
380       check = true;
381       while (check) {
382         check = previousPaths.remove(nullPath);
383       }
384       newPaths.addAll(previousPaths);
385       activeNodes.clear();
386       for (TreePath newPath : newPaths) {
387         pathi = newPath;
388         activeNodes.add((MSNode) pathi.getLastPathComponent());
389       }
390       if (activeNode != null) {
391         TreePath activePath = new TreePath(activeNode);
392         expandPath(activePath.getParentPath());
393         makeVisible(activePath);
394         scrollPathToVisible(activePath);
395       }
396       // We now have a non-redundant set of Active Paths; and a
397       // non-redundant set of removed paths
398       ArrayList<MSNode> picks = new ArrayList<>();
399       // Clear highlight of de-selected nodes
400       for (TreePath r : removedPaths) {
401         boolean change = true;
402         for (TreePath n : newPaths) {
403           if (n.isDescendant(r)) {
404             change = false;
405           }
406         }
407         if (change) {
408           MSNode f = (MSNode) r.getLastPathComponent();
409           f.setSelected(false);
410           picks.add(f);
411         }
412       }
413       for (TreePath n : newPaths) {
414         boolean change = true;
415         for (TreePath p : previousPaths) {
416           if (p.isDescendant(n)) {
417             change = false;
418           }
419         }
420         if (change) {
421           MSNode f = (MSNode) n.getLastPathComponent();
422           f.setSelected(true);
423           picks.add(f);
424         }
425       }
426       if (RendererCache.highlightSelections) {
427         mainPanel
428             .getGraphics3D()
429             .updateScene(picks, false, false, null, true, RendererCache.ColorModel.SELECT);
430       } else if (RendererCache.labelAtoms || RendererCache.labelResidues) {
431         mainPanel.getGraphics3D().setLabelsUpdated();
432       }
433     }
434   }
435 
436   /**
437    * addSelection
438    *
439    * @param f a {@link ffx.potential.bonded.MSNode} object.
440    */
441   private void addSelection(MSNode f) {
442     if (f == null) {
443       return;
444     }
445     synchronized (this) {
446       TreePath path = new TreePath(f.getPath());
447       addSelectionPath(path);
448       f.setSelected(true);
449     }
450   }
451 
452   /**
453    * addSystemNode
454    *
455    * @param newSystem a {@link ffx.ui.FFXSystem} object.
456    */
457   void addSystemNode(FFXSystem newSystem) {
458     addTreeNode(newSystem, root, root.getChildCount());
459   }
460 
461   /**
462    * addTreeNode
463    *
464    * @param nodeToAdd a {@link ffx.potential.bonded.MSNode} object.
465    * @param parent a {@link ffx.potential.bonded.MSNode} object.
466    * @param index a int.
467    */
468   private void addTreeNode(MSNode nodeToAdd, MSNode parent, int index) {
469     synchronized (this) {
470       if (nodeToAdd == null || nodeToAdd.getParent() != null) {
471         return;
472       }
473       int childCount = parent.getChildCount();
474       if (index < 0 || index > childCount) {
475         index = parent.getChildCount();
476       }
477 
478       String name = nodeToAdd.getName();
479 
480       for (int i = 0; i < parent.getChildCount(); i++) {
481         MSNode node = (MSNode) parent.getChildAt(i);
482         if (node.getName().equals(name)) {
483           logger.fine(" Parent already has a node with the name " + name);
484         }
485       }
486 
487       // Add a parallel node if the ffx.lang.parallel flag was set
488       if (ROLSP.GO_PARALLEL) {
489         ROLSP parallelNode = new ROLSP();
490         parallelNode.add(nodeToAdd);
491         hierarchyModel.insertNodeInto(parallelNode, parent, index);
492       } else {
493         hierarchyModel.insertNodeInto(nodeToAdd, parent, index);
494       }
495       if (nodeToAdd instanceof FFXSystem) {
496         attach((FFXSystem) nodeToAdd);
497         hierarchyModel.nodeStructureChanged(nodeToAdd);
498       }
499       onlySelection(nodeToAdd);
500       if (!isRootVisible()) {
501         setRootVisible(true);
502       }
503     }
504   }
505 
506   private void attach(FFXSystem newModel) {
507     if (newModel == null) {
508       return;
509     }
510     newModel.finalize(true, newModel.getForceField());
511     GraphicsCanvas graphics = mainPanel.getGraphics3D();
512     if (graphics != null) {
513       graphics.attachModel(newModel);
514       if (newModel.getBondList().isEmpty()) {
515         mainPanel
516             .getGraphics3D()
517             .updateScene(newModel, false, true, RendererCache.ViewModel.SPACEFILL, false, null);
518       }
519     }
520   }
521 
522   /** collapseAll */
523   private void collapseAll() {
524     int row = getRowCount() - 1;
525     while (row >= 0) {
526       collapseRow(row);
527       row--;
528     }
529   }
530 
531   /**
532    * Getter for the field <code>activeNodes</code>.
533    *
534    * @return a {@link java.util.ArrayList} object.
535    */
536   ArrayList<MSNode> getActiveNodes() {
537     return activeNodes;
538   }
539 
540   /**
541    * getNonActiveSystems
542    *
543    * @return an array of {@link ffx.ui.FFXSystem} objects.
544    */
545   FFXSystem[] getNonActiveSystems() {
546     synchronized (this) {
547       int childCount = root.getChildCount();
548       if (childCount == 0) {
549         return null;
550       }
551       FFXSystem[] systems = new FFXSystem[childCount - 1];
552       int index = 0;
553       for (Enumeration<TreeNode> e = root.children(); e.hasMoreElements(); ) {
554         FFXSystem system = (FFXSystem) e.nextElement();
555         if (system != getActive()) {
556           systems[index++] = system;
557         }
558       }
559       return systems;
560     }
561   }
562 
563   /** Initialize the Tree representation based on the Root data node */
564   private void initTree() {
565     if (!GraphicsEnvironment.isHeadless() && !SwingUtilities.isEventDispatchThread()) {
566       logger.severe(" Initialization of Hierarchy outside EDT.");
567     }
568     addTreeSelectionListener(this);
569     setExpandsSelectedPaths(true);
570     setScrollsOnExpand(true);
571     // setLargeModel(true);
572     setEditable(false);
573     putClientProperty("JTree.lineStyle", "Angled");
574     setShowsRootHandles(true);
575     treeCellRenderer = new DefaultTreeCellRenderer();
576     treeCellRenderer.setBackgroundSelectionColor(Color.yellow);
577     treeCellRenderer.setBorderSelectionColor(Color.black);
578     treeCellRenderer.setTextSelectionColor(Color.black);
579     setCellRenderer(treeCellRenderer);
580     hierarchyModel = new DefaultTreeModel(root);
581     root.setUserObject(hierarchyModel);
582     setModel(hierarchyModel);
583     hierarchyModel.addTreeModelListener(this);
584     // Store a reference to the DefaultTreeModel, so it can be updated if nodes are added/removed.
585     treeSelectionModel = new DefaultTreeSelectionModel();
586     setSelectionModel(treeSelectionModel);
587     setRootVisible(false);
588   }
589 
590   /**
591    * onlySelection
592    *
593    * @param f a {@link ffx.potential.bonded.MSNode} object.
594    */
595   void onlySelection(MSNode f) {
596     synchronized (this) {
597       int num = activeNodes.size();
598       TreePath[] paths = new TreePath[num];
599       for (int i = 0; i < num; i++) {
600         paths[i] = new TreePath((activeNodes.get(i)).getPath());
601       }
602       removeSelectionPaths(paths);
603       collapseAll();
604       addSelection(f);
605     }
606   }
607 
608   /**
609    * removeSelection
610    *
611    * @param f a {@link ffx.potential.bonded.MSNode} object.
612    */
613   private void removeSelection(MSNode f) {
614     synchronized (this) {
615       if (f == null) {
616         return;
617       }
618       TreePath path = new TreePath(f.getPath());
619       for (Enumeration<TreePath> e = getExpandedDescendants(path); e.hasMoreElements(); ) {
620         TreePath treePath = new TreePath(e.nextElement());
621         collapsePath(treePath);
622       }
623       removeSelectionPath(path);
624       f.setSelected(false);
625     }
626   }
627 
628   /**
629    * removeTreeNode
630    *
631    * @param nodeToRemove a {@link ffx.potential.bonded.MSNode} object.
632    */
633   void removeTreeNode(MSNode nodeToRemove) {
634     synchronized (this) {
635       if (nodeToRemove == null) {
636         return;
637       }
638 
639       hierarchyModel.removeNodeFromParent(nodeToRemove);
640 
641       /*
642        The DefaultTreeModel and DefaultTreeSelectionModel classes retain
643        references to removed nodes. To work around this, we create new
644        instances of these classes whenever an FFXSystem is removed.
645       */
646       if (nodeToRemove instanceof FFXSystem) {
647         hierarchyModel = new DefaultTreeModel(root);
648         // Store a reference to the DefaultTreeModel, so it can be updated if nodes are added/removed.
649         root.setUserObject(hierarchyModel);
650         setModel(hierarchyModel);
651         hierarchyModel.addTreeModelListener(this);
652         treeSelectionModel = new DefaultTreeSelectionModel();
653         setSelectionModel(treeSelectionModel);
654       }
655 
656       // Whenever a node is removed, clear and reset activeNodes and path instances.
657       activeNodes.clear();
658       previousPaths.clear();
659       newPaths.clear();
660       removedPaths.clear();
661 
662       if (getActive() == nodeToRemove && root.getChildCount() != 0) {
663         FFXSystem m = (FFXSystem) root.getChildAt(0);
664         setActive(m);
665         onlySelection(activeSystem);
666       } else {
667         setActive(null);
668       }
669 
670       if (root.getChildCount() <= 1) {
671         setRootVisible(false);
672       }
673     }
674   }
675 
676   /** selectAll */
677   void selectAll() {
678     synchronized (this) {
679       if (activeSystem == null) {
680         return;
681       }
682       onlySelection(root);
683     }
684   }
685 
686   /**
687    * setHighlighting
688    *
689    * @param h a boolean.
690    */
691   void setHighlighting(boolean h) {
692     synchronized (this) {
693       if (RendererCache.highlightSelections != h) {
694         RendererCache.highlightSelections = h;
695         for (MSNode node : activeNodes) {
696           node.setSelected(h);
697         }
698         mainPanel
699             .getGraphics3D()
700             .updateScene(activeNodes, false, false, null, true, RendererCache.ColorModel.SELECT);
701       }
702     }
703   }
704 
705   /**
706    * Setter for the field <code>status</code>.
707    *
708    * @param s a {@link javax.swing.JLabel} object.
709    * @param t a {@link javax.swing.JLabel} object.
710    * @param e a {@link javax.swing.JLabel} object.
711    */
712   void setStatus(JLabel s, JLabel t, JLabel e) {
713     status = s;
714     step = t;
715     energy = e;
716   }
717 
718   /**
719    * toggleSelection
720    *
721    * @param f a {@link ffx.potential.bonded.MSNode} object.
722    */
723   void toggleSelection(MSNode f) {
724     synchronized (this) {
725       if (f == null) {
726         return;
727       }
728       TreePath path = new TreePath(f.getPath());
729       if (isPathSelected(path)) {
730         removeSelectionPath(path);
731       } else {
732         addSelectionPath(path);
733       }
734     }
735   }
736 
737   /** updateStatus */
738   void updateStatus() {
739     if (activeSystem == null) {
740       status.setText("  ");
741       step.setText("  ");
742       energy.setText("  ");
743       return;
744     }
745     if (activeSystem.getFile() != null) {
746       status.setText("  " + activeSystem.toFFString());
747     } else {
748       status.setText("  " + activeSystem.toString());
749     }
750 
751     if (activeSystem.getCycles() > 1) {
752       step.setText(activeSystem.getCurrentCycle() + "/" + activeSystem.getCycles());
753     } else {
754       step.setText("");
755     }
756 
757     if (activeSystem.getCycles() > 1) {
758       energy.setText("");
759     } else {
760       energy.setText("");
761     }
762   }
763 
764   @Override
765   public void treeNodesChanged(TreeModelEvent e) {
766   }
767 
768   @Override
769   public void treeNodesInserted(TreeModelEvent e) {
770   }
771 
772   @Override
773   public void treeNodesRemoved(TreeModelEvent e) {
774   }
775 
776   @Override
777   public void treeStructureChanged(TreeModelEvent e) {
778   }
779 }