Merge branch 'feature/JAL-3855_ebi_alphafold_import' (commit '7856e91f60adc5c2d4943ba...
authorJim Procter <j.procter@dundee.ac.uk>
Thu, 26 Aug 2021 13:57:47 +0000 (14:57 +0100)
committerJim Procter <j.procter@dundee.ac.uk>
Mon, 30 Aug 2021 11:17:32 +0000 (12:17 +0100)
36 files changed:
README [changed mode: 0755->0644]
build.gradle
doc/old_README [new file with mode: 0755]
gradle.properties
src/jalview/bin/Cache.java
src/jalview/bin/HiDPISetting.java
src/jalview/datamodel/ResidueCount.java
src/jalview/fts/service/alphafold/AlphafoldRestClient.java [new file with mode: 0644]
src/jalview/fts/service/pdb/PDBFTSRestClient.java
src/jalview/gui/AppJmol.java
src/jalview/gui/JalviewChimeraXBindingModel.java
src/jalview/gui/SeqCanvas.java
src/jalview/gui/SeqPanel.java
src/jalview/gui/StructureViewerBase.java
src/jalview/jbgui/GPreferences.java
src/jalview/structure/StructureSelectionManager.java
src/jalview/util/ChannelProperties.java
src/jalview/util/HttpUtils.java
src/jalview/ws/dbsources/EBIAlfaFold.java [new file with mode: 0644]
test/jalview/datamodel/ResidueCountTest.java
test/jalview/gui/SeqPanelTest.java
utils/channels/default/resources/channel.props
utils/channels/develop/images/jalview_develop_getdown_background.png
utils/channels/develop/images/jalview_develop_getdown_background.xcf
utils/channels/develop/images/jalview_develop_getdown_background_error.png
utils/channels/develop/images/jalview_develop_getdown_background_initialising.png
utils/channels/develop/resources/channel.props
utils/channels/jalviewjs/resources/channel.props
utils/channels/release/resources/channel.props
utils/channels/test-release/resources/channel.props
utils/getdown/bin/jalview.bat [new file with mode: 0755]
utils/getdown/bin/jalview.ps1 [new file with mode: 0755]
utils/getdown/bin/jalview.sh [new file with mode: 0755]
utils/install4j/install4j8_template.install4j
utils/osx_signing/README [new file with mode: 0644]
utils/osx_signing/entitlements.txt [new file with mode: 0644]

diff --git a/README b/README
old mode 100755 (executable)
new mode 100644 (file)
index e8c44a9..b374479
--- a/README
+++ b/README
@@ -1,72 +1,5 @@
-Jalview Readme
---------------
+Please see doc/building.md for up to date build and running instructions for the Java desktop application.
 
-
-The source is available as a tar file and comes complete with the GNU General Public License. 
-
-To build the application you will need a J2SDK 1.7+. 
-
-An Ant build file (build.xml) is provided, you will need to install Apache Ant first. 
-Run ant to view usage which will display a list of useful build targets.
-
-Jalview is primarily developed with eclipse, and a .project file is provided to simplify importing the source into your own eclipse workspace. A NetBeans nbbuild.xml file is also provided for developing Jalview with NetBeans - but this is not officially supported.
-
-You may also be able to use Borland JBuilder to build Jalview. A JBuilder project file 
-(JalviewX.jpx, JalviewApplet.jpx) for both application and applet is provided, 
-but the library dependencies are almost certainly out of date. See the build.xml 
-for current dependencies.
-
-##
-
-Jalview-JS
-
-To enable transpilation of Jalview's code:
-
-1. Locate the 'dropins' directory in your eclipse installation and copy swingjs/net.sf.j2s.core.jar to it.
-  - typically it is at the top of the Eclipse installation, or on OSX under Eclipse.app/Contents/Eclipse
-  
-2. Restart Eclipse
-
-3. If all is well you should see the 'Java2Script' builder is listed as the primary builder for the Jalview project.
-  if not, this is because your properties file needs to have the standard java builder replaced with the following:
-  <name>net.sf.j2s.core.java2scriptbuilder</name>
-
-- otherwise Javascript files will now be generated in the site/swingjs/j2s directory whenever a build occurs
-
-4. Execute the 'unzip-to-site' task (if it isn't automatically run) to update the site directory with the latest versions of SwingJS, varna-js, JSmol and other dependencies required by Jalview.
-
-
-  
-
-
-##
-
-For more help, read the file doc/building.html
-
-
-##################
-
-To run application...
-[ NOTE: when using the -classpath option with the '*' wildcard, the argument must be quoted to avoid shell expansion of the wildcard,
-  ALSO, the wildcard MUST be as DIR/* and not DIR/*.jar etc or it will not be interpreted correctly ]
-
-on Windows use:
-  java -classpath "JALVIEW_HOME/lib/*;JALVIEW_HOME/jalview.jar" jalview.bin.Jalview
-and on MacOS or Linux:
-  java -classpath "JALVIEW_HOME/lib/*:JALVIEW_HOME/jalview.jar" jalview.bin.Jalview
-
-Replace JALVIEW_HOME with the full path to Jalview Installation Directory. If building from source:
-
-  java -classpath "JALVIEW_BUILD/dist/*" jalview.bin.Jalview
-
-
-##################
-
-
-If you use a proxy server add 
-
--Dhttp.proxyServer=YOUR.SERVER -Dhttp.proxyPort=YOURPORT
-
-If the proxy server requires authentication, add
-
--Dhttp.proxyUser=USERNAME -Dhttp.proxyPassword=PASSWORD
+JalviewJS.
+See  README_GRADLE_JALVIEWJS-2019-10-22.md  for build instructions for JalviewJS.
+This is a little sparse but enough to do the transpilation.
index e2a5605..ee7be0f 100644 (file)
@@ -204,7 +204,7 @@ ext {
   install4jDMGDSStore = "${install4j_images_dir}/${install4j_dmg_ds_store}"
   install4jDMGBackgroundImage = "${install4j_images_dir}/${install4j_dmg_background}"
   install4jInstallerName = "${jalview_name} Non-Release Installer"
-  install4jExecutableName = jalview_name.replaceAll("[^\\w]+", "_").toLowerCase()
+  install4jExecutableName = install4j_executable_name
   install4jExtraScheme = "jalviewx"
   install4jMacIconsFile = string("${install4j_images_dir}/${install4j_mac_icons_file}")
   install4jWindowsIconsFile = string("${install4j_images_dir}/${install4j_windows_icons_file}")
@@ -370,6 +370,7 @@ ext {
                                     .replaceAll("_*-_*", "-") // collapse _-_
                                     .toLowerCase()
 
+  getdownWrapperLink = install4jUnixApplicationFolder // e.g. "jalview_local"
   getdownAppDir = string("${getdownWebsiteDir}/${getdownAppDistDir}")
   //getdownJ11libDir = "${getdownWebsiteDir}/${getdown_j11lib_dir}"
   getdownResourceDir = string("${getdownWebsiteDir}/${getdown_resource_dir}")
@@ -1368,13 +1369,12 @@ task linkCheck(type: JavaExec) {
   args = [ "${helpBuildDir}/${help_dir}", "-nointernet" ]
 
   def outFOS = new FileOutputStream(helpLinksCheckerOutFile, false) // false == don't append
-  def errFOS = outFOS
   standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
     outFOS,
-    standardOutput)
+    System.out)
   errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
     outFOS,
-    errorOutput)
+    System.err)
 
   inputs.dir(helpBuildDir)
   outputs.file(helpLinksCheckerOutFile)
@@ -1572,6 +1572,18 @@ task getdownWebsite() {
         into getdownResourceDir
       }
     }
+    
+    def getdownWrapperScripts = [ getdown_bash_wrapper_script, getdown_powershell_wrapper_script, getdown_batch_wrapper_script ]
+    getdownWrapperScripts.each{ script ->
+      def s = file( "${jalviewDir}/utils/getdown/${getdown_wrapper_script_dir}/${script}" )
+      if (s.exists()) {
+        copy {
+          from s
+          into "${getdownWebsiteDir}/${getdown_wrapper_script_dir}"
+        }
+        getdownTextString += "resource = ${getdown_wrapper_script_dir}/${script}\n"
+      }
+    }
 
     def codeFiles = []
     fileTree(file(package_dir)).each{ f ->
@@ -1900,6 +1912,10 @@ task installers(type: com.install4j.gradle.Install4jTask) {
     'WINDOWS_APPLICATION_ID': install4jWinApplicationId,
     'MACOS_DMG_DS_STORE': install4jDMGDSStore,
     'MACOS_DMG_BG_IMAGE': install4jDMGBackgroundImage,
+    'WRAPPER_LINK': getdownWrapperLink,
+    'BASH_WRAPPER_SCRIPT': getdown_bash_wrapper_script,
+    'POWERSHELL_WRAPPER_SCRIPT': getdown_powershell_wrapper_script,
+    'WRAPPER_SCRIPT_BIN_DIR': getdown_wrapper_script_dir,
     'INSTALLER_NAME': install4jInstallerName,
     'INSTALL4J_UTILS_DIR': install4j_utils_dir,
     'GETDOWN_WEBSITE_DIR': getdown_website_dir,
@@ -1949,7 +1965,7 @@ task installers(type: com.install4j.gradle.Install4jTask) {
     println("Using projectFile "+projectFile)
     if (!disableNotarization) { println("Will notarize OSX App DMG") }
   }
-  verbose=true
+  //verbose=true
 
   inputs.dir(getdownWebsiteDir)
   inputs.file(install4jConfFile)
@@ -2531,12 +2547,12 @@ DEBUG: ${eclipseDebug}
         new org.apache.tools.ant.util.TeeOutputStream(
           logOutFOS,
           stdout),
-        standardOutput)
+        System.out)
       errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
         new org.apache.tools.ant.util.TeeOutputStream(
           logErrFOS,
           stderr),
-        errorOutput)
+        System.err)
     } else {
       standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
         logOutFOS,
@@ -2931,7 +2947,13 @@ task jalviewjsServer {
   def htmlFile = "${jalviewDirAbsolutePath}/${filename}"
   doLast {
 
-    SimpleHttpFileServerFactory factory = new SimpleHttpFileServerFactory()
+    def factory
+    try {
+      def f = Class.forName("org.gradle.plugins.javascript.envjs.http.simple.SimpleHttpFileServerFactory")
+      factory = f.newInstance()
+    } catch (ClassNotFoundException e) {
+      throw new GradleException("Unable to create SimpleHttpFileServerFactory")
+    }
     def port = Integer.valueOf(jalviewjs_server_port)
     def start = port
     def running = false
diff --git a/doc/old_README b/doc/old_README
new file mode 100755 (executable)
index 0000000..e8c44a9
--- /dev/null
@@ -0,0 +1,72 @@
+Jalview Readme
+--------------
+
+
+The source is available as a tar file and comes complete with the GNU General Public License. 
+
+To build the application you will need a J2SDK 1.7+. 
+
+An Ant build file (build.xml) is provided, you will need to install Apache Ant first. 
+Run ant to view usage which will display a list of useful build targets.
+
+Jalview is primarily developed with eclipse, and a .project file is provided to simplify importing the source into your own eclipse workspace. A NetBeans nbbuild.xml file is also provided for developing Jalview with NetBeans - but this is not officially supported.
+
+You may also be able to use Borland JBuilder to build Jalview. A JBuilder project file 
+(JalviewX.jpx, JalviewApplet.jpx) for both application and applet is provided, 
+but the library dependencies are almost certainly out of date. See the build.xml 
+for current dependencies.
+
+##
+
+Jalview-JS
+
+To enable transpilation of Jalview's code:
+
+1. Locate the 'dropins' directory in your eclipse installation and copy swingjs/net.sf.j2s.core.jar to it.
+  - typically it is at the top of the Eclipse installation, or on OSX under Eclipse.app/Contents/Eclipse
+  
+2. Restart Eclipse
+
+3. If all is well you should see the 'Java2Script' builder is listed as the primary builder for the Jalview project.
+  if not, this is because your properties file needs to have the standard java builder replaced with the following:
+  <name>net.sf.j2s.core.java2scriptbuilder</name>
+
+- otherwise Javascript files will now be generated in the site/swingjs/j2s directory whenever a build occurs
+
+4. Execute the 'unzip-to-site' task (if it isn't automatically run) to update the site directory with the latest versions of SwingJS, varna-js, JSmol and other dependencies required by Jalview.
+
+
+  
+
+
+##
+
+For more help, read the file doc/building.html
+
+
+##################
+
+To run application...
+[ NOTE: when using the -classpath option with the '*' wildcard, the argument must be quoted to avoid shell expansion of the wildcard,
+  ALSO, the wildcard MUST be as DIR/* and not DIR/*.jar etc or it will not be interpreted correctly ]
+
+on Windows use:
+  java -classpath "JALVIEW_HOME/lib/*;JALVIEW_HOME/jalview.jar" jalview.bin.Jalview
+and on MacOS or Linux:
+  java -classpath "JALVIEW_HOME/lib/*:JALVIEW_HOME/jalview.jar" jalview.bin.Jalview
+
+Replace JALVIEW_HOME with the full path to Jalview Installation Directory. If building from source:
+
+  java -classpath "JALVIEW_BUILD/dist/*" jalview.bin.Jalview
+
+
+##################
+
+
+If you use a proxy server add 
+
+-Dhttp.proxyServer=YOUR.SERVER -Dhttp.proxyPort=YOURPORT
+
+If the proxy server requires authentication, add
+
+-Dhttp.proxyUser=USERNAME -Dhttp.proxyPassword=PASSWORD
index 3088b48..d0fb57d 100644 (file)
@@ -123,6 +123,7 @@ install4j_info_plist_file_associations = file_associations_auto-Info_plist.xml
 install4j_installer_file_associations = file_associations_auto-install4j8.xml
 #install4j_DMG_uninstaller_app_files = uninstall_old_jalview_files.xml
 install4j_build_dir = build/install4j
+install4j_executable_name = jalviewg
 install4j_media_types = windows,macosArchive,unixArchive,unixInstaller
 install4j_faster = false
 install4j_application_categories = Science;Biology;Java;
@@ -133,6 +134,10 @@ install4j_png_icon_file = jalview_logo.png
 install4j_background = jalview_logo_background_fade-640x480.png
 install4j_dmg_background = jalview_dmg_background-NON-RELEASE.png
 install4j_dmg_ds_store = jalview_dmg_DS_Store
+getdown_wrapper_script_dir = bin
+getdown_bash_wrapper_script = jalview.sh
+getdown_powershell_wrapper_script = jalview.ps1
+getdown_batch_wrapper_script = jalview.bat
 
 OSX_KEYSTORE =
 OSX_KEYPASS =
index 4a83b35..353f449 100755 (executable)
@@ -376,10 +376,18 @@ public class Cache
   public static void loadProperties(String propsFile)
   {
     propertiesFile = propsFile;
+    String releasePropertiesFile = null;
+    boolean defaultProperties = false;
     if (propsFile == null && !propsAreReadOnly)
     {
+      String channelPrefsFilename = ChannelProperties
+              .getProperty("preferences.filename");
+      String releasePrefsFilename = ".jalview_properties";
       propertiesFile = System.getProperty("user.home") + File.separatorChar
-              + ".jalview_properties";
+              + channelPrefsFilename;
+      releasePropertiesFile = System.getProperty("user.home")
+              + File.separatorChar + releasePrefsFilename;
+      defaultProperties = true;
     }
     else
     {
@@ -399,20 +407,29 @@ public class Cache
         InputStream fis;
         try
         {
+          // props file provided as URL
           fis = new URL(propertiesFile).openStream();
           System.out.println(
                   "Loading jalview properties from : " + propertiesFile);
           System.out.println(
                   "Disabling Jalview writing to user's local properties file.");
           propsAreReadOnly = true;
-
         } catch (Exception ex)
         {
           fis = null;
         }
         if (fis == null)
         {
-          fis = new FileInputStream(propertiesFile);
+          String readPropertiesFile = propertiesFile;
+          // if we're using the usual properties file and the channel properties
+          // file doesn't exist, read .jalview_properties
+          // (but we'll still save to the channel properties file).
+          if (defaultProperties && (!new File(propertiesFile).exists())
+                  && (new File(releasePropertiesFile).exists()))
+          {
+            readPropertiesFile = releasePropertiesFile;
+          }
+          fis = new FileInputStream(readPropertiesFile);
         }
         applicationProperties.clear();
         applicationProperties.load(fis);
index d6d440a..38b7587 100644 (file)
@@ -34,6 +34,8 @@ public class HiDPISetting
 
   public static int scale = 0;
 
+  public final static int MAX_SCALE = 8;
+
   private static boolean doneInit = false;
 
   private static boolean allowScalePropertyArg = false;
@@ -66,8 +68,17 @@ public class HiDPISetting
 
     // get and use command line property values first
     String setHiDPIProperty = System.getProperty(setHiDPIPropertyName);
-    setHiDPI = setHiDPIProperty != null
-            && setHiDPIProperty.equalsIgnoreCase("true");
+    boolean setHiDPIPropertyBool = Boolean.parseBoolean(setHiDPIProperty);
+
+    // allow -DsetHiDPI=false to turn off HiDPI scaling
+    if (setHiDPIProperty != null && !setHiDPIPropertyBool)
+    {
+      clear();
+      doneInit = true;
+      return;
+    }
+
+    setHiDPI = setHiDPIProperty != null && setHiDPIPropertyBool;
 
     String setHiDPIScaleProperty = System
             .getProperty(setHiDPIScalePropertyName);
@@ -76,6 +87,12 @@ public class HiDPISetting
       try
       {
         setHiDPIScale = Integer.parseInt(setHiDPIScaleProperty);
+        // if setHiDPIScale property is validly set and setHiDPI property wasn't
+        // attempted to be set we assume setHiDPIScale to be true
+        if (setHiDPIProperty == null)
+        {
+          setHiDPI = true;
+        }
       } catch (NumberFormatException e)
       {
         System.err.println(setHiDPIScalePropertyName + " property give ("
@@ -149,6 +166,14 @@ public class HiDPISetting
 
     int dimensionScale = 1 + (mindimension / bigScreenThreshold);
 
+    // reject outrageous values -- dpiScale in particular could be mistaken
+    if (dpiScale > MAX_SCALE) {
+      dpiScale = 1;
+    }
+    if (dimensionScale > MAX_SCALE) {
+      dimensionScale = 1;
+    }
+
     // choose larger of dimensionScale or dpiScale (most likely dimensionScale
     // as dpiScale often misreported)
     int autoScale = Math.max(dpiScale, dimensionScale);
index 74eb887..efab97c 100644 (file)
@@ -70,7 +70,7 @@ public class ResidueCount
    */
   private static final String AAS = "ACDEFGHIKLMNPQRSTUVWXY";
 
-  private static final int GAP_COUNT = 0;
+  static final int GAP_COUNT = 0;
 
   /*
    * fast lookup tables holding the index into our count
@@ -212,7 +212,12 @@ public class ResidueCount
         counts[offset] = (short) ++newValue;
       }
     }
-    maxCount = Math.max(maxCount, newValue);
+
+    if (offset!=GAP_COUNT)
+    {
+      // update modal residue count
+      maxCount = Math.max(maxCount, newValue);
+    }
     return newValue;
   }
 
@@ -301,15 +306,7 @@ public class ResidueCount
    */
   public int addGap()
   {
-    int newValue;
-    if (useIntCounts)
-    {
-      newValue = ++intCounts[GAP_COUNT];
-    }
-    else
-    {
-      newValue = ++counts[GAP_COUNT];
-    }
+    int newValue = increment(GAP_COUNT);
     return newValue;
   }
 
diff --git a/src/jalview/fts/service/alphafold/AlphafoldRestClient.java b/src/jalview/fts/service/alphafold/AlphafoldRestClient.java
new file mode 100644 (file)
index 0000000..6b855fc
--- /dev/null
@@ -0,0 +1,157 @@
+package jalview.fts.service.alphafold;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.DBRefSource;
+import jalview.datamodel.SequenceI;
+import jalview.fts.api.FTSData;
+import jalview.fts.api.FTSDataColumnI;
+import jalview.fts.core.FTSRestRequest;
+import jalview.util.DBRefUtils;
+import jalview.util.HttpUtils;
+import jalview.ws.dbsources.EBIAlfaFold;
+
+public class AlphafoldRestClient
+{
+
+  /**
+   * turns a uniprot ID into a fake alphafold entry for the structure chooser -
+   * fakes PDB fields in response
+   * 
+   * @param UniprotID
+   * @return null or an FTS Record (if alphafold thinks it has a structure)
+   */
+  public static List<FTSData> getFTSData(// Map<String, Object> pdbJsonDoc,
+          FTSRestRequest request)
+  {
+    List<FTSData> records = new ArrayList<FTSData>();
+    String primaryKey = null;
+
+    Object[] summaryRowData;
+
+    SequenceI associatedSequence;
+
+    Collection<FTSDataColumnI> diplayFields = request.getWantedFields();
+    SequenceI associatedSeq = request.getAssociatedSequence();
+
+    for (DBRefEntry upref : DBRefUtils
+            .selectRefs(associatedSeq.getPrimaryDBRefs(), new String[]
+            { DBRefSource.UNIPROT }))
+    {
+      String alphaFoldId = "AF-" + upref.getAccessionId() + "-F1";
+      try
+      {
+        String urls = EBIAlfaFold.getAlphaFoldCifDownloadUrl(alphaFoldId);
+        URL url = new URL(urls);
+        if (!HttpUtils.checkUrlAvailable(url, 50))
+        {
+          continue;
+        }
+      } catch (Exception mfe)
+      {
+        jalview.bin.Cache.log.debug("Exception accessing urls", mfe);
+        continue;
+      }
+      int colCounter = 0;
+      summaryRowData = new Object[(associatedSeq != null)
+              ? diplayFields.size() + 1
+              : diplayFields.size()];
+      if (associatedSeq != null)
+      {
+        associatedSequence = associatedSeq;
+        summaryRowData[0] = associatedSequence;
+        colCounter = 1;
+      }
+
+      for (FTSDataColumnI field : diplayFields)
+      {
+        String fieldData = "alphafold";// (pdbJsonDoc.get(field.getCode()) ==
+                                       // null) ? ""
+        // : pdbJsonDoc.get(field.getCode()).toString();
+        if (field.isPrimaryKeyColumn())
+        {
+          primaryKey = alphaFoldId;
+          summaryRowData[colCounter++] = alphaFoldId;
+        }
+        else if (fieldData == null || fieldData.isEmpty())
+        {
+          summaryRowData[colCounter++] = null;
+        }
+        else
+        {
+          try
+          {
+            summaryRowData[colCounter++] = (field.getDataType()
+                    .getDataTypeClass() == Integer.class)
+                            ? 1
+                            : (field.getDataType()
+                                    .getDataTypeClass() == Double.class)
+                                            ? 1.3131313
+                                            : "AlphaFold clarity";
+          } catch (Exception e)
+          {
+            e.printStackTrace();
+            System.out.println("offending value:" + fieldData);
+          }
+        }
+      }
+
+      final String primaryKey1 = primaryKey;
+
+      final Object[] summaryRowData1 = summaryRowData;
+      records.add(new FTSData()
+      {
+        @Override
+        public Object[] getSummaryData()
+        {
+          return summaryRowData1;
+        }
+
+        @Override
+        public Object getPrimaryKey()
+        {
+          return primaryKey1;
+        }
+
+        /**
+         * Returns a string representation of this object;
+         */
+        @Override
+        public String toString()
+        {
+          StringBuilder summaryFieldValues = new StringBuilder();
+          for (Object summaryField : summaryRowData1)
+          {
+            summaryFieldValues.append(
+                    summaryField == null ? " " : summaryField.toString())
+                    .append("\t");
+          }
+          return summaryFieldValues.toString();
+        }
+
+        /**
+         * Returns hash code value for this object
+         */
+        @Override
+        public int hashCode()
+        {
+          return Objects.hash(primaryKey1, this.toString());
+        }
+
+        @Override
+        public boolean equals(Object that)
+        {
+          return this.toString().equals(that.toString());
+        }
+      });
+    }
+    return records;
+  }
+}
index 313f0b6..04fb17c 100644 (file)
@@ -47,6 +47,7 @@ import jalview.fts.core.FTSDataColumnPreferences.PreferenceSource;
 import jalview.fts.core.FTSRestClient;
 import jalview.fts.core.FTSRestRequest;
 import jalview.fts.core.FTSRestResponse;
+import jalview.fts.service.alphafold.AlphafoldRestClient;
 import jalview.util.JSONUtils;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
@@ -321,20 +322,23 @@ public static String parseJsonExceptionString(String jsonErrorResponse)
               .get("QTime").toString();
       int numFound = Integer
               .valueOf(pdbResponse.get("numFound").toString());
+      List<Object> docs = (List<Object>) pdbResponse.get("docs");
+      // add in any alphafold bits at the top
+      result = AlphafoldRestClient.getFTSData(pdbRestRequest);
       if (numFound > 0)
       {
-        result = new ArrayList<>();
-        List<Object> docs = (List<Object>) pdbResponse.get("docs");
+
         for (Iterator<Object> docIter = docs.iterator(); docIter
                 .hasNext();)
         {
           Map<String, Object> doc = (Map<String, Object>) docIter.next();
           result.add(getFTSData(doc, pdbRestRequest));
         }
-        searchResult.setNumberOfItemsFound(numFound);
-        searchResult.setResponseTime(queryTime);
-        searchResult.setSearchSummary(result);
       }
+      searchResult.setNumberOfItemsFound(result.size());
+      searchResult.setResponseTime(queryTime);
+      searchResult.setSearchSummary(result);
+
     } catch (ParseException e)
     {
       e.printStackTrace();
index 7cf10e7..d085117 100644 (file)
@@ -51,6 +51,7 @@ import jalview.util.BrowserLauncher;
 import jalview.util.ImageMaker;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
+import jalview.ws.dbsources.EBIAlfaFold;
 import jalview.ws.dbsources.Pdb;
 
 public class AppJmol extends StructureViewerBase
@@ -437,6 +438,8 @@ public class AppJmol extends StructureViewerBase
       // TODO: replace with reference fetching/transfer code (validate PDBentry
       // as a DBRef?)
       Pdb pdbclient = new Pdb();
+      EBIAlfaFold afclient = new EBIAlfaFold();
+      
       for (int pi = 0; pi < jmb.getPdbCount(); pi++)
       {
         String file = jmb.getPdbEntry(pi).getFile();
@@ -452,7 +455,12 @@ public class AppJmol extends StructureViewerBase
                   { pdbid }), hdl);
           try
           {
-            pdbseq = pdbclient.getSequenceRecords(pdbid);
+            if (afclient.isValidReference(pdbid))
+            {
+              pdbseq = afclient.getSequenceRecords(pdbid);
+            } else {
+              pdbseq = pdbclient.getSequenceRecords(pdbid);
+            }
           } catch (OutOfMemoryError oomerror)
           {
             new OOMWarning("Retrieving PDB id " + pdbid, oomerror);
index aecad00..cfd44c7 100644 (file)
@@ -11,6 +11,7 @@ import jalview.ext.rbvi.chimera.ChimeraXCommands;
 import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
 import jalview.structure.AtomSpec;
+import jalview.structure.StructureCommand;
 import jalview.structure.StructureSelectionManager;
 
 public class JalviewChimeraXBindingModel extends JalviewChimeraBindingModel
@@ -41,8 +42,7 @@ public class JalviewChimeraXBindingModel extends JalviewChimeraBindingModel
     int modelNumber = chimeraMaps.size() + 1;
     String command = "setattr #" + modelNumber + " models name "
             + pe.getId();
-    // FIXME reinstate this for ChimeraX 1.2, see https://plato.cgl.ucsf.edu/trac/ChimeraX/ticket/4211#comment:2
- //   executeCommand(new StructureCommand(command), false);
+   executeCommand(new StructureCommand(command), false);
     modelsToMap.add(new ChimeraModel(pe.getId(), ModelType.PDB_MODEL,
             modelNumber, 0));
   }
index b27208a..a9b9d4d 100755 (executable)
@@ -456,7 +456,7 @@ public class SeqCanvas extends JPanel implements ViewportListenerI
 
       if (av.getWrapAlignment())
       {
-        drawWrappedPanel(gg, width, height, ranges.getStartRes());
+        drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
       }
       else
       {
index 4ffefba..163ae25 100644 (file)
@@ -275,6 +275,9 @@ public class SeqPanel extends JPanel
   /**
    * Computes the column and sequence row (and possibly annotation row when in
    * wrapped mode) for the given mouse position
+   * <p>
+   * Mouse position is not set if in wrapped mode with the cursor either between
+   * sequences, or over the left or right vertical scale.
    * 
    * @param evt
    * @return
@@ -342,6 +345,9 @@ public class SeqPanel extends JPanel
   /**
    * Returns the aligned sequence position (base 0) at the mouse position, or
    * the closest visible one
+   * <p>
+   * Returns -1 if in wrapped mode with the mouse over either left or right
+   * vertical scale.
    * 
    * @param evt
    * @return
index a0b199b..b349559 100644 (file)
@@ -63,6 +63,7 @@ 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;
 
 /**
@@ -1121,6 +1122,7 @@ 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()
@@ -1138,7 +1140,12 @@ public abstract class StructureViewerBase extends GStructureViewer
     // { pdbid }));
     try
     {
-      pdbseq = pdbclient.getSequenceRecords(pdbid);
+      if (afclient.isValidReference(pdbid))
+      {
+        pdbseq = afclient.getSequenceRecords(pdbid);
+      } else {
+        pdbseq = pdbclient.getSequenceRecords(pdbid);
+      }
     } catch (Exception e)
     {
       System.err.println(
index 5af94e4..1a4a44b 100755 (executable)
@@ -1799,6 +1799,14 @@ public class GPreferences extends JPanel
     String choice = null;
     JFileChooser chooser = new JFileChooser();
 
+    // Enable appBundleIsTraversable in macOS FileChooser to allow selecting
+    // hidden executables within .app dirs
+    if (Platform.isMac())
+    {
+      chooser.putClientProperty("JFileChooser.appBundleIsTraversable",
+              true);
+    }
+
     // chooser.setFileView(new JalviewFileView());
     chooser.setDialogTitle(
             MessageManager.getString("label.open_local_file"));
index 53644e9..9fcb336 100644 (file)
@@ -410,7 +410,7 @@ public class StructureSelectionManager
         registerPDBFile(pdb.getId().trim(), pdbFile);
       }
       // if PDBId is unavailable then skip SIFTS mapping execution path
-      isMapUsingSIFTs = isMapUsingSIFTs && pdb.isPPDBIdAvailable();
+      isMapUsingSIFTs = isMapUsingSIFTs && pdb.isPPDBIdAvailable() && !pdb.getId().startsWith("AF-");
 
     } catch (Exception ex)
     {
index ede528f..109eaa5 100644 (file)
@@ -55,6 +55,7 @@ public class ChannelProperties
     defaultProps.put("uod_banner.32", "/default_images/UoD_banner-32.png");
     defaultProps.put("default_appbase",
             "https://www.jalview.org/getdown/release/1.8");
+    defaultProps.put("preferences.filename", ".jalview_properties");
 
     // load channel_properties
     Properties tryChannelProps = new Properties();
@@ -63,12 +64,11 @@ public class ChannelProperties
     if (channelPropsURL == null)
     {
       // complete failure of channel_properties, set all properties to defaults
-      System.err
-              .println("Failed to find '" + CHANNEL_PROPERTIES_FILENAME
-                      + "' file at '"
-                      + (channelPropsURL == null ? "null"
-                              : channelPropsURL.toString())
-                      + "'. Using class defaultProps.");
+      System.err.println("Failed to find '" + CHANNEL_PROPERTIES_FILENAME
+              + "' file at '"
+              + (channelPropsURL == null ? "null"
+                      : channelPropsURL.toString())
+              + "'. Using class defaultProps.");
       tryChannelProps = defaultProps;
     }
     else
index 74f77a2..5473f5a 100644 (file)
@@ -22,7 +22,12 @@ package jalview.util;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.ProtocolException;
 import java.net.URL;
+import java.util.List;
+
+import javax.ws.rs.HttpMethod;
 
 public class HttpUtils
 {
@@ -68,5 +73,32 @@ public class HttpUtils
   {
     return file.startsWith("http://") || file.startsWith("https://");
   }
+  
+
+  /**
+   * wrapper to get/post to a URL or check headers
+   * @param url
+   * @param ids
+   * @param readTimeout
+   * @return
+   * @throws IOException
+   * @throws ProtocolException
+   */
+  public static boolean checkUrlAvailable(URL url,
+          int readTimeout) throws IOException, ProtocolException
+  {
+    // System.out.println(System.currentTimeMillis() + " " + url);
+
+    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+
+    connection.setRequestMethod(HttpMethod.HEAD);
+
+    connection.setDoInput(true);
+
+    connection.setUseCaches(false);
+    connection.setConnectTimeout(300);
+    connection.setReadTimeout(readTimeout);
+    return connection.getResponseCode() == 200;
+  }
 
 }
diff --git a/src/jalview/ws/dbsources/EBIAlfaFold.java b/src/jalview/ws/dbsources/EBIAlfaFold.java
new file mode 100644 (file)
index 0000000..5edcafa
--- /dev/null
@@ -0,0 +1,281 @@
+
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.ws.dbsources;
+
+import jalview.api.FeatureSettingsModelI;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.DBRefSource;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.PDBEntry.Type;
+import jalview.datamodel.SequenceI;
+import jalview.io.DataSourceType;
+import jalview.io.FileFormat;
+import jalview.io.FileFormatI;
+import jalview.io.FormatAdapter;
+import jalview.io.PDBFeatureSettings;
+import jalview.structure.StructureImportSettings;
+import jalview.util.HttpUtils;
+import jalview.util.MessageManager;
+import jalview.ws.ebi.EBIFetchClient;
+import jalview.ws.utils.UrlDownloadClient;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.stevesoft.pat.Regex;
+
+/**
+ * @author JimP
+ * 
+ */
+public class EBIAlfaFold extends EbiFileRetrievedProxy
+{
+  private static final String SEPARATOR = "|";
+
+  private static final String COLON = ":";
+
+  private static final int PDB_ID_LENGTH = 4;
+
+  public EBIAlfaFold()
+  {
+    super();
+  }
+
+  /*
+   * (non-Javadoc)
+   * 
+   * @see jalview.ws.DbSourceProxy#getAccessionSeparator()
+   */
+  @Override
+  public String getAccessionSeparator()
+  {
+    return null;
+  }
+
+  /*
+   * (non-Javadoc)
+   * 
+   * @see jalview.ws.DbSourceProxy#getAccessionValidator()
+   */
+  @Override
+  public Regex getAccessionValidator()
+  {
+    return new Regex("(AF-[A-Z]+[0-9]+[A-Z0-9]+-F1)");
+  }
+
+  /*
+   * (non-Javadoc)
+   * 
+   * @see jalview.ws.DbSourceProxy#getDbSource()
+   */
+  @Override
+  public String getDbSource()
+  {
+    return "ALPHAFOLD";
+  }
+
+  /*
+   * (non-Javadoc)
+   * 
+   * @see jalview.ws.DbSourceProxy#getDbVersion()
+   */
+  @Override
+  public String getDbVersion()
+  {
+    return "1";
+  }
+  public static String getAlphaFoldCifDownloadUrl(String id)
+  {
+    return "https://alphafold.ebi.ac.uk/files/"+id+"-model_v1.cif";
+  }
+  /*
+   * (non-Javadoc)
+   * 
+   * @see jalview.ws.DbSourceProxy#getSequenceRecords(java.lang.String[])
+   */
+  @Override
+  public AlignmentI getSequenceRecords(String queries) throws Exception
+  {
+    AlignmentI pdbAlignment = null;
+    String chain = null;
+    String id = null;
+    if (queries.indexOf(COLON) > -1)
+    {
+      chain = queries.substring(queries.indexOf(COLON) + 1);
+      id = queries.substring(0, queries.indexOf(COLON));
+    }
+    else
+    {
+      id = queries;
+    }
+
+    if (!isValidReference(id))
+    {
+      System.err.println("(AFClient) Ignoring invalid pdb query: '" + id + "'");
+      stopQuery();
+      return null;
+    }
+    String alphaFoldCif = getAlphaFoldCifDownloadUrl(id);
+    
+    try {
+      File tmpFile = File.createTempFile(id,"cif");
+      UrlDownloadClient.download(alphaFoldCif, tmpFile);
+      file = tmpFile.getAbsolutePath();
+      if (file == null)
+      {
+      return null;
+    }
+      // todo get rid of Type and use FileFormatI instead?
+      FileFormatI fileFormat = FileFormat.MMCif;
+      pdbAlignment = new FormatAdapter().readFile(tmpFile, DataSourceType.FILE,
+              fileFormat);
+      if (pdbAlignment != null)
+      {
+        List<SequenceI> toremove = new ArrayList<SequenceI>();
+        for (SequenceI pdbcs : pdbAlignment.getSequences())
+        {
+          String chid = null;
+          // Mapping map=null;
+          for (PDBEntry pid : pdbcs.getAllPDBEntries())
+          {
+            if (pid.getFile() == file)
+            {
+              chid = pid.getChainCode();
+
+            }
+          }
+          if (chain == null || (chid != null && (chid.equals(chain)
+                  || chid.trim().equals(chain.trim())
+                  || (chain.trim().length() == 0 && chid.equals("_")))))
+          {
+            // FIXME seems to result in 'PDB|1QIP|1qip|A' - 1QIP is redundant.
+            // TODO: suggest simplify naming to 1qip|A as default name defined
+            pdbcs.setName(id
+                    + SEPARATOR + pdbcs.getName());
+            // Might need to add more metadata to the PDBEntry object
+            // like below
+            /*
+             * PDBEntry entry = new PDBEntry(); // Construct the PDBEntry
+             * entry.setId(id); if (entry.getProperty() == null)
+             * entry.setProperty(new Hashtable());
+             * entry.getProperty().put("chains", pdbchain.id + "=" +
+             * sq.getStart() + "-" + sq.getEnd());
+             * sq.getDatasetSequence().addPDBId(entry);
+             */
+            // Add PDB DB Refs
+            // We make a DBRefEtntry because we have obtained the PDB file from
+            // a
+            // verifiable source
+            // JBPNote - PDB DBRefEntry should also carry the chain and mapping
+            // information
+            DBRefEntry dbentry = new DBRefEntry(getDbSource(),
+                    getDbVersion(), (chid == null ? id : id + chid));
+            // dbentry.setMap()
+            pdbcs.addDBRef(dbentry);
+          }
+          else
+          {
+            // mark this sequence to be removed from the alignment
+            // - since it's not from the right chain
+            toremove.add(pdbcs);
+          }
+        }
+        // now remove marked sequences
+        for (SequenceI pdbcs : toremove)
+        {
+          pdbAlignment.deleteSequence(pdbcs);
+          if (pdbcs.getAnnotation() != null)
+          {
+            for (AlignmentAnnotation aa : pdbcs.getAnnotation())
+            {
+              pdbAlignment.deleteAnnotation(aa);
+            }
+          }
+        }
+      }
+
+      if (pdbAlignment == null || pdbAlignment.getHeight() < 1)
+      {
+        throw new Exception(MessageManager.formatMessage(
+                "exception.no_pdb_records_for_chain", new String[]
+                { id, ((chain == null) ? "' '" : chain) }));
+      }
+
+    } catch (Exception ex) // Problem parsing PDB file
+    {
+      stopQuery();
+      throw (ex);
+    }
+    return pdbAlignment;
+  }
+
+  /*
+   * (non-Javadoc)
+   * 
+   * @see jalview.ws.DbSourceProxy#isValidReference(java.lang.String)
+   */
+  @Override
+  public boolean isValidReference(String accession)
+  {
+    Regex r = getAccessionValidator();
+    return r.search(accession.trim());
+  }
+
+  /**
+   * human glyoxalase
+   */
+  @Override
+  public String getTestQuery()
+  {
+    return "1QIP";
+  }
+
+  @Override
+  public String getDbName()
+  {
+    return "PDB"; // getDbSource();
+  }
+
+  @Override
+  public int getTier()
+  {
+    return 0;
+  }
+
+  /**
+   * Returns a descriptor for suitable feature display settings with
+   * <ul>
+   * <li>ResNums or insertions features visible</li>
+   * <li>insertions features coloured red</li>
+   * <li>ResNum features coloured by label</li>
+   * <li>Insertions displayed above (on top of) ResNums</li>
+   * </ul>
+   */
+  @Override
+  public FeatureSettingsModelI getFeatureColourScheme()
+  {
+    return new PDBFeatureSettings();
+  }
+}
index 4eb6dbf..e90e1a9 100644 (file)
@@ -92,6 +92,18 @@ public class ResidueCountTest
     assertEquals(rc.getCount('.'), 4);
     assertFalse(rc.isUsingOtherData());
     assertFalse(rc.isCountingInts());
+    
+    rc.set(ResidueCount.GAP_COUNT, Short.MAX_VALUE-2);
+    assertEquals(rc.getGapCount(), Short.MAX_VALUE-2);
+    assertFalse(rc.isCountingInts());
+    rc.addGap();
+    assertEquals(rc.getGapCount(), Short.MAX_VALUE-1);
+    assertFalse(rc.isCountingInts());
+    rc.addGap();
+    assertEquals(rc.getGapCount(), Short.MAX_VALUE);
+    rc.addGap();
+    assertTrue(rc.isCountingInts());
+    assertEquals(rc.getGapCount(), Short.MAX_VALUE+1);
   }
 
   @Test(groups = "Functional")
index a03819d..b0fc116 100644 (file)
@@ -24,6 +24,17 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertTrue;
 
+import java.awt.EventQueue;
+import java.awt.FontMetrics;
+import java.awt.event.MouseEvent;
+import java.lang.reflect.InvocationTargetException;
+
+import javax.swing.JLabel;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
 import jalview.api.AlignViewportI;
 import jalview.bin.Cache;
 import jalview.bin.Jalview;
@@ -33,6 +44,8 @@ import jalview.commands.EditCommand.Edit;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SearchResults;
+import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
 import jalview.gui.SeqPanel.MousePos;
@@ -40,17 +53,6 @@ import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
 import jalview.util.MessageManager;
 import jalview.viewmodel.ViewportRanges;
-
-import java.awt.EventQueue;
-import java.awt.event.MouseEvent;
-import java.lang.reflect.InvocationTargetException;
-
-import javax.swing.JLabel;
-
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.Test;
-
 import junit.extensions.PA;
 
 public class SeqPanelTest
@@ -635,6 +637,7 @@ public class SeqPanelTest
   {
     Cache.applicationProperties.setProperty("SHOW_ANNOTATIONS", "false");
     Cache.applicationProperties.setProperty("WRAP_ALIGNMENT", "true");
+    Cache.applicationProperties.setProperty("FONT_SIZE", "10");
     AlignFrame alignFrame = new FileLoader().LoadFileWaitTillLoaded(
             "examples/uniref50.fa", DataSourceType.FILE);
     AlignViewportI av = alignFrame.getViewport();
@@ -902,4 +905,157 @@ public class SeqPanelTest
       e.printStackTrace();
     }
   }
