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