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