JAL-3071 corrections to IdPanel$ScrollThread
[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       scrollThread = null;
277     }
278   }
279
280   /**
281    * DOCUMENT ME!
282    * 
283    * @param e
284    *          DOCUMENT ME!
285    */
286   @Override
287   public void mouseExited(MouseEvent e)
288   {
289     if (av.getWrapAlignment())
290     {
291       return;
292     }
293
294     if (mouseDragging)
295     {
296       /*
297        * on mouse drag above or below the panel, start 
298        * scrolling if there are more sequences to show
299        */
300       ViewportRanges ranges = av.getRanges();
301       if (e.getY() < 0 && ranges.getStartSeq() > 0)
302       {
303         startScrolling(true);
304       }
305       else if (e.getY() >= getHeight()
306               && ranges.getEndSeq() <= av.getAlignment().getHeight())
307       {
308         startScrolling(false);
309       }
310     }
311   }
312
313   /**
314    * Starts scrolling either up or down
315    * 
316    * @param up
317    */
318   void startScrolling(boolean up)
319   {
320     scrollThread = new ScrollThread(up);
321     if (!Platform.isJS())
322     {
323       /*
324        * Java - run in a new thread
325        */
326       scrollThread.start();
327     }
328     else
329     {
330       /*
331        * for JalviewJS using Swing Timer
332        */
333       Timer t = new Timer(20, new ActionListener()
334       {
335         @Override
336         public void actionPerformed(ActionEvent e)
337         {
338           if (scrollThread != null)
339           {
340             // if (!scrollOnce() {t.stop();}) gives compiler error :-(
341             scrollThread.scrollOnce();
342           }
343         }
344       });
345       t.addActionListener(new ActionListener()
346       {
347         @Override
348         public void actionPerformed(ActionEvent e)
349         {
350           if (scrollThread == null)
351           {
352             // IdPanel.stopScrolling called
353             t.stop();
354           }
355         }
356       });
357       t.start();
358     }
359   }
360
361   /**
362    * Respond to a mouse press. Does nothing for (left) double-click as this is
363    * handled by mouseClicked().
364    * 
365    * Right mouse down - construct and show context menu.
366    * 
367    * Ctrl-down or Shift-down - add to or expand current selection group if there
368    * is one.
369    * 
370    * Mouse down - select this sequence.
371    * 
372    * @param e
373    */
374   @Override
375   public void mousePressed(MouseEvent e)
376   {
377     if (e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(e))
378     {
379       return;
380     }
381
382     MousePos pos = alignPanel.getSeqPanel().findMousePosition(e);
383     
384     if (e.isPopupTrigger()) // Mac reports this in mousePressed
385     {
386       showPopupMenu(e, pos);
387       return;
388     }
389
390     /*
391      * defer right-mouse click handling to mouseReleased on Windows
392      * (where isPopupTrigger() will answer true)
393      * NB isRightMouseButton is also true for Cmd-click on Mac
394      */
395     if (Platform.isWinRightButton(e))
396     {
397       return;
398     }
399
400     if ((av.getSelectionGroup() == null)
401             || (!jalview.util.Platform.isControlDown(e) && !e.isShiftDown()
402                     && av.getSelectionGroup() != null))
403     {
404       av.setSelectionGroup(new SequenceGroup());
405       av.getSelectionGroup().setStartRes(0);
406       av.getSelectionGroup().setEndRes(av.getAlignment().getWidth() - 1);
407     }
408
409     if (e.isShiftDown() && (lastid != -1))
410     {
411       selectSeqs(lastid, pos.seqIndex);
412     }
413     else
414     {
415       selectSeq(pos.seqIndex);
416     }
417
418     av.isSelectionGroupChanged(true);
419
420     alignPanel.paintAlignment(false, false);
421   }
422
423   /**
424    * Build and show the popup-menu at the right-click mouse position
425    * 
426    * @param e
427    */
428   void showPopupMenu(MouseEvent e, MousePos pos)
429   {
430     if (pos.isOverAnnotation())
431     {
432       showAnnotationMenu(e, pos);
433       return;
434     }
435
436     Sequence sq = (Sequence) av.getAlignment().getSequenceAt(pos.seqIndex);
437
438     /*
439      *  build a new links menu based on the current links
440      *  and any non-positional features
441      */
442     List<SequenceFeature> features = null;
443     if (sq != null)
444     {
445     List<String> nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
446       features = sq.getFeatures().getNonPositionalFeatures();
447     for (SequenceFeature sf : features)
448     {
449       if (sf.links != null)
450       {
451         nlinks.addAll(sf.links);
452       }
453     }
454     }
455
456     PopupMenu pop = new PopupMenu(alignPanel, sq, features,
457             Preferences.getGroupURLLinks());
458     pop.show(this, e.getX(), e.getY());
459   }
460
461   /**
462    * On right mouse click on a Consensus annotation label, shows a limited popup
463    * menu, with options to configure the consensus calculation and rendering.
464    * 
465    * @param e
466    * @param pos
467    * @see AnnotationLabels#showPopupMenu(MouseEvent)
468    */
469   void showAnnotationMenu(MouseEvent e, MousePos pos)
470   {
471     if (pos.annotationIndex == -1)
472     {
473       return;
474     }
475     AlignmentAnnotation[] anns = this.av.getAlignment()
476             .getAlignmentAnnotation();
477     if (anns == null || pos.annotationIndex >= anns.length)
478     {
479       return;
480     }
481     AlignmentAnnotation ann = anns[pos.annotationIndex];
482     if (!ann.label.contains("Consensus"))
483     {
484       return;
485     }
486
487     JPopupMenu pop = new JPopupMenu(
488             MessageManager.getString("label.annotations"));
489     AnnotationLabels.addConsensusMenuOptions(this.alignPanel, ann, pop);
490     pop.show(this, e.getX(), e.getY());
491   }
492
493   /**
494    * Toggle whether the sequence is part of the current selection group.
495    * 
496    * @param seq
497    */
498   void selectSeq(int seq)
499   {
500     lastid = seq;
501
502     SequenceI pickedSeq = av.getAlignment().getSequenceAt(seq);
503     av.getSelectionGroup().addOrRemove(pickedSeq, false);
504   }
505
506   /**
507    * Add contiguous rows of the alignment to the current selection group. Does
508    * nothing if there is no selection group.
509    * 
510    * @param start
511    * @param end
512    */
513   void selectSeqs(int start, int end)
514   {
515     if (av.getSelectionGroup() == null)
516     {
517       return;
518     }
519
520     if (end >= av.getAlignment().getHeight())
521     {
522       end = av.getAlignment().getHeight() - 1;
523     }
524
525     lastid = start;
526
527     if (end < start)
528     {
529       int tmp = start;
530       start = end;
531       end = tmp;
532       lastid = end;
533     }
534
535     for (int i = start; i <= end; i++)
536     {
537       av.getSelectionGroup().addSequence(av.getAlignment().getSequenceAt(i),
538               false);
539     }
540   }
541
542   /**
543    * Respond to mouse released. Refreshes the display and triggers broadcast of
544    * the new selection group to any listeners.
545    * 
546    * @param e
547    */
548   @Override
549   public void mouseReleased(MouseEvent e)
550   {
551     if (scrollThread != null)
552     {
553       stopScrolling();
554     }
555     MousePos pos = alignPanel.getSeqPanel().findMousePosition(e);
556
557     mouseDragging = false;
558     PaintRefresher.Refresh(this, av.getSequenceSetId());
559     // always send selection message when mouse is released
560     av.sendSelection();
561
562     if (e.isPopupTrigger()) // Windows reports this in mouseReleased
563     {
564       showPopupMenu(e, pos);
565     }
566   }
567
568   /**
569    * Highlight sequence ids that match the given list, and if necessary scroll
570    * to the start sequence of the list.
571    * 
572    * @param list
573    */
574   public void highlightSearchResults(List<SequenceI> list)
575   {
576     getIdCanvas().setHighlighted(list);
577
578     if (list == null || list.isEmpty())
579     {
580       return;
581     }
582
583     int index = av.getAlignment().findIndex(list.get(0));
584
585     // do we need to scroll the panel?
586     if ((av.getRanges().getStartSeq() > index)
587             || (av.getRanges().getEndSeq() < index))
588     {
589       av.getRanges().setStartSeq(index);
590     }
591   }
592
593   public IdCanvas getIdCanvas()
594   {
595     return idCanvas;
596   }
597
598   public void setIdCanvas(IdCanvas idCanvas)
599   {
600     this.idCanvas = idCanvas;
601   }
602
603   /**
604    * Performs scrolling of the visible alignment up or down, adding newly
605    * visible sequences to the current selection
606    */
607   class ScrollThread extends Thread
608   {
609     private boolean running = false;
610
611     private boolean up;
612
613     /**
614      * Constructor for a thread that scrolls either up or down
615      * 
616      * @param up
617      */
618     public ScrollThread(boolean up)
619     {
620       this.up = up;
621       setName("IdPanel$ScrollThread$" + String.valueOf(up));
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 }