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