Merge remote-tracking branch 'origin/bug/JAL-3141_Messages_discrepancy_fix' into...
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 5 Mar 2019 15:07:51 +0000 (15:07 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 5 Mar 2019 15:07:51 +0000 (15:07 +0000)
24 files changed:
help/help.jhm
help/html/features/pdbsequencefetcher.html
help/html/features/uniprotsequencefetcher.html
src/jalview/analysis/Finder.java
src/jalview/api/FinderI.java [new file with mode: 0644]
src/jalview/appletgui/Finder.java
src/jalview/appletgui/IdPanel.java
src/jalview/datamodel/SearchResultMatchI.java
src/jalview/datamodel/SearchResults.java
src/jalview/datamodel/SearchResultsI.java
src/jalview/datamodel/SequenceI.java
src/jalview/fts/core/GFTSPanel.java
src/jalview/fts/service/pdb/PDBFTSPanel.java
src/jalview/fts/service/uniprot/UniprotFTSPanel.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/Finder.java
src/jalview/gui/Help.java
src/jalview/gui/IdPanel.java
src/jalview/gui/SeqCanvas.java
src/jalview/jbgui/GFinder.java
test/jalview/analysis/AlignSeqTest.java
test/jalview/analysis/FinderTest.java
test/jalview/controller/AlignViewControllerTest.java
test/jalview/datamodel/SearchResultsTest.java

index 78f86b6..1666cc6 100755 (executable)
    
    <mapID target="biojson" url="html/features/bioJsonFormat.html" />
    <mapID target="pdbfetcher" url="html/features/pdbsequencefetcher.html" />
+   <mapID target="pdbfts" url="html/features/pdbsequencefetcher.html#pdbfts" />
    <mapID target="siftsmapping" url="html/features/siftsmapping.html" />
    <mapID target="pdbchooser" url="html/features/structurechooser.html" />
    <mapID target="selectcolbyannot" url="html/features/columnFilterByAnnotation.html" />
    <mapID target="ensemblfetch" url="html/features/ensemblsequencefetcher.html" />
    
    <mapID target="uniprotfetcher" url="html/features/uniprotsequencefetcher.html" />
+   <mapID target="uniprotfts" url="html/features/uniprotsequencefetcher.html#uniprotfts" />
    
    <mapID target="urllinks" url="html/webServices/urllinks.html" />
    <mapID target="linksprefs" url="html/features/preferences.html#links" />
index bb63bed..9251277 100644 (file)
@@ -43,7 +43,7 @@
     alt="PDB sequence fetcher (introduced in Jalview 2.9)" />
 
   <p>
-    <strong>Searching the PDB Database</strong>
+    <a name="pdbfts"><strong>Searching the PDB Database</strong></a>
   </p>
   <p>To search the PDB, begin typing in the text box. If the
     'autosearch' checkbox is enabled, then the results of your query
index 4a64f52..72e7649 100644 (file)
@@ -44,7 +44,7 @@
   </p>
 
   <p>
-    <strong>Searching the UniProt Database</strong>
+    <a name="uniprotfts"><strong>Searching the UniProt Database</strong></a>
   </p>
   <p>To search UniProt, simply begin typing in the text box. If the
     'autosearch' check box is enabled, then after a short delay (about
index 191f6e8..3cbef6d 100644 (file)
  */
 package jalview.analysis;
 
+import jalview.api.AlignViewportI;
+import jalview.api.FinderI;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Range;
 import jalview.datamodel.SearchResultMatchI;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.VisibleContigsIterator;
 import jalview.util.Comparison;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Vector;
 
 import com.stevesoft.pat.Regex;
 
-public class Finder
+/**
+ * Implements the search algorithm for the Find dialog
+ */
+public class Finder implements FinderI
 {
-  /**
-   * Implements the search algorithms for the Find dialog box.
+  /*
+   * matched residue locations
    */
-  SearchResultsI searchResults;
+  private SearchResultsI searchResults;
 
-  AlignmentI alignment;
+  /*
+   * sequences matched by id or description
+   */
+  private Vector<SequenceI> idMatches;
 
-  SequenceGroup selection = null;
+  /*
+   * the viewport to search over
+   */
+  private AlignViewportI viewport;
 
-  Vector<SequenceI> idMatch = null;
+  /*
+   * sequence index in alignment to search from
+   */
+  private int sequenceIndex;
 
-  boolean caseSensitive = false;
+  /*
+   * column position in sequence to search from, base 0
+   * - absolute column number including any hidden columns
+   * (position after start of last match for a repeat search)
+   */
+  private int columnIndex;
 
-  private boolean includeDescription = false;
+  /**
+   * Constructor for searching a viewport
+   * 
+   * @param av
+   */
+  public Finder(AlignViewportI av)
+  {
+    this.viewport = av;
+    this.sequenceIndex = 0;
+    this.columnIndex = -1;
+  }
 
-  boolean findAll = false;
+  @Override
+  public void findAll(String theSearchString, boolean matchCase,
+          boolean searchDescription)
+  {
+    /*
+     * search from the start
+     */
+    sequenceIndex = 0;
+    columnIndex = -1;
 
-  Regex regex = null;
+    doFind(theSearchString, matchCase, searchDescription, true);
 
-  /**
-   * holds last-searched position between calls to find(false)
-   */
-  int seqIndex = 0, resIndex = -1;
+    /*
+     * reset to start for next search
+     */
+    sequenceIndex = 0;
+    columnIndex = -1;
+  }
 
-  public Finder(AlignmentI alignment, SequenceGroup selection)
+  @Override
+  public void findNext(String theSearchString, boolean matchCase,
+          boolean searchDescription)
   {
-    this.alignment = alignment;
-    this.selection = selection;
+    doFind(theSearchString, matchCase, searchDescription, false);
+    
+    if (searchResults.isEmpty() && idMatches.isEmpty())
+    {
+      /*
+       * search failed - reset to start for next search
+       */
+      sequenceIndex = 0;
+      columnIndex = -1;
+    }
   }
 
   /**
-   * restart search at given sequence and residue on alignment and (optionally)
-   * contained in selection
+   * Performs a 'find next' or 'find all'
    * 
-   * @param alignment
-   * @param selectionGroup
-   * @param seqIndex
-   * @param resIndex
+   * @param theSearchString
+   * @param matchCase
+   * @param searchDescription
+   * @param findAll
    */
-  public Finder(AlignmentI alignment, SequenceGroup selectionGroup,
-          int seqIndex, int resIndex)
+  protected void doFind(String theSearchString, boolean matchCase,
+          boolean searchDescription, boolean findAll)
   {
-    this(alignment, selectionGroup);
-    this.seqIndex = seqIndex;
-    this.resIndex = resIndex;
-  }
+    String searchString = matchCase ? theSearchString
+            : theSearchString.toUpperCase();
+    Regex searchPattern = new Regex(searchString);
+    searchPattern.setIgnoreCase(!matchCase);
 
-  public boolean find(String searchString)
-  {
-    boolean hasResults = false;
-    if (!caseSensitive)
-    {
-      searchString = searchString.toUpperCase();
-    }
-    regex = new Regex(searchString);
-    regex.setIgnoreCase(!caseSensitive);
     searchResults = new SearchResults();
-    idMatch = new Vector<SequenceI>();
-    String item = null;
-    boolean found = false;
-    int end = alignment.getHeight();
+    idMatches = new Vector<>();
 
-    // /////////////////////////////////////////////
-
-    if (selection != null)
+    SequenceGroup selection = viewport.getSelectionGroup();
+    if (selection != null && selection.getSize() < 1)
     {
-      if ((selection.getSize() < 1)
-              || ((selection.getEndRes() - selection.getStartRes()) < 2))
-      {
-        selection = null;
-      }
+      selection = null; // ? ignore column-only selection
     }
-    SearchResultMatchI lastm = null;
 
-    while (!found && (seqIndex < end))
-    {
-      SequenceI seq = alignment.getSequenceAt(seqIndex);
+    AlignmentI alignment = viewport.getAlignment();
+    int end = alignment.getHeight();
 
-      if ((selection != null && selection.getSize() > 0)
-              && !selection.getSequences(null).contains(seq))
+    while (sequenceIndex < end)
+    {
+      SequenceI seq = alignment.getSequenceAt(sequenceIndex);
+      boolean found = findNextMatch(seq, searchString, searchPattern,
+              searchDescription);
+      if (found && !findAll)
       {
-        seqIndex++;
-        resIndex = -1;
-
-        continue;
+        return;
       }
-      if (resIndex < 0)
+      if (!found)
       {
-        resIndex = 0;
-        // test for one off matches - sequence position and sequence ID
-        // //// is the searchString a residue number?
-        try
-        {
-          int res = Integer.parseInt(searchString);
-          // possibly a residue number - check if valid for seq
-          if (seq.getEnd() >= res)
-          {
-            searchResults.addResult(seq, res, res);
-            hasResults = true;
-            // resIndex=seq.getLength();
-            // seqIndex++;
-            if (!findAll)
-            {
-              found = true;
-              break;
-            }
-          }
-        } catch (NumberFormatException ex)
-        {
-        }
-
-        if (regex.search(seq.getName()) && !idMatch.contains(seq))
-        {
-          idMatch.addElement(seq);
-          hasResults = true;
-          if (!findAll)
-          {
-            // stop and return the match
-            found = true;
-            break;
-          }
-        }
-
-        if (isIncludeDescription() && seq.getDescription() != null
-                && regex.search(seq.getDescription())
-                && !idMatch.contains(seq))
-        {
-          idMatch.addElement(seq);
-          hasResults = true;
-          if (!findAll)
-          {
-            // stop and return the match
-            found = true;
-            break;
-          }
-        }
+        sequenceIndex++;
+        columnIndex = -1;
       }
-      item = seq.getSequenceAsString();
+    }
+  }
 
-      if ((selection != null)
-              && (selection.getEndRes() < alignment.getWidth() - 1))
-      {
-        item = item.substring(0, selection.getEndRes() + 1);
-      }
+  /**
+   * Answers the start-end column range of the visible region of
+   * <code>sequence</code> starting at or after the given <code>column</code>.
+   * If there are no hidden columns, this just returns the remaining width of
+   * the sequence. The range is restricted to the current <code>selection</code>
+   * if there is one. Answers null if there are no visible columns at or after
+   * <code>column</code>.
+   */
+  protected Range getNextVisibleSequenceRegion(SequenceI sequence,
+          int column)
+  {
+    int seqColStart = column;
+    int seqColEnd = sequence.getLength() - 1;
 
-      // /Shall we ignore gaps???? - JBPNote: Add Flag for forcing this or not
-      StringBuilder noGapsSB = new StringBuilder();
-      int insertCount = 0;
-      List<Integer> spaces = new ArrayList<Integer>();
+    /*
+     * restrict search to (next) visible column region, 
+     * in case there are hidden columns
+     */
+    AlignmentI alignment = viewport.getAlignment();
+    VisibleContigsIterator visibleRegions = alignment.getHiddenColumns()
+            .getVisContigsIterator(column, alignment.getWidth(),
+                    false);
+    int[] visible = visibleRegions.hasNext() ? visibleRegions.next() : null;
+    if (visible == null)
+    {
+      columnIndex = seqColEnd + 1;
+      return null;
+    }
+    seqColStart = Math.max(seqColStart, visible[0]);
+    seqColEnd = Math.min(seqColEnd, visible[1]);
 
-      for (int j = 0; j < item.length(); j++)
+    /*
+     * restrict search to selected region if there is one
+     */
+    SequenceGroup selection = viewport.getSelectionGroup();
+    if (selection != null)
+    {
+      int selectionStart = selection.getStartRes();
+      int selectionEnd = selection.getEndRes();
+      if (selectionStart > seqColEnd || selectionEnd < seqColStart)
       {
-        if (!Comparison.isGap(item.charAt(j)))
-        {
-          noGapsSB.append(item.charAt(j));
-          spaces.add(Integer.valueOf(insertCount));
-        }
-        else
-        {
-          insertCount++;
-        }
+        /*
+         * sequence region doesn't overlap selection region 
+         */
+        columnIndex = seqColEnd + 1;
+        return null;
       }
+      seqColStart = Math.max(seqColStart, selectionStart);
+      seqColEnd = Math.min(seqColEnd, selectionEnd);
+    }
 
-      String noGaps = noGapsSB.toString();
-      for (int r = resIndex; r < noGaps.length(); r++)
-      {
+    return new Range(seqColStart, seqColEnd);
+  }
 
-        if (regex.searchFrom(noGaps, r))
-        {
-          resIndex = regex.matchedFrom();
-
-          if ((selection != null && selection.getSize() > 0) && (resIndex
-                  + spaces.get(resIndex) < selection.getStartRes()))
-          {
-            continue;
-          }
-          // if invalid string used, then regex has no matched to/from
-          int sres = seq.findPosition(resIndex + spaces.get(resIndex));
-          int eres = seq.findPosition(regex.matchedTo() - 1
-                  + (spaces.get(regex.matchedTo() - 1)));
-          // only add result if not contained in previous result
-          if (lastm == null || (lastm.getSequence() != seq
-                  || (!(lastm.getStart() <= sres
-                          && lastm.getEnd() >= eres))))
-          {
-            lastm = searchResults.addResult(seq, sres, eres);
-          }
-          hasResults = true;
-          if (!findAll)
-          {
-            // thats enough, break and display the result
-            found = true;
-            resIndex++;
-
-            break;
-          }
-
-          r = resIndex;
-        }
-        else
-        {
-          break;
-        }
-      }
+  /**
+   * Finds the next match in the given sequence, starting at column at
+   * <code>columnIndex</code>. Answers true if a match is found, else false. If
+   * a match is found, <code>columnIndex</code> is advanced to the column after
+   * the start of the matched region, ready for a search from the next position.
+   * 
+   * @param seq
+   * @param searchString
+   * @param searchPattern
+   * @param matchDescription
+   * @return
+   */
+  protected boolean findNextMatch(SequenceI seq, String searchString,
+          Regex searchPattern, boolean matchDescription)
+  {
+    SequenceGroup selection = viewport.getSelectionGroup();
+    if (selection != null && !selection.contains(seq))
+    {
+      /*
+       * this sequence is not in the selection - advance to next sequence
+       */
+      return false;
+    }
 
-      if (!found)
+    if (columnIndex < 0)
+    {
+      /*
+       * at start of sequence; try find by residue number, in sequence id,
+       * or (optionally) in sequence description
+       */
+      if (doNonMotifSearches(seq, searchString, searchPattern,
+              matchDescription))
       {
-        seqIndex++;
-        resIndex = -1;
+        return true;
       }
     }
 
-    /**
-     * We now search the Id string in the main search loop. for (int id = 0; id
-     * < alignment.getHeight(); id++) { if
-     * (regex.search(alignment.getSequenceAt(id).getName())) {
-     * idMatch.addElement(alignment.getSequenceAt(id)); hasResults = true; } }
+    /*
+     * search for next match in sequence string
      */
-    return hasResults;
-  }
-
-  /**
-   * @return the alignment
-   */
-  public AlignmentI getAlignment()
-  {
-    return alignment;
+    int end = seq.getLength();
+    while (columnIndex < end)
+    {
+      if (searchNextVisibleRegion(seq, searchPattern))
+      {
+        return true;
+      }
+    }
+    return false;
   }
 
   /**
-   * @param alignment
-   *          the alignment to set
+   * Searches the sequence, starting from <code>columnIndex</code>, and adds the
+   * next match (if any) to <code>searchResults</code>. The search is restricted
+   * to the next visible column region, and to the <code>selection</code> region
+   * if there is one. Answers true if a match is added, else false.
+   * 
+   * @param seq
+   * @param searchPattern
+   * @return
    */
-  public void setAlignment(AlignmentI alignment)
+  protected boolean searchNextVisibleRegion(SequenceI seq, Regex searchPattern)
   {
-    this.alignment = alignment;
-  }
+    Range visible = getNextVisibleSequenceRegion(seq, columnIndex);
+    if (visible == null)
+    {
+      return false;
+    }
+    String seqString = seq.getSequenceAsString(visible.start, visible.end + 1);
+    String noGaps = AlignSeq.extractGaps(Comparison.GapChars, seqString);
 
-  /**
-   * @return the caseSensitive
-   */
-  public boolean isCaseSensitive()
-  {
-    return caseSensitive;
-  }
+    if (searchPattern.search(noGaps))
+    {
+      int sequenceStartPosition = seq.findPosition(visible.start);
+      recordMatch(seq, searchPattern, sequenceStartPosition);
+      return true;
+    }
+    else
+    {
+      /*
+       * no match - advance columnIndex past this visible region
+       * so the next visible region (if any) is searched next
+       */
+      columnIndex = visible.end + 1;
+    }
 
-  /**
-   * @param caseSensitive
-   *          the caseSensitive to set
-   */
-  public void setCaseSensitive(boolean caseSensitive)
-  {
-    this.caseSensitive = caseSensitive;
+    return false;
   }
 
   /**
-   * @return the findAll
+   * Adds the match held in the <code>searchPattern</code> Regex to the
+   * <code>searchResults</code>, unless it is a subregion of the last match
+   * recorded. <code>columnIndex</code> is advanced to the position after the
+   * start of the matched region, ready for the next search. Answers true if a
+   * match was added, else false.
+   * 
+   * @param seq
+   * @param searchPattern
+   * @param firstResiduePosition
+   * @return
    */
-  public boolean isFindAll()
+  protected boolean recordMatch(SequenceI seq, Regex searchPattern,
+          int firstResiduePosition)
   {
-    return findAll;
-  }
+    /*
+     * get start/end of the match in sequence coordinates
+     */
+    int offset = searchPattern.matchedFrom();
+    int matchStartPosition = firstResiduePosition + offset;
+    int matchEndPosition = matchStartPosition
+            + searchPattern.charsMatched() - 1;
+
+    /*
+     * update columnIndex to next column after the start of the match
+     * (findIndex returns a value base 1, columnIndex is held base 0)
+     */
+    columnIndex = seq.findIndex(matchStartPosition);
 
-  /**
-   * @param findAll
-   *          the findAll to set
-   */
-  public void setFindAll(boolean findAll)
-  {
-    this.findAll = findAll;
-  }
+    /*
+     * check that this match is not a subset of the previous one (JAL-2302)
+     */
+    List<SearchResultMatchI> matches = searchResults.getResults();
+    SearchResultMatchI lastMatch = matches.isEmpty() ? null
+            : matches.get(matches.size() - 1);
 
-  /**
-   * @return the selection
-   */
-  public jalview.datamodel.SequenceGroup getSelection()
-  {
-    return selection;
-  }
+    if (lastMatch == null || !lastMatch.contains(seq, matchStartPosition,
+            matchEndPosition))
+    {
+      searchResults.addResult(seq, matchStartPosition, matchEndPosition);
+      return true;
+    }
 
-  /**
-   * @param selection
-   *          the selection to set
-   */
-  public void setSelection(jalview.datamodel.SequenceGroup selection)
-  {
-    this.selection = selection;
+    return false;
   }
 
   /**
-   * Returns the (possibly empty) list of matching sequences (when search
-   * includes searching sequence names)
+   * Does searches other than for residue patterns. Currently this includes
+   * <ul>
+   * <li>find residue by position (if search string is a number)</li>
+   * <li>match search string to sequence id</li>
+   * <li>match search string to sequence description (optional)</li>
+   * </ul>
+   * Answers true if a match is found, else false.
    * 
+   * @param seq
+   * @param searchString
+   * @param searchPattern
+   * @param includeDescription
    * @return
    */
-  public Vector<SequenceI> getIdMatch()
+  protected boolean doNonMotifSearches(SequenceI seq, String searchString,
+          Regex searchPattern, boolean includeDescription)
   {
-    return idMatch;
-  }
+    /*
+     * position sequence search to start of sequence
+     */
+    columnIndex = 0;
 
-  /**
-   * @return the regex
-   */
-  public com.stevesoft.pat.Regex getRegex()
-  {
-    return regex;
+    if (searchForResidueNumber(seq, searchString))
+    {
+      return true;
+    }
+    if (searchSequenceName(seq, searchPattern))
+    {
+      return true;
+    }
+    if (includeDescription && searchSequenceDescription(seq, searchPattern))
+    {
+      return true;
+    }
+    return false;
   }
 
   /**
-   * @return the searchResults
+   * Searches for a match with the sequence description, and if found, adds the
+   * sequence to the list of match ids (but not as a duplicate). Answers true if
+   * a match was added, else false.
+   * 
+   * @param seq
+   * @param searchPattern
+   * @return
    */
-  public SearchResultsI getSearchResults()
+  protected boolean searchSequenceDescription(SequenceI seq, Regex searchPattern)
   {
-    return searchResults;
+    String desc = seq.getDescription();
+    if (desc != null && searchPattern.search(desc) && !idMatches.contains(seq))
+    {
+      idMatches.addElement(seq);
+      return true;
+    }
+    return false;
   }
 
   /**
-   * @return the resIndex
+   * Searches for a match with the sequence name, and if found, adds the
+   * sequence to the list of match ids (but not as a duplicate). Answers true if
+   * a match was added, else false.
+   * 
+   * @param seq
+   * @param searchPattern
+   * @return
    */
-  public int getResIndex()
+  protected boolean searchSequenceName(SequenceI seq, Regex searchPattern)
   {
-    return resIndex;
+    if (searchPattern.search(seq.getName()) && !idMatches.contains(seq))
+    {
+      idMatches.addElement(seq);
+      return true;
+    }
+    return false;
   }
 
   /**
-   * @param resIndex
-   *          the resIndex to set
+   * Tries to interpret the search string as a residue position, and if valid,
+   * adds the position to the search results and returns true, else answers
+   * false
    */
-  public void setResIndex(int resIndex)
+  protected boolean searchForResidueNumber(SequenceI seq, String searchString)
   {
-    this.resIndex = resIndex;
+    try
+    {
+      int res = Integer.parseInt(searchString);
+      if (seq.getStart() <= res && seq.getEnd() >= res)
+      {
+        searchResults.addResult(seq, res, res);
+        return true;
+      }
+    } catch (NumberFormatException ex)
+    {
+    }
+    return false;
   }
 
-  /**
-   * @return the seqIndex
+  /* (non-Javadoc)
+   * @see jalview.analysis.FinderI#getIdMatch()
    */
-  public int getSeqIndex()
+  @Override
+  public Vector<SequenceI> getIdMatches()
   {
-    return seqIndex;
+    return idMatches;
   }
 
-  /**
-   * @param seqIndex
-   *          the seqIndex to set
+  /* (non-Javadoc)
+   * @see jalview.analysis.FinderI#getSearchResults()
    */
-  public void setSeqIndex(int seqIndex)
-  {
-    this.seqIndex = seqIndex;
-  }
-
-  public boolean isIncludeDescription()
-  {
-    return includeDescription;
-  }
-
-  public void setIncludeDescription(boolean includeDescription)
+  @Override
+  public SearchResultsI getSearchResults()
   {
-    this.includeDescription = includeDescription;
+    return searchResults;
   }
 }
diff --git a/src/jalview/api/FinderI.java b/src/jalview/api/FinderI.java
new file mode 100644 (file)
index 0000000..19f6136
--- /dev/null
@@ -0,0 +1,62 @@
+package jalview.api;
+
+import jalview.datamodel.SearchResultsI;
+import jalview.datamodel.SequenceI;
+
+import java.util.List;
+
+/**
+ * An interface for searching for a pattern in an aligment
+ */
+public interface FinderI
+{
+
+  /**
+   * Performs a find for the given search string (interpreted as a regular
+   * expression). Search may optionally be case-sensitive, and may optionally
+   * including match in sequence description (sequence id is always searched).
+   * If the viewport has an active selection, then the find is restricted to the
+   * selection region. Sequences matched by id or description can be retrieved
+   * by getIdMatches(), and matched residue patterns by getSearchResults().
+   * 
+   * @param theSearchString
+   * @param caseSensitive
+   * @param searchDescription
+   * @return
+   */
+  void findAll(String theSearchString, boolean caseSensitive,
+          boolean searchDescription);
+
+  /**
+   * Finds the next match for the given search string (interpreted as a regular
+   * expression), starting from the position after the last match found. Search
+   * may optionally be case-sensitive, and may optionally including match in
+   * sequence description (sequence id is always searched). If the viewport has
+   * an active selection, then the find is restricted to the selection region.
+   * Sequences matched by id or description can be retrieved by getIdMatches(),
+   * and matched residue patterns by getSearchResults().
+   * 
+   * @param theSearchString
+   * @param caseSensitive
+   * @param searchDescription
+   * @return
+   */
+  void findNext(String theSearchString, boolean caseSensitive,
+          boolean searchDescription);
+
+  /**
+   * Returns the (possibly empty) list of sequences matched on sequence name or
+   * description
+   * 
+   * @return
+   */
+  List<SequenceI> getIdMatches();
+
+  /**
+   * Answers the search results (possibly empty) from the last search
+   * 
+   * @return
+   */
+  SearchResultsI getSearchResults();
+
+}
\ No newline at end of file
index 675b862..2fc3441 100644 (file)
  */
 package jalview.appletgui;
 
+import jalview.api.AlignViewportI;
+import jalview.api.FinderI;
 import jalview.datamodel.SearchResultMatchI;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.util.MessageManager;
-import jalview.viewmodel.AlignmentViewport;
 
 import java.awt.Button;
 import java.awt.Checkbox;
@@ -42,25 +43,39 @@ import java.awt.event.KeyEvent;
 import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
-import java.util.Vector;
+import java.util.Map;
 
 public class Finder extends Panel implements ActionListener
 {
-  AlignmentViewport av;
+  private AlignViewportI av;
 
-  AlignmentPanel ap;
+  private AlignmentPanel ap;
 
-  Frame frame;
+  private TextField textfield = new TextField();
 
-  SearchResultsI searchResults;
+  private Button findAll = new Button();
 
-  int seqIndex = 0;
+  private Button findNext = new Button();
 
-  int resIndex = -1;
+  private Button createFeatures = new Button();
+
+  private Checkbox caseSensitive = new Checkbox();
+
+  private Checkbox searchDescription = new Checkbox();
+
+  private SearchResultsI searchResults;
+
+  /*
+   * Finder agent per viewport searched
+   */
+  Map<AlignViewportI, FinderI> finders;
 
   public Finder(final AlignmentPanel ap)
   {
+    finders = new HashMap<>();
+
     try
     {
       jbInit();
@@ -72,7 +87,7 @@ public class Finder extends Panel implements ActionListener
 
     this.av = ap.av;
     this.ap = ap;
-    frame = new Frame();
+    Frame frame = new Frame();
     frame.add(this);
     jalview.bin.JalviewLite.addFrame(frame,
             MessageManager.getString("action.find"), 340, 120);
@@ -103,20 +118,18 @@ public class Finder extends Panel implements ActionListener
 
     else if (evt.getSource() == findAll)
     {
-      resIndex = -1;
-      seqIndex = 0;
       doSearch(true);
     }
-    else if (evt.getSource() == createNewGroup)
+    else if (evt.getSource() == createFeatures)
     {
-      createNewGroup_actionPerformed();
+      createFeatures_actionPerformed();
     }
   }
 
-  public void createNewGroup_actionPerformed()
+  public void createFeatures_actionPerformed()
   {
-    List<SequenceI> seqs = new ArrayList<SequenceI>();
-    List<SequenceFeature> features = new ArrayList<SequenceFeature>();
+    List<SequenceI> seqs = new ArrayList<>();
+    List<SequenceFeature> features = new ArrayList<>();
     String searchString = textfield.getText().trim();
 
     for (SearchResultMatchI match : searchResults.getResults())
@@ -135,48 +148,48 @@ public class Finder extends Panel implements ActionListener
     }
   }
 
-  void doSearch(boolean findAll)
+  void doSearch(boolean doFindAll)
   {
     if (ap.av.applet.currentAlignFrame != null)
     {
       ap = ap.av.applet.currentAlignFrame.alignPanel;
       av = ap.av;
     }
-    createNewGroup.setEnabled(false);
-    jalview.analysis.Finder finder = new jalview.analysis.Finder(
-            av.getAlignment(), av.getSelectionGroup(), seqIndex, resIndex);
-    finder.setCaseSensitive(caseSensitive.getState());
-    finder.setIncludeDescription(searchDescription.getState());
-    finder.setFindAll(findAll);
+    createFeatures.setEnabled(false);
+    FinderI finder = finders.get(av);
+    if (finder == null)
+    {
+      /*
+       * first time we searched this viewport
+       */
+      finder = new jalview.analysis.Finder(av);
+      finders.put(av, finder);
+    }
 
     String searchString = textfield.getText();
-
-    finder.find(searchString);
-    seqIndex = finder.getSeqIndex();
-    resIndex = finder.getResIndex();
-    searchResults = finder.getSearchResults();
-    Vector<SequenceI> idMatch = finder.getIdMatch();
-    boolean haveResults = false;
-    // set or reset the GUI
-    if ((idMatch.size() > 0))
+    boolean isCaseSensitive = caseSensitive.getState();
+    boolean doSearchDescription = searchDescription.getState();
+    if (doFindAll)
     {
-      haveResults = true;
-      ap.idPanel.highlightSearchResults(idMatch);
+      finder.findAll(searchString, isCaseSensitive, doSearchDescription);
     }
     else
     {
-      ap.idPanel.highlightSearchResults(null);
+      finder.findNext(searchString, isCaseSensitive, doSearchDescription);
     }
 
-    if (searchResults.getSize() > 0)
-    {
-      haveResults = true;
-      createNewGroup.setEnabled(true);
+    searchResults = finder.getSearchResults();
+
+    List<SequenceI> idMatches = finder.getIdMatches();
+    ap.idPanel.highlightSearchResults(idMatches);
 
+    if (searchResults.isEmpty())
+    {
+      searchResults = null;
     }
     else
     {
-      searchResults = null;
+      createFeatures.setEnabled(true);
     }
 
     // if allResults is null, this effectively switches displaySearch flag in
@@ -184,20 +197,18 @@ public class Finder extends Panel implements ActionListener
     ap.highlightSearchResults(searchResults);
     // TODO: add enablers for 'SelectSequences' or 'SelectColumns' or
     // 'SelectRegion' selection
-    if (!haveResults)
+    if (idMatches.isEmpty() && searchResults == null)
     {
       ap.alignFrame.statusBar.setText(
               MessageManager.getString("label.finished_searching"));
-      resIndex = -1;
-      seqIndex = 0;
     }
     else
     {
-      if (findAll)
+      if (doFindAll)
       {
-        String message = (idMatch.size() > 0) ? "" + idMatch.size() + " IDs"
+        String message = (idMatches.size() > 0) ? "" + idMatches.size() + " IDs"
                 : "";
-        if (idMatch.size() > 0 && searchResults != null
+        if (idMatches.size() > 0 && searchResults != null
                 && searchResults.getSize() > 0)
         {
           message += " and ";
@@ -221,28 +232,10 @@ public class Finder extends Panel implements ActionListener
     }
   }
 
-  Label jLabel1 = new Label();
-
-  protected TextField textfield = new TextField();
-
-  protected Button findAll = new Button();
-
-  protected Button findNext = new Button();
-
-  Panel actionsPanel = new Panel();
-
-  GridLayout gridLayout1 = new GridLayout();
-
-  protected Button createNewGroup = new Button();
-
-  Checkbox caseSensitive = new Checkbox();
-
-  Checkbox searchDescription = new Checkbox();
-
   private void jbInit() throws Exception
   {
+    Label jLabel1 = new Label(MessageManager.getString("action.find"));
     jLabel1.setFont(new java.awt.Font("Verdana", 0, 12));
-    jLabel1.setText(MessageManager.getString("action.find"));
     jLabel1.setBounds(new Rectangle(3, 30, 34, 15));
     this.setLayout(null);
     textfield.setFont(new java.awt.Font("Verdana", Font.PLAIN, 10));
@@ -253,7 +246,7 @@ public class Finder extends Panel implements ActionListener
       @Override
       public void keyTyped(KeyEvent e)
       {
-        textfield_keyTyped(e);
+        textfield_keyTyped();
       }
     });
     textfield.addActionListener(this);
@@ -264,15 +257,18 @@ public class Finder extends Panel implements ActionListener
     findNext.setFont(new java.awt.Font("Verdana", Font.PLAIN, 10));
     findNext.setLabel(MessageManager.getString("action.find_next"));
     findNext.addActionListener(this);
+
+    Panel actionsPanel = new Panel();
     actionsPanel.setBounds(new Rectangle(195, 5, 141, 64));
+    GridLayout gridLayout1 = new GridLayout();
     actionsPanel.setLayout(gridLayout1);
     gridLayout1.setHgap(0);
     gridLayout1.setRows(3);
     gridLayout1.setVgap(2);
-    createNewGroup.setEnabled(false);
-    createNewGroup.setFont(new java.awt.Font("Verdana", Font.PLAIN, 10));
-    createNewGroup.setLabel(MessageManager.getString("label.new_feature"));
-    createNewGroup.addActionListener(this);
+    createFeatures.setEnabled(false);
+    createFeatures.setFont(new java.awt.Font("Verdana", Font.PLAIN, 10));
+    createFeatures.setLabel(MessageManager.getString("label.new_feature"));
+    createFeatures.addActionListener(this);
     caseSensitive.setLabel(MessageManager.getString("label.match_case"));
     caseSensitive.setBounds(new Rectangle(30, 39, 126, 23));
 
@@ -281,7 +277,7 @@ public class Finder extends Panel implements ActionListener
     searchDescription.setBounds(new Rectangle(30, 59, 170, 23));
     actionsPanel.add(findNext, null);
     actionsPanel.add(findAll, null);
-    actionsPanel.add(createNewGroup, null);
+    actionsPanel.add(createFeatures, null);
     this.add(caseSensitive);
     this.add(textfield, null);
     this.add(jLabel1, null);
@@ -289,7 +285,7 @@ public class Finder extends Panel implements ActionListener
     this.add(searchDescription);
   }
 
-  void textfield_keyTyped(KeyEvent e)
+  void textfield_keyTyped()
   {
     findNext.setEnabled(true);
   }
index 15e269c..af1c47b 100755 (executable)
@@ -395,7 +395,7 @@ public class IdPanel extends Panel
   {
     idCanvas.setHighlighted(list);
 
-    if (list == null)
+    if (list == null || list.isEmpty())
     {
       return;
     }
index a47ca8b..661ad6c 100644 (file)
@@ -47,4 +47,14 @@ public interface SearchResultMatchI
    */
   int getEnd();
 
+  /**
+   * Answers true if this match is for the given sequence and includes (matches
+   * or encloses) the given start-end range
+   * 
+   * @param seq
+   * @param start
+   * @param end
+   * @return
+   */
+  boolean contains(SequenceI seq, int start, int end);
 }
\ No newline at end of file
index d1e3fcc..31736e5 100755 (executable)
@@ -91,27 +91,18 @@ public class SearchResults implements SearchResultsI
       }
     }
 
-    /* (non-Javadoc)
-     * @see jalview.datamodel.SearchResultMatchI#getSequence()
-     */
     @Override
     public SequenceI getSequence()
     {
       return sequence;
     }
 
-    /* (non-Javadoc)
-     * @see jalview.datamodel.SearchResultMatchI#getStart()
-     */
     @Override
     public int getStart()
     {
       return start;
     }
 
-    /* (non-Javadoc)
-     * @see jalview.datamodel.SearchResultMatchI#getEnd()
-     */
     @Override
     public int getEnd()
     {
@@ -162,22 +153,25 @@ public class SearchResults implements SearchResultsI
       return (sequence == m.getSequence() && start == m.getStart()
               && end == m.getEnd());
     }
+
+    @Override
+    public boolean contains(SequenceI seq, int from, int to)
+    {
+      return (sequence == seq && start <= from && end >= to);
+    }
   }
 
-  /* (non-Javadoc)
-   * @see jalview.datamodel.SearchResultsI#addResult(jalview.datamodel.SequenceI, int, int)
-   */
   @Override
   public SearchResultMatchI addResult(SequenceI seq, int start, int end)
   {
     Match m = new Match(seq, start, end);
-    matches.add(m);
+    if (!matches.contains(m))
+    {
+      matches.add(m);
+    }
     return m;
   }
 
-  /* (non-Javadoc)
-   * @see jalview.datamodel.SearchResultsI#involvesSequence(jalview.datamodel.SequenceI)
-   */
   @Override
   public boolean involvesSequence(SequenceI sequence)
   {
@@ -193,9 +187,6 @@ public class SearchResults implements SearchResultsI
     return false;
   }
 
-  /* (non-Javadoc)
-   * @see jalview.datamodel.SearchResultsI#getResults(jalview.datamodel.SequenceI, int, int)
-   */
   @Override
   public int[] getResults(SequenceI sequence, int start, int end)
   {
@@ -290,27 +281,18 @@ public class SearchResults implements SearchResultsI
     return count;
   }
 
-  /* (non-Javadoc)
-   * @see jalview.datamodel.SearchResultsI#getSize()
-   */
   @Override
   public int getSize()
   {
     return matches.size();
   }
 
-  /* (non-Javadoc)
-   * @see jalview.datamodel.SearchResultsI#isEmpty()
-   */
   @Override
   public boolean isEmpty()
   {
     return matches.isEmpty();
   }
 
-  /* (non-Javadoc)
-   * @see jalview.datamodel.SearchResultsI#getResults()
-   */
   @Override
   public List<SearchResultMatchI> getResults()
   {
index c3dc0e8..3c3ad4e 100644 (file)
@@ -31,14 +31,11 @@ public interface SearchResultsI
 {
 
   /**
-   * Adds one region to the results
+   * Adds one region to the results (unless already added, to avoid duplicates)
    * 
    * @param seq
-   *          Sequence
    * @param start
-   *          int
    * @param end
-   *          int
    * @return
    */
   SearchResultMatchI addResult(SequenceI seq, int start, int end);
index 84643d1..15b61d2 100755 (executable)
@@ -112,9 +112,11 @@ public interface SequenceI extends ASequenceI
    * get a range on the sequence as a string
    * 
    * @param start
-   *          position relative to start of sequence including gaps (from 0)
+   *          (inclusive) position relative to start of sequence including gaps
+   *          (from 0)
    * @param end
-   *          position relative to start of sequence including gaps (from 0)
+   *          (exclusive) position relative to start of sequence including gaps
+   *          (from 0)
    * 
    * @return String containing all gap and symbols in specified range
    */
index 86710e1..232561c 100644 (file)
@@ -34,6 +34,7 @@ import jalview.util.MessageManager;
 import java.awt.BorderLayout;
 import java.awt.CardLayout;
 import java.awt.Dimension;
+import java.awt.Font;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.FocusAdapter;
@@ -86,6 +87,8 @@ import javax.swing.text.JTextComponent;
 @SuppressWarnings("serial")
 public abstract class GFTSPanel extends JPanel implements GFTSPanelI
 {
+  private static final Font VERDANA_12 = new Font("Verdana", 0, 12);
+
   protected JInternalFrame mainFrame = new JInternalFrame(
           getFTSFrameTitle());
 
@@ -300,11 +303,11 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
     Integer height = getTempUserPrefs().get("FTSPanel.height") == null ? 400
             : getTempUserPrefs().get("FTSPanel.height");
     lbl_warning.setVisible(false);
-    lbl_warning.setFont(new java.awt.Font("Verdana", 0, 12));
+    lbl_warning.setFont(VERDANA_12);
     lbl_loading.setVisible(false);
-    lbl_loading.setFont(new java.awt.Font("Verdana", 0, 12));
+    lbl_loading.setFont(VERDANA_12);
     lbl_blank.setVisible(true);
-    lbl_blank.setFont(new java.awt.Font("Verdana", 0, 12));
+    lbl_blank.setFont(VERDANA_12);
 
     tbl_summary.setAutoCreateRowSorter(true);
     tbl_summary.getTableHeader().setReorderingAllowed(false);
@@ -357,6 +360,19 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
       }
     });
 
+    JButton txt_help = new JButton("?");
+    txt_help.setFont(VERDANA_12);
+    txt_help.setPreferredSize(new Dimension(15, 15));
+    txt_help.setToolTipText(MessageManager.getString("action.help"));
+    txt_help.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        showHelp();
+      }
+    });
+
     btn_autosearch.setText(MessageManager.getString("option.autosearch"));
     btn_autosearch.setToolTipText(
             MessageManager.getString("option.enable_disable_autosearch"));
