Clover icon

Coverage Report

  1. Project Clover database Thu Aug 13 2020 12:04:21 BST
  2. Package jalview.gui

File SeqPanel.java

 

Coverage histogram

../../img/srcFileCovDistChart2.png
50% of files have more coverage

Code metrics

532
902
63
3
2,902
1,947
409
0.45
14.32
21
6.49

Classes

Class Line # Actions
SeqPanel 85 869 389
0.1481223914.8%
SeqPanel.MousePos 98 11 7
0.2222222222.2%
SeqPanel.ScrollThread 2587 22 13
0.00%
 

Contributing tests

This file is covered by 99 tests. .

Source view

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.Color;
25    import java.awt.Font;
26    import java.awt.FontMetrics;
27    import java.awt.Point;
28    import java.awt.event.ActionEvent;
29    import java.awt.event.ActionListener;
30    import java.awt.event.MouseEvent;
31    import java.awt.event.MouseListener;
32    import java.awt.event.MouseMotionListener;
33    import java.awt.event.MouseWheelEvent;
34    import java.awt.event.MouseWheelListener;
35    import java.util.ArrayList;
36    import java.util.Collections;
37    import java.util.List;
38   
39    import javax.swing.JLabel;
40    import javax.swing.JPanel;
41    import javax.swing.JToolTip;
42    import javax.swing.SwingUtilities;
43    import javax.swing.Timer;
44    import javax.swing.ToolTipManager;
45   
46    import jalview.api.AlignViewportI;
47    import jalview.bin.Cache;
48    import jalview.commands.EditCommand;
49    import jalview.commands.EditCommand.Action;
50    import jalview.commands.EditCommand.Edit;
51    import jalview.datamodel.AlignmentAnnotation;
52    import jalview.datamodel.AlignmentI;
53    import jalview.datamodel.ColumnSelection;
54    import jalview.datamodel.HiddenColumns;
55    import jalview.datamodel.MappedFeatures;
56    import jalview.datamodel.SearchResultMatchI;
57    import jalview.datamodel.SearchResults;
58    import jalview.datamodel.SearchResultsI;
59    import jalview.datamodel.Sequence;
60    import jalview.datamodel.SequenceFeature;
61    import jalview.datamodel.SequenceGroup;
62    import jalview.datamodel.SequenceI;
63    import jalview.io.SequenceAnnotationReport;
64    import jalview.renderer.ResidueShaderI;
65    import jalview.schemes.ResidueProperties;
66    import jalview.structure.SelectionListener;
67    import jalview.structure.SelectionSource;
68    import jalview.structure.SequenceListener;
69    import jalview.structure.StructureSelectionManager;
70    import jalview.structure.VamsasSource;
71    import jalview.util.Comparison;
72    import jalview.util.MappingUtils;
73    import jalview.util.MessageManager;
74    import jalview.util.Platform;
75    import jalview.viewmodel.AlignmentViewport;
76    import jalview.viewmodel.ViewportRanges;
77    import jalview.viewmodel.seqfeatures.FeatureRendererModel;
78   
79    /**
80    * DOCUMENT ME!
81    *
82    * @author $author$
83    * @version $Revision: 1.130 $
84    */
 
85    public class SeqPanel extends JPanel
86    implements MouseListener, MouseMotionListener, MouseWheelListener,
87    SequenceListener, SelectionListener
88    {
89    /*
90    * a class that holds computed mouse position
91    * - column of the alignment (0...)
92    * - sequence offset (0...)
93    * - annotation row offset (0...)
94    * where annotation offset is -1 unless the alignment is shown
95    * in wrapped mode, annotations are shown, and the mouse is
96    * over an annnotation row
97    */
 
98    static class MousePos
99    {
100    /*
101    * alignment column position of cursor (0...)
102    */
103    final int column;
104   
105    /*
106    * index in alignment of sequence under cursor,
107    * or nearest above if cursor is not over a sequence
108    */
109    final int seqIndex;
110   
111    /*
112    * index in annotations array of annotation under the cursor
113    * (only possible in wrapped mode with annotations shown),
114    * or -1 if cursor is not over an annotation row
115    */
116    final int annotationIndex;
117   
 
118  48 toggle MousePos(int col, int seq, int ann)
119    {
120  48 column = col;
121  48 seqIndex = seq;
122  48 annotationIndex = ann;
123    }
124   
 
125  0 toggle boolean isOverAnnotation()
126    {
127  0 return annotationIndex != -1;
128    }
129   
 
130  0 toggle @Override
131    public boolean equals(Object obj)
132    {
133  0 if (obj == null || !(obj instanceof MousePos))
134    {
135  0 return false;
136    }
137  0 MousePos o = (MousePos) obj;
138  0 boolean b = (column == o.column && seqIndex == o.seqIndex
139    && annotationIndex == o.annotationIndex);
140    // System.out.println(obj + (b ? "= " : "!= ") + this);
141  0 return b;
142    }
143   
144    /**
145    * A simple hashCode that ensures that instances that satisfy equals() have
146    * the same hashCode
147    */
 
148  0 toggle @Override
149    public int hashCode()
150    {
151  0 return column + seqIndex + annotationIndex;
152    }
153   
154    /**
155    * toString method for debug output purposes only
156    */
 
157  0 toggle @Override
158    public String toString()
159    {
160  0 return String.format("c%d:s%d:a%d", column, seqIndex,
161    annotationIndex);
162    }
163    }
164   
165    private static final int MAX_TOOLTIP_LENGTH = 300;
166   
167    public SeqCanvas seqCanvas;
168   
169    public AlignmentPanel ap;
170   
171    /*
172    * last position for mouseMoved event
173    */
174    private MousePos lastMousePosition;
175   
176    protected int editLastRes;
177   
178    protected int editStartSeq;
179   
180    protected AlignViewport av;
181   
182    ScrollThread scrollThread = null;
183   
184    boolean mouseDragging = false;
185   
186    boolean editingSeqs = false;
187   
188    boolean groupEditing = false;
189   
190    // ////////////////////////////////////////
191    // ///Everything below this is for defining the boundary of the rubberband
192    // ////////////////////////////////////////
193    int oldSeq = -1;
194   
195    boolean changeEndSeq = false;
196   
197    boolean changeStartSeq = false;
198   
199    boolean changeEndRes = false;
200   
201    boolean changeStartRes = false;
202   
203    SequenceGroup stretchGroup = null;
204   
205    boolean remove = false;
206   
207    Point lastMousePress;
208   
209    boolean mouseWheelPressed = false;
210   
211    StringBuffer keyboardNo1;
212   
213    StringBuffer keyboardNo2;
214   
215    private final SequenceAnnotationReport seqARep;
216   
217    /*
218    * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
219    * - the tooltip is not set again if unchanged
220    * - this is the tooltip text _before_ formatting as html
221    */
222    private String lastTooltip;
223   
224    /*
225    * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
226    * - used to decide where to place the tooltip in getTooltipLocation()
227    * - this is the tooltip text _after_ formatting as html
228    */
229    private String lastFormattedTooltip;
230   
231    EditCommand editCommand;
232   
233    StructureSelectionManager ssm;
234   
235    SearchResultsI lastSearchResults;
236   
237    /**
238    * Creates a new SeqPanel object
239    *
240    * @param viewport
241    * @param alignPanel
242    */
 
243  267 toggle public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
244    {
245  267 seqARep = new SequenceAnnotationReport(true);
246  267 ToolTipManager.sharedInstance().registerComponent(this);
247  267 ToolTipManager.sharedInstance().setInitialDelay(0);
248  267 ToolTipManager.sharedInstance().setDismissDelay(10000);
249   
250   
251  267 this.av = viewport;
252  267 setBackground(Color.white);
253   
254  267 seqCanvas = new SeqCanvas(alignPanel);
255  267 setLayout(new BorderLayout());
256  267 add(seqCanvas, BorderLayout.CENTER);
257   
258  267 this.ap = alignPanel;
259   
260  267 if (!viewport.isDataset())
261    {
262  267 addMouseMotionListener(this);
263  267 addMouseListener(this);
264  267 addMouseWheelListener(this);
265  267 ssm = viewport.getStructureSelectionManager();
266  267 ssm.addStructureViewerListener(this);
267  267 ssm.addSelectionListener(this);
268    }
269    }
270   
271    int startWrapBlock = -1;
272   
273    int wrappedBlock = -1;
274   
275    /**
276    * Computes the column and sequence row (and possibly annotation row when in
277    * wrapped mode) for the given mouse position
278    *
279    * @param evt
280    * @return
281    */
 
282  48 toggle MousePos findMousePosition(MouseEvent evt)
283    {
284  48 int col = findColumn(evt);
285  48 int seqIndex = -1;
286  48 int annIndex = -1;
287  48 int y = evt.getY();
288   
289  48 int charHeight = av.getCharHeight();
290  48 int alignmentHeight = av.getAlignment().getHeight();
291  48 if (av.getWrapAlignment())
292    {
293  48 seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
294    seqCanvas.getHeight());
295   
296    /*
297    * yPos modulo height of repeating width
298    */
299  48 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
300   
301    /*
302    * height of sequences plus space / scale above,
303    * plus gap between sequences and annotations
304    */
305  48 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
306    + alignmentHeight * charHeight
307    + SeqCanvas.SEQS_ANNOTATION_GAP;
308  48 if (yOffsetPx >= alignmentHeightPixels)
309    {
310    /*
311    * mouse is over annotations; find annotation index, also set
312    * last sequence above (for backwards compatible behaviour)
313    */
314  16 AlignmentAnnotation[] anns = av.getAlignment()
315    .getAlignmentAnnotation();
316  16 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
317  16 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
318  16 seqIndex = alignmentHeight - 1;
319    }
320    else
321    {
322    /*
323    * mouse is over sequence (or the space above sequences)
324    */
325  32 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
326  32 if (yOffsetPx >= 0)
327    {
328  19 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
329    }
330    }
331    }
332    else
333    {
334  0 ViewportRanges ranges = av.getRanges();
335  0 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
336    alignmentHeight - 1);
337  0 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
338    }
339   
340  48 return new MousePos(col, seqIndex, annIndex);
341    }
342    /**
343    * Returns the aligned sequence position (base 0) at the mouse position, or
344    * the closest visible one
345    *
346    * @param evt
347    * @return
348    */
 
