Merge branch 'releases/Release_2_11_3_Branch'
[jalview.git] / src / jalview / gui / StructureViewerBase.java
index 0c5c5f0..bd757e8 100644 (file)
@@ -26,6 +26,7 @@ import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
+import java.beans.PropertyVetoException;
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileOutputStream;
@@ -34,12 +35,12 @@ import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Random;
 import java.util.Vector;
 
 import javax.swing.ButtonGroup;
 import javax.swing.JCheckBoxMenuItem;
-import javax.swing.JColorChooser;
 import javax.swing.JMenu;
 import javax.swing.JMenuItem;
 import javax.swing.JRadioButtonMenuItem;
@@ -47,10 +48,13 @@ import javax.swing.event.MenuEvent;
 import javax.swing.event.MenuListener;
 
 import jalview.api.AlignmentViewPanel;
+import jalview.api.structures.JalviewStructureDisplayI;
 import jalview.bin.Cache;
+import jalview.bin.Console;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
+import jalview.gui.JalviewColourChooser.ColourChooserListener;
 import jalview.gui.StructureViewer.ViewerType;
 import jalview.gui.ViewSelectionMenu.ViewSetProvider;
 import jalview.io.DataSourceType;
@@ -61,8 +65,11 @@ import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemes;
 import jalview.structure.StructureMapping;
 import jalview.structures.models.AAStructureBindingModel;
+import jalview.util.BrowserLauncher;
 import jalview.util.MessageManager;
+import jalview.ws.dbsources.EBIAlfaFold;
 import jalview.ws.dbsources.Pdb;
+import jalview.ws.utils.UrlDownloadClient;
 
 /**
  * Base class with common functionality for JMol, Chimera or other structure
@@ -83,6 +90,30 @@ public abstract class StructureViewerBase extends GStructureViewer
   }
 
   /**
+   * Singleton list of all (open) instances of structureViewerBase TODO:
+   * JAL-3362 - review and adopt the swingJS-safe singleton pattern so each
+   * structure viewer base instance is kept to its own JalviewJS parent
+   */
+  private static List<JalviewStructureDisplayI> svbs = new ArrayList<>();
+
+  /**
+   * 
+   * @return list with all existing StructureViewers instance
+   */
+  public static List<JalviewStructureDisplayI> getAllStructureViewerBases()
+  {
+    List<JalviewStructureDisplayI> goodSvbs = new ArrayList<>();
+    for (JalviewStructureDisplayI s : svbs)
+    {
+      if (s != null && !goodSvbs.contains(s))
+      {
+        goodSvbs.add(s);
+      }
+    }
+    return goodSvbs;
+  }
+
+  /**
    * list of sequenceSet ids associated with the view
    */
   protected List<String> _aps = new ArrayList<>();
@@ -112,6 +143,8 @@ public abstract class StructureViewerBase extends GStructureViewer
 
   protected boolean allChainsSelected = false;
 