@@ -371,7 +387,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
                 Boolean.toString(btn_autosearch.isSelected()));
       }
     });
-    btn_back.setFont(new java.awt.Font("Verdana", 0, 12));
+    btn_back.setFont(VERDANA_12);
     btn_back.setText(MessageManager.getString("action.back"));
     btn_back.addActionListener(new java.awt.event.ActionListener()
     {
@@ -394,7 +410,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
     });
 
     btn_ok.setEnabled(false);
-    btn_ok.setFont(new java.awt.Font("Verdana", 0, 12));
+    btn_ok.setFont(VERDANA_12);
     btn_ok.setText(MessageManager.getString("action.ok"));
     btn_ok.addActionListener(new java.awt.event.ActionListener()
     {
@@ -418,7 +434,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
     btn_next_page.setEnabled(false);
     btn_next_page.setToolTipText(
             MessageManager.getString("label.next_page_tooltip"));
-    btn_next_page.setFont(new java.awt.Font("Verdana", 0, 12));
+    btn_next_page.setFont(VERDANA_12);
     btn_next_page.setText(MessageManager.getString("action.next_page"));
     btn_next_page.addActionListener(new java.awt.event.ActionListener()
     {
@@ -443,7 +459,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
     btn_prev_page.setEnabled(false);
     btn_prev_page.setToolTipText(
             MessageManager.getString("label.prev_page_tooltip"));
-    btn_prev_page.setFont(new java.awt.Font("Verdana", 0, 12));
+    btn_prev_page.setFont(VERDANA_12);
     btn_prev_page.setText(MessageManager.getString("action.prev_page"));
     btn_prev_page.addActionListener(new java.awt.event.ActionListener()
     {
@@ -476,7 +492,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
       btn_next_page.setVisible(false);
     }
 
-    btn_cancel.setFont(new java.awt.Font("Verdana", 0, 12));
+    btn_cancel.setFont(VERDANA_12);
     btn_cancel.setText(MessageManager.getString("action.cancel"));
     btn_cancel.addActionListener(new java.awt.event.ActionListener()
     {
@@ -499,7 +515,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
     });
     scrl_searchResult.setPreferredSize(new Dimension(width, height));
 
-    cmb_searchTarget.setFont(new java.awt.Font("Verdana", 0, 12));
+    cmb_searchTarget.setFont(VERDANA_12);
     cmb_searchTarget.addItemListener(new ItemListener()
     {
       @Override
@@ -532,7 +548,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
       }
     });
 
-    txt_search.setFont(new java.awt.Font("Verdana", 0, 12));
+    txt_search.setFont(VERDANA_12);
 
     txt_search.getEditor().getEditorComponent()
             .addKeyListener(new KeyAdapter()
@@ -666,6 +682,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
     pnl_results.add(tabbedPane);
     pnl_inputs.add(cmb_searchTarget);
     pnl_inputs.add(txt_search);
+    pnl_inputs.add(txt_help);
     pnl_inputs.add(btn_autosearch);
     pnl_inputs.add(lbl_loading);
     pnl_inputs.add(lbl_warning);
@@ -710,6 +727,8 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
     Desktop.addInternalFrame(mainFrame, getFTSFrameTitle(), width, height);
   }
 
+  abstract protected void showHelp();
+
   protected void closeAction()
   {
     getTempUserPrefs().put("FTSPanel.width", this.getWidth());
index 053d91b..da164f0 100644 (file)
@@ -26,6 +26,8 @@ import jalview.fts.api.FTSRestClientI;
 import jalview.fts.core.FTSRestRequest;
 import jalview.fts.core.FTSRestResponse;
 import jalview.fts.core.GFTSPanel;
+import jalview.gui.Help;
+import jalview.gui.Help.HelpId;
 import jalview.gui.SequenceFetcher;
 import jalview.util.MessageManager;
 
@@ -33,13 +35,15 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 
+import javax.help.HelpSetException;
+
 @SuppressWarnings("serial")
 public class PDBFTSPanel extends GFTSPanel
 {
   private static String defaultFTSFrameTitle = MessageManager
           .getString("label.pdb_sequence_fetcher");
 
-  private static Map<String, Integer> tempUserPrefs = new HashMap<String, Integer>();
+  private static Map<String, Integer> tempUserPrefs = new HashMap<>();
 
   private static final String PDB_FTS_CACHE_KEY = "CACHE.PDB_FTS";
 
@@ -190,7 +194,7 @@ public class PDBFTSPanel extends GFTSPanel
     // mainFrame.dispose();
     disableActionButtons();
     StringBuilder selectedIds = new StringBuilder();
-    HashSet<String> selectedIdsSet = new HashSet<String>();
+    HashSet<String> selectedIdsSet = new HashSet<>();
     int primaryKeyColIndex = 0;
     try
     {
@@ -288,4 +292,16 @@ public class PDBFTSPanel extends GFTSPanel
   {
     return PDB_AUTOSEARCH;
   }
+
+  @Override
+  protected void showHelp()
+  {
+    try
+    {
+      Help.showHelpWindow(HelpId.PdbFts);
+    } catch (HelpSetException e1)
+    {
+      e1.printStackTrace();
+    }
+ }
 }
\ No newline at end of file
index df54dea..977abfa 100644 (file)
@@ -26,6 +26,8 @@ import jalview.fts.api.FTSRestClientI;
 import jalview.fts.core.FTSRestRequest;
 import jalview.fts.core.FTSRestResponse;
 import jalview.fts.core.GFTSPanel;
+import jalview.gui.Help;
+import jalview.gui.Help.HelpId;
 import jalview.gui.SequenceFetcher;
 import jalview.util.MessageManager;
 
@@ -33,6 +35,8 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 
+import javax.help.HelpSetException;
+
 @SuppressWarnings("serial")
 public class UniprotFTSPanel extends GFTSPanel
 {
@@ -245,4 +249,16 @@ public class UniprotFTSPanel extends GFTSPanel
   {
     return UNIPROT_AUTOSEARCH;
   }
+
+  @Override
+  protected void showHelp()
+  {
+    try
+    {
+      Help.showHelpWindow(HelpId.UniprotFts);
+    } catch (HelpSetException e1)
+    {
+      e1.printStackTrace();
+    }
+  }
 }
index a15a607..4526517 100644 (file)
@@ -1194,22 +1194,6 @@ public class FeatureSettings extends JPanel
         }
       }
     });
-    help.setFont(JvSwingUtils.getLabelFont());
-    help.setText(MessageManager.getString("action.help"));
-    help.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        try
-        {
-          Help.showHelpWindow(HelpId.SequenceFeatureSettings);
-        } catch (HelpSetException e1)
-        {
-          e1.printStackTrace();
-        }
-      }
-    });
 
     JButton cancel = new JButton(MessageManager.getString("action.cancel"));
     cancel.setFont(JvSwingUtils.getLabelFont());