349  61 toggle int findColumn(MouseEvent evt)
350    {
351  61 int res = 0;
352  61 int x = evt.getX();
353   
354  61 final int startRes = av.getRanges().getStartRes();
355  61 final int charWidth = av.getCharWidth();
356   
357  61 if (av.getWrapAlignment())
358    {
359  55 int hgap = av.getCharHeight();
360  55 if (av.getScaleAboveWrapped())
361    {
362  22 hgap += av.getCharHeight();
363    }
364   
365  55 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
366    + hgap + seqCanvas.getAnnotationHeight();
367   
368  55 int y = evt.getY();
369  55 y = Math.max(0, y - hgap);
370  55 x -= seqCanvas.getLabelWidthWest();
371  55 if (x < 0)
372    {
373    // mouse is over left scale
374  1 return -1;
375    }
376   
377  54 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
378  54 if (cwidth < 1)
379    {
380  0 return 0;
381    }
382  54 if (x >= cwidth * charWidth)
383    {
384    // mouse is over right scale
385  1 return -1;
386    }
387   
388  53 wrappedBlock = y / cHeight;
389  53 wrappedBlock += startRes / cwidth;
390    // allow for wrapped view scrolled right (possible from Overview)
391  53 int startOffset = startRes % cwidth;
392  53 res = wrappedBlock * cwidth + startOffset
393    + Math.min(cwidth - 1, x / charWidth);
394    }
395    else
396    {
397    /*
398    * make sure we calculate relative to visible alignment,
399    * rather than right-hand gutter
400    */
401  6 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
402  6 res = (x / charWidth) + startRes;
403  6 res = Math.min(res, av.getRanges().getEndRes());
404    }
405   
406  59 if (av.hasHiddenColumns())
407    {
408  1 res = av.getAlignment().getHiddenColumns()
409    .visibleToAbsoluteColumn(res);
410    }
411   
412  59 return res;
413    }
414   
415    /**
416    * When all of a sequence of edits are complete, put the resulting edit list
417    * on the history stack (undo list), and reset flags for editing in progress.
418    */
 
419  0 toggle void endEditing()
420    {
421  0 try
422    {
423  0 if (editCommand != null && editCommand.getSize() > 0)
424    {
425  0 ap.alignFrame.addHistoryItem(editCommand);
426  0 av.firePropertyChange("alignment", null,
427    av.getAlignment().getSequences());
428    }
429    } finally
430    {
431    /*
432    * Tidy up come what may...
433    */
434  0 editStartSeq = -1;
435  0 editLastRes = -1;
436  0 editingSeqs = false;
437  0 groupEditing = false;
438  0 keyboardNo1 = null;
439  0 keyboardNo2 = null;
440  0 editCommand = null;
441    }
442    }
443   
 
444  0 toggle void setCursorRow()
445    {
446  0 seqCanvas.cursorY = getKeyboardNo1() - 1;
447  0 scrollToVisible(true);
448    }
449   
 
450  0 toggle void setCursorColumn()
451    {
452  0 seqCanvas.cursorX = getKeyboardNo1() - 1;
453  0 scrollToVisible(true);
454    }
455   
 
456  0 toggle void setCursorRowAndColumn()
457    {
458  0 if (keyboardNo2 == null)
459    {
460  0 keyboardNo2 = new StringBuffer();
461    }
462    else
463    {
464  0 seqCanvas.cursorX = getKeyboardNo1() - 1;
465  0 seqCanvas.cursorY = getKeyboardNo2() - 1;
466  0 scrollToVisible(true);
467    }
468    }
469   
 
470  0 toggle void setCursorPosition()
471    {
472  0 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
473   
474  0 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
475  0 scrollToVisible(true);
476    }
477   
 
478  0 toggle void moveCursor(int dx, int dy)
479    {
480  0 seqCanvas.cursorX += dx;
481  0 seqCanvas.cursorY += dy;
482   
483  0 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
484   
485  0 if (av.hasHiddenColumns() && !hidden.isVisible(seqCanvas.cursorX))
486    {
487  0 int original = seqCanvas.cursorX - dx;
488  0 int maxWidth = av.getAlignment().getWidth();
489   
490  0 if (!hidden.isVisible(seqCanvas.cursorX))
491    {
492  0 int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
493  0 int[] region = hidden.getRegionWithEdgeAtRes(visx);
494   
495  0 if (region != null) // just in case
496    {
497  0 if (dx == 1)
498    {
499    // moving right
500  0 seqCanvas.cursorX = region[1] + 1;
501    }
502  0 else if (dx == -1)
503    {
504    // moving left
505  0 seqCanvas.cursorX = region[0] - 1;
506    }
507    }
508  0 seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
509    }
510   
511  0 if (seqCanvas.cursorX >= maxWidth
512    || !hidden.isVisible(seqCanvas.cursorX))
513    {
514  0 seqCanvas.cursorX = original;
515    }
516    }
517   
518  0 scrollToVisible(false);
519    }
520   
521    /**
522    * Scroll to make the cursor visible in the viewport.
523    *
524    * @param jump
525    * just jump to the location rather than scrolling
526    */
 
527  0 toggle void scrollToVisible(boolean jump)
528    {
529  0 if (seqCanvas.cursorX < 0)
530    {
531  0 seqCanvas.cursorX = 0;
532    }
533  0 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
534    {
535  0 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
536    }
537   
538  0 if (seqCanvas.cursorY < 0)
539    {
540  0 seqCanvas.cursorY = 0;
541    }
542  0 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
543    {
544  0 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
545    }
546   
547  0 endEditing();
548   
549  0 boolean repaintNeeded = true;
550  0 if (jump)
551    {
552    // only need to repaint if the viewport did not move, as otherwise it will
553    // get a repaint
554  0 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
555    seqCanvas.cursorY);
556    }
557    else
558    {
559  0 if (av.getWrapAlignment())
560    {
561    // scrollToWrappedVisible expects x-value to have hidden cols subtracted
562  0 int x = av.getAlignment().getHiddenColumns()
563    .absoluteToVisibleColumn(seqCanvas.cursorX);
564  0 av.getRanges().scrollToWrappedVisible(x);
565    }
566    else
567    {
568  0 av.getRanges().scrollToVisible(seqCanvas.cursorX,
569    seqCanvas.cursorY);
570    }
571    }
572   
573  0 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
574    {
575  0 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
576    seqCanvas.cursorX, seqCanvas.cursorY);
577    }
578   
579  0 if (repaintNeeded)
580    {
581  0 seqCanvas.repaint();
582    }
583    }
584   
585   
 
586  0 toggle void setSelectionAreaAtCursor(boolean topLeft)
587    {
588  0 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
589   
590  0 if (av.getSelectionGroup() != null)
591    {
592  0 SequenceGroup sg = av.getSelectionGroup();
593    // Find the top and bottom of this group
594  0 int min = av.getAlignment().getHeight(), max = 0;
595  0 for (int i = 0; i < sg.getSize(); i++)
596    {
597  0 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
598  0 if (index > max)
599    {
600  0 max = index;
601    }
602  0 if (index < min)
603    {
604  0 min = index;
605    }
606    }
607   
608  0 max++;
609   
610  0 if (topLeft)
611    {
612  0 sg.setStartRes(seqCanvas.cursorX);
613  0 if (sg.getEndRes() < seqCanvas.cursorX)
614    {
615  0 sg.setEndRes(seqCanvas.cursorX);
616    }
617   
618  0 min = seqCanvas.cursorY;
619    }
620    else
621    {
622  0 sg.setEndRes(seqCanvas.cursorX);
623  0 if (sg.getStartRes() > seqCanvas.cursorX)
624    {
625  0 sg.setStartRes(seqCanvas.cursorX);
626    }
627   
628  0 max = seqCanvas.cursorY + 1;
629    }
630   
631  0 if (min > max)
632    {
633    // Only the user can do this
634  0 av.setSelectionGroup(null);
635    }
636    else
637    {
638    // Now add any sequences between min and max
639  0 sg.getSequences(null).clear();
640  0 for (int i = min; i < max; i++)
641    {
642  0 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
643    }
644    }
645    }
646   
647  0 if (av.getSelectionGroup() == null)
648    {
649  0 SequenceGroup sg = new SequenceGroup();
650  0 sg.setStartRes(seqCanvas.cursorX);
651  0 sg.setEndRes(seqCanvas.cursorX);
652  0 sg.addSequence(sequence, false);
653  0 av.setSelectionGroup(sg);
654    }
655   
656  0 ap.paintAlignment(false, false);
657  0 av.sendSelection();
658    }
659   
 
660  0 toggle void insertGapAtCursor(boolean group)
661    {
662  0 groupEditing = group;
663  0 editStartSeq = seqCanvas.cursorY;
664  0 editLastRes = seqCanvas.cursorX;
665  0 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
666  0 endEditing();
667    }
668   
 
669  0 toggle void deleteGapAtCursor(boolean group)
670    {
671  0 groupEditing = group;
672  0 editStartSeq = seqCanvas.cursorY;
673  0 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
674  0 editSequence(false, false, seqCanvas.cursorX);
675  0 endEditing();
676    }
677   
 
678  0 toggle void insertNucAtCursor(boolean group, String nuc)
679    {
680    // TODO not called - delete?
681  0 groupEditing = group;
682  0 editStartSeq = seqCanvas.cursorY;
683  0 editLastRes = seqCanvas.cursorX;
684  0 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
685  0 endEditing();
686    }
687   
 
688  0 toggle void numberPressed(char value)
689    {
690  0 if (keyboardNo1 == null)
691    {
692  0 keyboardNo1 = new StringBuffer();
693    }
694   
695  0 if (keyboardNo2 != null)
696    {
697  0 keyboardNo2.append(value);
698    }
699    else
700    {
701  0 keyboardNo1.append(value);
702    }
703    }
704   
 
705  0 toggle int getKeyboardNo1()
706    {
707  0 try
708    {
709  0 if (keyboardNo1 != null)
710    {
711  0 int value = Integer.parseInt(keyboardNo1.toString());
712  0 keyboardNo1 = null;
713  0 return value;
714    }
715    } catch (Exception x)
716    {
717    }
718  0 keyboardNo1 = null;
719  0 return 1;
720    }
721   
 
722  0 toggle int getKeyboardNo2()
723    {
724  0 try
725    {
726  0 if (keyboardNo2 != null)
727    {
728  0 int value = Integer.parseInt(keyboardNo2.toString());
729  0 keyboardNo2 = null;
730  0 return value;
731    }
732    } catch (Exception x)
733    {
734    }
735  0 keyboardNo2 = null;
736  0 return 1;
737    }
738   
739    /**
740    * DOCUMENT ME!
741    *
742    * @param evt
743    * DOCUMENT ME!
744    */
 