+  @Test(groups = "Functional")
+  public void testFindMousePosition_wrapped_scales_longSequence()
+  {
+    Cache.applicationProperties.setProperty("SHOW_ANNOTATIONS", "false");
+    Cache.applicationProperties.setProperty("WRAP_ALIGNMENT", "true");
+    Cache.applicationProperties.setProperty("FONT_SIZE", "14");
+    Cache.applicationProperties.setProperty("FONT_NAME", "SansSerif");
+    Cache.applicationProperties.setProperty("FONT_STYLE", "0");
+    // sequence of 50 bases, doubled 10 times, = 51200 bases
+    String dna = "ATGGCCATTGGGCCCAAATTTCCCAAAGGGTTTCCCTGAGGTCAGTCAGA";
+    for (int i = 0 ; i < 10 ; i++)
+    {
+      dna += dna;
+    }
+    assertEquals(dna.length(), 51200);
+    AlignFrame alignFrame = new FileLoader()
+            .LoadFileWaitTillLoaded(dna, DataSourceType.PASTE);
+    SeqPanel testee = alignFrame.alignPanel.getSeqPanel();
+    AlignViewport av = alignFrame.getViewport();
+    av.setScaleAboveWrapped(true);
+    av.setScaleLeftWrapped(true);
+    av.setScaleRightWrapped(true);
+    alignFrame.alignPanel.updateLayout();
+
+    try
+    {
+      Thread.sleep(200);
+    } catch (InterruptedException e)
+    {
+    }
+  
+    final int charHeight = av.getCharHeight();
+    final int charWidth = av.getCharWidth();
+    assertEquals(charHeight, 17);
+    assertEquals(charWidth, 12);
+    
+    FontMetrics fm = testee.getFontMetrics(av.getFont());
+    int labelWidth = fm.stringWidth("00000") + charWidth;
+    assertEquals(labelWidth, 57); // 5 x 9 + charWidth
+    assertEquals(testee.seqCanvas.getLabelWidthWest(), labelWidth);
+
+    int x = 0;
+    int y = 0;
+  
+    /*
+     * mouse at top left of wrapped panel; there is a gap of 2 * charHeight
+     * above the alignment
+     */
+    MouseEvent evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y,
+            0, 0, 0, false, 0);
+    MousePos pos = testee.findMousePosition(evt);
+    assertEquals(pos.column, -1); // over scale left, not an alignment column
+    assertEquals(pos.seqIndex, -1); // above sequences
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor over scale above first sequence
+     */
+    y += charHeight;
+    x = labelWidth;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, -1);
+    assertEquals(pos.column, 0);
+    assertEquals(pos.annotationIndex, -1);
+    
+    /*
+     * cursor over scale left of first sequence
+     */
+    y += charHeight;
+    x = 0;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.column, -1);
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * cursor over start of first sequence
+     */
+    x = labelWidth;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.column, 0);
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * move one character right, to bottom pixel of same row
+     */
+    x += charWidth;
+    y += charHeight - 1;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.column, 1);
+    
+    /*
+     * move down one pixel - now in the no man's land between rows
+     */
+    y += 1;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, -1);
+    assertEquals(pos.column, 1);
+    
+    /*
+     * move down two char heights less one pixel - still in the no man's land
+     * (scale above + spacer line)
+     */
+    y += (2 * charHeight - 1);
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, -1);
+    assertEquals(pos.column, 1);
+    
+    /*
+     * move down one more pixel - now on the next row of the sequence
+     */
+    y += 1;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.column, 1 + av.getWrappedWidth());
+    
+    /*
+     * scroll to near the end of the sequence
+     */
+    SearchResultsI sr = new SearchResults();
+    int scrollTo = dna.length() - 1000;
+    sr.addResult(av.getAlignment().getSequenceAt(0), scrollTo, scrollTo); 
+    alignFrame.alignPanel.scrollToPosition(sr);
+    
+    /*
+     * place the mouse on the first column of the 6th sequence, and
+     * verify that (computed) findMousePosition matches (actual) ViewportRanges
+     */
+    x = labelWidth;
+    y = 17 * charHeight; // 17 = 6 times two header rows and 5 sequence rows
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    int expected = av.getRanges().getStartRes() + 5 * av.getWrappedWidth();
+    assertEquals(pos.column, expected);
+  }
 }