index 84540f4..a4d7ad0 100755 (executable)
@@ -20,6 +20,8 @@
  */
 package jalview.gui;
 
+import jalview.api.AlignViewportI;
+import jalview.api.FinderI;
 import jalview.datamodel.SearchResultMatchI;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceFeature;
@@ -32,8 +34,9 @@ import java.awt.Dimension;
 import java.awt.event.ActionEvent;
 import java.awt.event.KeyEvent;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
-import java.util.Vector;
+import java.util.Map;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 
@@ -42,6 +45,7 @@ import javax.swing.JComponent;
 import javax.swing.JInternalFrame;
 import javax.swing.JLayeredPane;
 import javax.swing.KeyStroke;
+import javax.swing.event.InternalFrameAdapter;
 import javax.swing.event.InternalFrameEvent;
 
 /**
@@ -57,33 +61,39 @@ import javax.swing.event.InternalFrameEvent;
  */
 public class Finder extends GFinder
 {
-  private static final int MY_HEIGHT = 120;
+  private static final int MIN_WIDTH = 350;
 
-  private static final int MY_WIDTH = 400;
+  private static final int MIN_HEIGHT = 120;
 
-  AlignmentViewport av;
+  private static final int MY_HEIGHT = 120;
 
-  AlignmentPanel ap;
+  private static final int MY_WIDTH = 400;
 
-  private static final int MIN_WIDTH = 350;
+  private AlignViewportI av;
 
-  private static final int MIN_HEIGHT = 120;
+  private AlignmentPanel ap;
 
-  JInternalFrame frame;
+  private JInternalFrame frame;
 
-  int seqIndex = 0;
+  /*
+   * Finder agent per viewport searched
+   */
+  private Map<AlignViewportI, FinderI> finders;
 
-  int resIndex = -1;
+  private SearchResultsI searchResults;
 
-  SearchResultsI searchResults;
+  /*
+   * true if we only search a given alignment view
+   */
+  private boolean focusfixed;
 
   /**
-   * Creates a new Finder object with no associated viewport or panel.
+   * Creates a new Finder object with no associated viewport or panel. Each Find
+   * or Find Next action will act on whichever viewport has focus at the time.
    */
   public Finder()
   {
     this(null, null);
-    focusfixed = false;
   }
 
   /**
@@ -97,12 +107,13 @@ public class Finder extends GFinder
   {
     av = viewport;
     ap = alignPanel;
-    focusfixed = true;
+    finders = new HashMap<>();
+    focusfixed = viewport != null;
     frame = new JInternalFrame();
     frame.setContentPane(this);
     frame.setLayer(JLayeredPane.PALETTE_LAYER);
     frame.addInternalFrameListener(
-            new javax.swing.event.InternalFrameAdapter()
+            new InternalFrameAdapter()
             {
               @Override
               public void internalFrameClosing(InternalFrameEvent e)
@@ -135,12 +146,10 @@ public class Finder extends GFinder
   }
 
   /**
-   * Performs the 'Find Next' action.
-   * 
-   * @param e
+   * Performs the 'Find Next' action on the alignment panel with focus
    */
   @Override
-  public void findNext_actionPerformed(ActionEvent e)
+  public void findNext_actionPerformed()
   {
     if (getFocusedViewport())
     {
@@ -149,27 +158,18 @@ public class Finder extends GFinder
   }
 
   /**
-   * Performs the 'Find All' action.
-   * 
-   * @param e
+   * Performs the 'Find All' action on the alignment panel with focus
    */
   @Override
-  public void findAll_actionPerformed(ActionEvent e)
+  public void findAll_actionPerformed()
   {
     if (getFocusedViewport())
     {
-      resIndex = -1;
-      seqIndex = 0;
       doSearch(true);
     }
   }
 
   /**
-   * do we only search a given alignment view ?
-   */
-  private boolean focusfixed;
-
-  /**
    * if !focusfixed and not in a desktop environment, checks that av and ap are
    * valid. Otherwise, gets the topmost alignment window and sets av and ap
    * accordingly
@@ -193,7 +193,8 @@ public class Finder extends GFinder
     for (int f = 0; f < frames.length; f++)
     {
       JInternalFrame alignFrame = frames[f];
-      if (alignFrame != null && alignFrame instanceof AlignFrame)
+      if (alignFrame != null && alignFrame instanceof AlignFrame
+              && !alignFrame.isIcon())
       {
         av = ((AlignFrame) alignFrame).viewport;
         ap = ((AlignFrame) alignFrame).alignPanel;
@@ -210,8 +211,8 @@ public class Finder extends GFinder
   @Override
   public void createFeatures_actionPerformed()
   {
-    List<SequenceI> seqs = new ArrayList<SequenceI>();
-    List<SequenceFeature> features = new ArrayList<SequenceFeature>();
+    List<SequenceI> seqs = new ArrayList<>();
+    List<SequenceFeature> features = new ArrayList<>();
 
     String searchString = searchBox.getEditor().getItem().toString().trim();
     String desc = "Search Results";
@@ -261,55 +262,48 @@ public class Finder extends GFinder
     // other stuff
     // TODO: add switches to control what is searched - sequences, IDS,
     // descriptions, features
-    jalview.analysis.Finder finder = new jalview.analysis.Finder(
-            av.getAlignment(), av.getSelectionGroup(), seqIndex, resIndex);
-    finder.setCaseSensitive(caseSensitive.isSelected());
-    finder.setIncludeDescription(searchDescription.isSelected());
-
-    finder.setFindAll(doFindAll);
-
-    finder.find(searchString); // returns true if anything was actually found
-
-    seqIndex = finder.getSeqIndex();
-    resIndex = finder.getResIndex();
+    FinderI finder = finders.get(av);
+    if (finder == null)
+    {
+      /*
+       * first time we've searched this viewport
+       */
+      finder = new jalview.analysis.Finder(av);
+      finders.put(av, finder);
+    }
 
-    searchResults = finder.getSearchResults(); // find(regex,
-    // caseSensitive.isSelected(), )
-    Vector<SequenceI> idMatch = finder.getIdMatch();
-    boolean haveResults = false;
-    // set or reset the GUI
-    if ((idMatch.size() > 0))
+    boolean isCaseSensitive = caseSensitive.isSelected();
+    boolean doSearchDescription = searchDescription.isSelected();
+    if (doFindAll)
     {
-      haveResults = true;
-      ap.getIdPanel().highlightSearchResults(idMatch);
+      finder.findAll(searchString, isCaseSensitive, doSearchDescription);
     }
     else
     {
-      ap.getIdPanel().highlightSearchResults(null);
+      finder.findNext(searchString, isCaseSensitive, doSearchDescription);
     }
 
-    if (searchResults.getSize() > 0)
+    searchResults = finder.getSearchResults();
+    List<SequenceI> idMatch = finder.getIdMatches();
+    ap.getIdPanel().highlightSearchResults(idMatch);
+
+    if (searchResults.isEmpty())
     {
-      haveResults = true;
-      createFeatures.setEnabled(true);
+      searchResults = null;
     }
     else
     {
-      searchResults = null;
+      createFeatures.setEnabled(true);
     }
 
-    // if allResults is null, this effectively switches displaySearch flag in
-    // seqCanvas
     ap.highlightSearchResults(searchResults);
     // TODO: add enablers for 'SelectSequences' or 'SelectColumns' or
     // 'SelectRegion' selection
-    if (!haveResults)
+    if (idMatch.isEmpty() && searchResults == null)
     {
       JvOptionPane.showInternalMessageDialog(this,
               MessageManager.getString("label.finished_searching"), null,
               JvOptionPane.INFORMATION_MESSAGE);
-      resIndex = -1;
-      seqIndex = 0;
     }
     else
     {
@@ -329,8 +323,6 @@ public class Finder extends GFinder
         }
         JvOptionPane.showInternalMessageDialog(this, message, null,
                 JvOptionPane.INFORMATION_MESSAGE);
-        resIndex = -1;
-        seqIndex = 0;
       }
     }
     searchBox.updateCache();