745  0 toggle @Override
746    public void mouseReleased(MouseEvent evt)
747    {
748  0 MousePos pos = findMousePosition(evt);
749  0 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
750    {
751  0 return;
752    }
753   
754  0 boolean didDrag = mouseDragging; // did we come here after a drag
755  0 mouseDragging = false;
756  0 mouseWheelPressed = false;
757   
758  0 if (evt.isPopupTrigger()) // Windows: mouseReleased
759    {
760  0 showPopupMenu(evt, pos);
761  0 evt.consume();
762  0 return;
763    }
764   
765  0 if (editingSeqs)
766    {
767  0 endEditing();
768    }
769    else
770    {
771  0 doMouseReleasedDefineMode(evt, didDrag);
772    }
773    }
774   
775    /**
776    * DOCUMENT ME!
777    *
778    * @param evt
779    * DOCUMENT ME!
780    */
 
781  0 toggle @Override
782    public void mousePressed(MouseEvent evt)
783    {
784  0 lastMousePress = evt.getPoint();
785  0 MousePos pos = findMousePosition(evt);
786  0 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
787    {
788  0 return;
789    }
790   
791  0 if (SwingUtilities.isMiddleMouseButton(evt))
792    {
793  0 mouseWheelPressed = true;
794  0 return;
795    }
796   
797  0 boolean isControlDown = Platform.isControlDown(evt);
798  0 if (evt.isShiftDown() || isControlDown)
799    {
800  0 editingSeqs = true;
801  0 if (isControlDown)
802    {
803  0 groupEditing = true;
804    }
805    }
806    else
807    {
808  0 doMousePressedDefineMode(evt, pos);
809  0 return;
810    }
811   
812  0 int seq = pos.seqIndex;
813  0 int res = pos.column;
814   
815  0 if ((seq < av.getAlignment().getHeight())
816    && (res < av.getAlignment().getSequenceAt(seq).getLength()))
817    {
818  0 editStartSeq = seq;
819  0 editLastRes = res;
820    }
821    else
822    {
823  0 editStartSeq = -1;
824  0 editLastRes = -1;
825    }
826   
827  0 return;
828    }
829   
830    String lastMessage;
831   
 
832  0 toggle @Override
833    public void mouseOverSequence(SequenceI sequence, int index, int pos)
834    {
835  0 String tmp = sequence.hashCode() + " " + index + " " + pos;
836   
837  0 if (lastMessage == null || !lastMessage.equals(tmp))
838    {
839    // System.err.println("mouseOver Sequence: "+tmp);
840  0 ssm.mouseOverSequence(sequence, index, pos, av);
841    }
842  0 lastMessage = tmp;
843    }
844   
845    /**
846    * Highlight the mapped region described by the search results object (unless
847    * unchanged). This supports highlight of protein while mousing over linked
848    * cDNA and vice versa. The status bar is also updated to show the location of
849    * the start of the highlighted region.
850    */
 
851  2 toggle @Override
852    public String highlightSequence(SearchResultsI results)
853    {
854  2 if (results == null || results.equals(lastSearchResults))
855    {
856  0 return null;
857    }
858  2 lastSearchResults = results;
859   
860  2 boolean wasScrolled = false;
861   
862  2 if (av.isFollowHighlight())
863    {
864    // don't allow highlight of protein/cDNA to also scroll a complementary
865    // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
866    // over residue to change abruptly, causing highlighted residue in panel 2
867    // to change, causing a scroll in panel 1 etc)
868  2 ap.setToScrollComplementPanel(false);
869  2 wasScrolled = ap.scrollToPosition(results);
870  2 if (wasScrolled)
871    {
872  0 seqCanvas.revalidate();
873    }
874  2 ap.setToScrollComplementPanel(true);
875    }
876   
877  2 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
878  2 if (seqCanvas.highlightSearchResults(results, fastPaint))
879    {
880  0 setStatusMessage(results);
881    }
882  2 return results.isEmpty() ? null : getHighlightInfo(results);
883    }
884   
885    /**
886    * temporary hack: answers a message suitable to show on structure hover
887    * label. This is normally null. It is a peptide variation description if
888    * <ul>
889    * <li>results are a single residue in a protein alignment</li>
890    * <li>there is a mapping to a coding sequence (codon)</li>
891    * <li>there are one or more SNP variant features on the codon</li>
892    * </ul>
893    * in which case the answer is of the format (e.g.) "p.Glu388Asp"
894    *
895    * @param results
896    * @return
897    */
 
898  2 toggle private String getHighlightInfo(SearchResultsI results)
899    {
900    /*
901    * ideally, just find mapped CDS (as we don't care about render style here);
902    * for now, go via split frame complement's FeatureRenderer
903    */
904  2 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
905  2 if (complement == null)
906    {
907  2 return null;
908    }
909  0 AlignFrame af = Desktop.getAlignFrameFor(complement);
910  0 FeatureRendererModel fr2 = af.getFeatureRenderer();
911   
912  0 int j = results.getSize();
913  0 List<String> infos = new ArrayList<>();
914  0 for (int i = 0; i < j; i++)
915    {
916  0 SearchResultMatchI match = results.getResults().get(i);
917  0 int pos = match.getStart();
918  0 if (pos == match.getEnd())
919    {
920  0 SequenceI seq = match.getSequence();
921  0 SequenceI ds = seq.getDatasetSequence() == null ? seq
922    : seq.getDatasetSequence();
923  0 MappedFeatures mf = fr2
924    .findComplementFeaturesAtResidue(ds, pos);
925  0 if (mf != null)
926    {
927  0 for (SequenceFeature sf : mf.features)
928    {
929  0 String pv = mf.findProteinVariants(sf);
930  0 if (pv.length() > 0 && !infos.contains(pv))
931    {
932  0 infos.add(pv);
933    }
934    }
935    }
936    }
937    }
938   
939  0 if (infos.isEmpty())
940    {
941  0 return null;
942    }
943  0 StringBuilder sb = new StringBuilder();
944  0 for (String info : infos)
945    {
946  0 if (sb.length() > 0)
947    {
948  0 sb.append("|");
949    }
950  0 sb.append(info);
951    }
952  0 return sb.toString();
953    }
954   
 
955  0 toggle @Override
956    public VamsasSource getVamsasSource()
957    {
958  0 return this.ap == null ? null : this.ap.av;
959    }
960   
 
961  0 toggle @Override
962    public void updateColours(SequenceI seq, int index)
963    {
964  0 System.out.println("update the seqPanel colours");
965    // repaint();
966    }
967   
968    /**
969    * Action on mouse movement is to update the status bar to show the current
970    * sequence position, and (if features are shown) to show any features at the
971    * position in a tooltip. Does nothing if the mouse move does not change
972    * residue position.
973    *
974    * @param evt
975    */
 
976  0 toggle @Override
977    public void mouseMoved(MouseEvent evt)
978    {
979  0 if (editingSeqs)
980    {
981    // This is because MacOSX creates a mouseMoved
982    // If control is down, other platforms will not.
983  0 mouseDragged(evt);
984    }
985   
986  0 final MousePos mousePos = findMousePosition(evt);
987  0 if (mousePos.equals(lastMousePosition))
988    {
989    /*
990    * just a pixel move without change of 'cell'
991    */
992  0 moveTooltip = false;
993  0 return;
994    }
995  0 moveTooltip = true;
996  0 lastMousePosition = mousePos;
997   
998  0 if (mousePos.isOverAnnotation())
999    {
1000  0 mouseMovedOverAnnotation(mousePos);
1001  0 return;
1002    }
1003  0 final int seq = mousePos.seqIndex;
1004   
1005  0 final int column = mousePos.column;
1006  0 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1007    {
1008  0 lastMousePosition = null;
1009  0 setToolTipText(null);
1010  0 lastTooltip = null;
1011  0 lastFormattedTooltip = null;
1012  0 ap.alignFrame.setStatus("");
1013  0 return;
1014    }
1015   
1016  0 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1017   
1018  0 if (column >= sequence.getLength())
1019    {
1020  0 return;
1021    }
1022   
1023    /*
1024    * set status bar message, returning residue position in sequence
1025    */
1026  0 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1027  0 final int pos = setStatusMessage(sequence, column, seq);
1028  0 if (ssm != null && !isGapped)
1029    {
1030  0 mouseOverSequence(sequence, column, pos);
1031    }
1032   
1033  0 StringBuilder tooltipText = new StringBuilder(64);
1034   
1035  0 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1036  0 if (groups != null)
1037    {
1038  0 for (int g = 0; g < groups.length; g++)
1039    {
1040  0 if (groups[g].getStartRes() <= column
1041    && groups[g].getEndRes() >= column)
1042    {
1043  0 if (!groups[g].getName().startsWith("JTreeGroup")
1044    && !groups[g].getName().startsWith("JGroup"))
1045    {
1046  0 tooltipText.append(groups[g].getName());
1047    }
1048   
1049  0 if (groups[g].getDescription() != null)
1050    {
1051  0 tooltipText.append(": " + groups[g].getDescription());
1052    }
1053    }
1054    }
1055    }
1056   
1057    /*
1058    * add any features at the position to the tooltip; if over a gap, only
1059    * add features that straddle the gap (pos may be the residue before or
1060    * after the gap)
1061    */
1062  0 int unshownFeatures = 0;
1063  0 if (av.isShowSequenceFeatures())
1064    {
1065  0 List<SequenceFeature> features = ap.getFeatureRenderer()
1066    .findFeaturesAtColumn(sequence, column + 1);
1067  0 unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
1068    features, this.ap.getSeqPanel().seqCanvas.fr,
1069    MAX_TOOLTIP_LENGTH);
1070   
1071    /*
1072    * add features in CDS/protein complement at the corresponding
1073    * position if configured to do so
1074    */
1075  0 if (av.isShowComplementFeatures())
1076    {
1077  0 if (!Comparison.isGap(sequence.getCharAt(column)))
1078    {
1079  0 AlignViewportI complement = ap.getAlignViewport()
1080    .getCodingComplement();
1081  0 AlignFrame af = Desktop.getAlignFrameFor(complement);
1082  0 FeatureRendererModel fr2 = af.getFeatureRenderer();
1083  0 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1084    pos);
1085  0 if (mf != null)
1086    {
1087  0 unshownFeatures += seqARep.appendFeatures(tooltipText,
1088    pos, mf, fr2, MAX_TOOLTIP_LENGTH);
1089    }
1090    }
1091    }
1092    }
1093  0 if (tooltipText.length() == 0) // nothing added
1094    {
1095  0 setToolTipText(null);
1096  0 lastTooltip = null;
1097    }
1098    else
1099    {
1100  0 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1101    {
1102  0 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1103  0 tooltipText.append("...");
1104    }
1105  0 if (unshownFeatures > 0)
1106    {
1107  0 tooltipText.append("<br/>").append("... ").append("<i>")
1108    .append(MessageManager.formatMessage(
1109    "label.features_not_shown", unshownFeatures))
1110    .append("</i>");
1111    }
1112  0 String textString = tooltipText.toString();
1113  0 if (!textString.equals(lastTooltip))
1114    {
1115  0 lastTooltip = textString;
1116  0 lastFormattedTooltip = JvSwingUtils.wrapTooltip(true,
1117    textString);
1118  0 setToolTipText(lastFormattedTooltip);
1119    }
1120    }
1121    }
1122   
1123    /**
1124    * When the view is in wrapped mode, and the mouse is over an annotation row,
1125    * shows the corresponding tooltip and status message (if any)
1126    *
1127    * @param pos
1128    * @param column
1129    */
 
