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