Clover icon

Coverage Report

  1. Project Clover database Thu Nov 28 2024 11:45:30 GMT
  2. Package jalview.gui

File SeqPanel.java

 

Coverage histogram

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

Code metrics

566
941
67
3
3,041
2,037
432
0.46
14.04
22.33
6.45

Classes

Class Line # Actions
SeqPanel 85 908 412
0.1636963616.4%
SeqPanel.MousePos 98 11 7
0.2222222222.2%
SeqPanel.ScrollThread 2724 22 13
0.00%
 

Contributing tests

This file is covered by 185 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    * 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  57 toggle MousePos(int col, int seq, int ann)
119    {
120  57 column = col;
121  57 seqIndex = seq;
122  57 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    // jalview.bin.Console.outPrintln(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  478 toggle public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
244    {
245  478 seqARep = new SequenceAnnotationReport(true);
246  478 ToolTipManager.sharedInstance().registerComponent(this);
247  478 ToolTipManager.sharedInstance().setInitialDelay(0);
248  478 ToolTipManager.sharedInstance().setDismissDelay(10000);
249   
250  478 this.av = viewport;
251  478 setBackground(Color.white);
252   
253  478 seqCanvas = new SeqCanvas(alignPanel);
254  478 setLayout(new BorderLayout());
255  478 add(seqCanvas, BorderLayout.CENTER);
256   
257  478 this.ap = alignPanel;
258   
259  478 if (!viewport.isDataset())
260    {
261  478 addMouseMotionListener(this);
262  478 addMouseListener(this);
263  478 addMouseWheelListener(this);
264  478 ssm = viewport.getStructureSelectionManager();
265  478 ssm.addStructureViewerListener(this);
266  478 ssm.addSelectionListener(this);
267    }
268    }
269   
270    int startWrapBlock = -1;
271   
272    int wrappedBlock = -1;
273   
274    /**
275    * Computes the column and sequence row (and possibly annotation row when in
276    * wrapped mode) for the given mouse position
277    * <p>
278    * Mouse position is not set if in wrapped mode with the cursor either between
279    * sequences, or over the left or right vertical scale.
280    *
281    * @param evt
282    * @return
283    */
 
284  57 toggle MousePos findMousePosition(MouseEvent evt)
285    {
286  57 int col = findColumn(evt);
287  57 int seqIndex = -1;
288  57 int annIndex = -1;
289  57 int y = evt.getY();
290   
291  57 int charHeight = av.getCharHeight();
292  57 int alignmentHeight = av.getAlignment().getHeight();
293  57 if (av.getWrapAlignment())
294    {
295  57 seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
296    seqCanvas.getHeight());
297   
298    /*
299    * yPos modulo height of repeating width
300    */
301  57 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
302   
303    /*
304    * height of sequences plus space / scale above,
305    * plus gap between sequences and annotations
306    */
307  57 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
308    + alignmentHeight * charHeight
309    + SeqCanvas.SEQS_ANNOTATION_GAP;
310  57 if (yOffsetPx >= alignmentHeightPixels)
311    {
312    /*
313    * mouse is over annotations; find annotation index, also set
314    * last sequence above (for backwards compatible behaviour)
315    */
316  16 AlignmentAnnotation[] anns = av.getAlignment()
317    .getAlignmentAnnotation();
318  16 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
319  16 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
320  16 seqIndex = alignmentHeight - 1;
321    }
322    else
323    {
324    /*
325    * mouse is over sequence (or the space above sequences)
326    */
327  41 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
328  41 if (yOffsetPx >= 0)
329    {
330  24 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
331    }
332    }
333    }
334    else
335    {
336  0 ViewportRanges ranges = av.getRanges();
337  0 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
338    alignmentHeight - 1);
339  0 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
340    }
341   
342  57 return new MousePos(col, seqIndex, annIndex);
343    }
344   
345    /**
346    * @param evt
347    * @return absolute column in alignment nearest to the mouse pointer
348    */
 
349  25 toggle int findAlignmentColumn(MouseEvent evt)
350    {
351  25 return findNearestColumn(evt, true);
352    }
353   
354    /**
355    * Returns the aligned sequence position (base 0) at the mouse position, or
356    * the closest visible one
357    * <p>
358    * Returns -1 if in wrapped mode with the mouse over either left or right
359    * vertical scale.
360    *
361    * @param evt
362    * @return
363    */
 
364  70 toggle int findColumn(MouseEvent evt)
365    {
366  70 return findNearestColumn(evt, false);
367    }
368   
369    /**
370    * @param nearestColumn
371    * when false returns negative values for out of bound positions - -1
372    * for scale left/right, <-1 if far to right
373    * @return nearest absolute column to mouse pointer
374    */
 
375  95 toggle private int findNearestColumn(MouseEvent evt, boolean nearestColumn)
376    {
377  95 int res = 0;
378  95 int x = evt.getX();
379   
380  95 final int startRes = av.getRanges().getStartRes();
381  95 final int charWidth = av.getCharWidth();
382   
383  95 if (av.getWrapAlignment())
384    {
385  71 int hgap = av.getCharHeight();
386  71 if (av.getScaleAboveWrapped())
387    {
388  31 hgap += av.getCharHeight();
389    }
390   
391  71 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
392    + hgap + seqCanvas.getAnnotationHeight();
393   
394  71 int y = evt.getY();
395  71 y = Math.max(0, y - hgap);
396  71 x -= seqCanvas.getLabelWidthWest();
397  71 if (x < 0)
398    {
399    // mouse is over left scale
400  4 if (!nearestColumn)
401    {
402  3 return -1;
403    }
404    else
405    {
406  1 x = 0;
407    }
408    }
409   
410  68 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
411  68 if (cwidth < 1)
412    {
413  0 return 0;
414    }
415  68 if (x >= cwidth * charWidth)
416    {
417  2 if (!nearestColumn)
418    {
419    // mouse is over right scale
420  1 return -1;
421    }
422    else
423    {
424  1 x = cwidth * charWidth - 1;
425    }
426    }
427   
428  67 wrappedBlock = y / cHeight;
429  67 wrappedBlock += startRes / cwidth;
430    // allow for wrapped view scrolled right (possible from Overview)
431  67 int startOffset = startRes % cwidth;
432  67 res = wrappedBlock * cwidth + startOffset
433    + Math.min(cwidth - 1, x / charWidth);
434    }
435    else
436    {
437    /*
438    * make sure we calculate relative to visible alignment,
439    * rather than right-hand gutter
440    */
441  24 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
442  24 if (nearestColumn)
443    {
444  18 x = Math.max(x, 0);
445    }
446   
447  24 res = (x / charWidth) + startRes;
448  24 res = Math.min(res, av.getRanges().getEndRes());
449   
450    }
451   
452  91 if (av.hasHiddenColumns())
453    {
454  15 res = av.getAlignment().getHiddenColumns()
455    .visibleToAbsoluteColumn(res);
456    }
457   
458  91 return res;
459    }
460   
461    /**
462    * When all of a sequence of edits are complete, put the resulting edit list
463    * on the history stack (undo list), and reset flags for editing in progress.
464    */
 
465  0 toggle void endEditing()
466    {
467  0 try
468    {
469  0 if (editCommand != null && editCommand.getSize() > 0)
470    {
471  0 ap.alignFrame.addHistoryItem(editCommand);
472  0 av.firePropertyChange("alignment", null,
473    av.getAlignment().getSequences());
474    }
475    } finally
476    {
477    /*
478    * Tidy up come what may...
479    */
480  0 editStartSeq = -1;
481  0 editLastRes = -1;
482  0 editingSeqs = false;
483  0 groupEditing = false;
484  0 keyboardNo1 = null;
485  0 keyboardNo2 = null;
486  0 editCommand = null;
487    }
488    }
489   
 
490  0 toggle void setCursorRow()
491    {
492  0 seqCanvas.cursorY = getKeyboardNo1() - 1;
493  0 scrollToVisible(true);
494    }
495   
 
496  0 toggle void setCursorColumn()
497    {
498  0 seqCanvas.cursorX = getKeyboardNo1() - 1;
499  0 scrollToVisible(true);
500    }
501   
 