1130  0 toggle protected void mouseMovedOverAnnotation(MousePos pos)
1131    {
1132  0 final int column = pos.column;
1133  0 final int rowIndex = pos.annotationIndex;
1134   
1135  0 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1136    || rowIndex < 0)
1137    {
1138  0 return;
1139    }
1140  0 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1141   
1142  0 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1143    anns);
1144  0 if (!tooltip.equals(lastTooltip))
1145    {
1146  0 lastTooltip = tooltip;
1147  0 lastFormattedTooltip = tooltip == null ? null
1148    : JvSwingUtils.wrapTooltip(true, tooltip);
1149  0 setToolTipText(lastFormattedTooltip);
1150    }
1151   
1152  0 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1153    anns[rowIndex]);
1154  0 ap.alignFrame.setStatus(msg);
1155    }
1156   
1157    /*
1158    * if Shift key is held down while moving the mouse,
1159    * the tooltip location is not changed once shown
1160    */
1161    private Point lastTooltipLocation = null;
1162   
1163    /*
1164    * this flag is false for pixel moves within a residue,
1165    * to reduce tooltip flicker
1166    */
1167    private boolean moveTooltip = true;
1168   
1169    /*
1170    * a dummy tooltip used to estimate where to position tooltips
1171    */
1172    private JToolTip tempTip = new JLabel().createToolTip();
1173   
1174    /*
1175    * (non-Javadoc)
1176    *
1177    * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1178    */
 
1179  0 toggle @Override
1180    public Point getToolTipLocation(MouseEvent event)
1181    {
1182    // BH 2018
1183   
1184  0 if (lastTooltip == null || !moveTooltip)
1185    {
1186  0 return null;
1187    }
1188   
1189  0 if (lastTooltipLocation != null && event.isShiftDown())
1190    {
1191  0 return lastTooltipLocation;
1192    }
1193   
1194  0 int x = event.getX();
1195  0 int y = event.getY();
1196  0 int w = getWidth();
1197   
1198  0 tempTip.setTipText(lastFormattedTooltip);
1199  0 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1200   
1201    // was x += (w - x < 200) ? -(w / 2) : 5;
1202  0 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1203  0 Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
1204   
1205  0 return lastTooltipLocation = p;
1206    }
1207   
1208    /**
1209    * set when the current UI interaction has resulted in a change that requires
1210    * shading in overviews and structures to be recalculated. this could be
1211    * changed to a something more expressive that indicates what actually has
1212    * changed, so selective redraws can be applied (ie. only structures, only
1213    * overview, etc)
1214    */
1215    private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1216   
1217    /**
1218    * set if av.getSelectionGroup() refers to a group that is defined on the
1219    * alignment view, rather than a transient selection
1220    */
1221    // private boolean editingDefinedGroup = false; // TODO: refactor to
1222    // avcontroller or viewModel
1223   
1224    /**
1225    * Sets the status message in alignment panel, showing the sequence number
1226    * (index) and id, and residue and residue position if not at a gap, for the
1227    * given sequence and column position. Returns the residue position returned
1228    * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1229    * if at a gapped position.
1230    *
1231    * @param sequence
1232    * aligned sequence object
1233    * @param column
1234    * alignment column
1235    * @param seqIndex
1236    * index of sequence in alignment
1237    * @return sequence position of residue at column, or adjacent residue if at a
1238    * gap
1239    */
 
1240  5 toggle int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1241    {
1242  5 char sequenceChar = sequence.getCharAt(column);
1243  5 int pos = sequence.findPosition(column);
1244  5 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1245   
1246  5 return pos;
1247    }
1248   
1249    /**
1250    * Builds the status message for the current cursor location and writes it to
1251    * the status bar, for example
1252    *
1253    * <pre>
1254    * Sequence 3 ID: FER1_SOLLC
1255    * Sequence 5 ID: FER1_PEA Residue: THR (4)
1256    * Sequence 5 ID: FER1_PEA Residue: B (3)
1257    * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1258    * </pre>
1259    *
1260    * @param seqName
1261    * @param seqIndex
1262    * sequence position in the alignment (1..)
1263    * @param sequenceChar
1264    * the character under the cursor
1265    * @param residuePos
1266    * the sequence residue position (if not over a gap)
1267    */
 
1268  5 toggle protected void setStatusMessage(String seqName, int seqIndex,
1269    char sequenceChar, int residuePos)
1270    {
1271  5 StringBuilder text = new StringBuilder(32);
1272   
1273    /*
1274    * Sequence number (if known), and sequence name.
1275    */
1276  5 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1277  5 text.append("Sequence").append(seqno).append(" ID: ")
1278    .append(seqName);
1279   
1280  5 String residue = null;
1281   
1282    /*
1283    * Try to translate the display character to residue name (null for gap).
1284    */
1285  5 boolean isGapped = Comparison.isGap(sequenceChar);
1286   
1287  5 if (!isGapped)
1288    {
1289  3 boolean nucleotide = av.getAlignment().isNucleotide();
1290  3 String displayChar = String.valueOf(sequenceChar);
1291  3 if (nucleotide)
1292    {
1293  0 residue = ResidueProperties.nucleotideName.get(displayChar);
1294    }
1295    else
1296    {
1297  3 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1298  3 : ("*".equals(displayChar) ? "STOP"
1299    : ResidueProperties.aa2Triplet.get(displayChar));
1300    }
1301  3 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1302  3 .append(": ").append(residue == null ? displayChar : residue);
1303   
1304  3 text.append(" (").append(Integer.toString(residuePos)).append(")");
1305    }
1306  5 ap.alignFrame.setStatus(text.toString());
1307    }
1308   
1309    /**
1310    * Set the status bar message to highlight the first matched position in
1311    * search results.
1312    *
1313    * @param results
1314    */
 
1315  0 toggle private void setStatusMessage(SearchResultsI results)
1316    {
1317  0 AlignmentI al = this.av.getAlignment();
1318  0 int sequenceIndex = al.findIndex(results);
1319  0 if (sequenceIndex == -1)
1320    {
1321  0 return;
1322    }
1323  0 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1324  0 SequenceI ds = alignedSeq.getDatasetSequence();
1325  0 for (SearchResultMatchI m : results.getResults())
1326    {
1327  0 SequenceI seq = m.getSequence();
1328  0 if (seq.getDatasetSequence() != null)
1329    {
1330  0 seq = seq.getDatasetSequence();
1331    }
1332   
1333  0 if (seq == ds)
1334    {
1335  0 int start = m.getStart();
1336  0 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1337    seq.getCharAt(start - 1), start);
1338  0 return;
1339    }
1340    }
1341    }
1342   
1343    /**
1344    * {@inheritDoc}
1345    */
 
1346  0 toggle @Override
1347    public void mouseDragged(MouseEvent evt)
1348    {
1349  0 MousePos pos = findMousePosition(evt);
1350  0 if (pos.isOverAnnotation() || pos.column == -1)
1351    {
1352  0 return;
1353    }
1354   
1355  0 if (mouseWheelPressed)
1356    {
1357  0 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1358  0 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1359   
1360  0 int oldWidth = av.getCharWidth();
1361   
1362    // Which is bigger, left-right or up-down?
1363  0 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1364    .abs(evt.getX() - lastMousePress.getX()))
1365    {
1366    /*
1367    * on drag up or down, decrement or increment font size
1368    */
1369  0 int fontSize = av.font.getSize();
1370  0 boolean fontChanged = false;
1371   
1372  0 if (evt.getY() < lastMousePress.getY())
1373    {
1374  0 fontChanged = true;
1375  0 fontSize--;
1376    }
1377  0 else if (evt.getY() > lastMousePress.getY())
1378    {
1379  0 fontChanged = true;
1380  0 fontSize++;
1381    }
1382   
1383  0 if (fontSize < 1)
1384    {
1385  0 fontSize = 1;
1386    }
1387   
1388  0 if (fontChanged)
1389    {
1390  0 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1391    fontSize);
1392  0 av.setFont(newFont, true);
1393  0 av.setCharWidth(oldWidth);
1394  0 ap.fontChanged();
1395  0 if (copyChanges)
1396    {
1397  0 ap.av.getCodingComplement().setFont(newFont, true);
1398  0 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1399    .getSplitViewContainer();
1400  0 splitFrame.adjustLayout();
1401  0 splitFrame.repaint();
1402    }
1403    }
1404    }
1405    else
1406    {
1407    /*
1408    * on drag left or right, decrement or increment character width
1409    */
1410  0 int newWidth = 0;
1411  0 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1412    {
1413  0 newWidth = av.getCharWidth() - 1;
1414  0 av.setCharWidth(newWidth);
1415    }
1416  0 else if (evt.getX() > lastMousePress.getX())
1417    {
1418  0 newWidth = av.getCharWidth() + 1;
1419  0 av.setCharWidth(newWidth);
1420    }
1421  0 if (newWidth > 0)
1422    {
1423  0 ap.paintAlignment(false, false);
1424  0 if (copyChanges)
1425    {
1426    /*
1427    * need to ensure newWidth is set on cdna, regardless of which
1428    * panel the mouse drag happened in; protein will compute its
1429    * character width as 1:1 or 3:1
1430    */
1431  0 av.getCodingComplement().setCharWidth(newWidth);
1432  0 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1433    .getSplitViewContainer();
1434  0 splitFrame.adjustLayout();
1435  0 splitFrame.repaint();
1436    }
1437    }
1438    }
1439   
1440  0 FontMetrics fm = getFontMetrics(av.getFont());
1441  0 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1442   
1443  0 lastMousePress = evt.getPoint();
1444   
1445  0 return;
1446    }
1447   
1448  0 if (!editingSeqs)
1449    {
1450  0 dragStretchGroup(evt);
1451  0 return;
1452    }
1453   
1454  0 int res = pos.column;
1455   
1456  0 if (res < 0)
1457    {
1458  0 res = 0;
1459    }
1460   
1461  0 if ((editLastRes == -1) || (editLastRes == res))
1462    {
1463  0 return;
1464    }
1465   
1466  0 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1467    {
1468    // dragLeft, delete gap
1469  0 editSequence(false, false, res);
1470    }
1471    else
1472    {
1473  0 editSequence(true, false, res);
1474    }
1475   
1476  0 mouseDragging = true;
1477  0 if (scrollThread != null)
1478    {
1479  0 scrollThread.setMousePosition(evt.getPoint());
1480    }
1481    }
1482   
1483    /**
1484    * Edits the sequence to insert or delete one or more gaps, in response to a
1485    * mouse drag or cursor mode command. The number of inserts/deletes may be
1486    * specified with the cursor command, or else depends on the mouse event
1487    * (normally one column, but potentially more for a fast mouse drag).
1488    * <p>
1489    * Delete gaps is limited to the number of gaps left of the cursor position
1490    * (mouse drag), or at or right of the cursor position (cursor mode).
1491    * <p>
1492    * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1493    * the current selection group.
1494    * <p>
1495    * In locked editing mode (with a selection group present), inserts/deletions
1496    * within the selection group are limited to its boundaries (and edits outside
1497    * the group stop at its border).
1498    *
1499    * @param insertGap
1500    * true to insert gaps, false to delete gaps
1501    * @param editSeq
1502    * (unused parameter)
1503    * @param startres
1504    * the column at which to perform the action; the number of columns
1505    * affected depends on <code>this.editLastRes</code> (cursor column
1506    * position)
1507    */
 
