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