502  0 toggle void setCursorRowAndColumn()
503    {
504  0 if (keyboardNo2 == null)
505    {
506  0 keyboardNo2 = new StringBuffer();
507    }
508    else
509    {
510  0 seqCanvas.cursorX = getKeyboardNo1() - 1;
511  0 seqCanvas.cursorY = getKeyboardNo2() - 1;
512  0 scrollToVisible(true);
513    }
514    }
515   
 
516  0 toggle void setCursorPosition()
517    {
518  0 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
519   
520  0 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
521  0 scrollToVisible(true);
522    }
523   
 
524  0 toggle void moveCursor(int dx, int dy)
525    {
526  0 moveCursor(dx, dy, false);
527    }
528   
 
529  0 toggle void moveCursor(int dx, int dy, boolean nextWord)
530    {
531  0 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
532   
533  0 if (nextWord)
534    {
535  0 int maxWidth = av.getAlignment().getWidth();
536  0 int maxHeight = av.getAlignment().getHeight();
537  0 SequenceI seqAtRow = av.getAlignment()
538    .getSequenceAt(seqCanvas.cursorY);
539    // look for next gap or residue
540  0 boolean isGap = Comparison
541    .isGap(seqAtRow.getCharAt(seqCanvas.cursorX));
542  0 int p = seqCanvas.cursorX, lastP, r = seqCanvas.cursorY, lastR;
543  0 do
544    {
545  0 lastP = p;
546  0 lastR = r;
547  0 if (dy != 0)
548    {
549  0 r += dy;
550  0 if (r < 0)
551    {
552  0 r = 0;
553    }
554  0 if (r >= maxHeight)
555    {
556  0 r = maxHeight - 1;
557    }
558  0 seqAtRow = av.getAlignment().getSequenceAt(r);
559    }
560  0 p = nextVisible(hidden, maxWidth, p, dx);
561  0 } while ((dx != 0 ? p != lastP : r != lastR)
562    && isGap == Comparison.isGap(seqAtRow.getCharAt(p)));
563  0 seqCanvas.cursorX = p;
564  0 seqCanvas.cursorY = r;
565    }
566    else
567    {
568  0 int maxWidth = av.getAlignment().getWidth();
569  0 seqCanvas.cursorX = nextVisible(hidden, maxWidth, seqCanvas.cursorX,
570    dx);
571  0 seqCanvas.cursorY += dy;
572    }
573  0 scrollToVisible(false);
574    }
575   
 
576  0 toggle private int nextVisible(HiddenColumns hidden, int maxWidth, int original,
577    int dx)
578    {
579  0 int newCursorX = original + dx;
580  0 if (av.hasHiddenColumns() && !hidden.isVisible(newCursorX))
581    {
582  0 int visx = hidden.absoluteToVisibleColumn(newCursorX - dx);
583  0 int[] region = hidden.getRegionWithEdgeAtRes(visx);
584   
585  0 if (region != null) // just in case
586    {
587  0 if (dx == 1)
588    {
589    // moving right
590  0 newCursorX = region[1] + 1;
591    }
592  0 else if (dx == -1)
593    {
594    // moving left
595  0 newCursorX = region[0] - 1;
596    }
597    }
598    }
599  0 newCursorX = (newCursorX < 0) ? 0 : newCursorX;
600  0 if (newCursorX >= maxWidth || !hidden.isVisible(newCursorX))
601    {
602  0 newCursorX = original;
603    }
604  0 return newCursorX;
605    }
606   
607    /**
608    * Scroll to make the cursor visible in the viewport.
609    *
610    * @param jump
611    * just jump to the location rather than scrolling
612    */
 
613  0 toggle void scrollToVisible(boolean jump)
614    {
615  0 if (seqCanvas.cursorX < 0)
616    {
617  0 seqCanvas.cursorX = 0;
618    }
619  0 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
620    {
621  0 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
622    }
623   
624  0 if (seqCanvas.cursorY < 0)
625    {
626  0 seqCanvas.cursorY = 0;
627    }
628  0 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
629    {
630  0 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
631    }
632   
633  0 endEditing();
634   
635  0 boolean repaintNeeded = true;
636  0 if (jump)
637    {
638    // only need to repaint if the viewport did not move, as otherwise it will
639    // get a repaint
640  0 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
641    seqCanvas.cursorY);
642    }
643    else
644    {
645  0 if (av.getWrapAlignment())
646    {
647    // scrollToWrappedVisible expects x-value to have hidden cols subtracted
648  0 int x = av.getAlignment().getHiddenColumns()
649    .absoluteToVisibleColumn(seqCanvas.cursorX);
650  0 av.getRanges().scrollToWrappedVisible(x);
651    }
652    else
653    {
654  0 av.getRanges().scrollToVisible(seqCanvas.cursorX,
655    seqCanvas.cursorY);
656    }
657    }
658   
659  0 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
660    {
661  0 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
662    seqCanvas.cursorX, seqCanvas.cursorY);
663    }
664   
665  0 if (repaintNeeded)
666    {
667  0 seqCanvas.repaint();
668    }
669    }
670   
 
671  0 toggle void setSelectionAreaAtCursor(boolean topLeft)
672    {
673  0 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
674   
675  0 if (av.getSelectionGroup() != null)
676    {
677  0 SequenceGroup sg = av.getSelectionGroup();
678    // Find the top and bottom of this group
679  0 int min = av.getAlignment().getHeight(), max = 0;
680  0 for (int i = 0; i < sg.getSize(); i++)
681    {
682  0 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
683  0 if (index > max)
684    {
685  0 max = index;
686    }
687  0 if (index < min)
688    {
689  0 min = index;
690    }
691    }
692   
693  0 max++;
694   
695  0 if (topLeft)
696    {
697  0 sg.setStartRes(seqCanvas.cursorX);
698  0 if (sg.getEndRes() < seqCanvas.cursorX)
699    {
700  0 sg.setEndRes(seqCanvas.cursorX);
701    }
702   
703  0 min = seqCanvas.cursorY;
704    }
705    else
706    {
707  0 sg.setEndRes(seqCanvas.cursorX);
708  0 if (sg.getStartRes() > seqCanvas.cursorX)
709    {
710  0 sg.setStartRes(seqCanvas.cursorX);
711    }
712   
713  0 max = seqCanvas.cursorY + 1;
714    }
715   
716  0 if (min > max)
717    {
718    // Only the user can do this
719  0 av.setSelectionGroup(null);
720    }
721    else
722    {
723    // Now add any sequences between min and max
724  0 sg.getSequences(null).clear();
725  0 for (int i = min; i < max; i++)
726    {
727  0 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
728    }
729    }
730    }
731   
732  0 if (av.getSelectionGroup() == null)
733    {
734  0 SequenceGroup sg = new SequenceGroup();
735  0 sg.setStartRes(seqCanvas.cursorX);
736  0 sg.setEndRes(seqCanvas.cursorX);
737  0 sg.addSequence(sequence, false);
738  0 av.setSelectionGroup(sg);
739    }
740   
741  0 ap.paintAlignment(false, false);
742  0 av.sendSelection();
743    }
744   
 
745  0 toggle void insertGapAtCursor(boolean group)
746    {
747  0 groupEditing = group;
748  0 editStartSeq = seqCanvas.cursorY;
749  0 editLastRes = seqCanvas.cursorX;
750  0 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
751  0 endEditing();
752    }
753   
 
754  0 toggle void deleteGapAtCursor(boolean group)
755    {
756  0 groupEditing = group;
757  0 editStartSeq = seqCanvas.cursorY;
758  0 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
759  0 editSequence(false, false, seqCanvas.cursorX);
760  0 endEditing();
761    }
762   
 
763  0 toggle void insertNucAtCursor(boolean group, String nuc)
764    {
765    // TODO not called - delete?
766  0 groupEditing = group;
767  0 editStartSeq = seqCanvas.cursorY;
768  0 editLastRes = seqCanvas.cursorX;
769  0 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
770  0 endEditing();
771    }
772   
 