1508  0 toggle synchronized void editSequence(boolean insertGap, boolean editSeq,
1509    final int startres)
1510    {
1511  0 int fixedLeft = -1;
1512  0 int fixedRight = -1;
1513  0 boolean fixedColumns = false;
1514  0 SequenceGroup sg = av.getSelectionGroup();
1515   
1516  0 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1517   
1518    // No group, but the sequence may represent a group
1519  0 if (!groupEditing && av.hasHiddenRows())
1520    {
1521  0 if (av.isHiddenRepSequence(seq))
1522    {
1523  0 sg = av.getRepresentedSequences(seq);
1524  0 groupEditing = true;
1525    }
1526    }
1527   
1528  0 StringBuilder message = new StringBuilder(64); // for status bar
1529   
1530    /*
1531    * make a name for the edit action, for
1532    * status bar message and Undo/Redo menu
1533    */
1534  0 String label = null;
1535  0 if (groupEditing)
1536    {
1537  0 message.append("Edit group:");
1538  0 label = MessageManager.getString("action.edit_group");
1539    }
1540    else
1541    {
1542  0 message.append("Edit sequence: " + seq.getName());
1543  0 label = seq.getName();
1544  0 if (label.length() > 10)
1545    {
1546  0 label = label.substring(0, 10);
1547    }
1548  0 label = MessageManager.formatMessage("label.edit_params",
1549    new String[]
1550    { label });
1551    }
1552   
1553    /*
1554    * initialise the edit command if there is not
1555    * already one being extended
1556    */
1557  0 if (editCommand == null)
1558    {
1559  0 editCommand = new EditCommand(label);
1560    }
1561   
1562  0 if (insertGap)
1563    {
1564  0 message.append(" insert ");
1565    }
1566    else
1567    {
1568  0 message.append(" delete ");
1569    }
1570   
1571  0 message.append(Math.abs(startres - editLastRes) + " gaps.");
1572  0 ap.alignFrame.setStatus(message.toString());
1573   
1574    /*
1575    * is there a selection group containing the sequence being edited?
1576    * if so the boundary of the group is the limit of the edit
1577    * (but the edit may be inside or outside the selection group)
1578    */
1579  0 boolean inSelectionGroup = sg != null
1580    && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1581  0 if (groupEditing || inSelectionGroup)
1582    {
1583  0 fixedColumns = true;
1584   
1585    // sg might be null as the user may only see 1 sequence,
1586    // but the sequence represents a group
1587  0 if (sg == null)
1588    {
1589  0 if (!av.isHiddenRepSequence(seq))
1590    {
1591  0 endEditing();
1592  0 return;
1593    }
1594  0 sg = av.getRepresentedSequences(seq);
1595    }
1596   
1597  0 fixedLeft = sg.getStartRes();
1598  0 fixedRight = sg.getEndRes();
1599   
1600  0 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1601    || (startres >= fixedLeft && editLastRes < fixedLeft)
1602    || (startres > fixedRight && editLastRes <= fixedRight)
1603    || (startres <= fixedRight && editLastRes > fixedRight))
1604    {
1605  0 endEditing();
1606  0 return;
1607    }
1608   
1609  0 if (fixedLeft > startres)
1610    {
1611  0 fixedRight = fixedLeft - 1;
1612  0 fixedLeft = 0;
1613    }
1614  0 else if (fixedRight < startres)
1615    {
1616  0 fixedLeft = fixedRight;
1617  0 fixedRight = -1;
1618    }
1619    }
1620   
1621  0 if (av.hasHiddenColumns())
1622    {
1623  0 fixedColumns = true;
1624  0 int y1 = av.getAlignment().getHiddenColumns()
1625    .getNextHiddenBoundary(true, startres);
1626  0 int y2 = av.getAlignment().getHiddenColumns()
1627    .getNextHiddenBoundary(false, startres);
1628   
1629  0 if ((insertGap && startres > y1 && editLastRes < y1)
1630    || (!insertGap && startres < y2 && editLastRes > y2))
1631    {
1632  0 endEditing();
1633  0 return;
1634    }
1635   
1636    // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1637    // Selection spans a hidden region
1638  0 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1639    {
1640  0 if (startres >= y2)
1641    {
1642  0 fixedLeft = y2;
1643    }
1644    else
1645    {
1646  0 fixedRight = y2 - 1;
1647    }
1648    }
1649    }
1650   
1651  0 boolean success = doEditSequence(insertGap, editSeq, startres,
1652    fixedRight, fixedColumns, sg);
1653   
1654    /*
1655    * report what actually happened (might be less than
1656    * what was requested), by inspecting the edit commands added
1657    */
1658  0 String msg = getEditStatusMessage(editCommand);
1659  0 ap.alignFrame.setStatus(msg == null ? " " : msg);
1660  0 if (!success)
1661    {
1662  0 endEditing();
1663    }
1664   
1665  0 editLastRes = startres;
1666  0 seqCanvas.repaint();
1667    }
1668   
1669    /**
1670    * A helper method that performs the requested editing to insert or delete
1671    * gaps (if possible). Answers true if the edit was successful, false if could
1672    * only be performed in part or not at all. Failure may occur in 'locked edit'
1673    * mode, when an insertion requires a matching gapped position (or column) to
1674    * delete, and deletion requires an adjacent gapped position (or column) to
1675    * remove.
1676    *
1677    * @param insertGap
1678    * true if inserting gap(s), false if deleting
1679    * @param editSeq
1680    * (unused parameter, currently always false)
1681    * @param startres
1682    * the column at which to perform the edit
1683    * @param fixedRight
1684    * fixed right boundary column of a locked edit (within or to the
1685    * left of a selection group)
1686    * @param fixedColumns
1687    * true if this is a locked edit
1688    * @param sg
1689    * the sequence group (if group edit is being performed)
1690    * @return
1691    */
 
