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