9ee47509483184268685f0698ac59f2460a6ee59
[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.bin.Jalview;
24 import jalview.datamodel.Sequence;
25 import jalview.datamodel.SequenceFeature;
26 import jalview.datamodel.SequenceGroup;
27 import jalview.datamodel.SequenceI;
28 import jalview.io.SequenceAnnotationReport;
29 import jalview.util.MessageManager;
30 import jalview.util.Platform;
31 import jalview.viewmodel.AlignmentViewport;
32 import jalview.viewmodel.ViewportRanges;
33
34 import java.awt.BorderLayout;
35 import java.awt.event.ActionEvent;
36 import java.awt.event.ActionListener;
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.SwingUtilities;
46 import javax.swing.Timer;
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    * Respond to mouse movement by constructing tooltip text for the sequence id
101    * under the mouse.
102    * 
103    * @param e
104    *          DOCUMENT ME!
105    */
106   @Override
107   public void mouseMoved(MouseEvent e)
108   {
109     SeqPanel sp = alignPanel.getSeqPanel();
110     int seq = Math.max(0, sp.findSeq(e));
111     if (seq > -1 && seq < av.getAlignment().getHeight())
112     {
113       SequenceI sequence = av.getAlignment().getSequenceAt(seq);
114       StringBuilder tip = new StringBuilder(64);
115       seqAnnotReport.createTooltipAnnotationReport(tip, sequence,
116               av.isShowDBRefs(), av.isShowNPFeats(), sp.seqCanvas.fr);
117       setToolTipText(JvSwingUtils.wrapTooltip(true,
118               sequence.getDisplayId(true) + " " + tip.toString()));
119     }
120   }
121
122   /**
123    * Responds to a mouse drag by selecting the sequences under the dragged
124    * region.
125    * 
126    * @param e
127    */
128   @Override
129   public void mouseDragged(MouseEvent e)
130   {
131     mouseDragging = true;
132
133     int seq = Math.max(0, alignPanel.getSeqPanel().findSeq(e));
134
135     if (seq < lastid)
136     {
137       selectSeqs(lastid - 1, seq);
138     }
139     else if (seq > lastid)
140     {
141       selectSeqs(lastid + 1, seq);
142     }
143
144     lastid = seq;
145     alignPanel.paintAlignment(false, false);
146   }
147
148   /**
149    * Response to the mouse wheel by scrolling the alignment panel.
150    */
151   @Override
152   public void mouseWheelMoved(MouseWheelEvent e)
153   {
154     e.consume();
155     double wheelRotation = e.getPreciseWheelRotation();
156     if (wheelRotation > 0)
157     {
158       if (e.isShiftDown())
159       {
160         av.getRanges().scrollRight(true);
161       }
162       else
163       {
164         av.getRanges().scrollUp(false);
165       }
166     }
167     else if (wheelRotation < 0)
168     {
169       if (e.isShiftDown())
170       {
171         av.getRanges().scrollRight(false);
172       }
173       else
174       {
175         av.getRanges().scrollUp(true);
176       }
177     }
178   }
179
180   /**
181    * Handle a mouse click event. Currently only responds to a double-click. The
182    * action is to try to open a browser window at a URL that searches for the
183    * selected sequence id. The search URL is configured in Preferences |
184    * Connections | URL link from Sequence ID. For example:
185    * 
186    * http://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$SEQUENCE_ID$
187    * 
188    * @param e
189    */
190   @Override
191   public void mouseClicked(MouseEvent e)
192   {
193     /*
194      * Ignore single click. Ignore 'left' click followed by 'right' click (user
195      * selects a row then its pop-up menu).
196      */
197     if (e.getClickCount() < 2 || SwingUtilities.isRightMouseButton(e))
198     {
199       // reinstate isRightMouseButton check to ignore mouse-related popup events
200       // note - this does nothing on default MacBookPro force-trackpad config!
201       return;
202     }
203
204     int seq = alignPanel.getSeqPanel().findSeq(e);
205     String id = av.getAlignment().getSequenceAt(seq).getName();
206     String url = Preferences.sequenceUrlLinks.getPrimaryUrl(id);
207
208     try
209     {
210       jalview.util.BrowserLauncher.openURL(url);
211     } catch (Exception ex)
212     {
213       JvOptionPane.showInternalMessageDialog(Desktop.desktop,
214               MessageManager.getString("label.web_browser_not_found_unix"),
215               MessageManager.getString("label.web_browser_not_found"),
216               JvOptionPane.WARNING_MESSAGE);
217       ex.printStackTrace();
218     }
219   }
220
221   /**
222    * On (re-)entering the panel, stop any scrolling
223    * 
224    * @param e
225    */
226   @Override
227   public void mouseEntered(MouseEvent e)
228   {
229     stopScrolling();
230   }
231
232   /**
233    * Interrupts the scroll thread if one is running
234    */
235   void stopScrolling()
236   {
237     if (scrollThread != null)
238     {
239       scrollThread.stopScrolling();
240     }
241   }
242
243   /**
244    * DOCUMENT ME!
245    * 
246    * @param e
247    *          DOCUMENT ME!
248    */
249   @Override
250   public void mouseExited(MouseEvent e)
251   {
252     if (av.getWrapAlignment())
253     {
254       return;
255     }
256
257     if (mouseDragging)
258     {
259       /*
260        * on mouse drag above or below the panel, start 
261        * scrolling if there are more sequences to show
262        */
263       ViewportRanges ranges = av.getRanges();
264       if (e.getY() < 0 && ranges.getStartSeq() > 0)
265       {
266         startScrolling(true);
267       }
268       else if (e.getY() >= getHeight()
269               && ranges.getEndSeq() <= av.getAlignment().getHeight())
270       {
271         startScrolling(false);
272       }
273     }
274   }
275
276   /**
277    * Starts scrolling either up or down
278    * 
279    * @param up
280    */
281   void startScrolling(boolean up)
282   {
283     scrollThread = new ScrollThread(up);
284     if (!Jalview.isJS())
285     {
286       /*
287        * Java - run in a new thread
288        */
289       scrollThread.start();
290     }
291     else
292     {
293       /*
294        * for JalviewJS using Swing Timer
295        */
296       Timer t = new Timer(20, new ActionListener()
297       {
298         @Override
299         public void actionPerformed(ActionEvent e)
300         {
301           if (scrollThread != null)
302           {
303             // if (!scrollOnce() {t.stop();}) gives compiler error :-(
304             scrollThread.scrollOnce();
305           }
306         }
307       });
308       t.addActionListener(new ActionListener()
309       {
310         @Override
311         public void actionPerformed(ActionEvent e)
312         {
313           if (scrollThread == null)
314           {
315             // finished and nulled itself
316             t.stop();
317           }
318         }
319       });
320       t.start();
321     }
322   }
323
324   /**
325    * Respond to a mouse press. Does nothing for (left) double-click as this is
326    * handled by mouseClicked().
327    * 
328    * Right mouse down - construct and show context menu.
329    * 
330    * Ctrl-down or Shift-down - add to or expand current selection group if there
331    * is one.
332    * 
333    * Mouse down - select this sequence.
334    * 
335    * @param e
336    */
337   @Override
338   public void mousePressed(MouseEvent e)
339   {
340     if (e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(e))
341     {
342       return;
343     }
344
345     if (e.isPopupTrigger()) // Mac reports this in mousePressed
346     {
347       showPopupMenu(e);
348       return;
349     }
350
351     /*
352      * defer right-mouse click handling to mouseReleased on Windows
353      * (where isPopupTrigger() will answer true)
354      * NB isRightMouseButton is also true for Cmd-click on Mac
355      */
356     if (Platform.isWinRightButton(e))
357     {
358       return;
359     }
360
361     if ((av.getSelectionGroup() == null)
362             || (!jalview.util.Platform.isControlDown(e) && !e.isShiftDown()
363                     && av.getSelectionGroup() != null))
364     {
365       av.setSelectionGroup(new SequenceGroup());
366       av.getSelectionGroup().setStartRes(0);
367       av.getSelectionGroup().setEndRes(av.getAlignment().getWidth() - 1);
368     }
369
370     int seq = alignPanel.getSeqPanel().findSeq(e);
371     if (e.isShiftDown() && (lastid != -1))
372     {
373       selectSeqs(lastid, seq);
374     }
375     else
376     {
377       selectSeq(seq);
378     }
379
380     av.isSelectionGroupChanged(true);
381
382     alignPanel.paintAlignment(false, false);
383   }
384
385   /**
386    * Build and show the popup-menu at the right-click mouse position
387    * 
388    * @param e
389    */
390   void showPopupMenu(MouseEvent e)
391   {
392     int seq2 = alignPanel.getSeqPanel().findSeq(e);
393     Sequence sq = (Sequence) av.getAlignment().getSequenceAt(seq2);
394
395     /*
396      *  build a new links menu based on the current links
397      *  and any non-positional features
398      */
399     List<String> nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
400     List<SequenceFeature> features = sq.getFeatures().getNonPositionalFeatures();
401     for (SequenceFeature sf : features)
402     {
403       if (sf.links != null)
404       {
405         for (String link : sf.links)
406         {
407           nlinks.add(link);
408         }
409       }
410     }
411
412     PopupMenu pop = new PopupMenu(alignPanel, sq, features,
413             Preferences.getGroupURLLinks());
414     pop.show(this, e.getX(), e.getY());
415   }
416
417   /**
418    * Toggle whether the sequence is part of the current selection group.
419    * 
420    * @param seq
421    */
422   void selectSeq(int seq)
423   {
424     lastid = seq;
425
426     SequenceI pickedSeq = av.getAlignment().getSequenceAt(seq);
427     av.getSelectionGroup().addOrRemove(pickedSeq, true);
428   }
429
430   /**
431    * Add contiguous rows of the alignment to the current selection group. Does
432    * nothing if there is no selection group.
433    * 
434    * @param start
435    * @param end
436    */
437   void selectSeqs(int start, int end)
438   {
439     if (av.getSelectionGroup() == null)
440     {
441       return;
442     }
443
444     if (end >= av.getAlignment().getHeight())
445     {
446       end = av.getAlignment().getHeight() - 1;
447     }
448
449     lastid = start;
450
451     if (end < start)
452     {
453       int tmp = start;
454       start = end;
455       end = tmp;
456       lastid = end;
457     }
458
459     for (int i = start; i <= end; i++)
460     {
461       av.getSelectionGroup().addSequence(av.getAlignment().getSequenceAt(i),
462               i == end);
463     }
464   }
465
466   /**
467    * Respond to mouse released. Refreshes the display and triggers broadcast of
468    * the new selection group to any listeners.
469    * 
470    * @param e
471    */
472   @Override
473   public void mouseReleased(MouseEvent e)
474   {
475     stopScrolling();
476
477     mouseDragging = false;
478     PaintRefresher.Refresh(this, av.getSequenceSetId());
479     // always send selection message when mouse is released
480     av.sendSelection();
481
482     if (e.isPopupTrigger()) // Windows reports this in mouseReleased
483     {
484       showPopupMenu(e);
485     }
486   }
487
488   /**
489    * Highlight sequence ids that match the given list, and if necessary scroll
490    * to the start sequence of the list.
491    * 
492    * @param list
493    */
494   public void highlightSearchResults(List<SequenceI> list)
495   {
496     getIdCanvas().setHighlighted(list);
497
498     if (list == null)
499     {
500       return;
501     }
502
503     int index = av.getAlignment().findIndex(list.get(0));
504
505     // do we need to scroll the panel?
506     if ((av.getRanges().getStartSeq() > index)
507             || (av.getRanges().getEndSeq() < index))
508     {
509       av.getRanges().setStartSeq(index);
510     }
511   }
512
513   public IdCanvas getIdCanvas()
514   {
515     return idCanvas;
516   }
517
518   public void setIdCanvas(IdCanvas idCanvas)
519   {
520     this.idCanvas = idCanvas;
521   }
522
523   /**
524    * Performs scrolling of the visible alignment up or down, adding newly
525    * visible sequences to the current selection
526    */
527   class ScrollThread extends Thread
528   {
529     private boolean running = false;
530
531     private boolean up;
532
533     /**
534      * Constructor for a thread that scrolls either up or down
535      * 
536      * @param up
537      */
538     public ScrollThread(boolean up)
539     {
540       this.up = up;
541       setName("IdPanel$ScrollThread$" + String.valueOf(up));
542       start();
543     }
544
545     /**
546      * Sets a flag to stop the scrolling
547      */
548     public void stopScrolling()
549     {
550       running = false;
551     }
552
553     /**
554      * Scrolls the alignment either up or down, one row at a time, adding newly
555      * visible sequences to the current selection. Speed is limited to a maximum
556      * of ten rows per second. The thread exits when the end of the alignment is
557      * reached or a flag is set to stop it by a call to stopScrolling.
558      */
559     @Override
560     public void run()
561     {
562       running = true;
563
564       while (running)
565       {
566         running = scrollOnce();
567         try
568         {
569           Thread.sleep(100);
570         } catch (Exception ex)
571         {
572         }
573       }
574       IdPanel.this.scrollThread = null;
575     }
576
577     /**
578      * Scrolls one row up or down. Answers true if a scroll could be done, false
579      * if not (top or bottom of alignment reached).
580      */
581     boolean scrollOnce()
582     {
583       ViewportRanges ranges = IdPanel.this.av.getRanges();
584       if (ranges.scrollUp(up))
585       {
586         int toSeq = up ? ranges.getStartSeq() : ranges.getEndSeq();
587         int fromSeq = toSeq < lastid ? lastid - 1 : lastid + 1;
588         IdPanel.this.selectSeqs(fromSeq, toSeq);
589         lastid = toSeq;
590         alignPanel.paintAlignment(false, false);
591         return true;
592       }
593
594       return false;
595     }
596   }
597 }