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