index 4e6d27a..3e7e153 100644 (file)
@@ -16,3 +16,4 @@ uod_banner.28=/images/UoD_banner-28.png
 uod_banner.30=/images/UoD_banner-30.png
 uod_banner.32=/images/UoD_banner-32.png
 default_appbase=https://www.jalview.org/getdown/release/1.8
+preferences.filename=.jalview_properties
index 3407b22..6d5c6f7 100644 (file)
Binary files a/utils/channels/develop/images/jalview_develop_getdown_background.png and b/utils/channels/develop/images/jalview_develop_getdown_background.png differ
index 59c1abd..1fb439a 100644 (file)
Binary files a/utils/channels/develop/images/jalview_develop_getdown_background.xcf and b/utils/channels/develop/images/jalview_develop_getdown_background.xcf differ
index 9288e2d..0820e5f 100644 (file)
Binary files a/utils/channels/develop/images/jalview_develop_getdown_background_error.png and b/utils/channels/develop/images/jalview_develop_getdown_background_error.png differ
index 53cf561..ba09b29 100644 (file)
Binary files a/utils/channels/develop/images/jalview_develop_getdown_background_initialising.png and b/utils/channels/develop/images/jalview_develop_getdown_background_initialising.png differ
index fa848de..8af68fa 100644 (file)
@@ -16,3 +16,4 @@ uod_banner.28=/images/UoD_banner-28.png
 uod_banner.30=/images/UoD_banner-30.png
 uod_banner.32=/images/UoD_banner-32.png
 default_appbase=https://www.jalview.org/getdown/develop/11