+  protected boolean allHetatmBeingSelected = false;
+
   protected JMenu viewSelectionMenu;
 
   /**
@@ -131,6 +164,8 @@ public abstract class StructureViewerBase extends GStructureViewer
   public StructureViewerBase()
   {
     super();
+    setFrameIcon(null);
+    svbs.add(this);
   }
 
   /**
@@ -154,6 +189,17 @@ public abstract class StructureViewerBase extends GStructureViewer
   }
 
   /**
+   * called by the binding model to indicate when adding structures is happening
+   * or has been completed
+   * 
+   * @param addingStructures
+   */
+  public synchronized void setAddingStructures(boolean addingStructures)
+  {
+    this.addingStructures = addingStructures;
+  }
+
+  /**
    * 
    * @param ap2
    * @return true if this Jmol instance is linked with the given alignPanel
@@ -163,6 +209,7 @@ public abstract class StructureViewerBase extends GStructureViewer
     return _aps.contains(ap2.av.getSequenceSetId());
   }
 
+  @Override
   public boolean isUsedforaligment(AlignmentViewPanel ap2)
   {
 
@@ -209,6 +256,10 @@ public abstract class StructureViewerBase extends GStructureViewer
       _alignwith.add(ap);
     }
     ;
+    // TODO: refactor to allow concrete classes to register buttons for adding
+    // here
+    // currently have to override to add buttons back in after they are cleared
+    // in this loop
     for (Component c : viewerActionMenu.getMenuComponents())
     {
       if (c != alignStructs)
@@ -426,8 +477,9 @@ public abstract class StructureViewerBase extends GStructureViewer
     {
       return;
     }
-    AlignmentPanel alignPanel = (AlignmentPanel) apanel; // Implementation error if this
-                                                 // cast fails
+    AlignmentPanel alignPanel = (AlignmentPanel) apanel; // Implementation error
+                                                         // if this
+    // cast fails
     useAlignmentPanelForSuperposition(alignPanel);
     addStructure(pdbentry, seq, chains, alignPanel.alignFrame);
   }
@@ -557,6 +609,88 @@ public abstract class StructureViewerBase extends GStructureViewer
     }
   }
 
+  void setHetatmMenuItems(Map<String, String> hetatmNames)
+  {
+    hetatmMenu.removeAll();
+    if (hetatmNames == null || hetatmNames.isEmpty())
+    {
+      hetatmMenu.setVisible(false);
+      return;
+    }
+    hetatmMenu.setVisible(true);
+    allHetatmBeingSelected = false;
+    JMenuItem allMenuItem = new JMenuItem(
+            MessageManager.getString("label.all"));
+    JMenuItem noneMenuItem = new JMenuItem(
+            MessageManager.getString("label.none"));
+    allMenuItem.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        {
+          allHetatmBeingSelected = true;
+          // Toggle state of everything - on
+          for (int i = 0; i < hetatmMenu.getItemCount(); i++)
+          {
+            if (hetatmMenu.getItem(i) instanceof JCheckBoxMenuItem)
+            {
+              ((JCheckBoxMenuItem) hetatmMenu.getItem(i)).setSelected(true);
+            }
+          }
+          allHetatmBeingSelected = false;
+          showSelectedHetatms();
+        }
+      }
+    });
+
+    noneMenuItem.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        {
+          allHetatmBeingSelected = true;
+          // Toggle state of everything off
+          for (int i = 0; i < hetatmMenu.getItemCount(); i++)
+          {
+            if (hetatmMenu.getItem(i) instanceof JCheckBoxMenuItem)
+            {
+              ((JCheckBoxMenuItem) hetatmMenu.getItem(i))
+                      .setSelected(false);
+            }
+          }
+          allHetatmBeingSelected = false;
+          showSelectedHetatms();
+        }
+      }
+    });
+    hetatmMenu.add(noneMenuItem);
+    hetatmMenu.add(allMenuItem);
+
+    for (Map.Entry<String, String> chain : hetatmNames.entrySet())
+    {
+      JCheckBoxMenuItem menuItem = new JCheckBoxMenuItem(chain.getKey(),
+              false);
+      menuItem.setToolTipText(chain.getValue());
+      menuItem.addItemListener(new ItemListener()
+      {
+        @Override
+        public void itemStateChanged(ItemEvent evt)
+        {
+          if (!allHetatmBeingSelected)
+          {
+            // update viewer only when we were clicked, not programmatically
+            // checked/unchecked
+            showSelectedHetatms();
+          }
+        }
+      });
+
+      hetatmMenu.add(menuItem);
+    }
+  }
+
   /**
    * Action on selecting one of Jalview's registered colour schemes
    */
@@ -564,9 +698,8 @@ public abstract class StructureViewerBase extends GStructureViewer
   public void changeColour_actionPerformed(String colourSchemeName)
   {
     AlignmentI al = getAlignmentPanel().av.getAlignment();
-    ColourSchemeI cs = ColourSchemes.getInstance()
-            .getColourScheme(colourSchemeName, getAlignmentPanel().av, al,
-                    null);
+    ColourSchemeI cs = ColourSchemes.getInstance().getColourScheme(
+            colourSchemeName, getAlignmentPanel().av, al, null);
     getBinding().colourByJalviewColourScheme(cs);
   }
 
@@ -677,7 +810,10 @@ public abstract class StructureViewerBase extends GStructureViewer
     });
 
     viewerColour = new JRadioButtonMenuItem();
