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