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