+preferences.filename=.jalview_develop_properties
index 02e117d..8195dc8 100644 (file)
@@ -16,3 +16,4 @@ uod_banner.28=/images/UoD_banner-28.png
 uod_banner.30=/images/UoD_banner-30.png
 uod_banner.32=/images/UoD_banner-32.png
 default_appbase=https://www.jalview.org/getdown/release/1.8
+preferences.filename=.jalview_properties
index d19b281..abdfd3f 100644 (file)
@@ -16,3 +16,4 @@ uod_banner.28=/images/UoD_banner-28.png
 uod_banner.30=/images/UoD_banner-30.png
 uod_banner.32=/images/UoD_banner-32.png
 default_appbase=https://www.jalview.org/getdown/release/1.8
+preferences.filename=.jalview_properties
index 3ef86d3..0ab4f86 100644 (file)
@@ -16,3 +16,4 @@ uod_banner.28=/images/UoD_banner-28.png
 uod_banner.30=/images/UoD_banner-30.png
 uod_banner.32=/images/UoD_banner-32.png
 default_appbase=https://www.jalview.org/getdown/release/1.8
+preferences.filename=.jalview_properties
diff --git a/utils/getdown/bin/jalview.bat b/utils/getdown/bin/jalview.bat
new file mode 100755 (executable)
index 0000000..0eca4e4
--- /dev/null
@@ -0,0 +1,50 @@
+@ECHO OFF
+
+REM This is the Jalview batch script wrapper to run the powershell script of the same name.
+REM There is nothing specific to Jalview.
+
+REM ******************************************************************************
+REM If you need to set a full path to the PowerShell executable please do so here:
+SET PWSHPATH=
+REM ******************************************************************************
+
+REM This is some DOS magic to substitute the extension in the full path of this batch script with .ps1
+SET SCRIPTPATH=%~dpn0.ps1
+
+REM PowerShell script isn't where it should be!
+IF NOT EXIST %SCRIPTPATH% (
+  ECHO Could not find PowerShell script %SCRIPTPATH%.  Is %~nx0 in the right folder?
+  EXIT /B 1
+)
+
+REM Look for either pwsh.exe or powershell.exe if not set in PWSHPATH above.
+REM pwsh.exe is preferred as it is likely to be a newer version.
+SET PWSH=
+IF DEFINED PWSHPATH (
+  SET PWSH=%PWSHPATH%
+)
+FOR %%X IN (pwsh.exe powershell.exe) DO (
+  IF NOT DEFINED PWSH ( 
+    IF NOT "%%~$PATH:X" == "" (
+      REM Found a PowerShell executable in the PATH
+      SET PWSH=%%X
+      GOTO end_looking
+    )
+  )
+)
+:end_looking
+
+IF NOT DEFINED PWSH (
+  REM No PowerShell executable found -- tell the user what to do.
+  ECHO No PowerShell found in %%PATH%%. If PowerShell is installed either
+  ECHO 1. add it to your PATH, or
+  ECHO 2. edit the PWSHPATH value at the top of this file:
+  ECHO    "%~dpnx0"
+  ECHO.
+  ECHO %~n0 on the command line requires PowerShell. To install PowerShell see
+  ECHO https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell
+  EXIT /B 2
+)
+
+REM Run the PowerShell script
+%PWSH% -NoProfile -ExecutionPolicy Bypass -Command "& '%SCRIPTPATH%' %*";
diff --git a/utils/getdown/bin/jalview.ps1 b/utils/getdown/bin/jalview.ps1
new file mode 100755 (executable)
index 0000000..7ae9a57
--- /dev/null
@@ -0,0 +1,87 @@
+#!/usr/bin/env pwsh
+
+# save args and first parameter
+$myArgs = $args.Clone()
+$myArg1 = $args[0]
+
+# setup for powershell version < 6.0
+[bool] $myIsWindows = 0
+[bool] $myIsMacOS = 0
+if ( $IsWindows -eq $null ) {
+  # for powershell version < 6.0 let's assume Windows
+  $myIsWindows = 1
+  $myIsMacOS = 0
+} else {
+  $myIsWindows = $IsWindows
+  $myIsMacOS = $IsMacOS
+}
+
+# parent dir of this actual script (which should be the getdown appdir/bin). Follow all symlinks.  Like GNU readlink -f
+function Readlink-f {
+  Param($Link)
+  $Return = $null
+  $c = 0
+  $max = 100 # just in case we end up in a loop
+  [bool] $found = 0
+  $file = Get-Item -Path $Link
+  $prevfile = $null
+  While ( $c -lt $max -and "${file}" -ne "${prevfile}" -and -not $found ) {
+    $prevfile = $file
+    [string] $target = ( $file ).Target
+    If ( $target -eq $null -or ( $file ).LinkType -ne "SymbolicLink" ) {
+      $Return = $file
+      $found = 1
+    } Else {
+      If ( $( Split-Path -Path $target -IsAbsolute ) ) {
+        $file = Get-Item -Path $target
+      } Else {
+# symbolic link is relative: combine previous link parent dir with the link target and resolve
+        $file = Get-Item -Path ( Join-Path -Path ( Split-Path -Path $prevfile -Parent ) -ChildPath $target -Resolve )
+      }
+    }
+    $c++
+  }
+  if ( -not $found ) {
+    throw "Could not determine path to actual file $( Split-Path -Path $Link -Leaf )"
+  }
+  $Return
+}
+
+# Avert problem with unix version of powershell and tell user the reason (Windows must always have .ps1 extension)
+if ( $MyInvocation.MyCommand.Path -eq $null ) {
+  throw "Script or link to script must have extension .ps1"
+}
+
+
+$CMDPATH = ( Get-Item $MyInvocation.MyCommand.Path )
+$SCRIPTPATH = Readlink-f -Link $CMDPATH
+$DIR = Split-Path -Path $SCRIPTPATH -Parent
+
+# set the "-open" parameter if myArg1 is non-zero-length, and not "open" or starts with a "-"
+$OPEN = ""
+if ( $myArg1.length -gt 0 -and ( -not $myArg1.StartsWith("-") ) -and $myArg1 -cne "open" ) {
+  $OPEN = "-open"
+}
+
+$APPDIR = If ( ( Split-Path -Path $DIR -Leaf ) -eq "bin" ) { Split-Path -Path $DIR -Parent } Else { $DIR }
+$JAVAEXE = If ( $myIsWindows ) { "java.exe" } Else { "java" }
+$JAVA = Join-Path -Path $APPDIR -ChildPath ( "jre/" + $( If ( $myIsMacOS ) { "Contents/Home/" } Else { "" } ) + "bin/${JAVAEXE}" )
+$GETDOWNTXT = Join-Path -Path $APPDIR -ChildPath "getdown.txt"
+
+# look for getdown.txt -- needed to create classpath
+if ( -not ( Test-Path -Path "${GETDOWNTXT}" ) ) {
+  throw "Cannot find ${GETDOWNTXT}"
+}
+
+# look for bundled JRE. Might not be there if unix installer used in which case just invoke "java"
+if ( -not ( Test-Path -Path "${JAVA}" ) ) {
+  Write-Host "Cannot find bundled ${JAVAEXE}. Using system ${JAVAEXE} and hoping for the best!"
+  $JAVA = $JAVAEXE
+}
+
+$CLASSPATH = ( Select-String -Path "${GETDOWNTXT}" -AllMatches -Pattern "code\s*=\s*(.*)$" | foreach { Join-Path -Path $APPDIR -ChildPath $($_.Matches.Groups[1].Value ) } ) -join $( If ( $myIsWindows ) { ";" } Else { ":" } )
+
+# quote the args and the command (in case of spaces) with escape chars (`) and precede with & to indicate command not string
+$myArgsString = '"' + $($myArgs -join '" "') + '"'
+Invoke-Expression -Command "& `"${JAVA}`" -cp `"${CLASSPATH}`" jalview.bin.Launcher ${OPEN} ${myArgsString}"
+
diff --git a/utils/getdown/bin/jalview.sh b/utils/getdown/bin/jalview.sh
new file mode 100755 (executable)
index 0000000..112c3e6
--- /dev/null
@@ -0,0 +1,132 @@
+#!/usr/bin/env bash
+
+declare -a ARGS=("${@}")
+ARG1=$1
+
+# this whole next part is because there's no readlink -f in Darwin
+function readlinkf() {
+  FINDFILE="$1"
+  FILE="${FINDFILE}"
+  PREVFILE=""
+  C=0
+  MAX=100 # just in case we end up in a loop
+  FOUND=0
+  while [ "${C}" -lt "${MAX}" -a "${FILE}" != "${PREVFILE}" -a "${FOUND}" -ne 1 ]; do
+    PREVFILE="${FILE}"
+    FILE="$(readlink "${FILE}")"
+    if [ -z "${FILE}" ]; then
+      # the readlink is empty means we've arrived at the script, let's canonicalize with pwd
+      FILE="$(cd "$(dirname "${PREVFILE}")" &> /dev/null && pwd -P)"/"$(basename "${PREVFILE}")"
+      FOUND=1
+    elif [ "${FILE#/}" = "${FILE}" ]; then
+      # FILE is not an absolute path link, we need to add the relative path to the previous dir
+      FILE="$(dirname "${PREVFILE}")/${FILE}"
+    fi
+    C=$((C+1))
+  done
+  if [ "${FOUND}" -ne 1 ]; then
+    echo "Could not determine path to actual file '$(basename "${FINDFILE}")'" >&2
+    exit 1
+  fi
+  echo "${FILE}"
+}
+
+ISMACOS=0
+if [ "$( uname -s )" = "Darwin" ]; then
+  ISMACOS=1
+fi
+
+declare -a JVMARGS=()
+
+# set vars for being inside the macos App Bundle
+if [ "${ISMACOS}" = 1 ]; then
+# MACOS ONLY
+  DIR="$(dirname "$(readlinkf "$0")")"
+  APP="${DIR%.app/Contents/*}".app
+  if [ "${APP}" = "${APP%.app}" ]; then
+    echo "Could not find Jalview.app" >&2
+    exit 2
+  fi
+  APPDIR="${APP}/Contents/Resources/app"
+  JAVA="${APPDIR}/jre/Contents/Home/bin/java"
+  JVMARGS=( "${JVMARGS[@]}" "-Xdock:icon=${APPDIR}/resource/jalview_logo.png" )
+else
+# NOT MACOS
+  DIR="$(dirname "$(readlink -f "$0")")"
+  APPDIR="${DIR%/bin}"
+  JAVA="${APPDIR}/jre/bin/java"
+fi
+
+SYSJAVA=java
+GETDOWNTXT="${APPDIR}/getdown.txt"
+
+CLASSPATH=""
+# save an array of JAR paths in case we're in WSL (see later)
+declare -a JARPATHS=()
+if [ -e "${GETDOWNTXT}" ]; then
+  # always check grep and sed regexes on macos -- they're not the same
+  for JAR in $(grep -e '^code\s*=\s*' "${GETDOWNTXT}" | sed -e 's/^code\s*=\s*//;'); do
+    [ -n "${CLASSPATH}" ] && CLASSPATH="${CLASSPATH}:"
+    CLASSPATH="${CLASSPATH}${APPDIR}/${JAR}"
+    JARPATHS=( "${JARPATHS[@]}" "${APPDIR}/${JAR}" )
+  done
+else
+  echo "Cannot find getdown.txt" >&2
+  exit 3
+fi
+
+# WINDOWS ONLY (Cygwin or WSL)
+# change paths for Cygwin or Windows Subsystem for Linux (WSL)
+if [ "${ISMACOS}" != 1 ]; then # macos doesn't like uname -o, best to avoid
+  if [ "$(uname -o)" = "Cygwin" ]; then
+  # CYGWIN
+    echo "When using relative paths in args within Cygwin, please start with './' or '../'" >&2
+    CLASSPATH=$(cygpath -pw "${CLASSPATH}")
+    # now for some arg paths fun. only translating paths starting with './', '../', '/' or '~'
+    ARGS=()
+    for ARG in "${@}"; do
+      if [ "${ARG}" != "${ARG#@(/|./|../|~)}" ]; then
+        ARGS=( "${ARGS[@]}" "$(cygpath -aw "${ARG}")" )
+      else
+        ARGS=( "${ARGS[@]}" "${ARG}" )
+      fi
+    done
+  elif uname -r | grep Microsoft >/dev/null; then
+  # WSL
+    echo "When using relative paths in args within WSL, please start with './' or '../'" >&2
+    CLASSPATH=""
+    for JARPATH in "${JARPATHS[@]}"; do
+      [ -n "${CLASSPATH}" ] && CLASSPATH="${CLASSPATH};"
+      CLASSPATH="${CLASSPATH}$(wslpath -aw "${JARPATH}")"
+    done
+    ARGS=()
+    for ARG in "${@}"; do
+      if [ "${ARG}" != "${ARG#@(/|./|../|~)}" ]; then
+        # annoyingly wslpath does not work if the file doesn't exist!
+        ARGBASENAME="$(basename "${ARG}")"
+        ARGDIRNAME="$(dirname "${ARG}")"
+        ARGS=( "${ARGS[@]}" "$(wslpath -aw "${ARGDIRNAME}")\\${ARGBASENAME}" )
+      else
+        ARGS=( "${ARGS[@]}" "${ARG}" )
+      fi
+    done
+    JAVA="${JAVA}.exe"
+    SYSJAVA="java.exe"
+  fi
+fi
+
+# Is there a bundled Java?  If not just try one in the PATH (do need .exe in WSL)
+if [ \! -e "${JAVA}" ]; then
+  JAVA=$SYSJAVA
+  echo "Cannot find bundled java, using system ${JAVA} and hoping for the best!" >&2
+fi
+
+# check to see if $1 is set and is not start of other cli args
+OPEN=""
+if [ -n "${ARG1}" -a "${ARG1}" = "${ARG1#-}" -a "${ARG1}" != "open" ]; then
+ # first argument exists and does not start with a "-" and is not "open"
+ OPEN="-open"
+fi
+
+# don't quote $OPEN (don't want it accidentally mistaken as an empty string arg!)
+"${JAVA}" "${JVMARGS[@]}" -cp "${CLASSPATH}" jalview.bin.Launcher ${OPEN} "${ARGS[@]}"
index eb52607..a14900c 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<install4j version="8.0.10" transformSequenceNumber="8">
-  <directoryPresets config="../.." />
+<install4j version="8.0.11" transformSequenceNumber="8">
+  <directoryPresets config="bin/jalview" />
   <application name="${compiler:JALVIEW_APPLICATION_NAME}" applicationId="${compiler:WINDOWS_APPLICATION_ID}" mediaDir="${compiler:BUILD_DIR}" lzmaCompression="true" shortName="${compiler:INTERNAL_ID}" publisher="University of Dundee" publisherWeb="https://www.jalview.org/" version="${compiler:JALVIEW_VERSION}" allPathsRelative="true" macVolumeId="5aac4968c304f65" javaMinVersion="${compiler:JAVA_MIN_VERSION}" javaMaxVersion="9999999999${compiler:JAVA_MAX_VERSION}" allowBetaVM="true" jdkMode="jdk" jdkName="JDK 11.0">
     <searchSequence>
       <directory location="${compiler:JRE_DIR}" />
       <variable name="INTERNAL_ID" value="Jalview" />
       <variable name="WINDOWS_APPLICATION_ID" value="6595-2347-1923-0725" />
       <variable name="MACOS_DMG_DS_STORE" value="jalview_dmg_DS_Store" />