index f2d8113..810fc92 100644 (file)
@@ -20,6 +20,7 @@
  */
 package jalview.gui;
 
+import java.awt.Point;
 import java.net.URL;
 
 import javax.help.BadIDException;
@@ -28,17 +29,17 @@ import javax.help.HelpSet;
 import javax.help.HelpSetException;
 
 /**
- * Utility class to show the help documentation window.
+ * Utility class to show the help documentation window
  * 
  * @author gmcarstairs
- *
  */
 public class Help
 {
   public enum HelpId
   {
     Home("home"), SequenceFeatureSettings("seqfeatures.settings"),
-    StructureViewer("viewingpdbs");
+    StructureViewer("viewingpdbs"), PdbFts("pdbfts"),
+    UniprotFts("uniprotfts");
 
     private String id;
 
@@ -54,9 +55,7 @@ public class Help
     }
   }
 
-  private static final long HALF_A_MO = 500; // half a second
-
-  private static long lastOpenedTime = 0L;
+  private static HelpBroker hb;
 
   /**
    * Not instantiable
@@ -67,42 +66,50 @@ public class Help
   }
 
   /**
-   * Show help text in a new window. But do nothing if within half a second of
-   * the last invocation.
-   * 
-   * This is a workaround for issue JAL-914 - both Desktop and AlignFrame
-   * responding to F1 key, resulting in duplicate help windows opened.
+   * Shows the help window, at the entry specified by the given helpId
    * 
    * @param id
-   *          TODO
    * 
    * @throws HelpSetException
    */
   public static void showHelpWindow(HelpId id) throws HelpSetException
   {
-    long timeNow = System.currentTimeMillis();
+    ClassLoader cl = Desktop.class.getClassLoader();
+    URL url = HelpSet.findHelpSet(cl, "help/help"); // $NON-NLS-$
+    HelpSet hs = new HelpSet(cl, url);
 
-    if (timeNow - lastOpenedTime > HALF_A_MO)
+    if (hb == null)
     {
-      lastOpenedTime = timeNow;
-      ClassLoader cl = Desktop.class.getClassLoader();
-      URL url = HelpSet.findHelpSet(cl, "help/help"); // $NON-NLS-$
-      HelpSet hs = new HelpSet(cl, url);
+      /*
+       * create help broker first time (only)
+       */
+      hb = hs.createHelpBroker();
+    }
 
-      HelpBroker hb = hs.createHelpBroker();
-      try
-      {
-        hb.setCurrentID(id.toString());
-      } catch (BadIDException bad)
-      {
-        System.out.println("Bad help link: " + id.toString()
-                + ": must match a target in help.jhm");
-        throw bad;
-      }
-      hb.setDisplayed(true);
+    try
+    {
+      hb.setCurrentID(id.toString());
+    } catch (BadIDException bad)
+    {
+      System.out.println("Bad help link: " + id.toString()
+              + ": must match a target in help.jhm");
+      throw bad;
     }
+
+    /*
+     * set Help visible - at its current location if it is already shown,
+     * else at a location as determined by the window manager
+     */
+    Point p = hb.getLocation();
+    hb.setLocation(p);
+    hb.setDisplayed(true);
   }
 
