JAL-3210 Improvements to eclipse detection. New src tree and SwingJS updated from...
[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.getDesktopPane(),
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        * for JalviewJS using Swing Timer
325        */
326       Timer t = new Timer(20, new ActionListener()
327       {
328         @Override
329         public void actionPerformed(ActionEvent e)
330         {
331           if (scrollThread != null)
332           {
333             // if (!scrollOnce() {t.stop();}) gives compiler error :-(
334             scrollThread.scrollOnce();
335           }
336         }
337       });
338       t.addActionListener(new ActionListener()
339       {
340         @Override
341         public void actionPerformed(ActionEvent e)
342         {
343           if (scrollThread == null)
344           {
345             // IdPanel.stopScrolling called
346             t.stop();
347           }
348         }
349       });
350       t.start();
351     }
352     else
353     /**
354      * Java only
355      * 
356      * @j2sIgnore
357      */
358     {
359       scrollThread.start();
360     }
361   }
362
363   /**
364    * Respond to a mouse press. Does nothing for (left) double-click as this is
365    * handled by mouseClicked().
366    * 
367    * Right mouse down - construct and show context menu.
368    * 
369    * Ctrl-down or Shift-down - add to or expand current selection group if there
370    * is one.
371    * 
372    * Mouse down - select this sequence.
373    * 
374    * @param e
375    */
376   @Override
377   public void mousePressed(MouseEvent e)
378   {
379     if (e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(e))
380     {
381       return;
382     }
383
384     MousePos pos = alignPanel.getSeqPanel().findMousePosition(e);
385     
386     if (e.isPopupTrigger()) // Mac reports this in mousePressed
387     {
388       showPopupMenu(e, pos);
389       return;
390     }
391
392     /*
393      * defer right-mouse click handling to mouseReleased on Windows
394      * (where isPopupTrigger() will answer true)
395      * NB isRightMouseButton is also true for Cmd-click on Mac
396      */
397     if (Platform.isWinRightButton(e))
398     {
399       return;
400     }
401
402     if ((av.getSelectionGroup() == null)
403             || (!jalview.util.Platform.isControlDown(e) && !e.isShiftDown()
404                     && av.getSelectionGroup() != null))
405     {
406       av.setSelectionGroup(new SequenceGroup());
407       av.getSelectionGroup().setStartRes(0);
408       av.getSelectionGroup().setEndRes(av.getAlignment().getWidth() - 1);
409     }
410
411     if (e.isShiftDown() && (lastid != -1))
412     {
413       selectSeqs(lastid, pos.seqIndex);
414     }
415     else
416     {
417       selectSeq(pos.seqIndex);
418     }
419
420     av.isSelectionGroupChanged(true);
421
422     alignPanel.paintAlignment(false, false);
423   }
424
425   /**
426    * Build and show the popup-menu at the right-click mouse position
427    * 
428    * @param e
429    */
430   void showPopupMenu(MouseEvent e, MousePos pos)
431   {
432     if (pos.isOverAnnotation())
433     {
434       showAnnotationMenu(e, pos);
435       return;
436     }
437
438     Sequence sq = (Sequence) av.getAlignment().getSequenceAt(pos.seqIndex);
439
440     /*
441      *  build a new links menu based on the current links
442      *  and any non-positional features
443      */
444     List<SequenceFeature> features = null;
445     if (sq != null)
446     {
447     List<String> nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
448       features = sq.getFeatures().getNonPositionalFeatures();
449     for (SequenceFeature sf : features)
450     {
451       if (sf.links != null)
452       {
453         nlinks.addAll(sf.links);
454       }
455     }
456     }
457
458     PopupMenu pop = new PopupMenu(alignPanel, sq, features,
459             Preferences.getGroupURLLinks() // empty list; not implemented
460     );
461     pop.show(this, e.getX(), e.getY());
462   }
463
464   /**
465    * On right mouse click on a Consensus annotation label, shows a limited popup
466    * menu, with options to configure the consensus calculation and rendering.
467    * 
468    * @param e
469    * @param pos
470    * @see AnnotationLabels#showPopupMenu(MouseEvent)
471    */
472   void showAnnotationMenu(MouseEvent e, MousePos pos)
473   {
474     if (pos.annotationIndex == -1)
475     {
476       return;
477     }
478     AlignmentAnnotation[] anns = this.av.getAlignment()
479             .getAlignmentAnnotation();
480     if (anns == null || pos.annotationIndex >= anns.length)
481     {
482       return;
483     }
484     AlignmentAnnotation ann = anns[pos.annotationIndex];
485     if (!ann.label.contains("Consensus"))
486     {
487       return;
488     }
489
490     JPopupMenu pop = new JPopupMenu(
491             MessageManager.getString("label.annotations"));
492     AnnotationLabels.addConsensusMenuOptions(this.alignPanel, ann, pop);
493     pop.show(this, e.getX(), e.getY());
494   }
495
496   /**
497    * Toggle whether the sequence is part of the current selection group.
498    * 
499    * @param seq
500    */
501   void selectSeq(int seq)
502   {
503     lastid = seq;
504
505     SequenceI pickedSeq = av.getAlignment().getSequenceAt(seq);
506     av.getSelectionGroup().addOrRemove(pickedSeq, false);
507   }
508
509   /**
510    * Add contiguous rows of the alignment to the current selection group. Does
511    * nothing if there is no selection group.
512    * 
513    * @param start
514    * @param end
515    */
516   void selectSeqs(int start, int end)
517   {
518     if (av.getSelectionGroup() == null)
519     {
520       return;
521     }
522
523     if (end >= av.getAlignment().getHeight())
524     {
525       end = av.getAlignment().getHeight() - 1;
526     }
527
528     lastid = start;
529
530     if (end < start)
531     {
532       int tmp = start;
533       start = end;
534       end = tmp;
535       lastid = end;
536     }
537
538     for (int i = start; i <= end; i++)
539     {
540       av.getSelectionGroup().addSequence(av.getAlignment().getSequenceAt(i),
541               false);
542     }
543   }
544
545   /**
546    * Respond to mouse released. Refreshes the display and triggers broadcast of
547    * the new selection group to any listeners.
548    * 
549    * @param e
550    */
551   @Override
552   public void mouseReleased(MouseEvent e)
553   {
554     if (scrollThread != null)
555     {
556       stopScrolling();
557     }
558     MousePos pos = alignPanel.getSeqPanel().findMousePosition(e);
559
560     mouseDragging = false;
561     PaintRefresher.Refresh(this, av.getSequenceSetId());
562     // always send selection message when mouse is released
563     av.sendSelection();
564
565     if (e.isPopupTrigger()) // Windows reports this in mouseReleased
566     {
567       showPopupMenu(e, pos);
568     }
569   }
570
571   /**
572    * Highlight sequence ids that match the given list, and if necessary scroll
573    * to the start sequence of the list.
574    * 
575    * @param list
576    */
577   public void highlightSearchResults(List<SequenceI> list)
578   {
579     getIdCanvas().setHighlighted(list);
580
581     if (list == null || list.isEmpty())
582     {
583       return;
584     }
585
586     int index = av.getAlignment().findIndex(list.get(0));
587
588     // do we need to scroll the panel?
589     if ((av.getRanges().getStartSeq() > index)
590             || (av.getRanges().getEndSeq() < index))
591     {
592       av.getRanges().setStartSeq(index);
593     }
594   }
595
596   public IdCanvas getIdCanvas()
597   {
598     return idCanvas;
599   }
600
601   public void setIdCanvas(IdCanvas idCanvas)
602   {
603     this.idCanvas = idCanvas;
604   }
605
606   /**
607    * Performs scrolling of the visible alignment up or down, adding newly
608    * visible sequences to the current selection
609    */
610   class ScrollThread extends Thread
611   {
612     private boolean running = false;
613
614     private boolean up;
615
616     /**
617      * Constructor for a thread that scrolls either up or down
618      * 
619      * @param up
620      */
621     public ScrollThread(boolean up)
622     {
623       this.up = up;
624       setName("IdPanel$ScrollThread$" + String.valueOf(up));
625     }
626
627     /**
628      * Sets a flag to stop the scrolling
629      */
630     public void stopScrolling()
631     {
632       running = false;
633     }
634
635     /**
636      * Scrolls the alignment either up or down, one row at a time, adding newly
637      * visible sequences to the current selection. Speed is limited to a maximum
638      * of ten rows per second. The thread exits when the end of the alignment is
639      * reached or a flag is set to stop it by a call to stopScrolling.
640      */
641     @Override
642     public void run()
643     {
644       running = true;
645
646       while (running)
647       {
648         running = scrollOnce();
649         try
650         {
651           Thread.sleep(100);
652         } catch (Exception ex)
653         {
654         }
655       }
656       IdPanel.this.scrollThread = null;
657     }
658
659     /**
660      * Scrolls one row up or down. Answers true if a scroll could be done, false
661      * if not (top or bottom of alignment reached).
662      */
663     boolean scrollOnce()
664     {
665       ViewportRanges ranges = IdPanel.this.av.getRanges();
666       if (ranges.scrollUp(up))
667       {
668         int toSeq = up ? ranges.getStartSeq() : ranges.getEndSeq();
669         int fromSeq = toSeq < lastid ? lastid - 1 : lastid + 1;
670         IdPanel.this.selectSeqs(fromSeq, toSeq);
671         lastid = toSeq;
672         alignPanel.paintAlignment(false, false);
673         return true;
674       }
675
676       return false;
677     }
678   }
679 }