-      <variable name="MACOS_DMG_BG_IMAGE" />
+      <variable name="MACOS_DMG_BG_IMAGE" value="jalview_dmg_background-72dpi.png" />
+      <variable name="WRAPPER_LINK" value="jalview" />
+      <variable name="BASH_WRAPPER_SCRIPT" value="jalview.sh" />
+      <variable name="WRAPPER_SCRIPT_BIN_DIR" value="bin" />
       <variable name="INSTALLER_NAME" value="Jalview Installer" />
       <variable name="INSTALL4J_UTILS_DIR" value="utils/install4j" />
       <variable name="GETDOWN_WEBSITE_DIR" value="getdown/website" />
         <entry>jspawnhelper</entry>
         <entry>libfreetype.dylib.6</entry>
         <entry>applet</entry>
+        <entry>jaotc</entry>
+        <entry>jfr</entry>
+        <entry>jrunscript</entry>
+        <entry>libjli.dylib</entry>
       </macAdditionalBinaries>
     </codeSigning>
   </application>
     </mountPoints>
     <entries>
       <dirEntry mountPoint="454" file="${compiler:JALVIEW_DIR}/${compiler:GETDOWN_FILES_DIR}/${compiler:JAVA_VERSION}" uninstallMode="2" overrideOverwriteMode="true" overrideUninstallMode="true" subDirectory="files" />
