JAL-3490 match count independent of contiguous matches count
[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.api.FinderI;
25 import jalview.datamodel.SearchResultMatchI;
26 import jalview.datamodel.SearchResultsI;
27 import jalview.datamodel.SequenceFeature;
28 import jalview.datamodel.SequenceI;
29 import jalview.jbgui.GFinder;
30 import jalview.util.MessageManager;
31
32 import java.awt.Dimension;
33 import java.awt.Graphics;
34 import java.awt.event.ActionEvent;
35 import java.awt.event.KeyEvent;
36 import java.util.ArrayList;
37 import java.util.HashMap;
38 import java.util.List;
39 import java.util.Map;
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 = 150;
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    * Finder agent per viewport searched
80    */
81   private Map<AlignViewportI, FinderI> finders;
82
83   private SearchResultsI searchResults;
84
85   /**
86    * Constructor given an associated alignment panel. Constructs and displays an
87    * internal frame where the user can enter a search string.
88    * 
89    * @param alignPanel
90    */
91   public Finder(AlignmentPanel alignPanel)
92   {
93     av = alignPanel.getAlignViewport();
94     ap = alignPanel;
95     finders = new HashMap<>();
96     frame = new JInternalFrame();
97     frame.setContentPane(this);
98     frame.setLayer(JLayeredPane.PALETTE_LAYER);
99     frame.addInternalFrameListener(
100             new InternalFrameAdapter()
101             {
102               @Override
103               public void internalFrameClosing(InternalFrameEvent e)
104               {
105                 closeAction();
106               }
107             });
108     addEscapeHandler();
109
110     Desktop.addInternalFrame(frame, MessageManager.getString("label.find"),
111             MY_WIDTH, MY_HEIGHT);
112     frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
113     searchBox.requestFocus();
114   }
115
116   /**
117    * Add a handler for the Escape key when the window has focus
118    */
119   private void addEscapeHandler()
120   {
121     getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
122             .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Cancel");
123     getRootPane().getActionMap().put("Cancel", new AbstractAction()
124     {
125       @Override
126       public void actionPerformed(ActionEvent e)
127       {
128         closeAction();
129       }
130     });
131   }
132
133   /**
134    * Performs the 'Find Next' action on the alignment panel with focus
135    */
136   @Override
137   public void findNext_actionPerformed()
138   {
139     if (getFocusedViewport())
140     {
141       doSearch(false);
142     }
143   }
144
145   /**
146    * Performs the 'Find All' action on the alignment panel with focus
147    */
148   @Override
149   public void findAll_actionPerformed()
150   {
151     if (getFocusedViewport())
152     {
153       doSearch(true);
154     }
155   }
156
157   /**
158    * if !focusfixed and not in a desktop environment, checks that av and ap are
159    * valid. Otherwise, gets the topmost alignment window and sets av and ap
160    * accordingly. Also sets the 'ignore hidden' checkbox disabled if the viewport
161    * has no hidden columns.
162    * 
163    * @return false if no alignment window was found
164    */
165   boolean getFocusedViewport()
166   {
167     if (Desktop.desktop == null)
168     {
169       if (ap != null && av != null)
170       {
171         return true;
172       }
173       // we aren't in a desktop environment, so give up now.
174       return false;
175     }
176     // now checks further down the window stack to fix bug
177     // https://mantis.lifesci.dundee.ac.uk/view.php?id=36008
178     JInternalFrame[] frames = Desktop.desktop.getAllFrames();
179     for (int f = 0; f < frames.length; f++)
180     {
181       JInternalFrame alignFrame = frames[f];
182       if (alignFrame != null && alignFrame instanceof AlignFrame
183               && !alignFrame.isIcon())
184       {
185         av = ((AlignFrame) alignFrame).viewport;
186         ap = ((AlignFrame) alignFrame).alignPanel;
187         ignoreHidden.setEnabled(av.hasHiddenColumns());
188         return true;
189       }
190     }
191     return false;
192   }
193
194   /**
195    * Opens a dialog that allows the user to create sequence features for the
196    * find match results.
197    */
198   @Override
199   public void createFeatures_actionPerformed()
200   {
201     List<SequenceI> seqs = new ArrayList<>();
202     List<SequenceFeature> features = new ArrayList<>();
203
204     String searchString = searchBox.getEditor().getItem().toString().trim();
205     String desc = "Search Results";
206
207     /*
208      * assemble dataset sequences, and template new sequence features,
209      * for the amend features dialog
210      */
211     for (SearchResultMatchI match : searchResults.getResults())
212     {
213       seqs.add(match.getSequence().getDatasetSequence());
214       features.add(new SequenceFeature(searchString, desc,
215               match
216               .getStart(), match.getEnd(), desc));
217     }
218
219     if (ap.getSeqPanel().seqCanvas.getFeatureRenderer().amendFeatures(seqs,
220             features, true, ap))
221     {
222       /*
223        * ensure feature display is turned on to show the new features,
224        * and remove them as highlighted regions
225        */
226       ap.alignFrame.showSeqFeatures.setSelected(true);
227       av.setShowSequenceFeatures(true);
228       ap.highlightSearchResults(null);
229     }
230   }
231
232   /**
233    * Search the alignment for the next or all matches. If 'all matches', a
234    * dialog is shown with the number of sequence ids and subsequences matched.
235    * 
236    * @param doFindAll
237    */
238   void doSearch(boolean doFindAll)
239   {
240     createFeatures.setEnabled(false);
241
242     String searchString = searchBox.getUserInput().trim();
243
244     if (isInvalidSearchString(searchString))
245     {
246       return;
247     }
248     // TODO: extend finder to match descriptions, features and annotation, and
249     // other stuff
250     // TODO: add switches to control what is searched - sequences, IDS,
251     // descriptions, features
252     FinderI finder = finders.get(av);
253     if (finder == null)
254     {
255       /*
256        * first time we've searched this viewport
257        */
258       finder = new jalview.analysis.Finder(av);
259       finders.put(av, finder);
260     }
261
262     boolean isCaseSensitive = caseSensitive.isSelected();
263     boolean doSearchDescription = searchDescription.isSelected();
264     boolean skipHidden = ignoreHidden.isSelected();
265     if (doFindAll)
266     {
267       finder.findAll(searchString, isCaseSensitive, doSearchDescription,
268               skipHidden);
269     }
270     else
271     {
272       finder.findNext(searchString, isCaseSensitive, doSearchDescription,
273               skipHidden);
274     }
275
276     searchResults = finder.getSearchResults();
277     List<SequenceI> idMatch = finder.getIdMatches();
278     ap.getIdPanel().highlightSearchResults(idMatch);
279
280     if (searchResults.isEmpty())
281     {
282       searchResults = null;
283     }
284     else
285     {
286       createFeatures.setEnabled(true);
287     }
288
289     ap.highlightSearchResults(searchResults);
290     // TODO: add enablers for 'SelectSequences' or 'SelectColumns' or
291     // 'SelectRegion' selection
292     if (idMatch.isEmpty() && searchResults == null)
293     {
294       JvOptionPane.showInternalMessageDialog(this,
295               MessageManager.getString("label.finished_searching"), null,
296               JvOptionPane.INFORMATION_MESSAGE);
297     }
298     else
299     {
300       if (doFindAll)
301       {
302         // then we report the matches that were found
303         String message = (idMatch.size() > 0) ? "" + idMatch.size() + " IDs"
304                 : "";
305         if (searchResults != null)
306         {
307           if (idMatch.size() > 0 && searchResults.getCount() > 0)
308           {
309             message += " and ";
310           }
311           message += searchResults.getCount()
312                   + " subsequence matches found.";
313         }
314         JvOptionPane.showInternalMessageDialog(this, message, null,
315                 JvOptionPane.INFORMATION_MESSAGE);
316       }
317     }
318     searchBox.updateCache();
319   }
320
321   /**
322    * Displays an error dialog, and answers false, if the search string is
323    * invalid, else answers true.
324    * 
325    * @param searchString
326    * @return
327    */
328   protected boolean isInvalidSearchString(String searchString)
329   {
330     String error = getSearchValidationError(searchString);
331     if (error == null)
332     {
333       return false;
334     }
335     JvOptionPane.showInternalMessageDialog(this, error,
336             MessageManager.getString("label.invalid_search"), // $NON-NLS-1$
337             JvOptionPane.ERROR_MESSAGE);
338     return true;
339   }
340
341   /**
342    * Returns an error message string if the search string is invalid, else
343    * returns null.
344    * 
345    * Currently validation is limited to checking the string is not empty, and is
346    * a valid regular expression (simple searches for base sub-sequences will
347    * pass this test). Additional validations may be added in future if the
348    * search syntax is expanded.
349    * 
350    * @param searchString
351    * @return
352    */
353   protected String getSearchValidationError(String searchString)
354   {
355     String error = null;
356     if (searchString == null || searchString.length() == 0)
357     {
358       error = MessageManager.getString("label.invalid_search");
359     }
360     try
361     {
362       Pattern.compile(searchString);
363     } catch (PatternSyntaxException e)
364     {
365       error = MessageManager.getString("error.invalid_regex") + ": "
366               + e.getDescription();
367     }
368     return error;
369   }
370
371   protected void closeAction()
372   {
373     frame.setVisible(false);
374     frame.dispose();
375     searchBox.persistCache();
376     if (getFocusedViewport())
377     {
378       ap.alignFrame.requestFocus();
379     }
380   }
381
382   @Override
383   protected void paintComponent(Graphics g)
384   {
385     /*
386      * enable 'hidden regions' option only if
387      * 'top' viewport has hidden columns
388      */
389     getFocusedViewport();
390     super.paintComponent(g);
391   }
392 }