773  0 toggle void numberPressed(char value)
774    {
775  0 if (keyboardNo1 == null)
776    {
777  0 keyboardNo1 = new StringBuffer();
778    }
779   
780  0 if (keyboardNo2 != null)
781    {
782  0 keyboardNo2.append(value);
783    }
784    else
785    {
786  0 keyboardNo1.append(value);
787    }
788    }
789   
 
790  0 toggle int getKeyboardNo1()
791    {
792  0 try
793    {
794  0 if (keyboardNo1 != null)
795    {
796  0 int value = Integer.parseInt(keyboardNo1.toString());
797  0 keyboardNo1 = null;
798  0 return value;
799    }
800    } catch (Exception x)
801    {
802    }
803  0 keyboardNo1 = null;
804  0 return 1;
805    }
806   
 
807  0 toggle int getKeyboardNo2()
808    {
809  0 try
810    {
811  0 if (keyboardNo2 != null)
812    {
813  0 int value = Integer.parseInt(keyboardNo2.toString());
814  0 keyboardNo2 = null;
815  0 return value;
816    }
817    } catch (Exception x)
818    {
819    }
820  0 keyboardNo2 = null;
821  0 return 1;
822    }
823   
824    /**
825    * DOCUMENT ME!
826    *
827    * @param evt
828    * DOCUMENT ME!
829    */
 
830  0 toggle @Override
831    public void mouseReleased(MouseEvent evt)
832    {
833  0 MousePos pos = findMousePosition(evt);
834  0 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
835    {
836  0 return;
837    }
838   
839  0 boolean didDrag = mouseDragging; // did we come here after a drag
840  0 mouseDragging = false;
841  0 mouseWheelPressed = false;
842   
843  0 if (evt.isPopupTrigger()) // Windows: mouseReleased
844    {
845  0 showPopupMenu(evt, pos);
846  0 evt.consume();
847  0 return;
848    }
849   
850  0 if (editingSeqs)
851    {
852  0 endEditing();
853    }
854    else
855    {
856  0 doMouseReleasedDefineMode(evt, didDrag);
857    }
858    }
859   
860    /**
861    * DOCUMENT ME!
862    *
863    * @param evt
864    * DOCUMENT ME!
865    */
 
866  0 toggle @Override
867    public void mousePressed(MouseEvent evt)
868    {
869  0 lastMousePress = evt.getPoint();
870  0 MousePos pos = findMousePosition(evt);
871  0 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
872    {
873  0 return;
874    }
875   
876  0 if (SwingUtilities.isMiddleMouseButton(evt))
877    {
878  0 mouseWheelPressed = true;
879  0 return;
880    }
881   
882  0 boolean isControlDown = Platform.isControlDown(evt);
883  0 if (evt.isShiftDown() || isControlDown)
884    {
885  0 editingSeqs = true;
886  0 if (isControlDown)
887    {
888  0 groupEditing = true;
889    }
890    }
891    else
892    {
893  0 doMousePressedDefineMode(evt, pos);
894  0 return;
895    }
896   
897  0 int seq = pos.seqIndex;
898  0 int res = pos.column;
899   
900  0 if ((seq < av.getAlignment().getHeight())
901    && (res < av.getAlignment().getSequenceAt(seq).getLength()))
902    {
903  0 editStartSeq = seq;
904  0 editLastRes = res;
905    }
906    else
907    {
908  0 editStartSeq = -1;
909  0 editLastRes = -1;
910    }
911   
912  0 return;
913    }
914   
915    String lastMessage;
916   
 
917  0 toggle @Override
918    public void mouseOverSequence(SequenceI sequence, int index, int pos)
919    {
920  0 String tmp = sequence.hashCode() + " " + index + " " + pos;
921   
922  0 if (lastMessage == null || !lastMessage.equals(tmp))
923    {
924    // jalview.bin.Console.errPrintln("mouseOver Sequence: "+tmp);
925  0 ssm.mouseOverSequence(sequence, index, pos, av);
926    }
927  0 lastMessage = tmp;
928    }
929   
930    /**
931    * Highlight the mapped region described by the search results object (unless
932    * unchanged). This supports highlight of protein while mousing over linked
933    * cDNA and vice versa. The status bar is also updated to show the location of
934    * the start of the highlighted region.
935    */
 
936  2 toggle @Override
937    public String highlightSequence(SearchResultsI results)
938    {
939  2 if (results == null || results.equals(lastSearchResults))
940    {
941  0 return null;
942    }
943  2 lastSearchResults = results;
944   
945  2 boolean wasScrolled = false;
946   
947  2 if (av.isFollowHighlight())
948    {
949    // don't allow highlight of protein/cDNA to also scroll a complementary
950    // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
951    // over residue to change abruptly, causing highlighted residue in panel 2
952    // to change, causing a scroll in panel 1 etc)
953  2 ap.setToScrollComplementPanel(false);
954  2 wasScrolled = ap.scrollToPosition(results);
955  2 if (wasScrolled)
956    {
957  0 seqCanvas.revalidate();
958    }
959  2 ap.setToScrollComplementPanel(true);
960    }
961   
962  2 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
963  2 if (seqCanvas.highlightSearchResults(results, fastPaint))
964    {
965  1 setStatusMessage(results);
966    }
967  2 return results.isEmpty() ? null : getHighlightInfo(results);
968    }
969   
970    /**
971    * temporary hack: answers a message suitable to show on structure hover
972    * label. This is normally null. It is a peptide variation description if
973    * <ul>
974    * <li>results are a single residue in a protein alignment</li>
975    * <li>there is a mapping to a coding sequence (codon)</li>
976    * <li>there are one or more SNP variant features on the codon</li>
977    * </ul>
978    * in which case the answer is of the format (e.g.) "p.Glu388Asp"
979    *
980    * @param results
981    * @return
982    */
 
983  2 toggle private String getHighlightInfo(SearchResultsI results)
984    {
985    /*
986    * ideally, just find mapped CDS (as we don't care about render style here);
987    * for now, go via split frame complement's FeatureRenderer
988    */
989  2 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
990  2 if (complement == null)
991    {
992  2 return null;
993    }
994  0 AlignFrame af = Desktop.getAlignFrameFor(complement);
995  0 FeatureRendererModel fr2 = af.getFeatureRenderer();
996   
997  0 List<SearchResultMatchI> matches = results.getResults();
998  0 int j = matches.size();
999  0 List<String> infos = new ArrayList<>();
1000  0 for (int i = 0; i < j; i++)
1001    {
1002  0 SearchResultMatchI match = matches.get(i);
1003  0 int pos = match.getStart();
1004  0 if (pos == match.getEnd())
1005    {
1006  0 SequenceI seq = match.getSequence();
1007  0 SequenceI ds = seq.getDatasetSequence() == null ? seq
1008    : seq.getDatasetSequence();
1009  0 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(ds, pos);
1010  0 if (mf != null)
1011    {
1012  0 for (SequenceFeature sf : mf.features)
1013    {
1014  0 String pv = mf.findProteinVariants(sf);
1015  0 if (pv.length() > 0 && !infos.contains(pv))
1016    {
1017  0 infos.add(pv);
1018    }
1019    }
1020    }
1021    }
1022    }
1023   
1024  0 if (infos.isEmpty())
1025    {
1026  0 return null;
1027    }
1028  0 StringBuilder sb = new StringBuilder();
1029  0 for (String info : infos)
1030    {
1031  0 if (sb.length() > 0)
1032    {
1033  0 sb.append("|");
1034    }
1035  0 sb.append(info);
1036    }
1037  0 return sb.toString();
1038    }
1039   
 
1040  0 toggle @Override
1041    public VamsasSource getVamsasSource()
1042    {
1043  0 return this.ap == null ? null : this.ap.av;
1044    }
1045   
 
1046  0 toggle @Override
1047    public void updateColours(SequenceI seq, int index)
1048    {
1049  0 jalview.bin.Console.outPrintln("update the seqPanel colours");
1050    // repaint();
1051    }
1052   
1053    /**
1054    * Action on mouse movement is to update the status bar to show the current
1055    * sequence position, and (if features are shown) to show any features at the
1056    * position in a tooltip. Does nothing if the mouse move does not change
1057    * residue position.
1058    *
1059    * @param evt
1060    */
 
