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