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