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