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.parsers.KeyFileFilter;
41  import ffx.potential.parsers.KeyFilter;
42  import ffx.ui.commands.DTDResolver;
43  import ffx.utilities.Keyword;
44  import java.awt.BorderLayout;
45  import java.awt.FlowLayout;
46  import java.awt.Font;
47  import java.awt.GridBagConstraints;
48  import java.awt.GridBagLayout;
49  import java.awt.Insets;
50  import java.awt.event.ActionEvent;
51  import java.awt.event.ActionListener;
52  import java.io.*;
53  import java.net.URL;
54  import java.util.ArrayList;
55  import java.util.Hashtable;
56  import java.util.Iterator;
57  import java.util.LinkedHashMap;
58  import java.util.Vector;
59  import java.util.logging.Level;
60  import java.util.logging.Logger;
61  import java.util.prefs.Preferences;
62  import javax.annotation.Nullable;
63  import javax.swing.BorderFactory;
64  import javax.swing.ImageIcon;
65  import javax.swing.JButton;
66  import javax.swing.JCheckBoxMenuItem;
67  import javax.swing.JComboBox;
68  import javax.swing.JFileChooser;
69  import javax.swing.JLabel;
70  import javax.swing.JOptionPane;
71  import javax.swing.JPanel;
72  import javax.swing.JScrollPane;
73  import javax.swing.JSplitPane;
74  import javax.swing.JTextArea;
75  import javax.swing.JToolBar;
76  import javax.swing.border.Border;
77  import javax.swing.border.EtchedBorder;
78  import javax.xml.parsers.DocumentBuilder;
79  import javax.xml.parsers.DocumentBuilderFactory;
80  import javax.xml.parsers.ParserConfigurationException;
81  import org.apache.commons.configuration2.CompositeConfiguration;
82  import org.apache.commons.configuration2.Configuration;
83  import org.apache.commons.configuration2.PropertiesConfiguration;
84  import org.w3c.dom.Document;
85  import org.w3c.dom.Element;
86  import org.w3c.dom.Node;
87  import org.w3c.dom.NodeList;
88  import org.xml.sax.SAXException;
89  
90  /**
91   * The KeywordPanel class provides a View and Control of TINKER Keyword (*.KEY) files.
92   *
93   * @author Michael J. Schnieders
94   */
95  public final class KeywordPanel extends JPanel implements ActionListener {
96  
97    @Serial
98    private static final long serialVersionUID = 1L;
99  
100   private static final Logger logger = Logger.getLogger(KeywordPanel.class.getName());
101   private static final Preferences preferences = Preferences.userNodeForPackage(KeywordPanel.class);
102   /** The MainPanel has references to many things the KeywordPanel uses. */
103   private final MainPanel mainPanel;
104 
105   private final JLabel statusLabel = new JLabel("  ");
106   private final GridBagLayout gridBagLayout = new GridBagLayout();
107   private final GridBagConstraints gridBagConstraints = new GridBagConstraints();
108   /** The gridPanel holds an array of KeywordComponents. */
109   private final JPanel gridPanel = new JPanel(gridBagLayout);
110   /** A simple label if no Keyword File is open. */
111   private final JLabel noSystemLabel =
112       new JLabel("Keywords for the active system are edited here. ");
113   // A simple label if no Keyword Description is available.
114   private final JLabel noKeywordLabel = new JLabel("Keyword desciptions are displayed here.");
115   private final JPanel noKeywordPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 5));
116   private final FlowLayout flowLayout = new FlowLayout(FlowLayout.LEFT, 5, 5);
117   private final BorderLayout borderLayout = new BorderLayout();
118   /** True if a Key File is open. */
119   private boolean fileOpen = false;
120   /** Currently open Keywords. */
121   private Hashtable<String, Keyword> currentKeys;
122   /** Currently open Key File. */
123   private File currentKeyFile;
124   /** FFXSystem associated with currently open Key File (if any). */
125   private FFXSystem currentSystem;
126   /** HashMap for Keyword GUI Components. */
127   private LinkedHashMap<String, KeywordComponent> keywordHashMap;
128   /** HashMap for Keyword Groups. */
129   private LinkedHashMap<String, String> groupHashMap;
130   /** JComboBox with Keyword Groups, plus a few special cases (Active, FlatFile). */
131   private JComboBox<String> groupComboBox;
132   /** The editPanel holds the toolBar (north), splitPane (center) and statusLabel (south). */
133   private JPanel editPanel;
134 
135   private JToolBar toolBar;
136   /** The splitPane holds the editScrollPane (top) and descriptScrollPane (bottom). */
137   private JSplitPane splitPane;
138   /** The editScrollPane holds the gridPanel, where KeywordComponents actually live. */
139   private JScrollPane editScrollPane;
140   /** The descriptScrollPane holds the descriptTextArea for Keyword Descriptions. */
141   private JScrollPane descriptScrollPane;
142   /** The descriptTextArea actually holds Keyword Descriptions. */
143   private JTextArea descriptTextArea;
144   /** Allow the user to show/hide Keyword Descriptions. */
145   private JCheckBoxMenuItem descriptCheckBox;
146   /**
147    * Lines in Keyword files that are comments, unrecognized keywords, or keywords where editing is
148    * not supported are stored in a big StringBuilder.
149    */
150   private StringBuilder commentStringBuffer = new StringBuilder();
151   /** This component shows what the saved Key file will look like (WYSIWYG). */
152   private JTextArea flatfileTextArea;
153 
154   private String[] paramNames = null;
155   private LinkedHashMap<String, String> paramHashtable = null;
156 
157   /**
158    * Default Construtor where parent is the FFX Window Frame object.
159    *
160    * @param f a {@link ffx.ui.MainPanel} object.
161    */
162   KeywordPanel(MainPanel f) {
163     super();
164     mainPanel = f;
165     initialize();
166   }
167 
168   /**
169    * Handles input from KeywordPanel ToolBar buttons.
170    *
171    * <p>{@inheritDoc}
172    */
173   @Override
174   public void actionPerformed(ActionEvent evt) {
175     String arg = evt.getActionCommand();
176     switch (arg) {
177       case "Open...":
178         keyOpen();
179         break;
180       case "Close":
181         keyClose();
182         break;
183       case "Save":
184         keySave(currentKeyFile);
185         break;
186       case "Save As...":
187         keySaveAs();
188         break;
189       case "Description":
190         JCheckBoxMenuItem box = (JCheckBoxMenuItem) evt.getSource();
191         setDivider(box.isSelected());
192         break;
193     }
194     if (evt.getSource() instanceof JComboBox) {
195       loadKeywordGroup();
196     }
197   }
198 
199   /**
200    * getKeyword
201    *
202    * @param key a {@link java.lang.String} object.
203    * @return a {@link java.lang.String} object.
204    */
205   public String getKeyword(String key) {
206     if (key == null) {
207       return null;
208     }
209     synchronized (this) {
210       KeywordComponent keyword = keywordHashMap.get(key.toUpperCase());
211       if (keyword == null) {
212         return null;
213       }
214       return keyword.toString();
215     }
216   }
217 
218   /**
219    * getKeywordDescription
220    *
221    * @param key a {@link java.lang.String} object.
222    * @return a {@link java.lang.String} object.
223    */
224   public String getKeywordDescription(String key) {
225     if (key == null) {
226       return null;
227     }
228     synchronized (this) {
229       KeywordComponent keyword = keywordHashMap.get(key.toUpperCase());
230       if (keyword == null) {
231         return null;
232       }
233       return keyword.getKeywordDescription();
234     }
235   }
236 
237   /**
238    * getKeywordValue
239    *
240    * @param key a {@link java.lang.String} object.
241    * @return a {@link java.lang.String} object.
242    */
243   public String getKeywordValue(String key) {
244     if (key == null) {
245       return null;
246     }
247     synchronized (this) {
248       KeywordComponent keyword = keywordHashMap.get(key.toUpperCase());
249       if (keyword == null) {
250         return null;
251       }
252       String value = keyword.toString().trim();
253       int firstSpace = value.indexOf(" ");
254       if (firstSpace < 1) {
255         return value;
256       }
257       return value.substring(firstSpace, value.length());
258     }
259   }
260 
261   /**
262    * getParamPath
263    *
264    * @param key a {@link java.lang.String} object.
265    * @return a {@link java.lang.String} object.
266    */
267   public String getParamPath(String key) {
268     return paramHashtable.get(key);
269   }
270 
271   /**
272    * isKeyword
273    *
274    * @param key a {@link java.lang.String} object.
275    * @return a boolean.
276    */
277   public boolean isKeyword(String key) {
278     synchronized (this) {
279       KeywordComponent keyword = keywordHashMap.get(key.toUpperCase());
280       return keyword != null;
281     }
282   }
283 
284   /** selected */
285   public void selected() {
286     setDivider(descriptCheckBox.isSelected());
287     validate();
288     repaint();
289   }
290 
291   /**
292    * Make the passed Keyword Group active in the editor.
293    *
294    * @param keygroup String
295    */
296   public void setKeywordGroup(String keygroup) {
297     synchronized (this) {
298       keygroup = groupHashMap.get(keygroup.toUpperCase());
299       if (keygroup == null) {
300         return;
301       }
302       if (!groupComboBox.getSelectedItem().equals(keygroup)) {
303         groupComboBox.setSelectedItem(keygroup);
304         loadKeywordGroup();
305       }
306     }
307   }
308 
309   /**
310    * Load a value into a KeywordComponent. Value is equivalent to one line in a TINKER key file,
311    * except without the keyword at the beginning. Value should be null to just indicate the Keyword
312    * is present (active). If this Keyword can apprear many times, value will be appended to the
313    * list.
314    *
315    * @param key String
316    * @param value String
317    */
318   public void setKeywordValue(String key, String value) {
319     synchronized (this) {
320       KeywordComponent keyword = keywordHashMap.get(key.toUpperCase());
321       if (keyword == null) {
322         return;
323       }
324       keyword.loadKeywordEntry(value);
325       String keygroup = keyword.getKeywordGroup();
326       if (!groupComboBox.getSelectedItem().equals(keygroup)) {
327         groupComboBox.setSelectedItem(keygroup);
328         loadKeywordGroup();
329       }
330       mainPanel.setPanel(MainPanel.KEYWORDS);
331     }
332   }
333 
334   /**
335    * Store the KeywordPanel's current keyword content into sys.
336    *
337    * @param sys FFXSystem
338    */
339   public void store(FFXSystem sys) {
340     synchronized (this) {
341       FFXSystem back = currentSystem;
342       currentSystem = sys;
343       storeActive();
344       currentSystem = back;
345     }
346   }
347 
348   /** {@inheritDoc} */
349   @Override
350   public String toString() {
351     return "Keyword Editor";
352   }
353 
354   /**
355    * Getter for the field <code>paramFiles</code>.
356    *
357    * @return an array of {@link java.lang.String} objects.
358    */
359   String[] getParamFiles() {
360     return paramNames;
361   }
362 
363   private void initialize() {
364     // Load the Keyword Definitions
365     loadXML();
366     // TextAreas
367     flatfileTextArea = new JTextArea();
368     flatfileTextArea.setEditable(false);
369     flatfileTextArea.setFont(Font.decode("monospaced plain 12"));
370     Insets insets = flatfileTextArea.getInsets();
371     insets.set(5, 5, 5, 5);
372     flatfileTextArea.setMargin(insets);
373     // Keyword Edit Panel
374     editPanel = new JPanel(flowLayout);
375     ClassLoader loader = getClass().getClassLoader();
376     ImageIcon icKeyPanel = new ImageIcon(loader.getResource("ffx/ui/icons/page_key.png"));
377     noSystemLabel.setIcon(icKeyPanel);
378     ImageIcon icon = new ImageIcon(loader.getResource("ffx/ui/icons/information.png"));
379     noKeywordLabel.setIcon(icon);
380     noKeywordPanel.add(noKeywordLabel);
381     editScrollPane =
382         new JScrollPane(
383             editPanel,
384             JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
385             JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
386     descriptScrollPane =
387         new JScrollPane(
388             descriptTextArea,
389             JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
390             JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
391     Border eb = BorderFactory.createEtchedBorder(EtchedBorder.RAISED);
392     descriptScrollPane.setBorder(eb);
393     // Add the Keyword Group Panel and Decription Panel to a JSplitPane
394     splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, editScrollPane, descriptScrollPane);
395     splitPane.setResizeWeight(1.0);
396     splitPane.setOneTouchExpandable(true);
397     statusLabel.setBorder(eb);
398     // Add the main pieces to the overall KeywordPanel (except the ToolBar)
399     setLayout(new BorderLayout());
400     add(splitPane, BorderLayout.CENTER);
401     add(statusLabel, BorderLayout.SOUTH);
402     // Init the GridBagContraints
403     gridBagConstraints.gridx = 0;
404     gridBagConstraints.gridy = 0;
405     gridBagConstraints.anchor = GridBagConstraints.WEST;
406     gridBagConstraints.gridheight = 1;
407     gridBagConstraints.gridwidth = 1;
408     gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;
409     initToolBar();
410     add(toolBar, BorderLayout.NORTH);
411     setParamPath();
412     loadPrefs();
413     loadKeywordGroup();
414   }
415 
416   /** initToolBar */
417   private void initToolBar() {
418     toolBar = new JToolBar("Keyword Editor");
419     toolBar.setLayout(flowLayout);
420     ClassLoader loader = getClass().getClassLoader();
421     JButton jbopen = new JButton(new ImageIcon(loader.getResource("ffx/ui/icons/folder_page.png")));
422     jbopen.setActionCommand("Open...");
423     jbopen.setToolTipText("Open a *.KEY File for Editing");
424     jbopen.addActionListener(this);
425     Insets insets = jbopen.getInsets();
426     insets.top = 2;
427     insets.bottom = 2;
428     insets.left = 2;
429     insets.right = 2;
430     jbopen.setMargin(insets);
431     // toolBar.add(jbopen);
432     JButton jbsave = new JButton(new ImageIcon(loader.getResource("ffx/ui/icons/disk.png")));
433     jbsave.setActionCommand("Save");
434     jbsave.setToolTipText("Save the Active *.KEY File");
435     jbsave.addActionListener(this);
436     jbsave.setMargin(insets);
437     toolBar.add(jbsave);
438     JButton jbsaveas = new JButton(new ImageIcon(loader.getResource("ffx/ui/icons/page_save.png")));
439     jbsaveas.setActionCommand("Save As...");
440     jbsaveas.setToolTipText("Save the Active *.KEY File Under a New Name");
441     jbsaveas.addActionListener(this);
442     jbsaveas.setMargin(insets);
443     toolBar.add(jbsaveas);
444     JButton jbclose = new JButton(new ImageIcon(loader.getResource("ffx/ui/icons/cancel.png")));
445     jbclose.setActionCommand("Close");
446     jbclose.setToolTipText("Close the Active *.KEY File");
447     jbclose.addActionListener(this);
448     jbclose.setMargin(insets);
449     // toolBar.add(jbclose);
450     toolBar.addSeparator();
451     groupComboBox.setMaximumSize(groupComboBox.getPreferredSize());
452     groupComboBox.addActionListener(this);
453     groupComboBox.setEditable(false);
454     toolBar.add(groupComboBox);
455     toolBar.addSeparator();
456     ImageIcon icinfo = new ImageIcon(loader.getResource("ffx/ui/icons/information.png"));
457     descriptCheckBox = new JCheckBoxMenuItem(icinfo);
458     descriptCheckBox.setActionCommand("Description");
459     descriptCheckBox.setToolTipText("Show/Hide Keyword Descriptions");
460     descriptCheckBox.addActionListener(this);
461     descriptCheckBox.setMargin(insets);
462     toolBar.add(descriptCheckBox);
463     toolBar.add(new JLabel(""));
464     toolBar.setBorderPainted(false);
465     toolBar.setFloatable(false);
466     toolBar.setRollover(true);
467     toolBar.setOrientation(JToolBar.HORIZONTAL);
468   }
469 
470   /**
471    * isFileOpen
472    *
473    * @return a boolean.
474    */
475   boolean isFileOpen() {
476     return fileOpen;
477   }
478 
479   /** keyClear */
480   private void keyClear() {
481     synchronized (this) {
482       // Clear each KeywordComponent
483       for (KeywordComponent kw : keywordHashMap.values()) {
484         kw.clearKeywordComponent();
485       }
486       KeywordComponent.setKeywordModified(false);
487       // Reset internal variables
488       fileOpen = false;
489       currentKeys = null;
490       currentSystem = null;
491       currentKeyFile = null;
492       // Reset the View
493       commentStringBuffer = new StringBuilder();
494       statusLabel.setText("  ");
495       loadKeywordGroup();
496     }
497   }
498 
499   /**
500    * keyClose
501    *
502    * @return a boolean.
503    */
504   private boolean keyClose() {
505     if (KeywordComponent.isKeywordModified()) {
506       return false;
507     }
508     keyClear();
509     return true;
510   }
511 
512   /** Give the user a File Dialog Box so they can select a key file. */
513   private void keyOpen() {
514     if (fileOpen && KeywordComponent.isKeywordModified()) {
515       int option =
516           JOptionPane.showConfirmDialog(
517               this,
518               "Save Changes First",
519               "Opening New File",
520               JOptionPane.YES_NO_CANCEL_OPTION,
521               JOptionPane.INFORMATION_MESSAGE);
522       if (option == JOptionPane.CANCEL_OPTION) {
523         return;
524       } else if (option == JOptionPane.YES_OPTION) {
525         keySave(currentKeyFile);
526       }
527       keyClear();
528     }
529     JFileChooser d = MainPanel.resetFileChooser();
530     if (currentSystem != null) {
531       File cwd = currentSystem.getFile();
532       if (cwd != null && cwd.getParentFile() != null) {
533         d.setCurrentDirectory(cwd.getParentFile());
534       }
535     }
536     d.setAcceptAllFileFilterUsed(false);
537     d.setFileFilter(MainPanel.keyFileFilter);
538     d.setDialogTitle("Open KEY File");
539     int result = d.showOpenDialog(this);
540     if (result == JFileChooser.APPROVE_OPTION) {
541       File newKeyFile = d.getSelectedFile();
542       if (newKeyFile != null && newKeyFile.exists() && newKeyFile.canRead()) {
543         keyOpen(newKeyFile);
544       }
545     }
546   }
547 
548   /**
549    * keyOpen
550    *
551    * @param newKeyFile a {@link java.io.File} object.
552    * @return a boolean.
553    */
554   private boolean keyOpen(@Nullable File newKeyFile) {
555     if (newKeyFile != null && newKeyFile.exists() && newKeyFile.canRead()) {
556       Hashtable<String, Keyword> newKeys = KeyFilter.open(newKeyFile);
557       if (newKeys != null && !newKeys.isEmpty()) {
558         if (currentSystem != null) {
559           currentSystem.setKeyFile(currentKeyFile);
560           currentSystem.setKeywords(currentKeys);
561         }
562         loadActive(currentSystem, newKeys, newKeyFile);
563         return true;
564       }
565     }
566     return false;
567   }
568 
569   /**
570    * keySave
571    *
572    * @param f a {@link java.io.File} object.
573    */
574   void keySave(File f) {
575     if (f != null) {
576       currentKeyFile = f;
577     }
578     if (!fileOpen || currentKeyFile == null) {
579       return;
580     }
581     storeActive();
582     saveKeywords(currentKeyFile, keywordHashMap, commentStringBuffer);
583   }
584 
585   /** keySaveAs */
586   private void keySaveAs() {
587     if (!fileOpen) {
588       return;
589     }
590     JFileChooser d = MainPanel.resetFileChooser();
591     d.setDialogTitle("Save KEY File");
592     d.setAcceptAllFileFilterUsed(false);
593     if (currentKeyFile != null) {
594       d.setCurrentDirectory(currentKeyFile.getParentFile());
595       d.setSelectedFile(currentKeyFile);
596     }
597     d.setFileFilter(new KeyFileFilter());
598     int result = d.showSaveDialog(this);
599     if (result == JFileChooser.APPROVE_OPTION) {
600       currentKeyFile = d.getSelectedFile();
601       keySave(currentKeyFile);
602     }
603   }
604 
605   /**
606    * loadActive
607    *
608    * @param newSystem a {@link ffx.ui.FFXSystem} object.
609    * @return a boolean.
610    */
611   boolean loadActive(FFXSystem newSystem) {
612     synchronized (this) {
613       if (newSystem == null) {
614         keyClear();
615         return false;
616       }
617       configToKeywords(newSystem);
618       File newKeyFile = newSystem.getKeyFile();
619       if (newKeyFile != null) {
620         // logger.info(String.format("Key File %s.", newKeyFile.getAbsolutePath()));
621       }
622       Hashtable<String, Keyword> newKeys = newSystem.getKeywords();
623       if (newKeyFile == null && newKeys == null) {
624         logger.info(String.format("Loaded %s with no keywords.", newSystem));
625         return false;
626       }
627       // logger.info(String.format("Loading %s with %d keywords.", newSystem.toString(),
628       // newKeys.size()));
629       return loadActive(newSystem, newKeys, newKeyFile);
630     }
631   }
632 
633   private void configToKeywords(FFXSystem newSystem) {
634 
635     CompositeConfiguration properties = newSystem.getProperties();
636     Hashtable<String, Keyword> keywordHash = new Hashtable<>();
637 
638     // Create the "Comments" property.
639     Keyword keyword = new Keyword("COMMENTS");
640     keywordHash.put("COMMENTS", keyword);
641 
642     // Loop over properties from the keyword file.
643     Configuration config = properties.getConfiguration(0);
644     if (config instanceof PropertiesConfiguration) {
645       PropertiesConfiguration prop = (PropertiesConfiguration) config;
646       Iterator<String> keys = prop.getKeys();
647       while (keys.hasNext()) {
648         String key = keys.next();
649         if (keywordHash.contains(key)) {
650           keyword = keywordHash.get(key);
651           keyword.append(prop.getStringArray(key));
652         } else {
653           String[] values = prop.getStringArray(key);
654           keyword = new Keyword(key, values);
655           keywordHash.put(key, keyword);
656         }
657       }
658     }
659 
660     newSystem.setKeywords(keywordHash);
661   }
662 
663   /**
664    * loadActive
665    *
666    * @param newSystem a {@link ffx.ui.FFXSystem} object.
667    * @param newKeys a {@link java.util.Hashtable} object.
668    * @param newKeyFile a {@link java.io.File} object.
669    * @return a boolean.
670    */
671   private boolean loadActive(
672       FFXSystem newSystem, @Nullable Hashtable<String, Keyword> newKeys, File newKeyFile) {
673 
674     synchronized (this) {
675       // Store changes made to the current system (if any) first.
676       if (currentKeys != null && KeywordComponent.isKeywordModified()) {
677         if (currentSystem != null && currentSystem != newSystem) {
678           storeActive();
679         } else {
680           saveChanges();
681         }
682       }
683       // Clear previous values
684       keyClear();
685       // No keywords to load
686       if (newKeys == null || newKeys.isEmpty()) {
687         return false;
688       }
689       currentSystem = newSystem;
690       currentKeyFile = newKeyFile;
691       currentKeys = newKeys;
692       fileOpen = true;
693       /*
694        * Keys to remove are those entries not recognized by the FFX
695        * Keyword Editor These keys are removed from the active System's
696        * keyword Hashtable and appended to the generic "Comments"
697        * KeywordData instance.
698        */
699       ArrayList<String> keysToRemove = new ArrayList<>();
700       Keyword comments = currentKeys.get("COMMENTS");
701       for (Keyword keyword : currentKeys.values()) {
702         String label = keyword.getKeyword();
703         Vector<String> data = keyword.getEntries();
704         if (label.equals("COMMENTS")) {
705           continue;
706         }
707         KeywordComponent tk = keywordHashMap.get(label.toUpperCase());
708         // If the keyword label isn't recognized, preserve the line
709         // as a Comment
710         if (tk == null) {
711           keysToRemove.add(keyword.getKeyword());
712           if (data.isEmpty()) {
713             comments.append(label);
714           } else if (label.equalsIgnoreCase("MULTIPOLE")) {
715             int count = 5;
716             for (String entry : data) {
717               count++;
718               if (count > 5) {
719                 comments.append(label + " " + entry);
720                 count = 1;
721               } else {
722                 comments.append(entry);
723               }
724             }
725           } else if (label.equalsIgnoreCase("TORTORS")) {
726             int points = 0;
727             int count = 0;
728             for (String entry : data) {
729               count++;
730               if (count > points) {
731                 String[] res = entry.split(" +");
732                 int xres = Integer.parseInt(res[5]);
733                 int yres = Integer.parseInt(res[5]);
734                 points = xres * yres;
735                 comments.append(label + " " + entry);
736                 count = 0;
737               } else {
738                 comments.append(entry);
739               }
740             }
741           } else {
742             for (String entry : data) {
743               comments.append(label + " " + entry);
744             }
745           }
746           continue;
747         }
748         // No data for this keyword (something like "verbose" that
749         // doesn't have modifiers)
750         if (data.isEmpty()) {
751           tk.loadKeywordEntry(label);
752         }
753         // One to many data entries (something "atom" or "bond" that
754         // can be repeated)
755         for (String s : data) {
756           tk.loadKeywordEntry(s);
757         }
758       }
759       // Load up the Comments
760       Vector<String> entries = comments.getEntries();
761       if (entries != null) {
762         for (String s : entries) {
763           if (s.length() > 1) {
764             if (!groupHashMap.containsKey(s.substring(1).toUpperCase().trim())) {
765               commentStringBuffer.append(s).append("\n");
766             }
767           } else {
768             commentStringBuffer.append(s).append("\n");
769           }
770         }
771       }
772       // Keywords that aren't recognized have been turned into Comments
773       for (String k : keysToRemove) {
774         currentKeys.remove(k);
775       }
776       if (currentSystem != null) {
777         currentSystem.setKeywords(currentKeys);
778         currentSystem.setKeyFile(currentKeyFile);
779       }
780       loadKeywordGroup();
781       return true;
782     }
783   }
784 
785   private void loadKeywordGroup() {
786     synchronized (this) {
787       editPanel.removeAll();
788       String selectedGroup = (String) groupComboBox.getSelectedItem();
789       if (currentKeys == null) {
790         editPanel.setLayout(flowLayout);
791         editPanel.add(noSystemLabel);
792         int temp = splitPane.getDividerLocation();
793         splitPane.setBottomComponent(noKeywordPanel);
794         splitPane.setDividerLocation(temp);
795       } else if (selectedGroup.equalsIgnoreCase("Flat File View")) {
796         editPanel.setLayout(borderLayout);
797         publishKeywords();
798         editPanel.add(flatfileTextArea, BorderLayout.CENTER);
799         int temp = splitPane.getDividerLocation();
800         splitPane.setBottomComponent(noKeywordPanel);
801         splitPane.setDividerLocation(temp);
802       } else if (selectedGroup.equalsIgnoreCase("Active Keywords")) {
803         gridPanel.removeAll();
804         gridBagConstraints.gridy = 0;
805         gridBagConstraints.anchor = GridBagConstraints.WEST;
806         gridBagConstraints.gridheight = 1;
807         gridBagConstraints.gridwidth = 1;
808         gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;
809         for (KeywordComponent keywordComponent : keywordHashMap.values()) {
810           if (keywordComponent.isActive()) {
811             JPanel jptemp = keywordComponent.getKeywordGUI();
812             gridBagLayout.setConstraints(jptemp, gridBagConstraints);
813             gridPanel.add(jptemp);
814             gridBagConstraints.gridy++;
815           }
816         }
817         KeywordComponent.fillPanel(gridPanel, gridBagLayout, gridBagConstraints);
818         editPanel.setLayout(flowLayout);
819         editPanel.add(gridPanel);
820         int temp = splitPane.getDividerLocation();
821         splitPane.setBottomComponent(descriptScrollPane);
822         splitPane.setDividerLocation(temp);
823       } else {
824         gridPanel.removeAll();
825         gridBagConstraints.gridy = 0;
826         gridBagConstraints.gridx = 0;
827         gridBagConstraints.anchor = GridBagConstraints.WEST;
828         gridBagConstraints.gridheight = 1;
829         gridBagConstraints.gridwidth = 1;
830         gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;
831         for (KeywordComponent keywordComponent : keywordHashMap.values()) {
832           if (keywordComponent.getKeywordGroup().equalsIgnoreCase(selectedGroup)) {
833             JPanel jptemp = keywordComponent.getKeywordGUI();
834             gridBagLayout.setConstraints(jptemp, gridBagConstraints);
835             gridPanel.add(jptemp);
836             gridBagConstraints.gridy++;
837           }
838         }
839         KeywordComponent.fillPanel(gridPanel, gridBagLayout, gridBagConstraints);
840         editPanel.setLayout(flowLayout);
841         editPanel.add(gridPanel);
842         int temp = splitPane.getDividerLocation();
843         splitPane.setBottomComponent(descriptScrollPane);
844         splitPane.setDividerLocation(temp);
845       }
846       if (currentKeyFile != null) {
847         statusLabel.setText("  " + currentKeyFile);
848       } else {
849         statusLabel.setText("  ");
850       }
851       editScrollPane.validate();
852       editScrollPane.repaint();
853     }
854   }
855 
856   /** loadPrefs */
857   private void loadPrefs() {
858     String c = KeywordPanel.class.getName();
859     descriptCheckBox.setSelected(!preferences.getBoolean(c + ".description", true));
860     descriptCheckBox.doClick();
861   }
862 
863   /** Load up the Force Field X Keyword Definitions */
864   private void loadXML() {
865     NodeList groups, keywords, values;
866     Element group, keyword, value;
867     String groupName;
868     String keywordName, keywordDescription, keywordGUI;
869     groups = null;
870     try {
871       // Build the Document from the keywords.xml file
872       DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
873       DocumentBuilder db = dbf.newDocumentBuilder();
874       db.setEntityResolver(new DTDResolver());
875       URL keyURL = getClass().getClassLoader().getResource("ffx/ui/commands/keywords.xml");
876       Document doc = db.parse(keyURL.openStream());
877       Element document = doc.getDocumentElement();
878       Element body = (Element) document.getElementsByTagName("body").item(0);
879       groups = body.getElementsByTagName("section");
880     } catch (ParserConfigurationException | SAXException | IOException e) {
881       logger.warning(e.toString());
882     }
883     keywordHashMap = new LinkedHashMap<>();
884     groupHashMap = new LinkedHashMap<>();
885     groupHashMap.put("ACTIVE KEYWORDS", "Active Keywords");
886     groupHashMap.put("FLAT FILE VIEW", "Flat File View");
887     groupComboBox = new JComboBox<>();
888     groupComboBox.addItem("Active Keywords");
889     groupComboBox.addItem("Flat File View");
890     descriptTextArea = new JTextArea();
891     descriptTextArea.setLineWrap(true);
892     descriptTextArea.setWrapStyleWord(true);
893     Insets insets = descriptTextArea.getInsets();
894     insets.set(5, 5, 5, 5);
895     descriptTextArea.setMargin(insets);
896     int length = groups.getLength();
897     // Iterate through the Keyword Groups
898     for (int i = 0; i < length; i++) {
899       group = (Element) groups.item(i);
900       groupName = group.getAttribute("name");
901       groupComboBox.addItem(groupName);
902       keywords = group.getElementsByTagName("subsection");
903       int klength = keywords.getLength();
904       for (int j = 0; j < klength; j++) {
905         keyword = (Element) keywords.item(j);
906         keywordName = keyword.getAttribute("name");
907         Node text = keyword.getFirstChild();
908         keywordDescription = text.getNodeValue().replace('\n', ' ');
909         keywordDescription = keywordDescription.replaceAll("  +", " ");
910         keywordGUI = keyword.getAttribute("rep");
911         KeywordComponent.SwingRepresentation type;
912         try {
913           type = KeywordComponent.SwingRepresentation.valueOf(keywordGUI.toUpperCase());
914         } catch (Exception e) {
915           type = null;
916           logger.log(
917               Level.WARNING, "{0}: Unknown GUI Component - {1}", new Object[] {keywordName, type});
918           System.exit(-1);
919         }
920         KeywordComponent key;
921         if (type == KeywordComponent.SwingRepresentation.CHECKBOXES
922             || type == KeywordComponent.SwingRepresentation.COMBOBOX) {
923           values = keyword.getElementsByTagName("Value");
924           String[] labels = new String[values.getLength()];
925           for (int k = 0; k < values.getLength(); k++) {
926             value = (Element) values.item(k);
927             labels[k] = value.getAttribute("name");
928           }
929           key =
930               new KeywordComponent(
931                   keywordName, groupName, type, keywordDescription, descriptTextArea, labels);
932         } else {
933           key =
934               new KeywordComponent(
935                   keywordName, groupName, type, keywordDescription, descriptTextArea);
936         }
937         keywordHashMap.put(keywordName.toUpperCase(), key);
938         groupHashMap.put(groupName.toUpperCase(), groupName);
939       }
940     }
941     groupComboBox.setSelectedIndex(0);
942   }
943 
944   /** publishKeywords */
945   private void publishKeywords() {
946     synchronized (this) {
947       flatfileTextArea.setText("");
948       boolean writegroup = false;
949       String pgroup = null;
950       // Write out keywords in groups
951       for (KeywordComponent keyword : keywordHashMap.values()) {
952         String group = keyword.getKeywordGroup();
953         if (pgroup == null || !group.equalsIgnoreCase(pgroup)) {
954           writegroup = true;
955           pgroup = group;
956         }
957         String line = keyword.toString();
958         if (line != null) {
959           if (writegroup) {
960             flatfileTextArea.append("\n");
961             flatfileTextArea.append("# " + group);
962             flatfileTextArea.append("\n");
963             writegroup = false;
964           }
965           flatfileTextArea.append(line);
966           flatfileTextArea.append("\n");
967         }
968       }
969       flatfileTextArea.append("\n");
970       String s = commentStringBuffer.toString();
971       if (!s.trim().isEmpty()) {
972         flatfileTextArea.append(s.trim());
973       }
974       flatfileTextArea.append("\n");
975     }
976   }
977 
978   /**
979    * saveChanges
980    *
981    * @return a boolean.
982    */
983   boolean saveChanges() {
984     if (KeywordComponent.isKeywordModified() && currentKeyFile != null) {
985       keySave(currentKeyFile);
986       return true;
987     }
988     return false;
989   }
990 
991   /**
992    * saveKeywords
993    *
994    * @param keyFile a {@link java.io.File} object.
995    * @param keywordHashMap a {@link java.util.LinkedHashMap} object.
996    * @param comments a {@link java.lang.StringBuilder} object.
997    * @return a boolean.
998    */
999   private boolean saveKeywords(
1000       File keyFile,
1001       LinkedHashMap<String, KeywordComponent> keywordHashMap,
1002       StringBuilder comments) {
1003     synchronized (this) {
1004       FileWriter fw = null;
1005       BufferedWriter bw = null;
1006       try {
1007         fw = new FileWriter(keyFile);
1008         bw = new BufferedWriter(fw);
1009         boolean writegroup = false;
1010         String pgroup = null;
1011         // Write out keywords in groups
1012         for (KeywordComponent keyword : keywordHashMap.values()) {
1013           String group = keyword.getKeywordGroup();
1014           if (pgroup == null || !group.equalsIgnoreCase(pgroup)) {
1015             writegroup = true;
1016             pgroup = group;
1017           }
1018           String line = keyword.toString();
1019           if (line != null) {
1020             if (writegroup) {
1021               bw.newLine();
1022               bw.write("# " + group);
1023               bw.newLine();
1024               writegroup = false;
1025             }
1026             bw.write(line);
1027             bw.newLine();
1028           }
1029         }
1030         bw.newLine();
1031         String s = comments.toString();
1032         if (s != null && !s.trim().isEmpty()) {
1033           bw.write(s.trim());
1034         }
1035         bw.newLine();
1036         bw.flush();
1037         KeywordComponent.setKeywordModified(false);
1038       } catch (IOException e) {
1039         logger.warning(e.toString());
1040         return false;
1041       } finally {
1042         try {
1043           if (bw != null) {
1044             bw.close();
1045           }
1046           if (fw != null) {
1047             fw.close();
1048           }
1049         } catch (Exception e) {
1050           logger.warning(e.toString());
1051         }
1052       }
1053       return true;
1054     }
1055   }
1056 
1057   /** savePrefs */
1058   void savePrefs() {
1059     String c = KeywordPanel.class.getName();
1060     preferences.putInt(c + ".divider", splitPane.getDividerLocation());
1061     preferences.putBoolean(c + ".description", descriptCheckBox.isSelected());
1062   }
1063 
1064   /**
1065    * setDivider
1066    *
1067    * @param b a boolean.
1068    */
1069   private void setDivider(boolean b) {
1070     if (b) {
1071       descriptCheckBox.setSelected(b);
1072       int spDivider = (int) (this.getHeight() * (3.0f / 5.0f));
1073       splitPane.setDividerLocation(spDivider);
1074     } else {
1075       splitPane.setDividerLocation(1.0);
1076     }
1077   }
1078 
1079   /** setParamPath */
1080   private void setParamPath() {
1081     // Location of TINKER parameter files
1082     File paramDir = new File(MainPanel.ffxDir.getAbsolutePath() + File.separator + "params");
1083     if (paramDir.exists()) {
1084       File[] paramFiles = paramDir.listFiles();
1085       paramHashtable = new LinkedHashMap<>();
1086       for (File f : paramFiles) {
1087         if (f.exists() && f.canRead() && MainPanel.forceFieldFileFilter.accept(f)) {
1088           paramHashtable.put(f.getName(), f.getAbsolutePath());
1089         }
1090       }
1091       int num = paramHashtable.size();
1092       paramNames = new String[num + 1];
1093       int i = 1;
1094       for (String name : paramHashtable.keySet()) {
1095         paramNames[i] = name;
1096         i++;
1097       }
1098       paramNames[0] = "AAA";
1099       java.util.Arrays.sort(paramNames);
1100       paramNames[0] = "Use an existing TINKER Key file".intern();
1101     }
1102   }
1103 
1104   /** Store the KeywordPanel's current keyword content in the activeSystem. */
1105   private void storeActive() {
1106     synchronized (this) {
1107       if (currentSystem == null) {
1108         return;
1109       }
1110       // No changes
1111       if (!KeywordComponent.isKeywordModified()) {
1112         return;
1113       }
1114       Hashtable<String, Keyword> currentKeys = currentSystem.getKeywords();
1115       Hashtable<String, Keyword> newKeys = new Hashtable<String, Keyword>();
1116       for (KeywordComponent kc : keywordHashMap.values()) {
1117         if (kc.isActive()) {
1118           Keyword keywordData = null;
1119           if (currentKeys != null) {
1120             keywordData = currentKeys.get(kc.getKeyword());
1121           }
1122           if (keywordData == null) {
1123             keywordData = new Keyword(kc.getKeyword());
1124           } else {
1125             keywordData.clear();
1126           }
1127           kc.getKeywordData(keywordData);
1128           newKeys.put(kc.getKeyword(), keywordData);
1129         }
1130       }
1131       Keyword comments = new Keyword("COMMENTS", commentStringBuffer.toString());
1132       newKeys.put("COMMENTS", comments);
1133       currentSystem.setKeywords(newKeys);
1134     }
1135   }
1136 }