1061  0 toggle @Override
1062    public void mouseMoved(MouseEvent evt)
1063    {
1064  0 if (editingSeqs)
1065    {
1066    // This is because MacOSX creates a mouseMoved
1067    // If control is down, other platforms will not.
1068  0 mouseDragged(evt);
1069    }
1070   
1071  0 final MousePos mousePos = findMousePosition(evt);
1072  0 if (mousePos.equals(lastMousePosition))
1073    {
1074    /*
1075    * just a pixel move without change of 'cell'
1076    */
1077  0 moveTooltip = false;
1078  0 return;
1079    }
1080  0 moveTooltip = true;
1081  0 lastMousePosition = mousePos;
1082   
1083  0 if (mousePos.isOverAnnotation())
1084    {
1085  0 mouseMovedOverAnnotation(mousePos);
1086  0 return;
1087    }
1088  0 final int seq = mousePos.seqIndex;
1089   
1090  0 final int column = mousePos.column;
1091  0 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1092    {
1093  0 lastMousePosition = null;
1094  0 setToolTipText(null);
1095  0 lastTooltip = null;
1096  0 lastFormattedTooltip = null;
1097  0 ap.alignFrame.setStatus("");
1098  0 return;
1099    }
1100   
1101  0 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1102   
1103  0 if (column >= sequence.getLength())
1104    {
1105  0 return;
1106    }
1107   
1108    /*
1109    * set status bar message, returning residue position in sequence
1110    */
1111  0 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1112  0 final int pos = setStatusMessage(sequence, column, seq);
1113  0 if (ssm != null && !isGapped)
1114    {
1115  0 mouseOverSequence(sequence, column, pos);
1116    }
1117   
1118  0 StringBuilder tooltipText = new StringBuilder(64);
1119   
1120  0 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1121  0 if (groups != null)
1122    {
1123  0 for (int g = 0; g < groups.length; g++)
1124    {
1125  0 if (groups[g].getStartRes() <= column
1126    && groups[g].getEndRes() >= column)
1127    {
1128  0 if (!groups[g].getName().startsWith("JTreeGroup")
1129    && !groups[g].getName().startsWith("JGroup"))
1130    {
1131  0 tooltipText.append(groups[g].getName());
1132    }
1133   
1134  0 if (groups[g].getDescription() != null)
1135    {
1136  0 tooltipText.append(": " + groups[g].getDescription());
1137    }
1138    }
1139    }
1140    }
1141   
1142    /*
1143    * add any features at the position to the tooltip; if over a gap, only
1144    * add features that straddle the gap (pos may be the residue before or
1145    * after the gap)
1146    */
1147  0 int unshownFeatures = 0;
1148  0 if (av.isShowSequenceFeatures())
1149    {
1150  0 List<SequenceFeature> features = ap.getFeatureRenderer()
1151    .findFeaturesAtColumn(sequence, column + 1);
1152  0 unshownFeatures = seqARep.appendFeatures(tooltipText, pos, features,
1153    this.ap.getSeqPanel().seqCanvas.fr, MAX_TOOLTIP_LENGTH);
1154   
1155    /*
1156    * add features in CDS/protein complement at the corresponding
1157    * position if configured to do so
1158    */
1159  0 if (av.isShowComplementFeatures())
1160    {
1161  0 if (!Comparison.isGap(sequence.getCharAt(column)))
1162    {
1163  0 AlignViewportI complement = ap.getAlignViewport()
1164    .getCodingComplement();
1165  0 AlignFrame af = Desktop.getAlignFrameFor(complement);
1166  0 FeatureRendererModel fr2 = af.getFeatureRenderer();
1167  0 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1168    pos);
1169  0 if (mf != null)
1170    {
1171  0 unshownFeatures += seqARep.appendFeatures(tooltipText, pos, mf,
1172    fr2, MAX_TOOLTIP_LENGTH);
1173    }
1174    }
1175    }
1176    }
1177  0 if (tooltipText.length() == 0) // nothing added
1178    {
1179  0 setToolTipText(null);
1180  0 lastTooltip = null;
1181    }
1182    else
1183    {
1184  0 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1185    {
1186  0 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1187  0 tooltipText.append("...");
1188    }
1189  0 if (unshownFeatures > 0)
1190    {
1191  0 tooltipText.append("<br/>").append("... ").append("<i>")
1192    .append(MessageManager.formatMessage(
1193    "label.features_not_shown", unshownFeatures))
1194    .append("</i>");
1195    }
1196  0 String textString = tooltipText.toString();
1197  0 if (!textString.equals(lastTooltip))
1198    {
1199  0 lastTooltip = textString;
1200  0 lastFormattedTooltip = JvSwingUtils.wrapTooltip(true, textString);
1201  0 setToolTipText(lastFormattedTooltip);
1202    }
1203    }
1204    }
1205   
1206    /**
1207    * When the view is in wrapped mode, and the mouse is over an annotation row,
1208    * shows the corresponding tooltip and status message (if any)
1209    *
1210    * @param pos
1211    * @param column
1212    */
 
1213  0 toggle protected void mouseMovedOverAnnotation(MousePos pos)
1214    {
1215  0 final int column = pos.column;
1216  0 final int rowIndex = pos.annotationIndex;
1217   
1218    // TODO - get yOffset for annotation, too
1219  0 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1220    || rowIndex < 0)
1221    {
1222  0 return;
1223    }
1224  0 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1225   
1226  0 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1227    anns, 0, av, ap);
1228  0 if (tooltip == null ? tooltip != lastTooltip
1229    : !tooltip.equals(lastTooltip))
1230    {
1231  0 lastTooltip = tooltip;
1232  0 lastFormattedTooltip = tooltip == null ? null
1233    : JvSwingUtils.wrapTooltip(true, tooltip);
1234  0 setToolTipText(lastFormattedTooltip);
1235    }
1236   
1237  0 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1238    anns[rowIndex], 0, av);
1239  0 ap.alignFrame.setStatus(msg);
1240    }
1241   
1242    /*
1243    * if Shift key is held down while moving the mouse,
1244    * the tooltip location is not changed once shown
1245    */
1246    private Point lastTooltipLocation = null;
1247   
1248    /*
1249    * this flag is false for pixel moves within a residue,
1250    * to reduce tooltip flicker
1251    */
1252    private boolean moveTooltip = true;
1253   
1254    /*
1255    * a dummy tooltip used to estimate where to position tooltips
1256    */
1257    private JToolTip tempTip = new JLabel().createToolTip();
1258   
1259    /*
1260    * (non-Javadoc)
1261    *
1262    * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1263    */
 
1264  0 toggle @Override
1265    public Point getToolTipLocation(MouseEvent event)
1266    {
1267    // BH 2018
1268   
1269  0 if (lastTooltip == null || !moveTooltip)
1270    {
1271  0 return null;
1272    }
1273   
1274  0 if (lastTooltipLocation != null && event.isShiftDown())
1275    {
1276  0 return lastTooltipLocation;
1277    }
1278   
1279  0 int x = event.getX();
1280  0 int y = event.getY();
1281  0 int w = getWidth();
1282   
1283  0 tempTip.setTipText(lastFormattedTooltip);
1284  0 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1285   
1286    // was x += (w - x < 200) ? -(w / 2) : 5;
1287  0 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1288  0 Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
1289   
1290  0 return lastTooltipLocation = p;
1291    }
1292   
1293    /**
1294    * set when the current UI interaction has resulted in a change that requires
1295    * shading in overviews and structures to be recalculated. this could be
1296    * changed to a something more expressive that indicates what actually has
1297    * changed, so selective redraws can be applied (ie. only structures, only
1298    * overview, etc)
1299    */
1300    private boolean updateOverviewAndStructs = false; // TODO: refactor to
1301    // avcontroller
1302   
1303    /**
1304    * set if av.getSelectionGroup() refers to a group that is defined on the
1305    * alignment view, rather than a transient selection
1306    */
1307    // private boolean editingDefinedGroup = false; // TODO: refactor to
1308    // avcontroller or viewModel
1309   
1310    /**
1311    * Sets the status message in alignment panel, showing the sequence number
1312    * (index) and id, and residue and residue position if not at a gap, for the
1313    * given sequence and column position. Returns the residue position returned
1314    * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1315    * if at a gapped position.
1316    *
1317    * @param sequence
1318    * aligned sequence object
1319    * @param column
1320    * alignment column
1321    * @param seqIndex
1322    * index of sequence in alignment
1323    * @return sequence position of residue at column, or adjacent residue if at a
1324    * gap
1325    */
 