-    // text is set in overrides of this method
+    viewerColour
+            .setText(MessageManager.getString("label.colour_with_viewer"));
+    viewerColour.setToolTipText(MessageManager
+            .getString("label.let_viewer_manage_structure_colours"));
     viewerColour.setName(ViewerColour.ByViewer.name());
     viewerColour.setSelected(!binding.isColourBySequence());
 
@@ -703,7 +839,7 @@ public abstract class StructureViewerBase extends GStructureViewer
                 }
                 else
                 {
-                  // update the Chimera display now.
+                  // update the viewer display now.
                   seqColour_actionPerformed();
                 }
               }
@@ -715,10 +851,18 @@ public abstract class StructureViewerBase extends GStructureViewer
       @Override
       public void itemStateChanged(ItemEvent e)
       {
-        alignStructs.setEnabled(!_alignwith.isEmpty());
-        alignStructs.setToolTipText(MessageManager.formatMessage(
-                "label.align_structures_using_linked_alignment_views",
-                _alignwith.size()));
+        if (_alignwith.isEmpty())
+        {
+          alignStructs.setEnabled(false);
+          alignStructs.setToolTipText(null);
+        }
+        else
+        {
+          alignStructs.setEnabled(true);
+          alignStructs.setToolTipText(MessageManager.formatMessage(
+                  "label.align_structures_using_linked_alignment_views",
+                  _alignwith.size()));
+        }
       }
     };
     viewSelectionMenu = new ViewSelectionMenu(
@@ -745,6 +889,10 @@ public abstract class StructureViewerBase extends GStructureViewer
       }
     });
 
+    viewerActionMenu.setText(getViewerName());
+    helpItem.setText(MessageManager.formatMessage("label.viewer_help",
+            getViewerName()));
+
     buildColourMenu();
   }
 
@@ -783,22 +931,29 @@ public abstract class StructureViewerBase extends GStructureViewer
       {
         sp.append("'" + alignPanel.getViewName() + "' ");
       }
-      Cache.log.info("Couldn't align structures with the " + sp.toString()
+      Console.info("Couldn't align structures with the " + sp.toString()
               + "associated alignment panels.", e);
     }
     return reply;
   }
 
+  /**
+   * Opens a colour chooser dialog, and applies the chosen colour to the
+   * background of the structure viewer
+   */
   @Override
   public void background_actionPerformed()
   {
-    Color col = JColorChooser.showDialog(this,
-            MessageManager.getString("label.select_background_colour"),
-            null);
-    if (col != null)
+    String ttl = MessageManager.getString("label.select_background_colour");
+    ColourChooserListener listener = new ColourChooserListener()
     {
-      getBinding().setBackgroundColour(col);
-    }
+      @Override
+      public void colourSelected(Color c)
+      {
+        getBinding().setBackgroundColour(c);
+      }
+    };
+    JalviewColourChooser.showColourChooser(this, ttl, null, listener);
   }
 
   @Override
@@ -856,6 +1011,7 @@ public abstract class StructureViewerBase extends GStructureViewer
   @Override
   public void pdbFile_actionPerformed()
   {
+    // TODO: JAL-3048 not needed for Jalview-JS - save PDB file
     JalviewFileChooser chooser = new JalviewFileChooser(
             Cache.getProperty("LAST_DIRECTORY"));
 
@@ -939,6 +1095,7 @@ public abstract class StructureViewerBase extends GStructureViewer
       return;
     }
     setChainMenuItems(binding.getChainNames());
+    setHetatmMenuItems(binding.getHetatmNames());
 
     this.setTitle(binding.getViewerTitle(getViewerName(), true));
 
@@ -946,7 +1103,7 @@ public abstract class StructureViewerBase extends GStructureViewer
      * enable 'Superpose with' if more than one mapped structure
      */
     viewSelectionMenu.setEnabled(false);