-      <dirEntry mountPoint="736" file="${compiler:JALVIEW_DIR}/${compiler:GETDOWN_WEBSITE_DIR}/${compiler:JAVA_VERSION}" uninstallMode="2" overrideOverwriteMode="true" overrideUninstallMode="true" subDirectory="files" />
+      <dirEntry mountPoint="736" file="${compiler:JALVIEW_DIR}/${compiler:GETDOWN_WEBSITE_DIR}/${compiler:JAVA_VERSION}" uninstallMode="2" overrideOverwriteMode="true" overrideUninstallMode="true" subDirectory="files">
+        <exclude>
+          <entry location="${compiler:WRAPPER_SCRIPT_BIN_DIR}" />
+        </exclude>
+      </dirEntry>
       <dirEntry mountPoint="736" file="${compiler:JALVIEW_DIR}/examples" overwriteMode="1" uninstallMode="2" overrideFileMode="true" overrideOverwriteMode="true" overrideUninstallMode="true" entryMode="subdir" subDirectory="examples" />
+      <dirEntry mountPoint="736" file="${compiler:JALVIEW_DIR}/${compiler:GETDOWN_WEBSITE_DIR}/${compiler:JAVA_VERSION}/${compiler:WRAPPER_SCRIPT_BIN_DIR}" fileMode="755" overrideFileMode="true" overrideUninstallMode="true" entryMode="subdir" subDirectory="${compiler:WRAPPER_SCRIPT_BIN_DIR}" overrideDirMode="true" />
       <dirEntry mountPoint="884" file="${compiler:MACOS_JAVA_VM_DIR}" fileMode="755" overrideFileMode="true" overrideUninstallMode="true" entryMode="subdir" subDirectory="${compiler:JRE_DIR}" />
       <dirEntry mountPoint="885" file="${compiler:WINDOWS_JAVA_VM_DIR}" fileMode="755" overrideFileMode="true" overrideUninstallMode="true" entryMode="subdir" subDirectory="${compiler:JRE_DIR}" />
       <dirEntry mountPoint="1875" file="${compiler:JALVIEW_DIR}/${compiler:GETDOWN_WEBSITE_DIR}/${compiler:JAVA_VERSION}/${compiler:GETDOWN_DIST_DIR}" overwriteMode="1" uninstallMode="2" overrideFileMode="true" overrideOverwriteMode="true" overrideUninstallMode="true" entryMode="subdir" subDirectory="${compiler:GETDOWN_DIST_DIR}" overrideDirMode="true" />
                   <property name="obtainIfAdminWin" type="boolean" value="false" />
                 </serializedBean>
               </action>
