Clover icon

Coverage Report

  1. Project Clover database Thu Dec 4 2025 14:43:25 GMT
  2. Package jalview.gui

File SeqPanel.java

 

Coverage histogram

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

Code metrics

576
952
70
3
3,086
2,060
440
0.46
13.6
23.33
6.29

Classes

Class Line # Actions
SeqPanel 85 919 420
0.1663417816.6%
SeqPanel.MousePos 99 11 7
0.2222222222.2%
SeqPanel.ScrollThread 2729 22 13
0.00%
 

Contributing tests

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