1692  0 toggle protected boolean doEditSequence(final boolean insertGap,
1693    final boolean editSeq, final int startres, int fixedRight,
1694    final boolean fixedColumns, final SequenceGroup sg)
1695    {
1696  0 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1697  0 SequenceI[] seqs = new SequenceI[] { seq };
1698   
1699  0 if (groupEditing)
1700    {
1701  0 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1702  0 int g, groupSize = vseqs.size();
1703  0 SequenceI[] groupSeqs = new SequenceI[groupSize];
1704  0 for (g = 0; g < groupSeqs.length; g++)
1705    {
1706  0 groupSeqs[g] = vseqs.get(g);
1707    }
1708   
1709    // drag to right
1710  0 if (insertGap)
1711    {
1712    // If the user has selected the whole sequence, and is dragging to
1713    // the right, we can still extend the alignment and selectionGroup
1714  0 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1715    && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1716    {
1717  0 sg.setEndRes(
1718    av.getAlignment().getWidth() + startres - editLastRes);
1719  0 fixedRight = sg.getEndRes();
1720    }
1721   
1722    // Is it valid with fixed columns??
1723    // Find the next gap before the end
1724    // of the visible region boundary
1725  0 boolean blank = false;
1726  0 for (; fixedRight > editLastRes; fixedRight--)
1727    {
1728  0 blank = true;
1729   
1730  0 for (g = 0; g < groupSize; g++)
1731    {
1732  0 for (int j = 0; j < startres - editLastRes; j++)
1733    {
1734  0 if (!Comparison
1735    .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1736    {
1737  0 blank = false;
1738  0 break;
1739    }
1740    }
1741    }
1742  0 if (blank)
1743    {
1744  0 break;
1745    }
1746    }
1747   
1748  0 if (!blank)
1749    {
1750  0 if (sg.getSize() == av.getAlignment().getHeight())
1751    {
1752  0 if ((av.hasHiddenColumns()
1753    && startres < av.getAlignment().getHiddenColumns()
1754    .getNextHiddenBoundary(false, startres)))
1755    {
1756  0 return false;
1757    }
1758   
1759  0 int alWidth = av.getAlignment().getWidth();
1760  0 if (av.hasHiddenRows())
1761    {
1762  0 int hwidth = av.getAlignment().getHiddenSequences()
1763    .getWidth();
1764  0 if (hwidth > alWidth)
1765    {
1766  0 alWidth = hwidth;
1767    }
1768    }
1769    // We can still insert gaps if the selectionGroup
1770    // contains all the sequences
1771  0 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1772  0 fixedRight = alWidth + startres - editLastRes;
1773    }
1774    else
1775    {
1776  0 return false;
1777    }
1778    }
1779    }
1780   
1781    // drag to left
1782  0 else if (!insertGap)
1783    {
1784    // / Are we able to delete?
1785    // ie are all columns blank?
1786   
1787  0 for (g = 0; g < groupSize; g++)
1788    {
1789  0 for (int j = startres; j < editLastRes; j++)
1790    {
1791  0 if (groupSeqs[g].getLength() <= j)
1792    {
1793  0 continue;
1794    }
1795   
1796  0 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1797    {
1798    // Not a gap, block edit not valid
1799  0 return false;
1800    }
1801    }
1802    }
1803    }
1804   
1805  0 if (insertGap)
1806    {
1807    // dragging to the right
1808  0 if (fixedColumns && fixedRight != -1)
1809    {
1810  0 for (int j = editLastRes; j < startres; j++)
1811    {
1812  0 insertGap(j, groupSeqs, fixedRight);
1813    }
1814    }
1815    else
1816    {
1817  0 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1818    startres - editLastRes, false);
1819    }
1820    }
1821    else
1822    {
1823    // dragging to the left
1824  0 if (fixedColumns && fixedRight != -1)
1825    {
1826  0 for (int j = editLastRes; j > startres; j--)
1827    {
1828  0 deleteChar(startres, groupSeqs, fixedRight);
1829    }
1830    }
1831    else
1832    {
1833  0 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1834    editLastRes - startres, false);
1835    }
1836    }
1837    }
1838    else
1839    {
1840    /*
1841    * editing a single sequence
1842    */
1843  0 if (insertGap)
1844    {
1845    // dragging to the right
1846  0 if (fixedColumns && fixedRight != -1)
1847    {
1848  0 for (int j = editLastRes; j < startres; j++)
1849    {
1850  0 if (!insertGap(j, seqs, fixedRight))
1851    {
1852    /*
1853    * e.g. cursor mode command specified
1854    * more inserts than are possible
1855    */
1856  0 return false;
1857    }
1858    }
1859    }
1860    else
1861    {
1862  0 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1863    startres - editLastRes, false);
1864    }
1865    }
1866    else
1867    {
1868  0 if (!editSeq)
1869    {
1870    // dragging to the left
1871  0 if (fixedColumns && fixedRight != -1)
1872    {
1873  0 for (int j = editLastRes; j > startres; j--)
1874    {
1875  0 if (!Comparison.isGap(seq.getCharAt(startres)))
1876    {
1877  0 return false;
1878    }
1879  0 deleteChar(startres, seqs, fixedRight);
1880    }
1881    }
1882    else
1883    {
1884    // could be a keyboard edit trying to delete none gaps
1885  0 int max = 0;
1886  0 for (int m = startres; m < editLastRes; m++)
1887    {
1888  0 if (!Comparison.isGap(seq.getCharAt(m)))
1889    {
1890  0 break;
1891    }
1892  0 max++;
1893    }
1894  0 if (max > 0)
1895    {
1896  0 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1897    }
1898    }
1899    }
1900    else
1901    {// insertGap==false AND editSeq==TRUE;
1902  0 if (fixedColumns && fixedRight != -1)
1903    {
1904  0 for (int j = editLastRes; j < startres; j++)
1905    {
1906  0 insertGap(j, seqs, fixedRight);
1907    }
1908    }
1909    else
1910    {
1911  0 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1912    startres - editLastRes, false);
1913    }
1914    }
1915    }
1916    }
1917   
1918  0 return true;
1919    }
1920   
1921    /**
1922    * Constructs an informative status bar message while dragging to insert or
1923    * delete gaps. Answers null if inserts and deletes cancel out.
1924    *
1925    * @param editCommand
1926    * a command containing the list of individual edits
1927    * @return
1928    */
 
1929  14 toggle protected static String getEditStatusMessage(EditCommand editCommand)
1930    {
1931  14 if (editCommand == null)
1932    {
1933  1 return null;
1934    }
1935   
1936    /*
1937    * add any inserts, and subtract any deletes,
1938    * not counting those auto-inserted when doing a 'locked edit'
1939    * (so only counting edits 'under the cursor')
1940    */
1941  13 int count = 0;
1942  13 for (Edit cmd : editCommand.getEdits())
1943    {
1944  63 if (!cmd.isSystemGenerated())
1945    {
1946  42 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1947    : -cmd.getNumber();
1948    }
1949    }
1950   
1951  13 if (count == 0)
1952    {
1953    /*
1954    * inserts and deletes cancel out
1955    */
1956  3 return null;
1957    }
1958   
1959  10 String msgKey = count > 1 ? "label.insert_gaps"
1960  7 : (count == 1 ? "label.insert_gap"
1961  4 : (count == -1 ? "label.delete_gap"
1962    : "label.delete_gaps"));
1963  10 count = Math.abs(count);
1964   
1965  10 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1966    }
1967   
1968    /**
1969    * Inserts one gap at column j, deleting the right-most gapped column up to
1970    * (and including) fixedColumn. Returns true if the edit is successful, false
1971    * if no blank column is available to allow the insertion to be balanced by a
1972    * deletion.
1973    *
1974    * @param j
1975    * @param seq
1976    * @param fixedColumn
1977    * @return
1978    */
 
1979  0 toggle boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1980    {
1981  0 int blankColumn = fixedColumn;
1982  0 for (int s = 0; s < seq.length; s++)
1983    {
1984    // Find the next gap before the end of the visible region boundary
1985    // If lastCol > j, theres a boundary after the gap insertion
1986   
1987  0 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1988    {
1989  0 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1990    {
1991    // Theres a space, so break and insert the gap
1992  0 break;
1993    }
1994    }
1995   
1996  0 if (blankColumn <= j)
1997    {
1998  0 blankColumn = fixedColumn;
1999  0 endEditing();
2000  0 return false;
2001    }
2002    }
2003   
2004  0 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
2005   
2006  0 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2007   
2008  0 return true;
2009    }
2010   
2011    /**
2012    * Helper method to add and perform one edit action
2013    *
2014    * @param action
2015    * @param seq
2016    * @param pos
2017    * @param count
2018    * @param systemGenerated
2019    * true if the edit is a 'balancing' delete (or insert) to match a
2020    * user's insert (or delete) in a locked editing region
2021    */
 
2022  0 toggle protected void appendEdit(Action action, SequenceI[] seq, int pos,
2023    int count, boolean systemGenerated)
2024    {
2025   
2026  0 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2027    av.getAlignment().getGapCharacter());
2028  0 edit.setSystemGenerated(systemGenerated);
2029   
2030  0 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2031    }
2032   
2033    /**
2034    * Deletes the character at column j, and inserts a gap at fixedColumn, in
2035    * each of the given sequences. The caller should ensure that all sequences
2036    * are gapped in column j.
2037    *
2038    * @param j
2039    * @param seqs
2040    * @param fixedColumn
2041    */
 
2042  0 toggle void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2043    {
2044  0 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2045   
2046  0 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2047    }
2048   
2049    /**
2050    * On reentering the panel, stops any scrolling that was started on dragging
2051    * out of the panel
2052    *
2053    * @param e
2054    */
 
2055  0 toggle @Override
2056    public void mouseEntered(MouseEvent e)
2057    {
2058  0 if (oldSeq < 0)
2059    {
2060  0 oldSeq = 0;
2061    }
2062  0 stopScrolling();
2063    }
2064   
2065    /**
2066    * On leaving the panel, if the mouse is being dragged, starts a thread to
2067    * scroll it until the mouse is released (in unwrapped mode only)
2068    *
2069    * @param e
2070    */
 
2071  0 toggle @Override
2072    public void mouseExited(MouseEvent e)
2073    {
2074  0 lastMousePosition = null;
2075  0 ap.alignFrame.setStatus(" ");
2076  0 if (av.getWrapAlignment())
2077    {
2078  0 return;
2079    }
2080   
2081  0 if (mouseDragging && scrollThread == null)
2082    {
2083  0 startScrolling(e.getPoint());
2084    }
2085    }
2086   
2087    /**
2088    * Handler for double-click on a position with one or more sequence features.
2089    * Opens the Amend Features dialog to allow feature details to be amended, or
2090    * the feature deleted.
2091    */
 
2092  0 toggle @Override
2093    public void mouseClicked(MouseEvent evt)
2094    {
2095  0 SequenceGroup sg = null;
2096  0 MousePos pos = findMousePosition(evt);
2097  0 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2098    {
2099  0 return;
2100    }
2101   
2102  0 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2103    {
2104  0 sg = av.getSelectionGroup();
2105  0 if (sg != null && sg.getSize() == 1
2106    && sg.getEndRes() - sg.getStartRes() < 2)
2107    {
2108  0 av.setSelectionGroup(null);
2109    }
2110   
2111  0 int column = pos.column;
2112   
2113    /*
2114    * find features at the position (if not gapped), or straddling
2115    * the position (if at a gap)
2116    */
2117  0 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2118  0 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2119    .findFeaturesAtColumn(sequence, column + 1);
2120   
2121  0 if (!features.isEmpty())
2122    {
2123    /*
2124    * highlight the first feature at the position on the alignment
2125    */
2126  0 SearchResultsI highlight = new SearchResults();
2127  0 highlight.addResult(sequence, features.get(0).getBegin(), features
2128    .get(0).getEnd());
2129  0 seqCanvas.highlightSearchResults(highlight, true);
2130   
2131    /*
2132    * open the Amend Features dialog
2133    */
2134  0 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2135    false).showDialog();
2136    }
2137    }
2138    }
2139   
 
2140  0 toggle @Override
2141    public void mouseWheelMoved(MouseWheelEvent e)
2142    {
2143  0 e.consume();
2144  0 double wheelRotation = e.getPreciseWheelRotation();
2145  0 if (wheelRotation > 0)
2146    {
2147  0 if (e.isShiftDown())
2148    {
2149  0 av.getRanges().scrollRight(true);
2150   
2151    }
2152    else
2153    {
2154  0 av.getRanges().scrollUp(false);
2155    }
2156    }
2157  0 else if (wheelRotation < 0)
2158    {
2159  0 if (e.isShiftDown())
2160    {
2161  0 av.getRanges().scrollRight(false);
2162    }
2163    else
2164    {
2165  0 av.getRanges().scrollUp(true);
2166    }
2167    }
2168   
2169    /*
2170    * update status bar and tooltip for new position
2171    * (need to synthesize a mouse movement to refresh tooltip)
2172    */
2173  0 mouseMoved(e);
2174  0 ToolTipManager.sharedInstance().mouseMoved(e);
2175    }
2176   
2177    /**
2178    * DOCUMENT ME!
2179    *
2180    * @param pos
2181    * DOCUMENT ME!
2182    */
 
