Class |
Line # |
Actions |
|||
---|---|---|---|---|---|
Finder | 64 | 107 | 43 |
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 | 0 | public Finder(AlignmentPanel alignPanel, boolean fixedFocus, String scope) |
105 | { | |
106 | 0 | av = alignPanel.getAlignViewport(); |
107 | 0 | ap = alignPanel; |
108 | 0 | focusFixed = fixedFocus; |
109 | 0 | finders = new HashMap<>(); |
110 | 0 | frame = new JInternalFrame(); |
111 | 0 | frame.setFrameIcon(null); |
112 | 0 | frame.setContentPane(this); |
113 | 0 | frame.setLayer(JLayeredPane.PALETTE_LAYER); |
114 | 0 | frame.addInternalFrameListener(new InternalFrameAdapter() |
115 | { | |
116 | 0 | @Override |
117 | public void internalFrameClosing(InternalFrameEvent e) | |
118 | { | |
119 | 0 | closeAction(); |
120 | } | |
121 | }); | |
122 | 0 | frame.addFocusListener(new FocusAdapter() |
123 | { | |
124 | 0 | @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 | 0 | getFocusedViewport(); |
132 | } | |
133 | }); | |
134 | ||
135 | 0 | addEscapeHandler(); |
136 | ||
137 | 0 | String title = MessageManager.getString("label.find"); |
138 | 0 | if (scope != null) |
139 | { | |
140 | 0 | title += " " + scope; |
141 | } | |
142 | 0 | Desktop.addInternalFrame(frame, title, MY_WIDTH, MY_HEIGHT); |
143 | 0 | frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT)); |
144 | 0 | searchBox.getComponent().requestFocus(); |
145 | } | |
146 | ||
147 | /** | |
148 | * Add a handler for the Escape key when the window has focus | |
149 | */ | |
150 | 0 | private void addEscapeHandler() |
151 | { | |
152 | 0 | getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) |
153 | .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Cancel"); | |
154 | 0 | getRootPane().getActionMap().put("Cancel", new AbstractAction() |
155 | { | |
156 | 0 | @Override |
157 | public void actionPerformed(ActionEvent e) | |
158 | { | |
159 | 0 | closeAction(); |
160 | } | |
161 | }); | |
162 | } | |
163 | ||
164 | /** | |
165 | * Performs the 'Find Next' action on the alignment panel with focus | |
166 | */ | |
167 | 0 | @Override |
168 | public void findNext_actionPerformed() | |
169 | { | |
170 | 0 | if (getFocusedViewport()) |
171 | { | |
172 | 0 | doSearch(false); |
173 | } | |
174 | } | |
175 | ||
176 | /** | |
177 | * Performs the 'Find All' action on the alignment panel with focus | |
178 | */ | |
179 | 0 | @Override |
180 | public void findAll_actionPerformed() | |
181 | { | |
182 | 0 | if (getFocusedViewport()) |
183 | { | |
184 | 0 | 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 | 0 | boolean getFocusedViewport() |
197 | { | |
198 | 0 | if (focusFixed || Desktop.desktop == null) |
199 | { | |
200 | 0 | if (ap != null && av != null) |
201 | { | |
202 | 0 | ignoreHidden.setEnabled(av.hasHiddenColumns()); |
203 | 0 | return true; |
204 | } | |
205 | // we aren't in a desktop environment, so give up now. | |
206 | 0 | 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 | 0 | JInternalFrame[] frames = Desktop.desktop.getAllFrames(); |
211 | 0 | for (int f = 0; f < frames.length; f++) |
212 | { | |
213 | 0 | JInternalFrame alignFrame = frames[f]; |
214 | 0 | if (alignFrame != null && alignFrame instanceof AlignFrame |
215 | && !alignFrame.isIcon()) | |
216 | { | |
217 | 0 | av = ((AlignFrame) alignFrame).viewport; |
218 | 0 | ap = ((AlignFrame) alignFrame).alignPanel; |
219 | 0 | ignoreHidden.setEnabled(av.hasHiddenColumns()); |
220 | 0 | return true; |
221 | } | |
222 | } | |
223 | 0 | 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 | 0 | @Override |
231 | public void createFeatures_actionPerformed() | |
232 | { | |
233 | 0 | if (searchResults.isEmpty()) |
234 | { | |
235 | 0 | return; // shouldn't happen |
236 | } | |
237 | 0 | List<SequenceI> seqs = new ArrayList<>(); |
238 | 0 | List<SequenceFeature> features = new ArrayList<>(); |
239 | ||
240 | 0 | String searchString = searchBox.getUserInput(); |
241 | 0 | String desc = "Search Results"; |
242 | ||
243 | /* | |
244 | * assemble dataset sequences, and template new sequence features, | |
245 | * for the amend features dialog | |
246 | */ | |
247 | 0 | for (SearchResultMatchI match : searchResults.getResults()) |
248 | { | |
249 | 0 | seqs.add(match.getSequence().getDatasetSequence()); |
250 | 0 | features.add(new SequenceFeature(searchString, desc, match.getStart(), |
251 | match.getEnd(), desc)); | |
252 | } | |
253 | ||
254 | 0 | new FeatureEditor(ap, seqs, features, true).showDialog(); |
255 | } | |
256 | ||
257 | 0 | @Override |
258 | protected void copyToClipboard_actionPerformed() | |
259 | { | |
260 | 0 | if (searchResults.isEmpty()) |
261 | { | |
262 | 0 | return; // shouldn't happen |
263 | } | |
264 | // assume viewport controller has same searchResults as we do... | |
265 | 0 | 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 | 0 | void doSearch(boolean doFindAll) |
275 | { | |
276 | 0 | createFeatures.setEnabled(false); |
277 | 0 | copyToClipboard.setEnabled(false); |
278 | ||
279 | 0 | String searchString = searchBox.getUserInput(); |
280 | ||
281 | 0 | if (isInvalidSearchString(searchString)) |
282 | { | |
283 | 0 | 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 | 0 | FinderI finder = finders.get(av); |
290 | 0 | if (finder == null) |
291 | { | |
292 | /* | |
293 | * first time we've searched this viewport | |
294 | */ | |
295 | 0 | finder = new jalview.analysis.Finder(av); |
296 | 0 | finders.put(av, finder); |
297 | } | |
298 | 0 | finder.setFeatureRenderer(ap.getFeatureRenderer()); |
299 | ||
300 | 0 | boolean isCaseSensitive = caseSensitive.isSelected(); |
301 | 0 | boolean doSearchDescription = searchDescription.isSelected(); |
302 | 0 | boolean doSearchfeatures = searchFeatures.isSelected(); |
303 | 0 | boolean skipHidden = ignoreHidden.isSelected(); |
304 | 0 | if (doFindAll) |
305 | { | |
306 | 0 | finder.findAll(searchString, isCaseSensitive, doSearchDescription, |
307 | doSearchfeatures, skipHidden); | |
308 | } | |
309 | else | |
310 | { | |
311 | 0 | finder.findNext(searchString, isCaseSensitive, doSearchDescription, |
312 | doSearchfeatures, skipHidden); | |
313 | } | |
314 | ||
315 | 0 | searchResults = finder.getSearchResults(); |
316 | 0 | List<SequenceI> idMatch = finder.getIdMatches(); |
317 | 0 | ap.getIdPanel().highlightSearchResults(idMatch); |
318 | ||
319 | 0 | if (searchResults.isEmpty()) |
320 | { | |
321 | 0 | searchResults = null; |
322 | } | |
323 | else | |
324 | { | |
325 | 0 | createFeatures.setEnabled(true); |
326 | 0 | copyToClipboard.setEnabled(true); |
327 | } | |
328 | ||
329 | 0 | searchBox.updateCache(); |
330 | ||
331 | 0 | ap.highlightSearchResults(searchResults); |
332 | // TODO: add enablers for 'SelectSequences' or 'SelectColumns' or | |
333 | // 'SelectRegion' selection | |
334 | 0 | if (idMatch.isEmpty() && searchResults == null) |
335 | { | |
336 | 0 | JvOptionPane.showInternalMessageDialog(this, |
337 | MessageManager.getString("label.finished_searching"), null, | |
338 | JvOptionPane.PLAIN_MESSAGE); | |
339 | } | |
340 | else | |
341 | { | |
342 | 0 | if (doFindAll) |
343 | { | |
344 | // then we report the matches that were found | |
345 | 0 | StringBuilder message = new StringBuilder(); |
346 | 0 | if (idMatch.size() > 0) |
347 | { | |
348 | 0 | message.append(idMatch.size()).append(" IDs"); |
349 | } | |
350 | 0 | if (searchResults != null) |
351 | { | |
352 | 0 | if (idMatch.size() > 0 && searchResults.getCount() > 0) |
353 | { | |
354 | 0 | message.append(" ").append(MessageManager.getString("label.and") |
355 | .toLowerCase(Locale.ROOT)).append(" "); | |
356 | } | |
357 | 0 | message.append(MessageManager.formatMessage( |
358 | "label.subsequence_matches_found", | |
359 | searchResults.getCount())); | |
360 | } | |
361 | 0 | 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 | 0 | protected boolean isInvalidSearchString(String searchString) |
375 | { | |
376 | 0 | String error = getSearchValidationError(searchString); |
377 | 0 | if (error == null) |
378 | { | |
379 | 0 | return false; |
380 | } | |
381 | 0 | JvOptionPane.showInternalMessageDialog(this, error, |
382 | MessageManager.getString("label.invalid_search"), // $NON-NLS-1$ | |
383 | JvOptionPane.ERROR_MESSAGE); | |
384 | 0 | 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 | 0 | protected String getSearchValidationError(String searchString) |
400 | { | |
401 | 0 | String error = null; |
402 | 0 | if (searchString == null || searchString.length() == 0) |
403 | { | |
404 | 0 | error = MessageManager.getString("label.invalid_search"); |
405 | } | |
406 | 0 | try |
407 | { | |
408 | 0 | Pattern.compile(searchString); |
409 | } catch (PatternSyntaxException e) | |
410 | { | |
411 | 0 | error = MessageManager.getString("error.invalid_regex") + ": " |
412 | + e.getDescription(); | |
413 | } | |
414 | 0 | return error; |
415 | } | |
416 | ||
417 | 0 | protected void closeAction() |
418 | { | |
419 | 0 | frame.setVisible(false); |
420 | 0 | frame.dispose(); |
421 | 0 | searchBox.persistCache(); |
422 | 0 | if (getFocusedViewport()) |
423 | { | |
424 | 0 | ap.alignFrame.requestFocus(); |
425 | } | |
426 | } | |
427 | } |