+  /**
+   * Show the Help window at the root entry
+   * 
+   * @throws HelpSetException
+   */
   public static void showHelpWindow() throws HelpSetException
   {
     showHelpWindow(HelpId.Home);
index 113fd9f..d11d7a1 100755 (executable)
@@ -510,7 +510,7 @@ public class IdPanel extends JPanel
   {
     getIdCanvas().setHighlighted(list);
 
-    if (list == null)
+    if (list == null || list.isEmpty())
     {
       return;
     }
index 2a370c4..b3fcc54 100755 (executable)
@@ -1655,7 +1655,7 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
       }
       ViewportRanges vpRanges = av.getRanges();
 
-      int range = vpRanges.getEndRes() - vpRanges.getStartRes();
+      int range = vpRanges.getEndRes() - vpRanges.getStartRes() + 1;
       if (scrollX > range)
       {
         scrollX = range;
@@ -1724,10 +1724,10 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
   {
     ViewportRanges ranges = av.getRanges();
 
-    if (Math.abs(scrollX) > ranges.getViewportWidth())
+    if (Math.abs(scrollX) >= ranges.getViewportWidth())
     {
       /*
-       * shift of more than one view width is 
+       * shift of one view width or more is 
        * overcomplicated to handle in this method
        */
       fastPaint = false;
@@ -1918,10 +1918,17 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
 
       while (y >= 0)
       {
+        /*
+         * shift 'widthToCopy' residues by 'positions' places to the right
+         */
         gg.copyArea(copyFromLeftStart, y, widthToCopy, heightToCopy,
                 positions * charWidth, 0);
         if (y > 0)
         {
+          /*
+           * copy 'positions' residue from the row above (right hand end)
+           * to this row's left hand end
+           */
           gg.copyArea(copyFromRightStart, y - wrappedRepeatHeightPx,
                   positions * charWidth, heightToCopy, -widthToCopy,
                   wrappedRepeatHeightPx);
index 1ea4ab5..b433570 100755 (executable)
@@ -47,41 +47,20 @@ import javax.swing.text.JTextComponent;
 
 public class GFinder extends JPanel
 {
-  JLabel jLabelFind = new JLabel();
+  private static final java.awt.Font VERDANA_12 = new java.awt.Font("Verdana", 0,
+          12);
 
-  protected JButton findAll = new JButton();
-
-  protected JButton findNext = new JButton();
-
-  JPanel actionsPanel = new JPanel();
-
-  GridLayout gridLayout1 = new GridLayout();
+  private static final String FINDER_CACHE_KEY = "CACHE.FINDER";
 
   protected JButton createFeatures = new JButton();
 
-  protected JvCacheableInputBox<String> searchBox = new JvCacheableInputBox<String>(
+  protected JvCacheableInputBox<String> searchBox = new JvCacheableInputBox<>(
           getCacheKey());
 
-  BorderLayout mainBorderLayout = new BorderLayout();
-
-  JPanel jPanel2 = new JPanel();
-
-  JPanel jPanel3 = new JPanel();
-
-  JPanel jPanel4 = new JPanel();
-
-  BorderLayout borderLayout2 = new BorderLayout();
-
-  JPanel jPanel6 = new JPanel();
-
   protected JCheckBox caseSensitive = new JCheckBox();
 
   protected JCheckBox searchDescription = new JCheckBox();
 
-  GridLayout optionsGridLayout = new GridLayout();
-
-  private static final String FINDER_CACHE_KEY = "CACHE.FINDER";
-
   public GFinder()
   {
     try
@@ -95,35 +74,47 @@ public class GFinder extends JPanel
 
   private void jbInit() throws Exception
   {
-    jLabelFind.setFont(new java.awt.Font("Verdana", 0, 12));
-    jLabelFind.setText(MessageManager.getString("label.find"));
+    BorderLayout mainBorderLayout = new BorderLayout();
     this.setLayout(mainBorderLayout);
-    findAll.setFont(new java.awt.Font("Verdana", 0, 12));
-    findAll.setText(MessageManager.getString("action.find_all"));
+    mainBorderLayout.setHgap(5);
+    mainBorderLayout.setVgap(5);
+
+    JLabel jLabelFind = new JLabel(MessageManager.getString("label.find"));
+    jLabelFind.setFont(VERDANA_12);
+
+    JButton findAll = new JButton(
+            MessageManager.getString("action.find_all"));
+    findAll.setFont(VERDANA_12);
     findAll.addActionListener(new java.awt.event.ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        findAll_actionPerformed(e);
+        findAll_actionPerformed();
       }
     });
-    findNext.setFont(new java.awt.Font("Verdana", 0, 12));
-    findNext.setText(MessageManager.getString("action.find_next"));
+
+    JButton findNext = new JButton(
+            MessageManager.getString("action.find_next"));
+    findNext.setFont(VERDANA_12);
     findNext.addActionListener(new java.awt.event.ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        findNext_actionPerformed(e);
+        findNext_actionPerformed();
       }
     });
+
+    JPanel actionsPanel = new JPanel();
+    GridLayout gridLayout1 = new GridLayout();
     actionsPanel.setLayout(gridLayout1);
     gridLayout1.setHgap(0);
     gridLayout1.setRows(3);
     gridLayout1.setVgap(2);
+
     createFeatures.setEnabled(false);
-    createFeatures.setFont(new java.awt.Font("Verdana", 0, 12));
+    createFeatures.setFont(VERDANA_12);
     createFeatures.setMargin(new Insets(0, 0, 0, 0));
     createFeatures.setText(MessageManager.getString("label.new_feature"));
     createFeatures.addActionListener(new java.awt.event.ActionListener()
@@ -141,7 +132,7 @@ public class GFinder extends JPanel
               @Override
               public void caretUpdate(CaretEvent e)
               {
-                textfield_caretUpdate(e);
+                textfield_caretUpdate();
               }
             });
     searchBox.getEditor().getEditorComponent()
@@ -153,11 +144,7 @@ public class GFinder extends JPanel
                 textfield_keyPressed(e);
               }
             });
-    mainBorderLayout.setHgap(5);
-    mainBorderLayout.setVgap(5);
-    jPanel4.setLayout(borderLayout2);
-    jPanel2.setPreferredSize(new Dimension(10, 1));
-    jPanel3.setPreferredSize(new Dimension(10, 1));
+
     caseSensitive.setHorizontalAlignment(SwingConstants.LEFT);
     caseSensitive.setText(MessageManager.getString("label.match_case"));
 
@@ -169,6 +156,13 @@ public class GFinder extends JPanel
     actionsPanel.add(createFeatures, null);
     this.add(jLabelFind, java.awt.BorderLayout.WEST);
     this.add(actionsPanel, java.awt.BorderLayout.EAST);
+
+    JPanel jPanel2 = new JPanel();
+    jPanel2.setPreferredSize(new Dimension(10, 1));
+    JPanel jPanel3 = new JPanel();
+    jPanel3.setPreferredSize(new Dimension(10, 1));
+    JPanel jPanel4 = new JPanel();
+    jPanel4.setLayout(new BorderLayout());
     this.add(jPanel2, java.awt.BorderLayout.SOUTH);
     this.add(jPanel3, java.awt.BorderLayout.NORTH);
     this.add(jPanel4, java.awt.BorderLayout.CENTER);
@@ -176,6 +170,7 @@ public class GFinder extends JPanel
 
     JPanel optionsPanel = new JPanel();
 
+    GridLayout optionsGridLayout = new GridLayout();
     optionsGridLayout.setHgap(0);
     optionsGridLayout.setRows(2);
     optionsGridLayout.setVgap(2);
@@ -193,16 +188,16 @@ public class GFinder extends JPanel
       if (!searchBox.isPopupVisible())
       {
         e.consume();
-        findNext_actionPerformed(null);
+        findNext_actionPerformed();
       }
     }
   }
 
