JAL-2473 added improvement to apply default / custom minimum dimension for various...
[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.datamodel.SearchResultMatchI;
24 import jalview.datamodel.SearchResultsI;
25 import jalview.datamodel.SequenceFeature;
26 import jalview.datamodel.SequenceI;
27 import jalview.jbgui.GFinder;
28 import jalview.util.MessageManager;
29 import jalview.viewmodel.AlignmentViewport;
30
31 import java.awt.Dimension;
32 import java.awt.event.ActionEvent;
33 import java.awt.event.KeyEvent;
34 import java.util.Vector;
35 import java.util.regex.Pattern;
36 import java.util.regex.PatternSyntaxException;
37
38 import javax.swing.AbstractAction;
39 import javax.swing.JComponent;
40 import javax.swing.JInternalFrame;
41 import javax.swing.JLayeredPane;
42 import javax.swing.KeyStroke;
43
44 /**
45  * Performs the menu option for searching the alignment, for the next or all
46  * matches. If matches are found, they are highlighted, and the user has the
47  * option to create a new feature on the alignment for the matched positions.
48  * 
49  * Searches can be for a simple base sequence, or may use a regular expression.
50  * Any gaps are ignored.
51  * 
52  * @author $author$
53  * @version $Revision$
54  */
55 public class Finder extends GFinder
56 {
57   private static final int HEIGHT = 120;
58
59   private static final int WIDTH = 400;
60
61   AlignmentViewport av;
62
63   AlignmentPanel ap;
64
65   private static final int MIN_WIDTH = 350;
66
67   private static final int MIN_HEIGHT = 120;
68
69   JInternalFrame frame;
70
71   int seqIndex = 0;
72
73   int resIndex = -1;
74
75   SearchResultsI searchResults;
76
77   /**
78    * Creates a new Finder object with no associated viewport or panel.
79    */
80   public Finder()
81   {
82     this(null, null);
83     focusfixed = false;
84   }
85
86   /**
87    * Constructor given an associated viewport and alignment panel. Constructs
88    * and displays an internal frame where the user can enter a search string.
89    * 
90    * @param viewport
91    * @param alignPanel
92    */
93   public Finder(AlignmentViewport viewport, AlignmentPanel alignPanel)
94   {
95     av = viewport;
96     ap = alignPanel;
97     focusfixed = true;
98     frame = new JInternalFrame();
99     frame.setContentPane(this);
100     frame.setLayer(JLayeredPane.PALETTE_LAYER);
101     addEscapeHandler();
102     Desktop.addInternalFrame(frame, MessageManager.getString("label.find"),
103             WIDTH, HEIGHT);
104     frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
105     textfield.requestFocus();
106   }
107
108   /**
109    * Add a handler for the Escape key when the window has focus
110    */
111   private void addEscapeHandler()
112   {
113     getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
114             KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Cancel");
115     getRootPane().getActionMap().put("Cancel", new AbstractAction()
116     {
117       @Override
118       public void actionPerformed(ActionEvent e)
119       {
120         escapeActionPerformed();
121       }
122     });
123   }
124
125   /**
126    * Close the panel on Escape key press
127    */
128   protected void escapeActionPerformed()
129   {
130     setVisible(false);
131     frame.dispose();
132   }
133
134   /**
135    * Performs the 'Find Next' action.
136    * 
137    * @param e
138    */
139   @Override
140   public void findNext_actionPerformed(ActionEvent e)
141   {
142     if (getFocusedViewport())
143     {
144       doSearch(false);
145     }
146   }
147
148   /**
149    * Performs the 'Find All' action.
150    * 
151    * @param e
152    */
153   @Override
154   public void findAll_actionPerformed(ActionEvent e)
155   {
156     if (getFocusedViewport())
157     {
158       resIndex = -1;
159       seqIndex = 0;
160       doSearch(true);
161     }
162   }
163
164   /**
165    * do we only search a given alignment view ?
166    */
167   private boolean focusfixed;
168
169   /**
170    * if !focusfixed and not in a desktop environment, checks that av and ap are
171    * valid. Otherwise, gets the topmost alignment window and sets av and ap
172    * accordingly
173    * 
174    * @return false if no alignment window was found
175    */
176   boolean getFocusedViewport()
177   {
178     if (focusfixed || Desktop.desktop == null)
179     {
180       if (ap != null && av != null)
181       {
182         return true;
183       }
184       // we aren't in a desktop environment, so give up now.
185       return false;
186     }
187     // now checks further down the window stack to fix bug
188     // https://mantis.lifesci.dundee.ac.uk/view.php?id=36008
189     JInternalFrame[] frames = Desktop.desktop.getAllFrames();
190     for (int f = 0; f < frames.length; f++)
191     {
192       JInternalFrame frame = frames[f];
193       if (frame != null && frame instanceof AlignFrame)
194       {
195         av = ((AlignFrame) frame).viewport;
196         ap = ((AlignFrame) frame).alignPanel;
197         return true;
198       }
199     }
200     return false;
201   }
202
203   /**
204    * DOCUMENT ME!
205    * 
206    * @param e
207    *          DOCUMENT ME!
208    */
209   @Override
210   public void createNewGroup_actionPerformed(ActionEvent e)
211   {
212     SequenceI[] seqs = new SequenceI[searchResults.getSize()];
213     SequenceFeature[] features = new SequenceFeature[searchResults
214             .getSize()];
215
216     int i = 0;
217     for (SearchResultMatchI match : searchResults.getResults())
218     {
219       seqs[i] = match.getSequence().getDatasetSequence();
220
221       features[i] = new SequenceFeature(textfield.getText().trim(),
222               "Search Results", null, match.getStart(), match.getEnd(),
223               "Search Results");
224       i++;
225     }
226
227     if (ap.getSeqPanel().seqCanvas.getFeatureRenderer().amendFeatures(seqs,
228             features, true, ap))
229     {
230       ap.alignFrame.showSeqFeatures.setSelected(true);
231       av.setShowSequenceFeatures(true);
232       ap.highlightSearchResults(null);
233     }
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 findAll
241    */
242   void doSearch(boolean findAll)
243   {
244     createNewGroup.setEnabled(false);
245
246     String searchString = textfield.getText().trim();
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     jalview.analysis.Finder finder = new jalview.analysis.Finder(
257             av.getAlignment(), av.getSelectionGroup(), seqIndex, resIndex);
258     finder.setCaseSensitive(caseSensitive.isSelected());
259     finder.setIncludeDescription(searchDescription.isSelected());
260
261     finder.setFindAll(findAll);
262
263     finder.find(searchString); // returns true if anything was actually found
264
265     seqIndex = finder.getSeqIndex();
266     resIndex = finder.getResIndex();
267
268     searchResults = finder.getSearchResults(); // find(regex,
269     // caseSensitive.isSelected(), )
270     Vector<SequenceI> idMatch = finder.getIdMatch();
271     boolean haveResults = false;
272     // set or reset the GUI
273     if ((idMatch.size() > 0))
274     {
275       haveResults = true;
276       ap.getIdPanel().highlightSearchResults(idMatch);
277     }
278     else
279     {
280       ap.getIdPanel().highlightSearchResults(null);
281     }
282
283     if (searchResults.getSize() > 0)
284     {
285       haveResults = true;
286       createNewGroup.setEnabled(true);
287     }
288     else
289     {
290       searchResults = null;
291     }
292
293     // if allResults is null, this effectively switches displaySearch flag in
294     // seqCanvas
295     ap.highlightSearchResults(searchResults);
296     // TODO: add enablers for 'SelectSequences' or 'SelectColumns' or
297     // 'SelectRegion' selection
298     if (!haveResults)
299     {
300       JvOptionPane.showInternalMessageDialog(this,
301               MessageManager.getString("label.finished_searching"), null,
302               JvOptionPane.INFORMATION_MESSAGE);
303       resIndex = -1;
304       seqIndex = 0;
305     }
306     else
307     {
308       if (findAll)
309       {
310         // then we report the matches that were found
311         String message = (idMatch.size() > 0) ? "" + idMatch.size()
312                 + " IDs" : "";
313         if (searchResults != null)
314         {
315           if (idMatch.size() > 0 && searchResults.getSize() > 0)
316           {
317             message += " and ";
318           }
319           message += searchResults.getSize()
320                   + " subsequence matches found.";
321         }
322         JvOptionPane.showInternalMessageDialog(this, message, null,
323                 JvOptionPane.INFORMATION_MESSAGE);
324         resIndex = -1;
325         seqIndex = 0;
326       }
327     }
328
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 }