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