-    if (getBinding().getStructureFiles().length > 1
+    if (getBinding().getMappedStructureCount() > 1
             && getBinding().getSequence().length > 1)
     {
       viewSelectionMenu.setEnabled(true);
@@ -998,14 +1155,17 @@ public abstract class StructureViewerBase extends GStructureViewer
     {
       return false;
     }
-    int p=0;
-    for (String pdbid:pdbids) {
+    int p = 0;
+    for (String pdbid : pdbids)
+    {
       StructureMapping sm[] = getBinding().getSsm().getMapping(pdbid);
-      if (sm!=null && sm.length>0 && sm[0]!=null) {
+      if (sm != null && sm.length > 0 && sm[0] != null)
+      {
         p++;
       }
     }
-    // only return true if there is a mapping for every structure file we have loaded
+    // only return true if there is a mapping for every structure file we have
+    // loaded
     if (p == 0 || p != pdbids.length)
     {
       return false;
@@ -1052,7 +1212,7 @@ public abstract class StructureViewerBase extends GStructureViewer
     progressBar = pi;
   }
 
-  protected void setProgressMessage(String message, long id)
+  public void setProgressMessage(String message, long id)
   {
     if (progressBar != null)
     {
@@ -1087,6 +1247,26 @@ public abstract class StructureViewerBase extends GStructureViewer
   }
 
   /**
+   * Display selected hetatms in viewer
+   */
+  protected void showSelectedHetatms()
+  {
+    List<String> toshow = new ArrayList<>();
+    for (int i = 0; i < hetatmMenu.getItemCount(); i++)
+    {
+      if (hetatmMenu.getItem(i) instanceof JCheckBoxMenuItem)
+      {
+        JCheckBoxMenuItem item = (JCheckBoxMenuItem) hetatmMenu.getItem(i);
+        if (item.isSelected())
+        {
+          toshow.add(item.getText());
+        }
+      }
+    }
+    getBinding().showHetatms(toshow);
+  }
+
+  /**
    * Tries to fetch a PDB file and save to a temporary local file. Returns the
    * saved file path if successful, or null if not.
    * 
@@ -1097,11 +1277,12 @@ public abstract class StructureViewerBase extends GStructureViewer
   {
     String filePath = null;
     Pdb pdbclient = new Pdb();
+    EBIAlfaFold afclient = new EBIAlfaFold();
     AlignmentI pdbseq = null;
     String pdbid = processingEntry.getId();
     long handle = System.currentTimeMillis()
             + Thread.currentThread().hashCode();
-  
+
     /*
      * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
      */
@@ -1114,10 +1295,42 @@ public abstract class StructureViewerBase extends GStructureViewer
     // { pdbid }));
     try
     {
-      pdbseq = pdbclient.getSequenceRecords(pdbid);
+      if (afclient.isValidReference(pdbid))
+      {
+        pdbseq = afclient.getSequenceRecords(pdbid,
+                processingEntry.getRetrievalUrl());
+      }
+      else
+      {
+        if (processingEntry.hasRetrievalUrl())
+        {
+          String safePDBId = java.net.URLEncoder.encode(pdbid, "UTF-8")
+                  .replace("%", "__");
+
+          // retrieve from URL to new local tmpfile
+          File tmpFile = File.createTempFile(safePDBId,
+                  "." + (PDBEntry.Type.MMCIF.toString().equals(
+                          processingEntry.getType().toString()) ? "cif"
+                                  : "pdb"));
+          String fromUrl = processingEntry.getRetrievalUrl();
+          UrlDownloadClient.download(fromUrl, tmpFile);
+
+          // may not need this check ?
+          String file = tmpFile.getAbsolutePath();
+          if (file != null)
+          {
+            pdbseq = EBIAlfaFold.importDownloadedStructureFromUrl(fromUrl,
+                    tmpFile, pdbid, null, null, null);
+          }
+        }
+        else
+        {
+          pdbseq = pdbclient.getSequenceRecords(pdbid);
+        }
+      }
     } catch (Exception e)
     {
-      System.err.println(
+      jalview.bin.Console.errPrintln(
               "Error retrieving PDB id " + pdbid + ": " + e.getMessage());
     } finally
     {
@@ -1147,8 +1360,167 @@ public abstract class StructureViewerBase extends GStructureViewer
    */
   public File saveSession()
   {
-    // TODO: a wait loop to ensure the file is written fully before returning?
-    return getBinding() == null ? null : getBinding().saveSession();
+    if (getBinding() == null)
+    {
+      return null;
+    }
+    File session = getBinding().saveSession();
+    long l = session.length();
+    int wait = 50;
+    do
+    {
+      try
+      {
+        Thread.sleep(5);
+      } catch (InterruptedException e)
+      {
+      }
+      long nextl = session.length();
+      if (nextl != l)
+      {
+        wait = 50;
+        l = nextl;
+      }
+    } while (--wait > 0);
+    return session;
+  }
+
+  private static boolean quitClose = false;
+
+  public static void setQuitClose(boolean b)
+  {
+    quitClose = b;
+  }
+
+  @Override
+  public boolean stillRunning()
+  {
+    AAStructureBindingModel binding = getBinding();
+    return binding != null && binding.isViewerRunning();
+  }
+
+  /**
+   * Close down this instance of Jalview's Chimera viewer, giving the user the
+   * option to close the associated Chimera window (process). They may wish to
+   * keep it open until they have had an opportunity to save any work.
+   * 
+   * @param forceClose
+   *          if true, close any linked Chimera process; if false, prompt first
+   */
+  @Override
+  public void closeViewer(boolean forceClose)
+  {
+    AAStructureBindingModel binding = getBinding();
+    if (stillRunning())
+    {
+      if (!forceClose)
+      {
+        String viewerName = getViewerName();
+
+        int confirm = JvOptionPane.CANCEL_OPTION;
+        if (QuitHandler.quitting())
+        {
+          // already asked about closing external windows
+          confirm = quitClose ? JvOptionPane.YES_OPTION
+                  : JvOptionPane.NO_OPTION;
+        }
+        else
+        {
+          String prompt = MessageManager
+                  .formatMessage("label.confirm_close_viewer", new Object[]
+                  { binding.getViewerTitle(viewerName, false),
+                      viewerName });
+          prompt = JvSwingUtils.wrapTooltip(true, prompt);
+          String title = MessageManager.getString("label.close_viewer");
+          confirm = showCloseDialog(title, prompt);
+        }
+
+        /*
+         * abort closure if user hits escape or Cancel
+         */
+        if (confirm == JvOptionPane.CANCEL_OPTION
+                || confirm == JvOptionPane.CLOSED_OPTION)
+        {
+          // abort possible quit handling if CANCEL chosen
+          if (confirm == JvOptionPane.CANCEL_OPTION)
+          {
+            try
+            {
+              // this is a bit futile
+              this.setClosed(false);
+            } catch (PropertyVetoException e)
+            {
+            }
+            QuitHandler.abortQuit();
+          }
+          return;
+        }
+        forceClose = confirm == JvOptionPane.YES_OPTION;
+      }
+    }
+    if (binding != null)
+    {
+      binding.closeViewer(forceClose);
+    }
+    setAlignmentPanel(null);
+    _aps.clear();
+    _alignwith.clear();
+    _colourwith.clear();
+    // TODO: check for memory leaks where instance isn't finalised because jmb
+    // holds a reference to the window
+    // jmb = null;
+
+    try
+    {
+      svbs.remove(this);
+    } catch (Throwable t)
+    {
+      Console.info(
+              "Unexpected exception when deregistering structure viewer",
+              t);
+    }
+    dispose();
+  }
+
+  private int showCloseDialog(final String title, final String prompt)
+  {
+    int confirmResponse = JvOptionPane.CANCEL_OPTION;
+    confirmResponse = JvOptionPane.showConfirmDialog(this, prompt,
+            MessageManager.getString("label.close_viewer"),
+            JvOptionPane.YES_NO_CANCEL_OPTION,
+            JvOptionPane.WARNING_MESSAGE);
+    return confirmResponse;
+  }
+
+  @Override
+  public void showHelp_actionPerformed()
+  {
+    /*
+    try
+    {
+    */
+    String url = getBinding().getHelpURL();
+    if (url != null)
+    {
+      BrowserLauncher.openURL(url);
+    }
+    /* 
+    }
+    catch (IOException ex)
+    {
+      System.err
+              .println("Show " + getViewerName() + " failed with: "
+                      + ex.getMessage());
+    }
+    */
+  }
+
+  @Override
+  public boolean hasViewerActionsMenu()
+  {
+    return viewerActionMenu != null && viewerActionMenu.isEnabled()
+            && viewerActionMenu.getItemCount() > 0
+            && viewerActionMenu.isVisible();
   }
 
 }