-  protected void findNext_actionPerformed(ActionEvent e)
+  protected void findNext_actionPerformed()
   {
   }
 
-  protected void findAll_actionPerformed(ActionEvent e)
+  protected void findAll_actionPerformed()
   {
   }
 
@@ -210,9 +205,10 @@ public class GFinder extends JPanel
   {
   }
 
-  public void textfield_caretUpdate(CaretEvent e)
+  public void textfield_caretUpdate()
   {
-    if (searchBox.getUserInput().indexOf(">") > -1)
+    // disabled as appears to be running a non-functional
+    if (false && searchBox.getUserInput().indexOf(">") > -1)
     {
       SwingUtilities.invokeLater(new Runnable()
       {
@@ -233,7 +229,7 @@ public class GFinder extends JPanel
             str = jalview.analysis.AlignSeq.extractGaps(
                     jalview.util.Comparison.GapChars,
                     al.getSequenceAt(0).getSequenceAsString());
-
+            // todo and what? set str as searchBox text?
           }
         }
       });
index 837e970..bd827f9 100644 (file)
@@ -52,6 +52,7 @@ public class AlignSeqTest
     assertEquals(AlignSeq.extractGaps(" -", " AC-G.T"), "ACG.T");
     assertEquals(AlignSeq.extractGaps(" -.", " AC-G.T ."), "ACGT");
     assertEquals(AlignSeq.extractGaps("-", " AC-G.T"), " ACG.T");
+    assertEquals(AlignSeq.extractGaps("-. ", " -. .-"), "");
   }
 
   @Test(groups = { "Functional" })
index d7a509f..e2a94ef 100644 (file)
@@ -24,21 +24,31 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertSame;
 import static org.testng.Assert.assertTrue;
 
+import jalview.api.AlignViewportI;
+import jalview.api.FinderI;
+import jalview.bin.Cache;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SearchResultMatchI;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceGroup;
 import jalview.gui.AlignFrame;
+import jalview.gui.AlignViewport;
 import jalview.gui.JvOptionPane;
 import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
 
 import java.util.List;
 
