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