+              <action name="Set unixUserBinDir (Linux or Unix)" id="2738" beanClass="com.install4j.runtime.beans.actions.control.SetVariableAction" rollbackBarrierExitCode="0">
+                <serializedBean>
+                  <property name="script">
+                    <object class="com.install4j.api.beans.ScriptProperty">
+                      <property name="value" type="string">String userHome = (String)context.getVariable("sys.userHome");
+
+String[] tryPaths = new String[] {
+    "bin",
+    ".local" + File.separator + "bin",
+    "local" + File.separator + "bin",
+    "opt" + File.separator + "bin"
+};
+
+for (int i = 0; i &lt; tryPaths.length; i++) {
+    String tryPath = tryPaths[i];
+    File unixUserBinDir = new File(userHome + File.separator + tryPath);
+    if (unixUserBinDir.exists()) {
+        return tryPath;
+    }
+}
+
+return null;
+</property>
+                    </object>
+                  </property>
+                  <property name="variableName" type="string">unixUserBinDir</property>
+                </serializedBean>
+                <condition>Util.isLinux() || Util.isUnixInstaller()</condition>
+              </action>
             </actions>
           </screen>
         </startup>
@@ -478,7 +519,7 @@ return console.askOkCancel(message, true);
                   </action>
                   <action id="2542" beanClass="com.install4j.runtime.beans.actions.control.SetMessageAction" actionElevationType="none" rollbackBarrierExitCode="0">
                     <serializedBean>
-                      <property name="statusMessage" type="string">Creating file associations</property>
+                      <property name="statusMessage" type="string">Finished creating file associations</property>
                       <property name="useDetail" type="boolean" value="true" />
                       <property name="useStatus" type="boolean" value="true" />
                     </serializedBean>
@@ -576,6 +617,44 @@ return console.askOkCancel(message, true);
                 </serializedBean>
                 <condition>context.getBooleanVariable("addToDockAction")</condition>
               </action>