1326  5 toggle int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1327    {
1328  5 char sequenceChar = sequence.getCharAt(column);
1329  5 int pos = sequence.findPosition(column);
1330  5 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1331   
1332  5 return pos;
1333    }
1334   
1335    /**
1336    * Builds the status message for the current cursor location and writes it to
1337    * the status bar, for example
1338    *
1339    * <pre>
1340    * Sequence 3 ID: FER1_SOLLC
1341    * Sequence 5 ID: FER1_PEA Residue: THR (4)
1342    * Sequence 5 ID: FER1_PEA Residue: B (3)
1343    * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1344    * </pre>
1345    *
1346    * @param seqName
1347    * @param seqIndex
1348    * sequence position in the alignment (1..)
1349    * @param sequenceChar
1350    * the character under the cursor
1351    * @param residuePos
1352    * the sequence residue position (if not over a gap)
1353    */
 
1354  6 toggle protected void setStatusMessage(String seqName, int seqIndex,
1355    char sequenceChar, int residuePos)
1356    {
1357  6 StringBuilder text = new StringBuilder(32);
1358   
1359    /*
1360    * Sequence number (if known), and sequence name.
1361    */
1362  6 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1363  6 text.append("Sequence").append(seqno).append(" ID: ").append(seqName);
1364   
1365  6 String residue = null;
1366   
1367    /*
1368    * Try to translate the display character to residue name (null for gap).
1369    */
1370  6 boolean isGapped = Comparison.isGap(sequenceChar);
1371   
1372  6 if (!isGapped)
1373    {
1374  4 boolean nucleotide = av.getAlignment().isNucleotide();
1375  4 String displayChar = String.valueOf(sequenceChar);
1376  4 if (nucleotide)
1377    {
1378  0 residue = ResidueProperties.nucleotideName.get(displayChar);
1379    }
1380    else
1381    {
1382  4 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1383  4 : ("*".equals(displayChar) ? "STOP"
1384    : ResidueProperties.aa2Triplet.get(displayChar));
1385    }
1386  4 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1387  4 .append(": ").append(residue == null ? displayChar : residue);
1388   
1389  4 text.append(" (").append(Integer.toString(residuePos)).append(")");
1390    }
1391  6 ap.alignFrame.setStatus(text.toString());
1392    }
1393   
1394    /**
1395    * Set the status bar message to highlight the first matched position in
1396    * search results.
1397    *
1398    * @param results
1399    */
 
1400  1 toggle private void setStatusMessage(SearchResultsI results)
1401    {
1402  1 AlignmentI al = this.av.getAlignment();
1403  1 int sequenceIndex = al.findIndex(results);
1404  1 if (sequenceIndex == -1)
1405    {
1406  0 return;
1407    }
1408  1 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1409  1 SequenceI ds = alignedSeq.getDatasetSequence();
1410  1 for (SearchResultMatchI m : results.getResults())
1411    {
1412  1 SequenceI seq = m.getSequence();
1413  1 if (seq.getDatasetSequence() != null)
1414    {
1415  1 seq = seq.getDatasetSequence();
1416    }
1417   
1418  1 if (seq == ds)
1419    {
1420  1 int start = m.getStart();
1421  1 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1422    seq.getCharAt(start - 1), start);
1423  1 return;
1424    }
1425    }
1426    }
1427   
1428    /**
1429    * {@inheritDoc}
1430    */
 
1431  0 toggle @Override
1432    public void mouseDragged(MouseEvent evt)
1433    {
1434  0 MousePos pos = findMousePosition(evt);
1435  0 if (pos.isOverAnnotation() || pos.column == -1)
1436    {
1437  0 return;
1438    }
1439   
1440  0 if (mouseWheelPressed)
1441    {
1442  0 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1443  0 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1444   
1445  0 int oldWidth = av.getCharWidth();
1446   
1447    // Which is bigger, left-right or up-down?
1448  0 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1449    .abs(evt.getX() - lastMousePress.getX()))
1450    {
1451    /*
1452    * on drag up or down, decrement or increment font size
1453    */
1454  0 int fontSize = av.font.getSize();
1455  0 boolean fontChanged = false;
1456   
1457  0 if (evt.getY() < lastMousePress.getY())
1458    {
1459  0 fontChanged = true;
1460  0 fontSize--;
1461    }
1462  0 else if (evt.getY() > lastMousePress.getY())
1463    {
1464  0 fontChanged = true;
1465  0 fontSize++;
1466    }
1467   
1468  0 if (fontSize < 1)
1469    {
1470  0 fontSize = 1;
1471    }
1472   
1473  0 if (fontChanged)
1474    {
1475  0 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1476    fontSize);
1477  0 av.setFont(newFont, true);
1478  0 av.setCharWidth(oldWidth);
1479  0 ap.fontChanged();
1480  0 if (copyChanges)
1481    {
1482  0 ap.av.getCodingComplement().setFont(newFont, true);
1483  0 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1484    .getSplitViewContainer();
1485  0 splitFrame.adjustLayout();
1486  0 splitFrame.repaint();
1487    }
1488    }
1489    }
1490    else
1491    {
1492    /*
1493    * on drag left or right, decrement or increment character width
1494    */
1495  0 int newWidth = 0;
1496  0 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1497    {
1498  0 newWidth = av.getCharWidth() - 1;
1499  0 av.setCharWidth(newWidth);
1500    }
1501  0 else if (evt.getX() > lastMousePress.getX())
1502    {
1503  0 newWidth = av.getCharWidth() + 1;
1504  0 av.setCharWidth(newWidth);
1505    }
1506  0 if (newWidth > 0)
1507    {
1508  0 ap.paintAlignment(false, false);
1509  0 if (copyChanges)
1510    {
1511    /*
1512    * need to ensure newWidth is set on cdna, regardless of which
1513    * panel the mouse drag happened in; protein will compute its
1514    * character width as 1:1 or 3:1
1515    */
1516  0 av.getCodingComplement().setCharWidth(newWidth);
1517  0 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1518    .getSplitViewContainer();
1519  0 splitFrame.adjustLayout();
1520  0 splitFrame.repaint();
1521    }
1522    }
1523    }
1524   
1525  0 FontMetrics fm = getFontMetrics(av.getFont());
1526  0 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1527   
1528  0 lastMousePress = evt.getPoint();
1529   
1530  0 return;
1531    }
1532   
1533  0 if (!editingSeqs)
1534    {
1535  0 dragStretchGroup(evt);
1536  0 return;
1537    }
1538   
1539  0 int res = pos.column;
1540   
1541  0 if (res < 0)
1542    {
1543  0 res = 0;
1544    }
1545   
1546  0 if ((editLastRes == -1) || (editLastRes == res))
1547    {
1548  0 return;
1549    }
1550   
1551  0 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1552    {
1553    // dragLeft, delete gap
1554  0 editSequence(false, false, res);
1555    }
1556    else
1557    {
1558  0 editSequence(true, false, res);
1559    }
1560   
1561  0 mouseDragging = true;
1562  0 if (scrollThread != null)
1563    {
1564  0 scrollThread.setMousePosition(evt.getPoint());
1565    }
1566    }
1567   
1568    /**
1569    * Edits the sequence to insert or delete one or more gaps, in response to a
1570    * mouse drag or cursor mode command. The number of inserts/deletes may be
1571    * specified with the cursor command, or else depends on the mouse event
1572    * (normally one column, but potentially more for a fast mouse drag).
1573    * <p>
1574    * Delete gaps is limited to the number of gaps left of the cursor position
1575    * (mouse drag), or at or right of the cursor position (cursor mode).
1576    * <p>
1577    * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1578    * the current selection group.
1579    * <p>
1580    * In locked editing mode (with a selection group present), inserts/deletions
1581    * within the selection group are limited to its boundaries (and edits outside
1582    * the group stop at its border).
1583    *
1584    * @param insertGap
1585    * true to insert gaps, false to delete gaps
1586    * @param editSeq
1587    * (unused parameter)
1588    * @param startres
1589    * the column at which to perform the action; the number of columns
1590    * affected depends on <code>this.editLastRes</code> (cursor column
1591    * position)
1592    */
 