+import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import junit.extensions.PA;
+
 public class FinderTest
 {
   @BeforeClass(alwaysRun = true)
@@ -52,32 +62,56 @@ public class FinderTest
 
   private AlignmentI al;
 
+  private AlignViewportI av;
+
   @BeforeClass(groups = "Functional")
   public void setUp()
   {
-    String seqData = "seq1 ABCD--EF-GHI\n" + "seq2 A--BCDefHI\n"
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+    Cache.applicationProperties.setProperty("PAD_GAPS",
+            Boolean.FALSE.toString());
+
+    String seqData = "seq1seq1/8-18 ABCD--EF-GHIJI\n" + "seq2 A--BCDefHI\n"
             + "seq3 --bcdEFH\n" + "seq4 aa---aMMMMMaaa\n";
     af = new FileLoader().LoadFileWaitTillLoaded(seqData,
             DataSourceType.PASTE);
-    al = af.getViewport().getAlignment();
+    av = af.getViewport();
+    al = av.getAlignment();
+  }
+
+  @AfterMethod
+  public void tearDownAfterTest()
+  {
+    av.setSelectionGroup(null);
   }
 
   /**
-   * Test for find all matches of a regular expression
+   * Test for find matches of a regular expression
    */
   @Test(groups = "Functional")
-  public void testFindAll_regex()
+  public void testFind_regex()
   {
-    Finder f = new Finder(al, null);
-    f.setFindAll(true);
-    f.find("E.H"); // 'E, any character, H'
+    /*
+     * find next match only
+     */
+    Finder f = new Finder(av);
+    f.findNext("E.H", false, false); // 'E, any character, H'
+    // should match seq2 efH only
+    SearchResultsI sr = f.getSearchResults();
+    assertEquals(sr.getSize(), 1);
+    List<SearchResultMatchI> matches = sr.getResults();
+    assertSame(matches.get(0).getSequence(), al.getSequenceAt(1));
+    assertEquals(matches.get(0).getStart(), 5);
+    assertEquals(matches.get(0).getEnd(), 7);
 
+    f = new Finder(av);
+    f.findAll("E.H", false, false); // 'E, any character, H'
     // should match seq2 efH and seq3 EFH
-    SearchResultsI sr = f.getSearchResults();
+    sr = f.getSearchResults();
     assertEquals(sr.getSize(), 2);
-    List<SearchResultMatchI> matches = sr.getResults();
-    assertSame(al.getSequenceAt(1), matches.get(0).getSequence());
-    assertSame(al.getSequenceAt(2), matches.get(1).getSequence());
+    matches = sr.getResults();
+    assertSame(matches.get(0).getSequence(), al.getSequenceAt(1));
+    assertSame(matches.get(1).getSequence(), al.getSequenceAt(2));
     assertEquals(matches.get(0).getStart(), 5);
     assertEquals(matches.get(0).getEnd(), 7);
     assertEquals(matches.get(1).getStart(), 4);
@@ -90,20 +124,40 @@ public class FinderTest
   @Test(groups = "Functional")
   public void testFind_residueNumber()
   {
-    Finder f = new Finder(al, null);
-    f.setFindAll(true);
-    f.find("9");
+    Finder f = new Finder(av);
 
-    // seq1 and seq4 have 9 residues; no match in other sequences
+    /*
+     * find first match should return seq1 residue 9
+     */
+    f.findNext("9", false, false);
     SearchResultsI sr = f.getSearchResults();
-    assertEquals(sr.getSize(), 2);
+    assertEquals(sr.getSize(), 1);
     List<SearchResultMatchI> matches = sr.getResults();
-    assertSame(al.getSequenceAt(0), matches.get(0).getSequence());
-    assertSame(al.getSequenceAt(3), matches.get(1).getSequence());
+    assertSame(matches.get(0).getSequence(), al.getSequenceAt(0));
+    assertEquals(matches.get(0).getStart(), 9);
+    assertEquals(matches.get(0).getEnd(), 9);
+
+    /*
+     * find all matches should return seq1 and seq4 (others are too short)
+     */
+    f = new Finder(av);
+    f.findAll("9", false, false);
+    sr = f.getSearchResults();
+    assertEquals(sr.getSize(), 2);
+    matches = sr.getResults();
+    assertSame(matches.get(0).getSequence(), al.getSequenceAt(0));
+    assertSame(matches.get(1).getSequence(), al.getSequenceAt(3));
     assertEquals(matches.get(0).getStart(), 9);
     assertEquals(matches.get(0).getEnd(), 9);
     assertEquals(matches.get(1).getStart(), 9);
     assertEquals(matches.get(1).getEnd(), 9);
+
+    /*
+     * parsing of search string as integer is strict
+     */
+    f = new Finder(av);
+    f.findNext(" 9", false, false);
+    assertTrue(f.getSearchResults().isEmpty());
   }
 
   /**
@@ -113,69 +167,84 @@ public class FinderTest
   public void testFindNext()
   {
     /*
-     * start at second sequence; resIndex of -1
+     * start at second sequence; colIndex of -1
      * means sequence id / description is searched
      */
-    Finder f = new Finder(al, null, 1, -1);
-    f.find("e"); // matches id
+    Finder f = new Finder(av);
+    PA.setValue(f, "sequenceIndex", 1);
+    PA.setValue(f, "columnIndex", -1);
+    f.findNext("e", false, false); // matches id
 
     assertTrue(f.getSearchResults().isEmpty());
-    assertEquals(f.getIdMatch().size(), 1);
-    assertSame(f.getIdMatch().get(0), al.getSequenceAt(1));
-
-    // resIndex is now 0 - for use in next find next
-    assertEquals(f.getResIndex(), 0);
-    f = new Finder(al, null, 1, 0);
-    f.find("e"); // matches in sequence
-    assertTrue(f.getIdMatch().isEmpty());
+    assertEquals(f.getIdMatches().size(), 1);
+    assertSame(f.getIdMatches().get(0), al.getSequenceAt(1));
+
+    // colIndex is now 0 - for use in next find next
+    // searching A--BCDefHI
+    assertEquals(PA.getValue(f, "columnIndex"), 0);
+    f = new Finder(av);
+    PA.setValue(f, "sequenceIndex", 1);
+    PA.setValue(f, "columnIndex", 0);
+    f.findNext("e", false, false); // matches in sequence
+    assertTrue(f.getIdMatches().isEmpty());
     assertEquals(f.getSearchResults().getSize(), 1);
     List<SearchResultMatchI> matches = f.getSearchResults().getResults();
     assertEquals(matches.get(0).getStart(), 5);
     assertEquals(matches.get(0).getEnd(), 5);
     assertSame(matches.get(0).getSequence(), al.getSequenceAt(1));
     // still in the second sequence
-    assertEquals(f.getSeqIndex(), 1);
-    // next residue position to search from is 5
-    // (used as base 0 by RegEx so the same as 6 if base 1)
-    assertEquals(f.getResIndex(), 5);
+    assertEquals(PA.getValue(f, "sequenceIndex"), 1);
+    // next column position to search from is 7
+    assertEquals(PA.getValue(f, "columnIndex"), 7);
 
     // find next from end of sequence - finds next sequence id
-    f = new Finder(al, null, 1, 5);
-    f.find("e");
-    assertEquals(f.getIdMatch().size(), 1);
-    assertSame(f.getIdMatch().get(0), al.getSequenceAt(2));
+    f = new Finder(av);
+    PA.setValue(f, "sequenceIndex", 1);
+    PA.setValue(f, "columnIndex", 7);
+    f.findNext("e", false, false);
+    assertEquals(f.getIdMatches().size(), 1);
+    assertSame(f.getIdMatches().get(0), al.getSequenceAt(2));
+    assertTrue(f.getSearchResults().isEmpty());
   }
 
   /**
    * Test for matching within sequence descriptions
    */
   @Test(groups = "Functional")
-  public void testFindAll_inDescription()
+  public void testFind_inDescription()
   {
     AlignmentI al2 = new Alignment(al);
     al2.getSequenceAt(0).setDescription("BRAF");
     al2.getSequenceAt(1).setDescription("braf");
-    Finder f = new Finder(al2, null);
-    f.setFindAll(true);
-    f.setIncludeDescription(true);
-
-    f.find("rAF");
-    assertEquals(f.getIdMatch().size(), 2);
-    assertSame(f.getIdMatch().get(0), al2.getSequenceAt(0));
-    assertSame(f.getIdMatch().get(1), al2.getSequenceAt(1));
+
+    AlignViewportI av2 = new AlignViewport(al2);
+
+    /*
+     * find first match only
+     */
+    Finder f = new Finder(av2);
+    f.findNext("rAF", false, true);
+    assertEquals(f.getIdMatches().size(), 1);
+    assertSame(f.getIdMatches().get(0), al2.getSequenceAt(0));
     assertTrue(f.getSearchResults().isEmpty());
 
     /*
-     * case sensitive
+     * find all matches
      */
-    f = new Finder(al2, null);
-    f.setFindAll(true);
-    f.setCaseSensitive(true);
-    f.setIncludeDescription(true);
+    f = new Finder(av2);
+    f.findAll("rAF", false, true);
+    assertEquals(f.getIdMatches().size(), 2);
+    assertSame(f.getIdMatches().get(0), al2.getSequenceAt(0));
+    assertSame(f.getIdMatches().get(1), al2.getSequenceAt(1));
+    assertTrue(f.getSearchResults().isEmpty());
 
-    f.find("RAF");
-    assertEquals(f.getIdMatch().size(), 1);
-    assertSame(f.getIdMatch().get(0), al2.getSequenceAt(0));
+    /*
+     * case sensitive
+     */
+    f = new Finder(av2);
+    f.findAll("RAF", true, true);
+    assertEquals(f.getIdMatches().size(), 1);
+    assertSame(f.getIdMatches().get(0), al2.getSequenceAt(0));
     assertTrue(f.getSearchResults().isEmpty());
 
     /*
@@ -184,27 +253,25 @@ public class FinderTest
     al2.getSequenceAt(0).setDescription("the efh sequence");
     al2.getSequenceAt(0).setName("mouseEFHkinase");
     al2.getSequenceAt(1).setName("humanEFHkinase");
-    f = new Finder(al2, null);
-    f.setFindAll(true);
-    f.setIncludeDescription(true);
+    f = new Finder(av2);
 
     /*
      * sequence matches should have no duplicates
      */
-    f.find("EFH");
-    assertEquals(f.getIdMatch().size(), 2);
-    assertSame(f.getIdMatch().get(0), al2.getSequenceAt(0));
-    assertSame(f.getIdMatch().get(1), al2.getSequenceAt(1));
+    f.findAll("EFH", false, true);
+    assertEquals(f.getIdMatches().size(), 2);
+    assertSame(f.getIdMatches().get(0), al2.getSequenceAt(0));
+    assertSame(f.getIdMatches().get(1), al2.getSequenceAt(1));
 
     assertEquals(f.getSearchResults().getSize(), 2);
     SearchResultMatchI match = f.getSearchResults().getResults().get(0);
-    assertSame(al2.getSequenceAt(1), match.getSequence());
-    assertEquals(5, match.getStart());
-    assertEquals(7, match.getEnd());
+    assertSame(match.getSequence(), al2.getSequenceAt(1));
+    assertEquals(match.getStart(), 5);
+    assertEquals(match.getEnd(), 7);
     match = f.getSearchResults().getResults().get(1);
-    assertSame(al2.getSequenceAt(2), match.getSequence());
-    assertEquals(4, match.getStart());
-    assertEquals(6, match.getEnd());
+    assertSame(match.getSequence(), al2.getSequenceAt(2));
+    assertEquals(match.getStart(), 4);
+    assertEquals(match.getEnd(), 6);
   }
 
   /**
@@ -213,105 +280,454 @@ public class FinderTest
   @Test(groups = "Functional")
   public void testFindAll_sequenceIds()
   {
-    Finder f = new Finder(al, null);
-    f.setFindAll(true);
+    Finder f = new Finder(av);
 
     /*
-     * case insensitive
+     * case insensitive; seq1 occurs twice in sequence id but
+     * only one match should be returned
      */
-    f.find("SEQ1");
-    assertEquals(f.getIdMatch().size(), 1);
-    assertSame(f.getIdMatch().get(0), al.getSequenceAt(0));
-    assertTrue(f.getSearchResults().isEmpty());
+    f.findAll("SEQ1", false, false);
+    assertEquals(f.getIdMatches().size(), 1);
+    assertSame(f.getIdMatches().get(0), al.getSequenceAt(0));
+    SearchResultsI searchResults = f.getSearchResults();
+    assertTrue(searchResults.isEmpty());
 
     /*
      * case sensitive
      */
-    f = new Finder(al, null);
-    f.setFindAll(true);
-    f.setCaseSensitive(true);
-    f.find("SEQ1");
-    assertTrue(f.getSearchResults().isEmpty());
+    f = new Finder(av);
+    f.findAll("SEQ1", true, false);
+    searchResults = f.getSearchResults();
+    assertTrue(searchResults.isEmpty());
 
     /*
      * match both sequence id and sequence
      */
     AlignmentI al2 = new Alignment(al);
+    AlignViewportI av2 = new AlignViewport(al2);
     al2.addSequence(new Sequence("aBz", "xyzabZpqrAbZ"));
-    f = new Finder(al2, null);
-    f.setFindAll(true);
-    f.find("ABZ");
-    assertEquals(f.getIdMatch().size(), 1);
-    assertSame(f.getIdMatch().get(0), al2.getSequenceAt(4));
-    assertEquals(f.getSearchResults().getSize(), 2);
-    SearchResultMatchI match = f.getSearchResults().getResults().get(0);
-    assertSame(al2.getSequenceAt(4), match.getSequence());
-    assertEquals(4, match.getStart());
-    assertEquals(6, match.getEnd());
-    match = f.getSearchResults().getResults().get(1);
-    assertSame(al2.getSequenceAt(4), match.getSequence());
-    assertEquals(10, match.getStart());
-    assertEquals(12, match.getEnd());
+    f = new Finder(av2);
+    f.findAll("ABZ", false, false);
+    assertEquals(f.getIdMatches().size(), 1);
+    assertSame(f.getIdMatches().get(0), al2.getSequenceAt(4));
+    searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 2);
+    SearchResultMatchI match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al2.getSequenceAt(4));
+    assertEquals(match.getStart(), 4);
+    assertEquals(match.getEnd(), 6);
+    match = searchResults.getResults().get(1);
+    assertSame(match.getSequence(), al2.getSequenceAt(4));
+    assertEquals(match.getStart(), 10);
+    assertEquals(match.getEnd(), 12);
   }
 
   /**
-   * Test finding all matches of a sequence pattern in an alignment
+   * Test finding next match of a sequence pattern in an alignment
    */
   @Test(groups = "Functional")
-  public void testFindAll_simpleMatch()
+  public void testFind_findNext()
   {
-    Finder f = new Finder(al, null);
-    f.setFindAll(true);
+    /*
+     * efh should be matched in seq2 only
+     */
+    FinderI f = new Finder(av);
+    f.findNext("EfH", false, false);
+    SearchResultsI searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 1);
+    SearchResultMatchI match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al.getSequenceAt(1));
+    assertEquals(match.getStart(), 5);
+    assertEquals(match.getEnd(), 7);
 
     /*
-     * case insensitive first
+     * I should be found in seq1 (twice) and seq2 (once)
      */
-    f.find("EfH");
+    f = new Finder(av);
+    f.findNext("I", false, false); // find next: seq1/16
+    searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 1);
+    match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al.getSequenceAt(0));
+    assertEquals(match.getStart(), 16);
+    assertEquals(match.getEnd(), 16);
+
+    f.findNext("I", false, false); // find next: seq1/18
+    searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 1);
+    match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al.getSequenceAt(0));
+    assertEquals(match.getStart(), 18);
+    assertEquals(match.getEnd(), 18);
+
+    f.findNext("I", false, false); // find next: seq2/8
+    searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 1);
+    match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al.getSequenceAt(1));
+    assertEquals(match.getStart(), 8);
+    assertEquals(match.getEnd(), 8);
+
+    f.findNext("I", false, false);
+    assertTrue(f.getSearchResults().isEmpty());
+
+    /*
+     * find should reset to start of alignment after a failed search
+     */
+    f.findNext("I", false, false); // find next: seq1/16
+    searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 1);
+    match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al.getSequenceAt(0));
+    assertEquals(match.getStart(), 16);
+    assertEquals(match.getEnd(), 16);
+  }
+
+  /**
+   * Test for JAL-2302 to verify that sub-matches are not included in a find all
+   * result
+   */
+  @Test(groups = "Functional")
+  public void testFind_maximalResultOnly()
+  {
+    Finder f = new Finder(av);
+    f.findAll("M+", false, false);
+    SearchResultsI searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 1);
+    SearchResultMatchI match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al.getSequenceAt(3));
+    assertEquals(match.getStart(), 4); // dataset sequence positions
+    assertEquals(match.getEnd(), 8); // base 1
+  }
+
+  /**
+   * Test finding all matches of a sequence pattern in an alignment
+   */
+  @Test(groups = "Functional")
+  public void testFind_findAll()
+  {
+    Finder f = new Finder(av);
+    f.findAll("EfH", false, false);
     SearchResultsI searchResults = f.getSearchResults();
     assertEquals(searchResults.getSize(), 2);
     SearchResultMatchI match = searchResults.getResults().get(0);
-    assertSame(al.getSequenceAt(1), match.getSequence());
-    assertEquals(5, match.getStart());
-    assertEquals(7, match.getEnd());
+    assertSame(match.getSequence(), al.getSequenceAt(1));
+    assertEquals(match.getStart(), 5);
+    assertEquals(match.getEnd(), 7);
     match = searchResults.getResults().get(1);
-    assertSame(al.getSequenceAt(2), match.getSequence());
-    assertEquals(4, match.getStart());
-    assertEquals(6, match.getEnd());
+    assertSame(match.getSequence(), al.getSequenceAt(2));
+    assertEquals(match.getStart(), 4);
+    assertEquals(match.getEnd(), 6);
 
     /*
-     * case sensitive
+     * find all I should find 2 positions in seq1, 1 in seq2
      */
-    f = new Finder(al, null);
-    f.setFindAll(true);
-    f.setCaseSensitive(true);
-    f.find("BC");
+    f.findAll("I", false, false);
     searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 3);
