Class |
Line # |
Actions |
|||
---|---|---|---|---|---|
IdPanel | 52 | 138 | 65 | ||
IdPanel.ScrollThread | 551 | 18 | 9 |
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.BorderLayout; | |
24 | import java.awt.event.ActionEvent; | |
25 | import java.awt.event.ActionListener; | |
26 | import java.awt.event.MouseEvent; | |
27 | import java.awt.event.MouseListener; | |
28 | import java.awt.event.MouseMotionListener; | |
29 | import java.util.List; | |
30 | ||
31 | import javax.swing.JPanel; | |
32 | import javax.swing.JPopupMenu; | |
33 | import javax.swing.SwingUtilities; | |
34 | import javax.swing.Timer; | |
35 | import javax.swing.ToolTipManager; | |
36 | ||
37 | import jalview.datamodel.AlignmentAnnotation; | |
38 | import jalview.datamodel.Sequence; | |
39 | import jalview.datamodel.SequenceGroup; | |
40 | import jalview.datamodel.SequenceI; | |
41 | import jalview.gui.SeqPanel.MousePos; | |
42 | import jalview.io.SequenceAnnotationReport; | |
43 | import jalview.util.MessageManager; | |
44 | import jalview.util.Platform; | |
45 | import jalview.viewmodel.AlignmentViewport; | |
46 | import jalview.viewmodel.ViewportRanges; | |
47 | ||
48 | /** | |
49 | * This panel hosts alignment sequence ids and responds to mouse clicks on them, | |
50 | * as well as highlighting ids matched by a search from the Find menu. | |
51 | */ | |
52 | public class IdPanel extends JPanel | |
53 | implements MouseListener, MouseMotionListener | |
54 | { | |
55 | private IdCanvas idCanvas; | |
56 | ||
57 | protected AlignmentViewport av; | |
58 | ||
59 | protected AlignmentPanel alignPanel; | |
60 | ||
61 | ScrollThread scrollThread = null; | |
62 | ||
63 | int offy; | |
64 | ||
65 | // int width; | |
66 | int lastid = -1; | |
67 | ||
68 | boolean mouseDragging = false; | |
69 | ||
70 | private final SequenceAnnotationReport seqAnnotReport; | |
71 | ||
72 | /** | |
73 | * Creates a new IdPanel object. | |
74 | * | |
75 | * @param av | |
76 | * @param parent | |
77 | */ | |
78 | 478 | public IdPanel(AlignViewport av, AlignmentPanel parent) |
79 | { | |
80 | 478 | this.av = av; |
81 | 478 | alignPanel = parent; |
82 | 478 | setIdCanvas(new IdCanvas(av)); |
83 | 478 | seqAnnotReport = new SequenceAnnotationReport(true); |
84 | 478 | setLayout(new BorderLayout()); |
85 | 478 | add(getIdCanvas(), BorderLayout.CENTER); |
86 | 478 | addMouseListener(this); |
87 | 478 | addMouseMotionListener(this); |
88 | 478 | addMouseWheelListener(alignPanel.getSeqPanel()); |
89 | 478 | ToolTipManager.sharedInstance().registerComponent(this); |
90 | } | |
91 | ||
92 | /** | |
93 | * Responds to mouse movement by setting tooltip text for the sequence id | |
94 | * under the mouse (or possibly annotation label, when in wrapped mode) | |
95 | * | |
96 | * @param e | |
97 | */ | |
98 | 0 | @Override |
99 | public void mouseMoved(MouseEvent e) | |
100 | { | |
101 | 0 | SeqPanel sp = alignPanel.getSeqPanel(); |
102 | 0 | MousePos pos = sp.findMousePosition(e); |
103 | 0 | if (pos.isOverAnnotation()) |
104 | { | |
105 | /* | |
106 | * mouse is over an annotation label in wrapped mode | |
107 | */ | |
108 | 0 | AlignmentAnnotation[] anns = av.getAlignment() |
109 | .getAlignmentAnnotation(); | |
110 | 0 | AlignmentAnnotation annotation = anns[pos.annotationIndex]; |
111 | 0 | setToolTipText(AnnotationLabels.getTooltip(annotation)); |
112 | 0 | alignPanel.alignFrame.setStatus( |
113 | AnnotationLabels.getStatusMessage(annotation, anns)); | |
114 | } | |
115 | else | |
116 | { | |
117 | 0 | int seq = Math.max(0, pos.seqIndex); |
118 | 0 | if (seq < av.getAlignment().getHeight()) |
119 | { | |
120 | 0 | SequenceI sequence = av.getAlignment().getSequenceAt(seq); |
121 | 0 | StringBuilder tip = new StringBuilder(64); |
122 | 0 | tip.append(sequence.getDisplayId(true)).append(" "); |
123 | 0 | seqAnnotReport.createTooltipAnnotationReport(tip, sequence, |
124 | av.isShowDBRefs(), av.isShowNPFeats(), sp.seqCanvas.fr); | |
125 | 0 | setToolTipText(JvSwingUtils.wrapTooltip(true, tip.toString())); |
126 | ||
127 | 0 | StringBuilder text = new StringBuilder(); |
128 | 0 | text.append("Sequence ").append(String.valueOf(seq + 1)) |
129 | .append(" ID: ").append(sequence.getName()); | |
130 | 0 | alignPanel.alignFrame.setStatus(text.toString()); |
131 | } | |
132 | } | |
133 | } | |
134 | ||
135 | /** | |
136 | * Responds to a mouse drag by selecting the sequences under the dragged | |
137 | * region. | |
138 | * | |
139 | * @param e | |
140 | */ | |
141 | 0 | @Override |
142 | public void mouseDragged(MouseEvent e) | |
143 | { | |
144 | 0 | mouseDragging = true; |
145 | ||
146 | 0 | MousePos pos = alignPanel.getSeqPanel().findMousePosition(e); |
147 | 0 | if (pos.isOverAnnotation()) |
148 | { | |
149 | // mouse is over annotation label in wrapped mode | |
150 | 0 | return; |
151 | } | |
152 | ||
153 | 0 | int seq = Math.max(0, pos.seqIndex); |
154 | ||
155 | 0 | if (seq < lastid) |
156 | { | |
157 | 0 | selectSeqs(lastid - 1, seq); |
158 | } | |
159 | 0 | else if (seq > lastid) |
160 | { | |
161 | 0 | selectSeqs(lastid + 1, seq); |
162 | } | |
163 | ||
164 | 0 | lastid = seq; |
165 | 0 | alignPanel.paintAlignment(false, false); |
166 | } | |
167 | ||
168 | /** | |
169 | * Handle a mouse click event. Currently only responds to a double-click. The | |
170 | * action is to try to open a browser window at a URL that searches for the | |
171 | * selected sequence id. The search URL is configured in Preferences | | |
172 | * Connections | URL link from Sequence ID. For example: | |
173 | * | |
174 | * http://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$SEQUENCE_ID$ | |
175 | * | |
176 | * @param e | |
177 | */ | |
178 | 0 | @Override |
179 | public void mouseClicked(MouseEvent e) | |
180 | { | |
181 | /* | |
182 | * Ignore single click. Ignore 'left' click followed by 'right' click (user | |
183 | * selects a row then its pop-up menu). | |
184 | */ | |
185 | 0 | if (e.getClickCount() < 2 || SwingUtilities.isRightMouseButton(e)) |
186 | { | |
187 | // reinstate isRightMouseButton check to ignore mouse-related popup events | |
188 | // note - this does nothing on default MacBookPro force-trackpad config! | |
189 | 0 | return; |
190 | } | |
191 | ||
192 | 0 | MousePos pos = alignPanel.getSeqPanel().findMousePosition(e); |
193 | 0 | int seq = pos.seqIndex; |
194 | 0 | if (pos.isOverAnnotation() || seq < 0) |
195 | { | |
196 | 0 | return; |
197 | } | |
198 | ||
199 | 0 | String id = av.getAlignment().getSequenceAt(seq).getName(); |
200 | 0 | String url = Preferences.sequenceUrlLinks.getPrimaryUrl(id); |
201 | ||
202 | 0 | try |
203 | { | |
204 | 0 | jalview.util.BrowserLauncher.openURL(url); |
205 | } catch (Exception ex) | |
206 | { | |
207 | 0 | JvOptionPane.showInternalMessageDialog(Desktop.desktop, |
208 | MessageManager.getString("label.web_browser_not_found_unix"), | |
209 | MessageManager.getString("label.web_browser_not_found"), | |
210 | JvOptionPane.WARNING_MESSAGE); | |
211 | 0 | ex.printStackTrace(); |
212 | } | |
213 | } | |
214 | ||
215 | /** | |
216 | * On (re-)entering the panel, stop any scrolling | |
217 | * | |
218 | * @param e | |
219 | */ | |
220 | 0 | @Override |
221 | public void mouseEntered(MouseEvent e) | |
222 | { | |
223 | 0 | stopScrolling(); |
224 | } | |
225 | ||
226 | /** | |
227 | * Interrupts the scroll thread if one is running | |
228 | */ | |
229 | 0 | void stopScrolling() |
230 | { | |
231 | 0 | if (scrollThread != null) |
232 | { | |
233 | 0 | scrollThread.stopScrolling(); |
234 | 0 | scrollThread = null; |
235 | } | |
236 | } | |
237 | ||
238 | /** | |
239 | * DOCUMENT ME! | |
240 | * | |
241 | * @param e | |
242 | * DOCUMENT ME! | |
243 | */ | |
244 | 0 | @Override |
245 | public void mouseExited(MouseEvent e) | |
246 | { | |
247 | 0 | if (av.getWrapAlignment()) |
248 | { | |
249 | 0 | return; |
250 | } | |
251 | ||
252 | 0 | if (mouseDragging) |
253 | { | |
254 | /* | |
255 | * on mouse drag above or below the panel, start | |
256 | * scrolling if there are more sequences to show | |
257 | */ | |
258 | 0 | ViewportRanges ranges = av.getRanges(); |
259 | 0 | if (e.getY() < 0 && ranges.getStartSeq() > 0) |
260 | { | |
261 | 0 | startScrolling(true); |
262 | } | |
263 | 0 | else if (e.getY() >= getHeight() |
264 | && ranges.getEndSeq() <= av.getAlignment().getHeight()) | |
265 | { | |
266 | 0 | startScrolling(false); |
267 | } | |
268 | } | |
269 | } | |
270 | ||
271 | /** | |
272 | * Starts scrolling either up or down | |
273 | * | |
274 | * @param up | |
275 | */ | |
276 | 0 | void startScrolling(boolean up) |
277 | { | |
278 | 0 | scrollThread = new ScrollThread(up); |
279 | 0 | if (Platform.isJS()) |
280 | { | |
281 | /* | |
282 | * for JalviewJS using Swing Timer | |
283 | */ | |
284 | 0 | Timer t = new Timer(20, new ActionListener() |
285 | { | |
286 | 0 | @Override |
287 | public void actionPerformed(ActionEvent e) | |
288 | { | |
289 | 0 | if (scrollThread != null) |
290 | { | |
291 | // if (!scrollOnce() {t.stop();}) gives compiler error :-( | |
292 | 0 | scrollThread.scrollOnce(); |
293 | } | |
294 | } | |
295 | }); | |
296 | 0 | t.addActionListener(new ActionListener() |
297 | { | |
298 | 0 | @Override |
299 | public void actionPerformed(ActionEvent e) | |
300 | { | |
301 | 0 | if (scrollThread == null) |
302 | { | |
303 | // IdPanel.stopScrolling called | |
304 | 0 | t.stop(); |
305 | } | |
306 | } | |
307 | }); | |
308 | 0 | t.start(); |
309 | } | |
310 | else | |
311 | /** | |
312 | * Java only | |
313 | * | |
314 | * @j2sIgnore | |
315 | */ | |
316 | { | |
317 | 0 | scrollThread.start(); |
318 | } | |
319 | } | |
320 | ||
321 | /** | |
322 | * Respond to a mouse press. Does nothing for (left) double-click as this is | |
323 | * handled by mouseClicked(). | |
324 | * | |
325 | * Right mouse down - construct and show context menu. | |
326 | * | |
327 | * Ctrl-down or Shift-down - add to or expand current selection group if there | |
328 | * is one. | |
329 | * | |
330 | * Mouse down - select this sequence. | |
331 | * | |
332 | * @param e | |
333 | */ | |
334 | 0 | @Override |
335 | public void mousePressed(MouseEvent e) | |
336 | { | |
337 | 0 | if (e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(e)) |
338 | { | |
339 | 0 | return; |
340 | } | |
341 | ||
342 | 0 | MousePos pos = alignPanel.getSeqPanel().findMousePosition(e); |
343 | ||
344 | 0 | if (e.isPopupTrigger()) // Mac reports this in mousePressed |
345 | { | |
346 | 0 | showPopupMenu(e, pos); |
347 | 0 | return; |
348 | } | |
349 | ||
350 | /* | |
351 | * defer right-mouse click handling to mouseReleased on Windows | |
352 | * (where isPopupTrigger() will answer true) | |
353 | * NB isRightMouseButton is also true for Cmd-click on Mac | |
354 | */ | |
355 | 0 | if (Platform.isWinRightButton(e)) |
356 | { | |
357 | 0 | return; |
358 | } | |
359 | ||
360 | 0 | if ((av.getSelectionGroup() == null) |
361 | || (!jalview.util.Platform.isControlDown(e) && !e.isShiftDown() | |
362 | && av.getSelectionGroup() != null)) | |
363 | { | |
364 | 0 | av.setSelectionGroup(new SequenceGroup()); |
365 | 0 | av.getSelectionGroup().setStartRes(0); |
366 | 0 | av.getSelectionGroup().setEndRes(av.getAlignment().getWidth() - 1); |
367 | } | |
368 | ||
369 | 0 | if (e.isShiftDown() && (lastid != -1)) |
370 | { | |
371 | 0 | selectSeqs(lastid, pos.seqIndex); |
372 | } | |
373 | else | |
374 | { | |
375 | 0 | selectSeq(pos.seqIndex); |
376 | } | |
377 | ||
378 | 0 | av.isSelectionGroupChanged(true); |
379 | ||
380 | 0 | alignPanel.paintAlignment(false, false); |
381 | } | |
382 | ||
383 | /** | |
384 | * Build and show the popup-menu at the right-click mouse position | |
385 | * | |
386 | * @param e | |
387 | */ | |
388 | 0 | void showPopupMenu(MouseEvent e, MousePos pos) |
389 | { | |
390 | 0 | if (pos.isOverAnnotation()) |
391 | { | |
392 | 0 | showAnnotationMenu(e, pos); |
393 | 0 | return; |
394 | } | |
395 | ||
396 | 0 | Sequence sq = (Sequence) av.getAlignment().getSequenceAt(pos.seqIndex); |
397 | 0 | if (sq != null) |
398 | { | |
399 | 0 | PopupMenu pop = new PopupMenu(alignPanel, sq, |
400 | Preferences.getGroupURLLinks()); | |
401 | 0 | pop.show(this, e.getX(), e.getY()); |
402 | } | |
403 | } | |
404 | ||
405 | /** | |
406 | * On right mouse click on a Consensus annotation label, shows a limited popup | |
407 | * menu, with options to configure the consensus calculation and rendering. | |
408 | * | |
409 | * @param e | |
410 | * @param pos | |
411 | * @see AnnotationLabels#showPopupMenu(MouseEvent) | |
412 | */ | |
413 | 0 | void showAnnotationMenu(MouseEvent e, MousePos pos) |
414 | { | |
415 | 0 | if (pos.annotationIndex == -1) |
416 | { | |
417 | 0 | return; |
418 | } | |
419 | 0 | AlignmentAnnotation[] anns = this.av.getAlignment() |
420 | .getAlignmentAnnotation(); | |
421 | 0 | if (anns == null || pos.annotationIndex >= anns.length) |
422 | { | |
423 | 0 | return; |
424 | } | |
425 | 0 | AlignmentAnnotation ann = anns[pos.annotationIndex]; |
426 | 0 | if (!ann.label.contains("Consensus")) |
427 | { | |
428 | 0 | return; |
429 | } | |
430 | ||
431 | 0 | JPopupMenu pop = new JPopupMenu( |
432 | MessageManager.getString("label.annotations")); | |
433 | 0 | AnnotationLabels.addConsensusMenuOptions(this.alignPanel, ann, pop); |
434 | 0 | pop.show(this, e.getX(), e.getY()); |
435 | } | |
436 | ||
437 | /** | |
438 | * Toggle whether the sequence is part of the current selection group. | |
439 | * | |
440 | * @param seq | |
441 | */ | |
442 | 0 | void selectSeq(int seq) |
443 | { | |
444 | 0 | lastid = seq; |
445 | ||
446 | 0 | SequenceI pickedSeq = av.getAlignment().getSequenceAt(seq); |
447 | 0 | av.getSelectionGroup().addOrRemove(pickedSeq, false); |
448 | } | |
449 | ||
450 | /** | |
451 | * Add contiguous rows of the alignment to the current selection group. Does | |
452 | * nothing if there is no selection group. | |
453 | * | |
454 | * @param start | |
455 | * @param end | |
456 | */ | |
457 | 0 | void selectSeqs(int start, int end) |
458 | { | |
459 | 0 | if (av.getSelectionGroup() == null) |
460 | { | |
461 | 0 | return; |
462 | } | |
463 | ||
464 | 0 | if (end >= av.getAlignment().getHeight()) |
465 | { | |
466 | 0 | end = av.getAlignment().getHeight() - 1; |
467 | } | |
468 | ||
469 | 0 | lastid = start; |
470 | ||
471 | 0 | if (end < start) |
472 | { | |
473 | 0 | int tmp = start; |
474 | 0 | start = end; |
475 | 0 | end = tmp; |
476 | 0 | lastid = end; |
477 | } | |
478 | ||
479 | 0 | for (int i = start; i <= end; i++) |
480 | { | |
481 | 0 | av.getSelectionGroup().addSequence(av.getAlignment().getSequenceAt(i), |
482 | false); | |
483 | } | |
484 | } | |
485 | ||
486 | /** | |
487 | * Respond to mouse released. Refreshes the display and triggers broadcast of | |
488 | * the new selection group to any listeners. | |
489 | * | |
490 | * @param e | |
491 | */ | |
492 | 0 | @Override |
493 | public void mouseReleased(MouseEvent e) | |
494 | { | |
495 | 0 | if (scrollThread != null) |
496 | { | |
497 | 0 | stopScrolling(); |
498 | } | |
499 | 0 | MousePos pos = alignPanel.getSeqPanel().findMousePosition(e); |
500 | ||
501 | 0 | mouseDragging = false; |
502 | 0 | PaintRefresher.Refresh(this, av.getSequenceSetId()); |
503 | // always send selection message when mouse is released | |
504 | 0 | av.sendSelection(); |
505 | ||
506 | 0 | if (e.isPopupTrigger()) // Windows reports this in mouseReleased |
507 | { | |
508 | 0 | showPopupMenu(e, pos); |
509 | } | |
510 | } | |
511 | ||
512 | /** | |
513 | * Highlight sequence ids that match the given list, and if necessary scroll | |
514 | * to the start sequence of the list. | |
515 | * | |
516 | * @param list | |
517 | */ | |
518 | 0 | public void highlightSearchResults(List<SequenceI> list) |
519 | { | |
520 | 0 | getIdCanvas().setHighlighted(list); |
521 | ||
522 | 0 | if (list == null || list.isEmpty()) |
523 | { | |
524 | 0 | return; |
525 | } | |
526 | ||
527 | 0 | int index = av.getAlignment().findIndex(list.get(0)); |
528 | ||
529 | // do we need to scroll the panel? | |
530 | 0 | if ((av.getRanges().getStartSeq() > index) |
531 | || (av.getRanges().getEndSeq() < index)) | |
532 | { | |
533 | 0 | av.getRanges().setStartSeq(index); |
534 | } | |
535 | } | |
536 | ||
537 | 11208 | public IdCanvas getIdCanvas() |
538 | { | |
539 | 11208 | return idCanvas; |
540 | } | |
541 | ||
542 | 478 | public void setIdCanvas(IdCanvas idCanvas) |
543 | { | |
544 | 478 | this.idCanvas = idCanvas; |
545 | } | |
546 | ||
547 | /** | |
548 | * Performs scrolling of the visible alignment up or down, adding newly | |
549 | * visible sequences to the current selection | |
550 | */ | |
551 | class ScrollThread extends Thread | |
552 | { | |
553 | private boolean running = false; | |
554 | ||
555 | private boolean up; | |
556 | ||
557 | /** | |
558 | * Constructor for a thread that scrolls either up or down | |
559 | * | |
560 | * @param up | |
561 | */ | |
562 | 0 | public ScrollThread(boolean up) |
563 | { | |
564 | 0 | this.up = up; |
565 | 0 | setName("IdPanel$ScrollThread$" + String.valueOf(up)); |
566 | } | |
567 | ||
568 | /** | |
569 | * Sets a flag to stop the scrolling | |
570 | */ | |
571 | 0 | public void stopScrolling() |
572 | { | |
573 | 0 | running = false; |
574 | } | |
575 | ||
576 | /** | |
577 | * Scrolls the alignment either up or down, one row at a time, adding newly | |
578 | * visible sequences to the current selection. Speed is limited to a maximum | |
579 | * of ten rows per second. The thread exits when the end of the alignment is | |
580 | * reached or a flag is set to stop it by a call to stopScrolling. | |
581 | */ | |
582 | 0 | @Override |
583 | public void run() | |
584 | { | |
585 | 0 | running = true; |
586 | ||
587 | 0 | while (running) |
588 | { | |
589 | 0 | running = scrollOnce(); |
590 | 0 | try |
591 | { | |
592 | 0 | Thread.sleep(100); |
593 | } catch (Exception ex) | |
594 | { | |
595 | } | |
596 | } | |
597 | 0 | IdPanel.this.scrollThread = null; |
598 | } | |
599 | ||
600 | /** | |
601 | * Scrolls one row up or down. Answers true if a scroll could be done, false | |
602 | * if not (top or bottom of alignment reached). | |
603 | */ | |
604 | 0 | boolean scrollOnce() |
605 | { | |
606 | 0 | ViewportRanges ranges = IdPanel.this.av.getRanges(); |
607 | 0 | if (ranges.scrollUp(up)) |
608 | { | |
609 | 0 | int toSeq = up ? ranges.getStartSeq() : ranges.getEndSeq(); |
610 | 0 | int fromSeq = toSeq < lastid ? lastid - 1 : lastid + 1; |
611 | 0 | IdPanel.this.selectSeqs(fromSeq, toSeq); |
612 | 0 | lastid = toSeq; |
613 | 0 | alignPanel.paintAlignment(false, false); |
614 | 0 | return true; |
615 | } | |
616 | ||
617 | 0 | return false; |
618 | } | |
619 | } | |
620 | } |