+              <action name="Linux/Unix symlink" id="2733" beanClass="com.install4j.runtime.beans.actions.files.CreateSymlinkAction" actionElevationType="elevated" rollbackBarrierExitCode="0" errorMessage="Could not make symlink to wrapper script">
+                <serializedBean>
+                  <property name="file">
+                    <object class="java.io.File">
+                      <string>${compiler:WRAPPER_SCRIPT_BIN_DIR}/${compiler:BASH_WRAPPER_SCRIPT}</string>
+                    </object>
+                  </property>
+                  <property name="linkFile">
+                    <object class="java.io.File">
+                      <string>${compiler:WRAPPER_SCRIPT_BIN_DIR}/${compiler:WRAPPER_LINK}</string>
+                    </object>
+                  </property>
+                </serializedBean>
+                <condition>!Util.isWindows()</condition>
+              </action>
+              <action name="Add Jalview bin to the user's path (Windows)" id="2740" beanClass="com.install4j.runtime.beans.actions.misc.ModifyEnvironmentVariableAction" actionElevationType="elevated" rollbackBarrierExitCode="0" errorMessage="Could not add &quot;${installer:sys.contentDir}\${compiler:WRAPPER_SCRIPT_BIN_DIR}&quot; to the Path environment variable">
+                <serializedBean>
+                  <property name="type" type="enum" class="com.install4j.runtime.beans.actions.misc.ModifyStringType" value="APPEND" />
+                  <property name="value" type="string">${installer:sys.contentDir}\${compiler:WRAPPER_SCRIPT_BIN_DIR}</property>
+                  <property name="variableName" type="string">Path</property>
+                </serializedBean>
+                <condition>context.getBooleanVariable("appendToPathAction")</condition>
+              </action>
+              <action name="Create symbolic link to jalview.sh in user's local bin" id="2739" beanClass="com.install4j.runtime.beans.actions.files.CreateSymlinkAction" actionElevationType="elevated" rollbackBarrierExitCode="0" errorMessage="Could not make a ${compiler:WRAPPER_LINK} symbolic link in ~/${installer:unixUserBinDir}">
+                <serializedBean>
+                  <property name="file">
+                    <object class="java.io.File">
+                      <string>${installer:sys.contentDir}/${compiler:WRAPPER_SCRIPT_BIN_DIR}/${compiler:BASH_WRAPPER_SCRIPT}</string>
+                    </object>
+                  </property>
+                  <property name="linkFile">
+                    <object class="java.io.File">
+                      <string>${installer:sys.userHome}/${installer:unixUserBinDir}/${compiler:WRAPPER_LINK}</string>
+                    </object>
+                  </property>
+                </serializedBean>
+                <condition>context.getBooleanVariable("makeSymbolicLink") &amp;&amp; ( Util.isLinux() || Util.isUnixInstaller() ) &amp;&amp; ( context.getVariable("unixUserBinDir") != null )</condition>
+              </action>
             </actions>
             <formComponents>
               <formComponent id="21" beanClass="com.install4j.runtime.beans.formcomponents.MultilineLabelComponent" insetBottom="10">
@@ -598,6 +677,22 @@ return console.askOkCancel(message, true);
                 </serializedBean>
                 <visibilityScript>Util.isMacOS()</visibilityScript>
               </formComponent>
+              <formComponent name="Add jalview bin to the user's Path environment variable (Windows)" id="2734" beanClass="com.install4j.runtime.beans.formcomponents.CheckboxComponent">
+                <serializedBean>
+                  <property name="checkboxText" type="string">Add ${compiler:JALVIEW_APPLICATION_NAME}'s bin folder to the Path environment variable</property>
+                  <property name="initiallySelected" type="boolean" value="true" />
+                  <property name="variableName" type="string">appendToPathAction</property>
+                </serializedBean>
+                <visibilityScript>Util.isWindows()</visibilityScript>
+              </formComponent>
+              <formComponent name="Make a symbolic link to jalview.sh bash script in the user's local bin (Linux or Unix)" id="2736" beanClass="com.install4j.runtime.beans.formcomponents.CheckboxComponent">
+                <serializedBean>
+                  <property name="checkboxText" type="string">Make a ${compiler:WRAPPER_LINK} symbolic link in ~/${installer:unixUserBinDir}</property>
+                  <property name="initiallySelected" type="boolean" value="true" />
+                  <property name="variableName" type="string">makeSymbolicLink</property>
+                </serializedBean>
+                <visibilityScript>( Util.isLinux() || Util.isUnixInstaller() ) &amp;&amp; ( context.getVariable("unixUserBinDir") != null )</visibilityScript>
+              </formComponent>
             </formComponents>
           </screen>
         </screens>
@@ -700,7 +795,7 @@ return console.askYesNo(message, true);
               </action>
               <action id="1525" beanClass="com.install4j.runtime.beans.actions.files.DeleteFileAction" actionElevationType="elevated" rollbackBarrierExitCode="0">
                 <serializedBean>
-                  <property name="files" type="array" class="java.io.File" length="36">
+                  <property name="files" type="array" class="java.io.File" length="40">
                     <element index="0">
                       <object class="java.io.File">
                         <string>jre</string>
@@ -881,6 +976,26 @@ return console.askYesNo(message, true);
                         <string>build_*</string>
                       </object>
                     </element>
+                    <element index="36">
+                      <object class="java.io.File">
+                        <string>${compiler:WRAPPER_SCRIPT_BIN_DIR}</string>
+                      </object>
+                    </element>
+                    <element index="37">
+                      <object class="java.io.File">
+                        <string>bin</string>
+                      </object>
+                    </element>
+                    <element index="38">
+                      <object class="java.io.File">
+                        <string>channel.props</string>
+                      </object>
+                    </element>
+                    <element index="39">
+                      <object class="java.io.File">
+                        <string>channel.propsv</string>
+                      </object>
+                    </element>
                   </property>
                   <property name="recursive" type="boolean" value="true" />
                 </serializedBean>
@@ -1173,6 +1288,7 @@ return console.askYesNo(message, true);
         <file name=".DS_Store" file="${compiler:JALVIEW_DIR}/${compiler:MACOS_DMG_DS_STORE}" />
         <file name="${compiler:JALVIEW_APPLICATION_NAME}.app/Contents/Resources/Jalview-File.icns" file="${compiler:JALVIEW_DIR}/${compiler:INSTALL4J_UTILS_DIR}/Jalview-File.icns" />
         <file name="${compiler:JALVIEW_APPLICATION_NAME}.app/Contents/Resources/Jalview-Launch.icns" file="${compiler:JALVIEW_DIR}/${compiler:INSTALL4J_UTILS_DIR}/Jalview-Launch.icns" />
+        <symlink name="${compiler:JALVIEW_APPLICATION_NAME}.app/Contents/MacOS/${compiler:WRAPPER_LINK}" target="../Resources/app/${compiler:WRAPPER_SCRIPT_BIN_DIR}/${compiler:BASH_WRAPPER_SCRIPT}" />
       </topLevelFiles>
     </macosArchive>
     <unixInstaller name="Linux x64 Shell Installer" id="1595" mediaFileName="${compiler:UNIX_APPLICATION_FOLDER}-${compiler:JALVIEW_VERSION}-linux_x64-java_${compiler:JAVA_INTEGER_VERSION}" installDir="${compiler:UNIX_APPLICATION_FOLDER}" customInstallBaseDir="~/opt/">
diff --git a/utils/osx_signing/README b/utils/osx_signing/README
new file mode 100644 (file)
index 0000000..057b5b8
--- /dev/null
@@ -0,0 +1,32 @@
+Signing and Notarizing install4j DMGs for OSX
+
+0. You will need an up to date Apple Developer ID subscription and have a valid developer key for signing/notarizing apps, installers and DMGs available on your system.
+
+1. Build the install4j installers - signing these for windows requires a Certum cryptokey or other suitable java codesigning cert. Details to be provided.
+
+2. Unpack the OSX installer to a local directory
+hdiutil attach build/install4j/11/Jalview_Develop-2_11_2_0dev-d20210128-macos-java_11.dmg 
+mkdir newdmg; ditto /Volumes/Jalview\ Develop\ Installer newdmg/
+
+3. Remove the uninstaller if necessary/and/or others, and then deep sign the dmg
+
+xattr -cr ./newdmg/Jalview\ Develop.app/Contents/Resources/app/jre/Contents/MacOS/libjli.dylib 
+codesign --verify --deep -v ./newdmg/Jalview\ Develop.app/Contents/Resources/app/jre/Contents/MacOS/libjli.dylib 
+
+codesign --force --deep -vvvv -s "Developer ID" --options runtime --entitlements ./utils/osx_signing/entitlements.txt ./newdmg/Jalview\ Develop.app/Contents/Resources/app/jre/Contents/MacOS/libjli.dylib 
+
+codesign --verify --deep -v ./newdmg/Jalview\ Develop.app/Contents/Resources/app/jre/Contents/MacOS/libjli.dylib 
+
+codesign --force --deep -vvvv -s "Developer ID" --options runtime --entitlements ./utils/osx_signing/entitlements.txt ./newdmg/Jalview\ Develop.app/Contents/MacOS/JavaApplicationStub 
+
+hdiutil create -megabytes 240 -srcfolder ./newdmg -volname 'Jalview Develop Installer (2.11.2)' Jalview_Develop-2_11_2-macos-java_11.dmg
+
+codesign --force --deep -vvvv -s "Developer ID" --options runtime --entitlements ./utils/osx_signing/entitlements.txt Jalview_Develop-2_11_2-macos-java_11.dmg
+
+codesign --deep -vvvv Jalview_Develop-2_11_2-macos-java_11.dmg
+
+4. Notarize
+xcrun altool --notarize-app --primary-bundle-id "org.jalview.jalview-desktop" -u jalview-dev-owner@jalview.org -p $ALTOOL_PASSWORD --file Jalview_Develop-2_11_2-macos-java_11.dmg 
+.. run with --notarization-info $notarization-session-id until complete
+
+5. Staple to dmg so it can be verified without a net connection.
diff --git a/utils/osx_signing/entitlements.txt b/utils/osx_signing/entitlements.txt
new file mode 100644 (file)
index 0000000..1446982
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+    <dict>
+        <key>com.apple.security.app-sandbox</key>
+        <false/>
+        <key>com.apple.security.network.server</key>
+        <true/>
+        <key>com.apple.security.network.client</key>
+        <true/>
+        <key>com.apple.security.files.user-selected.read-write</key>
+        <true/>
+        <key>com.apple.security.cs.allow-jit</key>
+        <true/>
+        <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
+        <true/>
+        <key>com.apple.security.cs.disable-executable-page-protection</key>
+        <true/>
+        <key>com.apple.security.cs.disable-library-validation</key>
+        <true/>
+        <key>com.apple.security.cs.allow-dyld-environment-variables</key>
+        <true/>
+    </dict>
+</plist>