2183  0 toggle protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2184    {
2185  0 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2186    {
2187  0 return;
2188    }
2189   
2190  0 final int res = pos.column;
2191  0 final int seq = pos.seqIndex;
2192  0 oldSeq = seq;
2193  0 updateOverviewAndStructs = false;
2194   
2195  0 startWrapBlock = wrappedBlock;
2196   
2197  0 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2198   
2199  0 if ((sequence == null) || (res > sequence.getLength()))
2200    {
2201  0 return;
2202    }
2203   
2204  0 stretchGroup = av.getSelectionGroup();
2205   
2206  0 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2207    {
2208  0 stretchGroup = av.getAlignment().findGroup(sequence, res);
2209  0 if (stretchGroup != null)
2210    {
2211    // only update the current selection if the popup menu has a group to
2212    // focus on
2213  0 av.setSelectionGroup(stretchGroup);
2214    }
2215    }
2216   
2217    /*
2218    * defer right-mouse click handling to mouseReleased on Windows
2219    * (where isPopupTrigger() will answer true)
2220    * NB isRightMouseButton is also true for Cmd-click on Mac
2221    */
2222  0 if (Platform.isWinRightButton(evt))
2223    {
2224  0 return;
2225    }
2226   
2227  0 if (evt.isPopupTrigger()) // Mac: mousePressed
2228    {
2229  0 showPopupMenu(evt, pos);
2230  0 return;
2231    }
2232   
2233  0 if (av.cursorMode)
2234    {
2235  0 seqCanvas.cursorX = res;
2236  0 seqCanvas.cursorY = seq;
2237  0 seqCanvas.repaint();
2238  0 return;
2239    }
2240   
2241  0 if (stretchGroup == null)
2242    {
2243  0 createStretchGroup(res, sequence);
2244    }
2245   
2246  0 if (stretchGroup != null)
2247    {
2248  0 stretchGroup.addPropertyChangeListener(seqCanvas);
2249    }
2250   
2251  0 seqCanvas.repaint();
2252    }
2253   
 
2254  0 toggle private void createStretchGroup(int res, SequenceI sequence)
2255    {
2256    // Only if left mouse button do we want to change group sizes
2257    // define a new group here
2258  0 SequenceGroup sg = new SequenceGroup();
2259  0 sg.setStartRes(res);
2260  0 sg.setEndRes(res);
2261  0 sg.addSequence(sequence, false);
2262  0 av.setSelectionGroup(sg);
2263  0 stretchGroup = sg;
2264   
2265  0 if (av.getConservationSelected())
2266    {
2267  0 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2268    ap.getViewName());
2269    }
2270   
2271  0 if (av.getAbovePIDThreshold())
2272    {
2273  0 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2274    ap.getViewName());
2275    }
2276    // TODO: stretchGroup will always be not null. Is this a merge error ?
2277    // or is there a threading issue here?
2278  0 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2279    {
2280    // Edit end res position of selected group
2281  0 changeEndRes = true;
2282    }
2283  0 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2284    {
2285    // Edit end res position of selected group
2286  0 changeStartRes = true;
2287    }
2288  0 stretchGroup.getWidth();
2289   
2290    }
2291   
2292    /**
2293    * Build and show a pop-up menu at the right-click mouse position
2294    *
2295    * @param evt
2296    * @param pos
2297    */
 
2298  0 toggle void showPopupMenu(MouseEvent evt, MousePos pos)
2299    {
2300  0 final int column = pos.column;
2301  0 final int seq = pos.seqIndex;
2302  0 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2303  0 if (sequence != null)
2304    {
2305  0 PopupMenu pop = new PopupMenu(ap, sequence, column);
2306  0 pop.show(this, evt.getX(), evt.getY());
2307    }
2308    }
2309   
2310    /**
2311    * Update the display after mouse up on a selection or group
2312    *
2313    * @param evt
2314    * mouse released event details
2315    * @param afterDrag
2316    * true if this event is happening after a mouse drag (rather than a
2317    * mouse down)
2318    */
 
2319  0 toggle protected void doMouseReleasedDefineMode(MouseEvent evt,
2320    boolean afterDrag)
2321    {
2322  0 if (stretchGroup == null)
2323    {
2324  0 return;
2325    }
2326   
2327  0 stretchGroup.removePropertyChangeListener(seqCanvas);
2328   
2329    // always do this - annotation has own state
2330    // but defer colourscheme update until hidden sequences are passed in
2331  0 boolean vischange = stretchGroup.recalcConservation(true);
2332  0 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2333    && afterDrag;
2334  0 if (stretchGroup.cs != null)
2335    {
2336  0 if (afterDrag)
2337    {
2338  0 stretchGroup.cs.alignmentChanged(stretchGroup,
2339    av.getHiddenRepSequences());
2340    }
2341   
2342  0 ResidueShaderI groupColourScheme = stretchGroup
2343    .getGroupColourScheme();
2344  0 String name = stretchGroup.getName();
2345  0 if (stretchGroup.cs.conservationApplied())
2346    {
2347  0 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2348    }
2349  0 if (stretchGroup.cs.getThreshold() > 0)
2350    {
2351  0 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2352    }
2353    }
2354  0 PaintRefresher.Refresh(this, av.getSequenceSetId());
2355    // TODO: structure colours only need updating if stretchGroup used to or now
2356    // does contain sequences with structure views
2357  0 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2358  0 updateOverviewAndStructs = false;
2359  0 changeEndRes = false;
2360  0 changeStartRes = false;
2361  0 stretchGroup = null;
2362  0 av.sendSelection();
2363    }
2364   
2365    /**
2366    * Resizes the borders of a selection group depending on the direction of
2367    * mouse drag
2368    *
2369    * @param evt
2370    */
 
2371  0 toggle protected void dragStretchGroup(MouseEvent evt)
2372    {
2373  0 if (stretchGroup == null)
2374    {
2375  0 return;
2376    }
2377   
2378  0 MousePos pos = findMousePosition(evt);
2379  0 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2380    {
2381  0 return;
2382    }
2383   
2384  0 int res = pos.column;
2385  0 int y = pos.seqIndex;
2386   
2387  0 if (wrappedBlock != startWrapBlock)
2388    {
2389  0 return;
2390    }
2391   
2392  0 res = Math.min(res, av.getAlignment().getWidth()-1);
2393   
2394  0 if (stretchGroup.getEndRes() == res)
2395    {
2396    // Edit end res position of selected group
2397  0 changeEndRes = true;
2398    }
2399  0 else if (stretchGroup.getStartRes() == res)
2400    {
2401    // Edit start res position of selected group
2402  0 changeStartRes = true;
2403    }
2404   
2405  0 if (res < av.getRanges().getStartRes())
2406    {
2407  0 res = av.getRanges().getStartRes();
2408    }
2409   
2410  0 if (changeEndRes)
2411    {
2412  0 if (res > (stretchGroup.getStartRes() - 1))
2413    {
2414  0 stretchGroup.setEndRes(res);
2415  0 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2416    }
2417    }
2418  0 else if (changeStartRes)
2419    {
2420  0 if (res < (stretchGroup.getEndRes() + 1))
2421    {
2422  0 stretchGroup.setStartRes(res);
2423  0 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2424    }
2425    }
2426   
2427  0 int dragDirection = 0;
2428   
2429  0 if (y > oldSeq)
2430    {
2431  0 dragDirection = 1;
2432    }
2433  0 else if (y < oldSeq)
2434    {
2435  0 dragDirection = -1;
2436    }
2437   
2438  0 while ((y != oldSeq) && (oldSeq > -1)
2439    && (y < av.getAlignment().getHeight()))
2440    {
2441    // This routine ensures we don't skip any sequences, as the
2442    // selection is quite slow.
2443  0 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2444   
2445  0 oldSeq += dragDirection;
2446   
2447  0 if (oldSeq < 0)
2448    {
2449  0 break;
2450    }
2451   
2452  0 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2453   
2454  0 if (stretchGroup.getSequences(null).contains(nextSeq))
2455    {
2456  0 stretchGroup.deleteSequence(seq, false);
2457  0 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2458    }
2459    else
2460    {
2461  0 if (seq != null)
2462    {
2463  0 stretchGroup.addSequence(seq, false);
2464    }
2465   
2466  0 stretchGroup.addSequence(nextSeq, false);
2467  0 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2468    }
2469    }
2470   
2471  0 if (oldSeq < 0)
2472    {
2473  0 oldSeq = -1;
2474    }
2475   
2476  0 mouseDragging = true;
2477   
2478  0 if (scrollThread != null)
2479    {
2480  0 scrollThread.setMousePosition(evt.getPoint());
2481    }
2482   
2483    /*
2484    * construct a status message showing the range of the selection
2485    */
2486  0 StringBuilder status = new StringBuilder(64);
2487  0 List<SequenceI> seqs = stretchGroup.getSequences();
2488  0 String name = seqs.get(0).getName();
2489  0 if (name.length() > 20)
2490    {
2491  0 name = name.substring(0, 20);
2492    }
2493  0 status.append(name).append(" - ");
2494  0 name = seqs.get(seqs.size() - 1).getName();
2495  0 if (name.length() > 20)
2496    {
2497  0 name = name.substring(0, 20);
2498    }
2499  0 status.append(name).append(" ");
2500  0 int startRes = stretchGroup.getStartRes();
2501  0 status.append(" cols ").append(String.valueOf(startRes + 1))
2502    .append("-");
2503  0 int endRes = stretchGroup.getEndRes();
2504  0 status.append(String.valueOf(endRes + 1));
2505  0 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2506    .append(String.valueOf(endRes - startRes + 1)).append(")");
2507  0 ap.alignFrame.setStatus(status.toString());
2508    }
2509   
2510    /**
2511    * Stops the scroll thread if it is running
2512    */
 
2513  3 toggle void stopScrolling()
2514    {
2515  3 if (scrollThread != null)
2516    {
2517  0 scrollThread.stopScrolling();
2518  0 scrollThread = null;
2519    }
2520  3 mouseDragging = false;
2521    }
2522   
2523    /**
2524    * Starts a thread to scroll the alignment, towards a given mouse position
2525    * outside the panel bounds, unless the alignment is in wrapped mode
2526    *
2527    * @param mousePos
2528    */
 
