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