1593  0 toggle synchronized void editSequence(boolean insertGap, boolean editSeq,
1594    final int startres)
1595    {
1596  0 int fixedLeft = -1;
1597  0 int fixedRight = -1;
1598  0 boolean fixedColumns = false;
1599  0 SequenceGroup sg = av.getSelectionGroup();
1600   
1601  0 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1602   
1603    // No group, but the sequence may represent a group
1604  0 if (!groupEditing && av.hasHiddenRows())
1605    {
1606  0 if (av.isHiddenRepSequence(seq))
1607    {
1608  0 sg = av.getRepresentedSequences(seq);
1609  0 groupEditing = true;
1610    }
1611    }
1612   
1613  0 StringBuilder message = new StringBuilder(64); // for status bar
1614   
1615    /*
1616    * make a name for the edit action, for
1617    * status bar message and Undo/Redo menu
1618    */
1619  0 String label = null;
1620  0 if (groupEditing)
1621    {
1622  0 message.append("Edit group:");
1623  0 label = MessageManager.getString("action.edit_group");
1624    }
1625    else
1626    {
1627  0 message.append("Edit sequence: " + seq.getName());
1628  0 label = seq.getName();
1629  0 if (label.length() > 10)
1630    {
1631  0 label = label.substring(0, 10);
1632    }
1633  0 label = MessageManager.formatMessage("label.edit_params",
1634    new String[]
1635    { label });
1636    }
1637   
1638    /*
1639    * initialise the edit command if there is not
1640    * already one being extended
1641    */
1642  0 if (editCommand == null)
1643    {
1644  0 editCommand = new EditCommand(label);
1645    }
1646   
1647  0 if (insertGap)
1648    {
1649  0 message.append(" insert ");
1650    }
1651    else
1652    {
1653  0 message.append(" delete ");
1654    }
1655   
1656  0 message.append(Math.abs(startres - editLastRes) + " gaps.");
1657  0 ap.alignFrame.setStatus(message.toString());
1658   
1659    /*
1660    * is there a selection group containing the sequence being edited?
1661    * if so the boundary of the group is the limit of the edit
1662    * (but the edit may be inside or outside the selection group)
1663    */
1664  0 boolean inSelectionGroup = sg != null
1665    && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1666  0 if (groupEditing || inSelectionGroup)
1667    {
1668  0 fixedColumns = true;
1669   
1670    // sg might be null as the user may only see 1 sequence,
1671    // but the sequence represents a group
1672  0 if (sg == null)
1673    {
1674  0 if (!av.isHiddenRepSequence(seq))
1675    {
1676  0 endEditing();
1677  0 return;
1678    }
1679  0 sg = av.getRepresentedSequences(seq);
1680    }
1681   
1682  0 fixedLeft = sg.getStartRes();
1683  0 fixedRight = sg.getEndRes();
1684   
1685  0 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1686    || (startres >= fixedLeft && editLastRes < fixedLeft)
1687    || (startres > fixedRight && editLastRes <= fixedRight)
1688    || (startres <= fixedRight && editLastRes > fixedRight))
1689    {
1690  0 endEditing();
1691  0 return;
1692    }
1693   
1694  0 if (fixedLeft > startres)
1695    {
1696  0 fixedRight = fixedLeft - 1;
1697  0 fixedLeft = 0;
1698    }
1699  0 else if (fixedRight < startres)
1700    {
1701  0 fixedLeft = fixedRight;
1702  0 fixedRight = -1;
1703    }
1704    }
1705   
1706  0 if (av.hasHiddenColumns())
1707    {
1708  0 fixedColumns = true;
1709  0 int y1 = av.getAlignment().getHiddenColumns()
1710    .getNextHiddenBoundary(true, startres);
1711  0 int y2 = av.getAlignment().getHiddenColumns()
1712    .getNextHiddenBoundary(false, startres);
1713   
1714  0 if ((insertGap && startres > y1 && editLastRes < y1)
1715    || (!insertGap && startres < y2 && editLastRes > y2))
1716    {
1717  0 endEditing();
1718  0 return;
1719    }
1720   
1721    // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1722    // Selection spans a hidden region
1723  0 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1724    {
1725  0 if (startres >= y2)
1726    {
1727  0 fixedLeft = y2;
1728    }
1729    else
1730    {
1731  0 fixedRight = y2 - 1;
1732    }
1733    }
1734    }
1735   
1736  0 boolean success = doEditSequence(insertGap, editSeq, startres,
1737    fixedRight, fixedColumns, sg);
1738   
1739    /*
1740    * report what actually happened (might be less than
1741    * what was requested), by inspecting the edit commands added
1742    */
1743  0 String msg = getEditStatusMessage(editCommand);
1744  0 ap.alignFrame.setStatus(msg == null ? " " : msg);
1745  0 if (!success)
1746    {
1747  0 endEditing();
1748    }
1749   
1750  0 editLastRes = startres;
1751  0 seqCanvas.repaint();
1752    }
1753   
1754    /**
1755    * A helper method that performs the requested editing to insert or delete
1756    * gaps (if possible). Answers true if the edit was successful, false if could
1757    * only be performed in part or not at all. Failure may occur in 'locked edit'
1758    * mode, when an insertion requires a matching gapped position (or column) to
1759    * delete, and deletion requires an adjacent gapped position (or column) to
1760    * remove.
1761    *
1762    * @param insertGap
1763    * true if inserting gap(s), false if deleting
1764    * @param editSeq
1765    * (unused parameter, currently always false)
1766    * @param startres
1767    * the column at which to perform the edit
1768    * @param fixedRight
1769    * fixed right boundary column of a locked edit (within or to the
1770    * left of a selection group)
1771    * @param fixedColumns
1772    * true if this is a locked edit
1773    * @param sg
1774    * the sequence group (if group edit is being performed)
1775    * @return
1776    */
 
