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