JAL-2839 refactored analysis.Finder and tests
[jalview.git] / src / jalview / gui / Finder.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import jalview.api.AlignViewportI;
24 import jalview.datamodel.SearchResultMatchI;
25 import jalview.datamodel.SearchResultsI;
26 import jalview.datamodel.SequenceFeature;
27 import jalview.datamodel.SequenceI;
28 import jalview.jbgui.GFinder;
29 import jalview.util.MessageManager;
30 import jalview.viewmodel.AlignmentViewport;
31
32 import java.awt.Dimension;
33 import java.awt.event.ActionEvent;
34 import java.awt.event.KeyEvent;
35 import java.util.ArrayList;
36 import java.util.HashMap;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.Vector;
40 import java.util.regex.Pattern;
41 import java.util.regex.PatternSyntaxException;
42
43 import javax.swing.AbstractAction;
44 import javax.swing.JComponent;
45 import javax.swing.JInternalFrame;
46 import javax.swing.JLayeredPane;
47 import javax.swing.KeyStroke;
48 import javax.swing.event.InternalFrameAdapter;
49 import javax.swing.event.InternalFrameEvent;
50
51 /**
52  * Performs the menu option for searching the alignment, for the next or all
53  * matches. If matches are found, they are highlighted, and the user has the
54  * option to create a new feature on the alignment for the matched positions.
55  * 
56  * Searches can be for a simple base sequence, or may use a regular expression.
57  * Any gaps are ignored.
58  * 
59  * @author $author$
60  * @version $Revision$
61  */
62 public class Finder extends GFinder
63 {
64   private static final int MIN_WIDTH = 350;
65
66   private static final int MIN_HEIGHT = 120;
67
68   private static final int MY_HEIGHT = 120;
69
70   private static final int MY_WIDTH = 400;
71
72   private AlignViewportI av;
73
74   private AlignmentPanel ap;
75
76   private JInternalFrame frame;
77
78   /*
79    * sequence and column position of the last match,
80    * as currently used, and saved for each viewport
81    */
82   private int seqIndex = 0;
83
84   private int colIndex = -1;
85
86   private Map<AlignViewportI, Integer> seqIndices;
87
88   private Map<AlignViewportI, Integer> colIndices;
89
90   private SearchResultsI searchResults;
91
92   /*
93    * true if we only search a given alignment view
94    */
95   private boolean focusfixed;
96
97   /**
98    * Creates a new Finder object with no associated viewport or panel. Each Find
99    * or Find Next action will act on whichever viewport has focus at the time.
100    */
101   public Finder()
102   {
103     this(null, null);
104   }
105
106   /**
107    * Constructor given an associated viewport and alignment panel. Constructs
108    * and displays an internal frame where the user can enter a search string.
109    * 
110    * @param viewport
111    * @param alignPanel
112    */
113   public Finder(AlignmentViewport viewport, AlignmentPanel alignPanel)
114   {
115     av = viewport;
116     ap = alignPanel;
117     seqIndices = new HashMap<>();
118     colIndices = new HashMap<>();
119     focusfixed = viewport != null;
120     frame = new JInternalFrame();
121     frame.setContentPane(this);
122     frame.setLayer(JLayeredPane.PALETTE_LAYER);
123     frame.addInternalFrameListener(
124             new InternalFrameAdapter()
125             {
126               @Override
127               public void internalFrameClosing(InternalFrameEvent e)
128               {
129                 closeAction();
130               }
131             });
132     addEscapeHandler();
133     Desktop.addInternalFrame(frame, MessageManager.getString("label.find"),
134             MY_WIDTH, MY_HEIGHT);
135     frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
136     searchBox.requestFocus();
137   }
138
139   /**
140    * Add a handler for the Escape key when the window has focus
141    */
142   private void addEscapeHandler()
143   {
144     getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
145             .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Cancel");
146     getRootPane().getActionMap().put("Cancel", new AbstractAction()
147     {
148       @Override
149       public void actionPerformed(ActionEvent e)
150       {
151         closeAction();
152       }
153     });
154   }
155
156   /**
157    * Performs the 'Find Next' action on the alignment panel with focus
158    */
159   @Override
160   public void findNext_actionPerformed()
161   {
162     if (getFocusedViewport())
163     {
164       doSearch(false);
165     }
166   }
167
168   /**
169    * Performs the 'Find All' action on the alignment panel with focus
170    */
171   @Override
172   public void findAll_actionPerformed()
173   {
174     if (getFocusedViewport())
175     {
176       colIndex = -1;
177       seqIndex = 0;
178       doSearch(true);
179     }
180   }
181
182   /**
183    * if !focusfixed and not in a desktop environment, checks that av and ap are
184    * valid. Otherwise, gets the topmost alignment window and sets av and ap
185    * accordingly
186    * 
187    * @return false if no alignment window was found
188    */
189   boolean getFocusedViewport()
190   {
191     if (focusfixed || Desktop.desktop == null)
192     {
193       if (ap != null && av != null)
194       {
195         return true;
196       }
197       // we aren't in a desktop environment, so give up now.
198       return false;
199     }
200     // now checks further down the window stack to fix bug
201     // https://mantis.lifesci.dundee.ac.uk/view.php?id=36008
202     JInternalFrame[] frames = Desktop.desktop.getAllFrames();
203     for (int f = 0; f < frames.length; f++)
204     {
205       JInternalFrame alignFrame = frames[f];
206       if (alignFrame != null && alignFrame instanceof AlignFrame
207               && !alignFrame.isIcon())
208       {
209         av = ((AlignFrame) alignFrame).viewport;
210         ap = ((AlignFrame) alignFrame).alignPanel;
211
212         /*
213          * restore search position if switching to a 
214          * panel where we have previously searched
215          */
216         seqIndex = 0;
217         colIndex = -1;
218         if (seqIndices.containsKey(av))
219         {
220           seqIndex = seqIndices.get(av).intValue();
221         }
222         if (colIndices.containsKey(av))
223         {
224           colIndex = colIndices.get(av).intValue();
225         }
226         return true;
227       }
228     }
229     return false;
230   }
231
232   /**
233    * Opens a dialog that allows the user to create sequence features for the
234    * find match results.
235    */
236   @Override
237   public void createFeatures_actionPerformed()
238   {
239     List<SequenceI> seqs = new ArrayList<>();
240     List<SequenceFeature> features = new ArrayList<>();
241
242     String searchString = searchBox.getEditor().getItem().toString().trim();
243     String desc = "Search Results";
244
245     /*
246      * assemble dataset sequences, and template new sequence features,
247      * for the amend features dialog
248      */
249     for (SearchResultMatchI match : searchResults.getResults())
250     {
251       seqs.add(match.getSequence().getDatasetSequence());
252       features.add(new SequenceFeature(searchString, desc,
253               match
254               .getStart(), match.getEnd(), desc));
255     }
256
257     if (ap.getSeqPanel().seqCanvas.getFeatureRenderer().amendFeatures(seqs,
258             features, true, ap))
259     {
260       /*
261        * ensure feature display is turned on to show the new features,
262        * and remove them as highlighted regions
263        */
264       ap.alignFrame.showSeqFeatures.setSelected(true);
265       av.setShowSequenceFeatures(true);
266       ap.highlightSearchResults(null);
267     }
268   }
269
270   /**
271    * Search the alignment for the next or all matches. If 'all matches', a
272    * dialog is shown with the number of sequence ids and subsequences matched.
273    * 
274    * @param doFindAll
275    */
276   void doSearch(boolean doFindAll)
277   {
278     createFeatures.setEnabled(false);
279
280     String searchString = searchBox.getUserInput().trim();
281
282     if (isInvalidSearchString(searchString))
283     {
284       return;
285     }
286     // TODO: extend finder to match descriptions, features and annotation, and
287     // other stuff
288     // TODO: add switches to control what is searched - sequences, IDS,
289     // descriptions, features
290     jalview.analysis.Finder finder = new jalview.analysis.Finder(
291             av.getAlignment(), av.getSelectionGroup(), seqIndex, colIndex);
292     finder.setCaseSensitive(caseSensitive.isSelected());
293     finder.setIncludeDescription(searchDescription.isSelected());
294
295     finder.setFindAll(doFindAll);
296
297     finder.find(searchString);
298
299     seqIndex = finder.getSequenceIndex();
300     colIndex = finder.getColumnIndex();
301     seqIndices.put(av, seqIndex);
302     colIndices.put(av, colIndex);
303
304     searchResults = finder.getSearchResults();
305     Vector<SequenceI> idMatch = finder.getIdMatch();
306     ap.getIdPanel().highlightSearchResults(idMatch);
307
308     if (searchResults.isEmpty())
309     {
310       searchResults = null;
311     }
312     else
313     {
314       createFeatures.setEnabled(true);
315     }
316
317     ap.highlightSearchResults(searchResults);
318     // TODO: add enablers for 'SelectSequences' or 'SelectColumns' or
319     // 'SelectRegion' selection
320     if (idMatch.isEmpty() && searchResults == null)
321     {
322       JvOptionPane.showInternalMessageDialog(this,
323               MessageManager.getString("label.finished_searching"), null,
324               JvOptionPane.INFORMATION_MESSAGE);
325       colIndex = -1;
326       seqIndex = 0;
327     }
328     else
329     {
330       if (doFindAll)
331       {
332         // then we report the matches that were found
333         String message = (idMatch.size() > 0) ? "" + idMatch.size() + " IDs"
334                 : "";
335         if (searchResults != null)
336         {
337           if (idMatch.size() > 0 && searchResults.getSize() > 0)
338           {
339             message += " and ";
340           }
341           message += searchResults.getSize()
342                   + " subsequence matches found.";
343         }
344         JvOptionPane.showInternalMessageDialog(this, message, null,
345                 JvOptionPane.INFORMATION_MESSAGE);
346         colIndex = -1;
347         seqIndex = 0;
348       }
349     }
350     searchBox.updateCache();
351   }
352
353   /**
354    * Displays an error dialog, and answers false, if the search string is
355    * invalid, else answers true.
356    * 
357    * @param searchString
358    * @return
359    */
360   protected boolean isInvalidSearchString(String searchString)
361   {
362     String error = getSearchValidationError(searchString);
363     if (error == null)
364     {
365       return false;
366     }
367     JvOptionPane.showInternalMessageDialog(this, error,
368             MessageManager.getString("label.invalid_search"), // $NON-NLS-1$
369             JvOptionPane.ERROR_MESSAGE);
370     return true;
371   }
372
373   /**
374    * Returns an error message string if the search string is invalid, else
375    * returns null.
376    * 
377    * Currently validation is limited to checking the string is not empty, and is
378    * a valid regular expression (simple searches for base sub-sequences will
379    * pass this test). Additional validations may be added in future if the
380    * search syntax is expanded.
381    * 
382    * @param searchString
383    * @return
384    */
385   protected String getSearchValidationError(String searchString)
386   {
387     String error = null;
388     if (searchString == null || searchString.length() == 0)
389     {
390       error = MessageManager.getString("label.invalid_search");
391     }
392     try
393     {
394       Pattern.compile(searchString);
395     } catch (PatternSyntaxException e)
396     {
397       error = MessageManager.getString("error.invalid_regex") + ": "
398               + e.getDescription();
399     }
400     return error;
401   }
402
403   protected void closeAction()
404   {
405     frame.setVisible(false);
406     frame.dispose();
407     searchBox.persistCache();
408     if (getFocusedViewport())
409     {
410       ap.alignFrame.requestFocus();
411     }
412   }
413 }