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