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