2529  0 toggle void startScrolling(Point mousePos)
2530    {
2531    /*
2532    * set this.mouseDragging in case this was called from
2533    * a drag in ScalePanel or AnnotationPanel
2534    */
2535  0 mouseDragging = true;
2536  0 if (!av.getWrapAlignment() && scrollThread == null)
2537    {
2538  0 scrollThread = new ScrollThread();
2539  0 scrollThread.setMousePosition(mousePos);
2540  0 if (Platform.isJS())
2541    {
2542    /*
2543    * Javascript - run every 20ms until scrolling stopped
2544    * or reaches the limit of scrollable alignment
2545    */
2546  0 Timer t = new Timer(20, new ActionListener()
2547    {
 
2548  0 toggle @Override
2549    public void actionPerformed(ActionEvent e)
2550    {
2551  0 if (scrollThread != null)
2552    {
2553    // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2554  0 scrollThread.scrollOnce();
2555    }
2556    }
2557    });
2558  0 t.addActionListener(new ActionListener()
2559    {
 
2560  0 toggle @Override
2561    public void actionPerformed(ActionEvent e)
2562    {
2563  0 if (scrollThread == null)
2564    {
2565    // SeqPanel.stopScrolling called
2566  0 t.stop();
2567    }
2568    }
2569    });
2570  0 t.start();
2571    }
2572    else
2573    {
2574    /*
2575    * Java - run in a new thread
2576    */
2577  0 scrollThread.start();
2578    }
2579    }
2580    }
2581   
2582    /**
2583    * Performs scrolling of the visible alignment left, right, up or down, until
2584    * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2585    * limit of the alignment is reached
2586    */
 
2587    class ScrollThread extends Thread
2588    {
2589    private Point mousePos;
2590   
2591    private volatile boolean keepRunning = true;
2592   
2593    /**
2594    * Constructor
2595    */
 
2596  0 toggle public ScrollThread()
2597    {
2598  0 setName("SeqPanel$ScrollThread");
2599    }
2600   
2601    /**
2602    * Sets the position of the mouse that determines the direction of the
2603    * scroll to perform. If this is called as the mouse moves, scrolling should
2604    * respond accordingly. For example, if the mouse is dragged right, scroll
2605    * right should start; if the drag continues down, scroll down should also
2606    * happen.
2607    *
2608    * @param p
2609    */
 
2610  0 toggle public void setMousePosition(Point p)
2611    {
2612  0 mousePos = p;
2613    }
2614   
2615    /**
2616    * Sets a flag that will cause the thread to exit
2617    */
 
2618  0 toggle public void stopScrolling()
2619    {
2620  0 keepRunning = false;
2621    }
2622   
2623    /**
2624    * Scrolls the alignment left or right, and/or up or down, depending on the
2625    * last notified mouse position, until the limit of the alignment is
2626    * reached, or a flag is set to stop the scroll
2627    */
 
2628  0 toggle @Override
2629    public void run()
2630    {
2631  0 while (keepRunning)
2632    {
2633  0 if (mousePos != null)
2634    {
2635  0 keepRunning = scrollOnce();
2636    }
2637  0 try
2638    {
2639  0 Thread.sleep(20);
2640    } catch (Exception ex)
2641    {
2642    }
2643    }
2644  0 SeqPanel.this.scrollThread = null;
2645    }
2646   
2647    /**
2648    * Scrolls
2649    * <ul>
2650    * <li>one row up, if the mouse is above the panel</li>
2651    * <li>one row down, if the mouse is below the panel</li>
2652    * <li>one column left, if the mouse is left of the panel</li>
2653    * <li>one column right, if the mouse is right of the panel</li>
2654    * </ul>
2655    * Answers true if a scroll was performed, false if not - meaning either
2656    * that the mouse position is within the panel, or the edge of the alignment
2657    * has been reached.
2658    */
 
2659  0 toggle boolean scrollOnce()
2660    {
2661    /*
2662    * quit after mouseUp ensures interrupt in JalviewJS
2663    */
2664  0 if (!mouseDragging)
2665    {
2666  0 return false;
2667    }
2668   
2669  0 boolean scrolled = false;
2670  0 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2671   
2672    /*
2673    * scroll up or down
2674    */
2675  0 if (mousePos.y < 0)
2676    {
2677    // mouse is above this panel - try scroll up
2678  0 scrolled = ranges.scrollUp(true);
2679    }
2680  0 else if (mousePos.y >= getHeight())
2681    {
2682    // mouse is below this panel - try scroll down
2683  0 scrolled = ranges.scrollUp(false);
2684    }
2685   
2686    /*
2687    * scroll left or right
2688    */
2689  0 if (mousePos.x < 0)
2690    {
2691  0 scrolled |= ranges.scrollRight(false);
2692    }
2693  0 else if (mousePos.x >= getWidth())
2694    {
2695  0 scrolled |= ranges.scrollRight(true);
2696    }
2697  0 return scrolled;
2698    }
2699    }
2700   
2701    /**
2702    * modify current selection according to a received message.
2703    */
 
2704  105 toggle @Override
2705    public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2706    HiddenColumns hidden, SelectionSource source)
2707    {
2708    // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2709    // handles selection messages...
2710    // TODO: extend config options to allow user to control if selections may be
2711    // shared between viewports.
2712  105 boolean iSentTheSelection = (av == source
2713    || (source instanceof AlignViewport
2714    && ((AlignmentViewport) source).getSequenceSetId()
2715    .equals(av.getSequenceSetId())));
2716   
2717  105 if (iSentTheSelection)
2718    {
2719    // respond to our own event by updating dependent dialogs
2720  22 if (ap.getCalculationDialog() != null)
2721    {
2722  0 ap.getCalculationDialog().validateCalcTypes();
2723    }
2724   
2725  22 return;
2726    }
2727   
2728    // process further ?
2729  83 if (!av.followSelection)
2730    {
2731  0 return;
2732    }
2733   
2734    /*
2735    * Ignore the selection if there is one of our own pending.
2736    */
2737  83 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2738    {
2739  0 return;
2740    }
2741   
2742    /*
2743    * Check for selection in a view of which this one is a dna/protein
2744    * complement.
2745    */
2746  83 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2747    {
2748  0 return;
2749    }
2750   
2751    // do we want to thread this ? (contention with seqsel and colsel locks, I
2752    // suspect)
2753    /*
2754    * only copy colsel if there is a real intersection between
2755    * sequence selection and this panel's alignment
2756    */
2757  83 boolean repaint = false;
2758  83 boolean copycolsel = false;
2759   
2760  83 SequenceGroup sgroup = null;
2761  83 if (seqsel != null && seqsel.getSize() > 0)
2762    {
2763  56 if (av.getAlignment() == null)
2764    {
2765  0 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2766    + " ViewId=" + av.getViewId()
2767    + " 's alignment is NULL! returning immediately.");
2768  0 return;
2769    }
2770  56 sgroup = seqsel.intersect(av.getAlignment(),
2771  56 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2772  56 if ((sgroup != null && sgroup.getSize() > 0))
2773    {
2774  0 copycolsel = true;
2775    }
2776    }
2777  83 if (sgroup != null && sgroup.getSize() > 0)
2778    {
2779  0 av.setSelectionGroup(sgroup);
2780    }
2781    else
2782    {
2783  83 av.setSelectionGroup(null);
2784    }
2785  83 av.isSelectionGroupChanged(true);
2786  83 repaint = true;
2787   
2788  83 if (copycolsel)
2789    {
2790    // the current selection is unset or from a previous message
2791    // so import the new colsel.
2792  0 if (colsel == null || colsel.isEmpty())
2793    {
2794  0 if (av.getColumnSelection() != null)
2795    {
2796  0 av.getColumnSelection().clear();
2797  0 repaint = true;
2798    }
2799    }
2800    else
2801    {
2802    // TODO: shift colSel according to the intersecting sequences
2803  0 if (av.getColumnSelection() == null)
2804    {
2805  0 av.setColumnSelection(new ColumnSelection(colsel));
2806    }
2807    else
2808    {
2809  0 av.getColumnSelection().setElementsFrom(colsel,
2810    av.getAlignment().getHiddenColumns());
2811    }
2812    }
2813  0 av.isColSelChanged(true);
2814  0 repaint = true;
2815    }
2816   
2817  83 if (copycolsel && av.hasHiddenColumns()
2818    && (av.getAlignment().getHiddenColumns() == null))
2819    {
2820  0 System.err.println("Bad things");
2821    }
2822  83 if (repaint) // always true!
2823    {
2824    // probably finessing with multiple redraws here
2825  83 PaintRefresher.Refresh(this, av.getSequenceSetId());
2826    // ap.paintAlignment(false);
2827    }
2828   
2829    // lastly, update dependent dialogs
2830  83 if (ap.getCalculationDialog() != null)
2831    {
2832  0 ap.getCalculationDialog().validateCalcTypes();
2833    }
2834   
2835    }
2836   
2837    /**
2838    * If this panel is a cdna/protein translation view of the selection source,
2839    * tries to map the source selection to a local one, and returns true. Else
2840    * returns false.
2841    *
2842    * @param seqsel
2843    * @param colsel
2844    * @param source
2845    */
 
2846  83 toggle protected boolean selectionFromTranslation(SequenceGroup seqsel,
2847    ColumnSelection colsel, HiddenColumns hidden,
2848    SelectionSource source)
2849    {
2850  83 if (!(source instanceof AlignViewportI))
2851    {
2852  0 return false;
2853    }
2854  83 final AlignViewportI sourceAv = (AlignViewportI) source;
2855  83 if (sourceAv.getCodingComplement() != av
2856    && av.getCodingComplement() != sourceAv)
2857    {
2858  83 return false;
2859    }
2860   
2861    /*
2862    * Map sequence selection
2863    */
2864  0 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2865  0 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2866  0 av.isSelectionGroupChanged(true);
2867   
2868    /*
2869    * Map column selection
2870    */
2871    // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2872    // av);
2873  0 ColumnSelection cs = new ColumnSelection();
2874  0 HiddenColumns hs = new HiddenColumns();
2875  0 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2876  0 av.setColumnSelection(cs);
2877  0 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2878   
2879    // lastly, update any dependent dialogs
2880  0 if (ap.getCalculationDialog() != null)
2881    {
2882  0 ap.getCalculationDialog().validateCalcTypes();
2883    }
2884   
2885    /*
2886    * repaint alignment, and also Overview or Structure
2887    * if hidden column selection has changed
2888    */
2889  0 ap.paintAlignment(hiddenChanged, hiddenChanged);
2890   
2891  0 return true;
2892    }
2893   
2894    /**
2895    *
2896    * @return null or last search results handled by this panel
2897    */
 
2898  1 toggle public SearchResultsI getLastSearchResults()
2899    {
2900  1 return lastSearchResults;
2901    }
2902    }