+    match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al.getSequenceAt(0));
+    assertEquals(match.getStart(), 16);
+    assertEquals(match.getEnd(), 16);
+    match = searchResults.getResults().get(1);
+    assertSame(match.getSequence(), al.getSequenceAt(0));
+    assertEquals(match.getStart(), 18);
+    assertEquals(match.getEnd(), 18);
+    match = searchResults.getResults().get(2);
+    assertSame(match.getSequence(), al.getSequenceAt(1));
+    assertEquals(match.getStart(), 8);
+    assertEquals(match.getEnd(), 8);
+  }
+
+  /**
+   * Test finding all matches, case-sensitive
+   */
+  @Test(groups = "Functional")
+  public void testFind_findAllCaseSensitive()
+  {
+    Finder f = new Finder(av);
+
+    /*
+     * BC should match seq1/9-10 and seq2/2-3
+     */
+    f.findAll("BC", true, false);
+    SearchResultsI searchResults = f.getSearchResults();
     assertEquals(searchResults.getSize(), 2);
+    SearchResultMatchI match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al.getSequenceAt(0));
+    assertEquals(match.getStart(), 9);
+    assertEquals(match.getEnd(), 10);
+    match = searchResults.getResults().get(1);
+    assertSame(match.getSequence(), al.getSequenceAt(1));
+    assertEquals(match.getStart(), 2);
+    assertEquals(match.getEnd(), 3);
+
+    /*
+     * bc should match seq3/1-2
+     */
+    f = new Finder(av);
+    f.findAll("bc", true, false);
+    searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 1);
+    match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al.getSequenceAt(2));
+    assertEquals(match.getStart(), 1);
+    assertEquals(match.getEnd(), 2);
+
+    f.findAll("bC", true, false);
+    assertTrue(f.getSearchResults().isEmpty());
+  }
+
+  /**
+   * Test finding next match of a sequence pattern in a selection group
+   */
+  @Test(groups = "Functional")
+  public void testFind_inSelection()
+  {
+    /*
+     * select sequences 2 and 3, columns 4-6 which contains
+     * BCD
+     * cdE
+     */
+    SequenceGroup sg = new SequenceGroup();
+    sg.setStartRes(3);
+    sg.setEndRes(5);
+    sg.addSequence(al.getSequenceAt(1), false);
+    sg.addSequence(al.getSequenceAt(2), false);
+    av.setSelectionGroup(sg);
+
+    FinderI f = new Finder(av);
+    f.findNext("b", false, false);
+    assertTrue(f.getIdMatches().isEmpty());
+    SearchResultsI searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 1);
+    SearchResultMatchI match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al.getSequenceAt(1));
+    assertEquals(match.getStart(), 2);
+    assertEquals(match.getEnd(), 2);
+
+    /*
+     * a second Find should not return the 'b' in seq3 as outside the selection
+     */
+    f.findNext("b", false, false);
+    assertTrue(f.getSearchResults().isEmpty());
+    assertTrue(f.getIdMatches().isEmpty());
+
+    f = new Finder(av);
+    f.findNext("d", false, false);
+    assertTrue(f.getIdMatches().isEmpty());
+    searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 1);
     match = searchResults.getResults().get(0);
-    assertSame(al.getSequenceAt(0), match.getSequence());
-    assertEquals(2, match.getStart());
-    assertEquals(3, match.getEnd());
+    assertSame(match.getSequence(), al.getSequenceAt(1));
+    assertEquals(match.getStart(), 4);
+    assertEquals(match.getEnd(), 4);
+    f.findNext("d", false, false);
+    assertTrue(f.getIdMatches().isEmpty());
+    searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 1);
+    match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al.getSequenceAt(2));
+    assertEquals(match.getStart(), 3);
+    assertEquals(match.getEnd(), 3);
+  }
+
+  /**
+   * Test finding all matches of a search pattern in a selection group
+   */
+  @Test(groups = "Functional")
+  public void testFind_findAllInSelection()
+  {
+    /*
+     * select sequences 2 and 3, columns 4-6 which contains
+     * BCD
+     * cdE
+     */
+    SequenceGroup sg = new SequenceGroup();
+    sg.setStartRes(3);
+    sg.setEndRes(5);
+    sg.addSequence(al.getSequenceAt(1), false);
+    sg.addSequence(al.getSequenceAt(2), false);
+    av.setSelectionGroup(sg);
+  
+    /*
+     * search for 'e' should match two sequence ids and one residue
+     */
+    Finder f = new Finder(av);
+    f.findAll("e", false, false);
+    assertEquals(f.getIdMatches().size(), 2);
+    assertSame(f.getIdMatches().get(0), al.getSequenceAt(1));
+    assertSame(f.getIdMatches().get(1), al.getSequenceAt(2));
+    SearchResultsI searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 1);
+    SearchResultMatchI match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al.getSequenceAt(2));
+    assertEquals(match.getStart(), 4);
+    assertEquals(match.getEnd(), 4);
+
+    /*
+     * search for 'Q' should match two sequence ids only
+     */
+    f = new Finder(av);
+    f.findAll("Q", false, false);
+    assertEquals(f.getIdMatches().size(), 2);
+    assertSame(f.getIdMatches().get(0), al.getSequenceAt(1));
+    assertSame(f.getIdMatches().get(1), al.getSequenceAt(2));
+    assertTrue(f.getSearchResults().isEmpty());
+  }
+
+  /**
+   * Test finding in selection with a sequence too short to reach it
+   */
+  @Test(groups = "Functional")
+  public void testFind_findAllInSelectionWithShortSequence()
+  {
+    /*
+     * select all sequences, columns 10-12
+     * BCD
+     * cdE
+     */
+    SequenceGroup sg = new SequenceGroup();
+    sg.setStartRes(9);
+    sg.setEndRes(11);
+    sg.addSequence(al.getSequenceAt(0), false);
+    sg.addSequence(al.getSequenceAt(1), false);
+    sg.addSequence(al.getSequenceAt(2), false);
+    sg.addSequence(al.getSequenceAt(3), false);
+    av.setSelectionGroup(sg);
+
+    /*
+     * search for 'I' should match two sequence positions
+     */
+    Finder f = new Finder(av);
+    f.findAll("I", false, false);
+    assertTrue(f.getIdMatches().isEmpty());
+    SearchResultsI searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 2);
+    SearchResultMatchI match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al.getSequenceAt(0));
+    assertEquals(match.getStart(), 16);
+    assertEquals(match.getEnd(), 16);
     match = searchResults.getResults().get(1);
-    assertSame(al.getSequenceAt(1), match.getSequence());
-    assertEquals(2, match.getStart());
-    assertEquals(3, match.getEnd());
+    assertSame(match.getSequence(), al.getSequenceAt(1));
+    assertEquals(match.getStart(), 8);
+    assertEquals(match.getEnd(), 8);
   }
 
   /**
-   * Test for JAL-2302 to verify that sub-matches are not included in a find all
-   * result
+   * Test that find does not report hidden positions
    */
   @Test(groups = "Functional")
-  public void testFind_maximalResultOnly()
+  public void testFind_withHiddenColumns()
   {
-    Finder f = new Finder(al, null);
-    f.setFindAll(true);
-    f.find("M+");
+    /*
+     * 0    5   9
+     * ABCD--EF-GHI
+     * A--BCDefHI
+     * --bcdEFH
+     * aa---aMMMMMaaa
+     */
+
+    /*
+     * hide 2-4 (CD- -BC bcd ---)
+     */
+    HiddenColumns hc = new HiddenColumns();
+    hc.hideColumns(2, 4);
+    al.setHiddenColumns(hc);
+
+    /*
+     * find all search for D should ignore hidden positions in seq1 and seq3,
+     * find the visible D in seq2
+     */
+    Finder f = new Finder(av);
+    f.findAll("D", false, false);
     SearchResultsI searchResults = f.getSearchResults();
     assertEquals(searchResults.getSize(), 1);
     SearchResultMatchI match = searchResults.getResults().get(0);
-    assertSame(al.getSequenceAt(3), match.getSequence());
-    assertEquals(4, match.getStart()); // dataset sequence positions
-    assertEquals(8, match.getEnd()); // base 1
+    assertSame(match.getSequence(), al.getSequenceAt(1));
+    assertEquals(match.getStart(), 4);
+    assertEquals(match.getEnd(), 4);
+
+    /*
+     * hide columns 2-5:
+     * find all 'aaa' should find end of seq4 only
+     */
+    hc.hideColumns(2, 5);
+    f = new Finder(av);
+    f.findAll("aaa", false, false);
+    searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 1);
+    match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al.getSequenceAt(3));
+    assertEquals(match.getStart(), 9);
+    assertEquals(match.getEnd(), 11);
+
+    /*
+     * find all 'BE' should not match across hidden columns in seq1
+     */
+    f.findAll("BE", false, false);
+    assertTrue(f.getSearchResults().isEmpty());
+
+    /*
+     * boundary case: hide columns at end of alignment
+     * search for H should match seq3/6 only
+     */
+    hc.revealAllHiddenColumns(new ColumnSelection());
+    hc.hideColumns(8, 13);
+    f = new Finder(av);
+    f.findNext("H", false, false);
+    searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 1);
+    match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al.getSequenceAt(2));
+    assertEquals(match.getStart(), 6);
+    assertEquals(match.getEnd(), 6);
+  }
+
+  @Test(groups = "Functional")
+  public void testFind_withHiddenColumnsAndSelection()
+  {
+    /*
+     * 0    5   9
+     * ABCD--EF-GHI
+     * A--BCDefHI
+     * --bcdEFH
+     * aa---aMMMMMaaa
+     */
+  
+    /*
+     * hide columns 2-4 and 6-7
+     */
+    HiddenColumns hc = new HiddenColumns();
+    hc.hideColumns(2, 4);
+    hc.hideColumns(6, 7);
+    al.setHiddenColumns(hc);
+  
+    /*
+     * select rows 2-3
+     */
+    SequenceGroup sg = new SequenceGroup();
+    sg.addSequence(al.getSequenceAt(1), false);
+    sg.addSequence(al.getSequenceAt(2), false);
+    sg.setStartRes(0);
+    sg.setEndRes(13);
+    av.setSelectionGroup(sg);
+
+    /*
+     * find all search for A or H
+     * should match seq2/1, seq2/7, not seq3/6
+     */
+    Finder f = new Finder(av);
+    f.findAll("[AH]", false, false);
+    SearchResultsI searchResults = f.getSearchResults();
+    assertEquals(searchResults.getSize(), 2);
+    SearchResultMatchI match = searchResults.getResults().get(0);
+    assertSame(match.getSequence(), al.getSequenceAt(1));
+    assertEquals(match.getStart(), 1);
+    assertEquals(match.getEnd(), 1);
+    match = searchResults.getResults().get(1);
+    assertSame(match.getSequence(), al.getSequenceAt(1));
+    assertEquals(match.getStart(), 7);
+    assertEquals(match.getEnd(), 7);
   }
 }
index 68b0b57..7990d21 100644 (file)
@@ -26,6 +26,7 @@ import static org.testng.AssertJUnit.assertTrue;
 import jalview.analysis.Finder;
 import jalview.api.AlignViewControllerI;
 import jalview.api.FeatureColourI;
+import jalview.api.FinderI;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResultsI;
@@ -223,10 +224,8 @@ public class AlignViewControllerTest
     /*
      *  test Match/Find works first
      */
-    Finder f = new Finder(af.getViewport().getAlignment(), null);
-    f.setFindAll(true);
-    f.setCaseSensitive(true);
-    f.find("M+");
+    FinderI f = new Finder(af.getViewport());
+    f.findAll("M+", true, false);
     assertEquals(
             "Finder found different set of results to manually created SearchResults",
             sr, f.getSearchResults());
index f1a6e20..349b5d1 100644 (file)
@@ -186,6 +186,25 @@ public class SearchResultsTest
     assertEquals(5, m.getEnd());
   }
 
+  @Test(groups = { "Functional" })
+  public void testMatchContains()
+  {
+    SequenceI seq1 = new Sequence("", "abcdefghijklm");
+    SequenceI seq2 = new Sequence("", "abcdefghijklm");
+    SearchResultMatchI m = new SearchResults().new Match(seq1, 2, 5);
+
+    assertTrue(m.contains(seq1, 2, 5));
+    assertTrue(m.contains(seq1, 3, 5));
+    assertTrue(m.contains(seq1, 2, 4));
+    assertTrue(m.contains(seq1, 3, 3));
+
+    assertFalse(m.contains(seq1, 2, 6));
+    assertFalse(m.contains(seq1, 1, 5));
+    assertFalse(m.contains(seq1, 1, 8));
+    assertFalse(m.contains(seq2, 3, 3));
+    assertFalse(m.contains(null, 3, 3));
+  }
+
   /**
    * test markColumns for creating column selections
    */
@@ -268,4 +287,20 @@ public class SearchResultsTest
             "Didn't set expected number of columns in total for two successive marks",
             2, tbs.cardinality());
   }
+
+  /**
+   * Test to verify adding doesn't create duplicate results
+   */
+  @Test(groups = { "Functional" })
+  public void testAddResult()
+  {
+    SequenceI seq1 = new Sequence("", "abcdefghijklm");
+    SearchResultsI sr = new SearchResults();
+    sr.addResult(seq1, 3, 5);
+    assertEquals(1, sr.getSize());
+    sr.addResult(seq1, 3, 5);
+    assertEquals(1, sr.getSize());
+    sr.addResult(seq1, 3, 6);
+    assertEquals(2, sr.getSize());
+  }
 }