1777  0 toggle protected boolean doEditSequence(final boolean insertGap,
1778    final boolean editSeq, final int startres, int fixedRight,
1779    final boolean fixedColumns, final SequenceGroup sg)
1780    {
1781  0 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1782  0 SequenceI[] seqs = new SequenceI[] { seq };
1783   
1784  0 if (groupEditing)
1785    {
1786  0 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1787  0 int g, groupSize = vseqs.size();
1788  0 SequenceI[] groupSeqs = new SequenceI[groupSize];
1789  0 for (g = 0; g < groupSeqs.length; g++)
1790    {
1791  0 groupSeqs[g] = vseqs.get(g);
1792    }
1793   
1794    // drag to right
1795  0 if (insertGap)
1796    {
1797    // If the user has selected the whole sequence, and is dragging to
1798    // the right, we can still extend the alignment and selectionGroup
1799  0 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1800    && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1801    {
1802  0 sg.setEndRes(
1803    av.getAlignment().getWidth() + startres - editLastRes);
1804  0 fixedRight = sg.getEndRes();
1805    }
1806   
1807    // Is it valid with fixed columns??
1808    // Find the next gap before the end
1809    // of the visible region boundary
1810  0 boolean blank = false;
1811  0 for (; fixedRight > editLastRes; fixedRight--)
1812    {
1813  0 blank = true;
1814   
1815  0 for (g = 0; g < groupSize; g++)
1816    {
1817  0 for (int j = 0; j < startres - editLastRes; j++)
1818    {
1819  0 if (!Comparison.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  0 toggle @Override
2140    public void mouseEntered(MouseEvent e)
2141    {
2142  0 if (oldSeq < 0)
2143    {
2144  0 oldSeq = 0;
2145    }
2146  0 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(),
2212    features.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    }
2491  0 PaintRefresher.Refresh(this, av.getSequenceSetId());
2492    // TODO: structure colours only need updating if stretchGroup used to or now
2493    // does contain sequences with structure views
2494  0 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2495  0 updateOverviewAndStructs = false;
2496  0 changeEndRes = false;
2497  0 changeStartRes = false;
2498  0 stretchGroup = null;
2499  0 av.sendSelection();
2500    }
2501   
2502    /**
2503    * Resizes the borders of a selection group depending on the direction of
2504    * mouse drag
2505    *
2506    * @param evt
2507    */
 
2508  0 toggle protected void dragStretchGroup(MouseEvent evt)
2509    {
2510  0 if (stretchGroup == null)
2511    {
2512  0 return;
2513    }
2514   
2515  0 MousePos pos = findMousePosition(evt);
2516  0 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2517    {
2518  0 return;
2519    }
2520   
2521  0 int res = pos.column;
2522  0 int y = pos.seqIndex;
2523   
2524  0 if (wrappedBlock != startWrapBlock)
2525    {
2526  0 return;
2527    }
2528   
2529  0 res = Math.min(res, av.getAlignment().getWidth() - 1);
2530   
2531  0 if (stretchGroup.getEndRes() == res)
2532    {
2533    // Edit end res position of selected group
2534  0 changeEndRes = true;
2535    }
2536  0 else if (stretchGroup.getStartRes() == res)
2537    {
2538    // Edit start res position of selected group
2539  0 changeStartRes = true;
2540    }
2541   
2542  0 if (res < av.getRanges().getStartRes())
2543    {
2544  0 res = av.getRanges().getStartRes();
2545    }
2546   
2547  0 if (changeEndRes)
2548    {
2549  0 if (res > (stretchGroup.getStartRes() - 1))
2550    {
2551  0 stretchGroup.setEndRes(res);
2552  0 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2553    }
2554    }
2555  0 else if (changeStartRes)
2556    {
2557  0 if (res < (stretchGroup.getEndRes() + 1))
2558    {
2559  0 stretchGroup.setStartRes(res);
2560  0 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2561    }
2562    }
2563   
2564  0 int dragDirection = 0;
2565   
2566  0 if (y > oldSeq)
2567    {
2568  0 dragDirection = 1;
2569    }
2570  0 else if (y < oldSeq)
2571    {
2572  0 dragDirection = -1;
2573    }
2574   
2575  0 while ((y != oldSeq) && (oldSeq > -1)
2576    && (y < av.getAlignment().getHeight()))
2577    {
2578    // This routine ensures we don't skip any sequences, as the
2579    // selection is quite slow.
2580  0 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2581   
2582  0 oldSeq += dragDirection;
2583   
2584  0 if (oldSeq < 0)
2585    {
2586  0 break;
2587    }
2588   
2589  0 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2590   
2591  0 if (stretchGroup.getSequences(null).contains(nextSeq))
2592    {
2593  0 stretchGroup.deleteSequence(seq, false);
2594  0 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2595    }
2596    else
2597    {
2598  0 if (seq != null)
2599    {
2600  0 stretchGroup.addSequence(seq, false);
2601    }
2602   
2603  0 stretchGroup.addSequence(nextSeq, false);
2604  0 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2605    }
2606    }
2607   
2608  0 if (oldSeq < 0)
2609    {
2610  0 oldSeq = -1;
2611    }
2612   
2613  0 mouseDragging = true;
2614   
2615  0 if (scrollThread != null)
2616    {
2617  0 scrollThread.setMousePosition(evt.getPoint());
2618    }
2619   
2620    /*
2621    * construct a status message showing the range of the selection
2622    */
2623  0 StringBuilder status = new StringBuilder(64);
2624  0 List<SequenceI> seqs = stretchGroup.getSequences();
2625  0 String name = seqs.get(0).getName();
2626  0 if (name.length() > 20)
2627    {
2628  0 name = name.substring(0, 20);
2629    }
2630  0 status.append(name).append(" - ");
2631  0 name = seqs.get(seqs.size() - 1).getName();
2632  0 if (name.length() > 20)
2633    {
2634  0 name = name.substring(0, 20);
2635    }
2636  0 status.append(name).append(" ");
2637  0 int startRes = stretchGroup.getStartRes();
2638  0 status.append(" cols ").append(String.valueOf(startRes + 1))
2639    .append("-");
2640  0 int endRes = stretchGroup.getEndRes();
2641  0 status.append(String.valueOf(endRes + 1));
2642  0 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2643    .append(String.valueOf(endRes - startRes + 1)).append(")");
2644  0 ap.alignFrame.setStatus(status.toString());
2645    }
2646   
2647    /**
2648    * Stops the scroll thread if it is running
2649    */
 
2650  3 toggle void stopScrolling()
2651    {
2652  3 if (scrollThread != null)
2653    {
2654  0 scrollThread.stopScrolling();
2655  0 scrollThread = null;
2656    }
2657  3 mouseDragging = false;
2658    }
2659   
2660    /**
2661    * Starts a thread to scroll the alignment, towards a given mouse position
2662    * outside the panel bounds, unless the alignment is in wrapped mode
2663    *
2664    * @param mousePos
2665    */
 
2666  0 toggle void startScrolling(Point mousePos)
2667    {
2668    /*
2669    * set this.mouseDragging in case this was called from
2670    * a drag in ScalePanel or AnnotationPanel
2671    */
2672  0 mouseDragging = true;
2673  0 if (!av.getWrapAlignment() && scrollThread == null)
2674    {
2675  0 scrollThread = new ScrollThread();
2676  0 scrollThread.setMousePosition(mousePos);
2677  0 if (Platform.isJS())
2678    {
2679    /*
2680    * Javascript - run every 20ms until scrolling stopped
2681    * or reaches the limit of scrollable alignment
2682    */
2683  0 Timer t = new Timer(20, new ActionListener()
2684    {
 
2685  0 toggle @Override
2686    public void actionPerformed(ActionEvent e)
2687    {
2688  0 if (scrollThread != null)
2689    {
2690    // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2691  0 scrollThread.scrollOnce();
2692    }
2693    }
2694    });
2695  0 t.addActionListener(new ActionListener()
2696    {
 
2697  0 toggle @Override
2698    public void actionPerformed(ActionEvent e)
2699    {
2700  0 if (scrollThread == null)
2701    {
2702    // SeqPanel.stopScrolling called
2703  0 t.stop();
2704    }
2705    }
2706    });
2707  0 t.start();
2708    }
2709    else
2710    {
2711    /*
2712    * Java - run in a new thread
2713    */
2714  0 scrollThread.start();
2715    }
2716    }
2717    }
2718   
2719    /**
2720    * Performs scrolling of the visible alignment left, right, up or down, until
2721    * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2722    * limit of the alignment is reached
2723    */
 
2724    class ScrollThread extends Thread
2725    {
2726    private Point mousePos;
2727   
2728    private volatile boolean keepRunning = true;
2729   
2730    /**
2731    * Constructor
2732    */
 
2733  0 toggle public ScrollThread()
2734    {
2735  0 setName("SeqPanel$ScrollThread");
2736    }
2737   
2738    /**
2739    * Sets the position of the mouse that determines the direction of the
2740    * scroll to perform. If this is called as the mouse moves, scrolling should
2741    * respond accordingly. For example, if the mouse is dragged right, scroll
2742    * right should start; if the drag continues down, scroll down should also
2743    * happen.
2744    *
2745    * @param p
2746    */
 
2747  0 toggle public void setMousePosition(Point p)
2748    {
2749  0 mousePos = p;
2750    }
2751   
2752    /**
2753    * Sets a flag that will cause the thread to exit
2754    */
 
2755  0 toggle public void stopScrolling()
2756    {
2757  0 keepRunning = false;
2758    }
2759   
2760    /**
2761    * Scrolls the alignment left or right, and/or up or down, depending on the
2762    * last notified mouse position, until the limit of the alignment is
2763    * reached, or a flag is set to stop the scroll
2764    */
 
2765  0 toggle @Override
2766    public void run()
2767    {
2768  0 while (keepRunning)
2769    {
2770  0 if (mousePos != null)
2771    {
2772  0 keepRunning = scrollOnce();
2773    }
2774  0 try
2775    {
2776  0 Thread.sleep(20);
2777    } catch (Exception ex)
2778    {
2779    }
2780    }
2781  0 SeqPanel.this.scrollThread = null;
2782    }
2783   
2784    /**
2785    * Scrolls
2786    * <ul>
2787    * <li>one row up, if the mouse is above the panel</li>
2788    * <li>one row down, if the mouse is below the panel</li>
2789    * <li>one column left, if the mouse is left of the panel</li>
2790    * <li>one column right, if the mouse is right of the panel</li>
2791    * </ul>
2792    * Answers true if a scroll was performed, false if not - meaning either
2793    * that the mouse position is within the panel, or the edge of the alignment
2794    * has been reached.
2795    */
 
2796  0 toggle boolean scrollOnce()
2797    {
2798    /*
2799    * quit after mouseUp ensures interrupt in JalviewJS
2800    */
2801  0 if (!mouseDragging)
2802    {
2803  0 return false;
2804    }
2805   
2806  0 boolean scrolled = false;
2807  0 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2808   
2809    /*
2810    * scroll up or down
2811    */
2812  0 if (mousePos.y < 0)
2813    {
2814    // mouse is above this panel - try scroll up
2815  0 scrolled = ranges.scrollUp(true);
2816    }
2817  0 else if (mousePos.y >= getHeight())
2818    {
2819    // mouse is below this panel - try scroll down
2820  0 scrolled = ranges.scrollUp(false);
2821    }
2822   
2823    /*
2824    * scroll left or right
2825    */
2826  0 if (mousePos.x < 0)
2827    {
2828  0 scrolled |= ranges.scrollRight(false);
2829    }
2830  0 else if (mousePos.x >= getWidth())
2831    {
2832  0 scrolled |= ranges.scrollRight(true);
2833    }
2834  0 return scrolled;
2835    }
2836    }
2837   
2838    /**
2839    * modify current selection according to a received message.
2840    */
 
2841  106 toggle @Override
2842    public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2843    HiddenColumns hidden, SelectionSource source)
2844    {
2845    // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2846    // handles selection messages...
2847    // TODO: extend config options to allow user to control if selections may be
2848    // shared between viewports.
2849  106 boolean iSentTheSelection = (av == source
2850    || (source instanceof AlignViewport
2851    && ((AlignmentViewport) source).getSequenceSetId()
2852    .equals(av.getSequenceSetId())));
2853   
2854  106 if (iSentTheSelection)
2855    {
2856    // respond to our own event by updating dependent dialogs
2857  24 if (ap.getCalculationDialog() != null)
2858    {
2859  0 ap.getCalculationDialog().validateCalcTypes();
2860    }
2861   
2862  24 return;
2863    }
2864   
2865    // process further ?
2866  82 if (!av.followSelection)
2867    {
2868  0 return;
2869    }
2870   
2871    /*
2872    * Ignore the selection if there is one of our own pending.
2873    */
2874  82 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2875    {
2876  0 return;
2877    }
2878   
2879    /*
2880    * Check for selection in a view of which this one is a dna/protein
2881    * complement.
2882    */
2883  82 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2884    {
2885  0 return;
2886    }
2887   
2888    // do we want to thread this ? (contention with seqsel and colsel locks, I
2889    // suspect)
2890    /*
2891    * only copy colsel if there is a real intersection between
2892    * sequence selection and this panel's alignment
2893    */
2894  82 boolean repaint = false;
2895  82 boolean copycolsel = false;
2896   
2897  82 SequenceGroup sgroup = null;
2898  82 if (seqsel != null && seqsel.getSize() > 0)
2899    {
2900  63 if (av.getAlignment() == null)
2901    {
2902  0 Console.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2903    + " ViewId=" + av.getViewId()
2904    + " 's alignment is NULL! returning immediately.");
2905  0 return;
2906    }
2907  63 sgroup = seqsel.intersect(av.getAlignment(),
2908  63 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2909  63 if ((sgroup != null && sgroup.getSize() > 0))
2910    {
2911  0 copycolsel = true;
2912    }
2913    }
2914  82 if (sgroup != null && sgroup.getSize() > 0)
2915    {
2916  0 av.setSelectionGroup(sgroup);
2917    }
2918    else
2919    {
2920  82 av.setSelectionGroup(null);
2921    }
2922  82 av.isSelectionGroupChanged(true);
2923  82 repaint = true;
2924   
2925  82 if (copycolsel)
2926    {
2927    // the current selection is unset or from a previous message
2928    // so import the new colsel.
2929  0 if (colsel == null || colsel.isEmpty())
2930    {
2931  0 if (av.getColumnSelection() != null)
2932    {
2933  0 av.getColumnSelection().clear();
2934  0 repaint = true;
2935    }
2936    }
2937    else
2938    {
2939    // TODO: shift colSel according to the intersecting sequences
2940  0 if (av.getColumnSelection() == null)
2941    {
2942  0 av.setColumnSelection(new ColumnSelection(colsel));
2943    }
2944    else
2945    {
2946  0 av.getColumnSelection().setElementsFrom(colsel,
2947    av.getAlignment().getHiddenColumns());
2948    }
2949    }
2950  0 av.isColSelChanged(true);
2951  0 repaint = true;
2952    }
2953   
2954  82 if (copycolsel && av.hasHiddenColumns()
2955    && (av.getAlignment().getHiddenColumns() == null))
2956    {
2957  0 jalview.bin.Console.errPrintln("Bad things");
2958    }
2959  82 if (repaint) // always true!
2960    {
2961    // probably finessing with multiple redraws here
2962  82 PaintRefresher.Refresh(this, av.getSequenceSetId());
2963    // ap.paintAlignment(false);
2964    }
2965   
2966    // lastly, update dependent dialogs
2967  82 if (ap.getCalculationDialog() != null)
2968    {
2969  0 ap.getCalculationDialog().validateCalcTypes();
2970    }
2971   
2972    }
2973   
2974    /**
2975    * If this panel is a cdna/protein translation view of the selection source,
2976    * tries to map the source selection to a local one, and returns true. Else
2977    * returns false.
2978    *
2979    * @param seqsel
2980    * @param colsel
2981    * @param source
2982    */
 
2983  82 toggle protected boolean selectionFromTranslation(SequenceGroup seqsel,
2984    ColumnSelection colsel, HiddenColumns hidden,
2985    SelectionSource source)
2986    {
2987  82 if (!(source instanceof AlignViewportI))
2988    {
2989  0 return false;
2990    }
2991  82 final AlignViewportI sourceAv = (AlignViewportI) source;
2992  82 if (sourceAv.getCodingComplement() != av
2993    && av.getCodingComplement() != sourceAv)
2994    {
2995  82 return false;
2996    }
2997   
2998    /*
2999    * Map sequence selection
3000    */
3001  0 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
3002  0 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
3003  0 av.isSelectionGroupChanged(true);
3004   
3005    /*
3006    * Map column selection
3007    */
3008    // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
3009    // av);
3010  0 ColumnSelection cs = new ColumnSelection();
3011  0 HiddenColumns hs = new HiddenColumns();
3012  0 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
3013  0 av.setColumnSelection(cs);
3014  0 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
3015   
3016    // lastly, update any dependent dialogs
3017  0 if (ap.getCalculationDialog() != null)
3018    {
3019  0 ap.getCalculationDialog().validateCalcTypes();
3020    }
3021   
3022    /*
3023    * repaint alignment, and also Overview or Structure
3024    * if hidden column selection has changed
3025    */
3026  0 ap.paintAlignment(hiddenChanged, hiddenChanged);
3027    // propagate any selection changes
3028  0 PaintRefresher.Refresh(ap, av.getSequenceSetId());
3029   
3030  0 return true;
3031    }
3032   
3033    /**
3034    *
3035    * @return null or last search results handled by this panel
3036    */
 
3037  1 toggle public SearchResultsI getLastSearchResults()
3038    {
3039  1 return lastSearchResults;
3040    }
3041    }