f881a1c5887d1f14c11a948774e326ce71b2e938
[jalview.git] / src / jalview / appletgui / AlignmentPanel.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.appletgui;
22
23 import jalview.analysis.AnnotationSorter;
24 import jalview.api.AlignViewportI;
25 import jalview.api.AlignmentViewPanel;
26 import jalview.api.SequenceRendererI;
27 import jalview.bin.JalviewLite;
28 import jalview.datamodel.AlignmentI;
29 import jalview.datamodel.SearchResultsI;
30 import jalview.datamodel.SequenceI;
31 import jalview.structure.StructureSelectionManager;
32 import jalview.viewmodel.ViewportListenerI;
33 import jalview.viewmodel.ViewportRanges;
34
35 import java.awt.BorderLayout;
36 import java.awt.Color;
37 import java.awt.Dimension;
38 import java.awt.FontMetrics;
39 import java.awt.Frame;
40 import java.awt.Graphics;
41 import java.awt.Panel;
42 import java.awt.Scrollbar;
43 import java.awt.event.AdjustmentEvent;
44 import java.awt.event.AdjustmentListener;
45 import java.awt.event.ComponentAdapter;
46 import java.awt.event.ComponentEvent;
47 import java.beans.PropertyChangeEvent;
48 import java.util.List;
49
50 public class AlignmentPanel extends Panel
51         implements AdjustmentListener, AlignmentViewPanel, ViewportListenerI
52 {
53
54   public AlignViewport av;
55
56   OverviewPanel overviewPanel;
57
58   SeqPanel seqPanel;
59
60   IdPanel idPanel;
61
62   IdwidthAdjuster idwidthAdjuster;
63
64   public AlignFrame alignFrame;
65
66   ScalePanel scalePanel;
67
68   AnnotationPanel annotationPanel;
69
70   AnnotationLabels alabels;
71
72   ViewportRanges vpRanges;
73
74   // this value is set false when selection area being dragged
75   boolean fastPaint = true;
76
77   public AlignmentPanel(AlignFrame af, final AlignViewport av)
78   {
79     try
80     {
81       jbInit();
82     } catch (Exception e)
83     {
84       e.printStackTrace();
85     }
86
87     alignFrame = af;
88     this.av = av;
89     vpRanges = av.getRanges();
90     seqPanel = new SeqPanel(av, this);
91     idPanel = new IdPanel(av, this);
92     scalePanel = new ScalePanel(av, this);
93     idwidthAdjuster = new IdwidthAdjuster(this);
94     annotationPanel = new AnnotationPanel(this);
95     annotationPanelHolder.add(annotationPanel, BorderLayout.CENTER);
96
97     sequenceHolderPanel.add(annotationPanelHolder, BorderLayout.SOUTH);
98     alabels = new AnnotationLabels(this);
99
100     setAnnotationVisible(av.isShowAnnotation());
101
102     idPanelHolder.add(idPanel, BorderLayout.CENTER);
103     idSpaceFillerPanel1.add(idwidthAdjuster, BorderLayout.CENTER);
104     annotationSpaceFillerHolder.add(alabels, BorderLayout.CENTER);
105     scalePanelHolder.add(scalePanel, BorderLayout.CENTER);
106     seqPanelHolder.add(seqPanel, BorderLayout.CENTER);
107
108     fontChanged();
109     setScrollValues(0, 0);
110
111     apvscroll.addAdjustmentListener(this);
112     hscroll.addAdjustmentListener(this);
113     vscroll.addAdjustmentListener(this);
114
115     addComponentListener(new ComponentAdapter()
116     {
117       @Override
118       public void componentResized(ComponentEvent evt)
119       {
120         // reset the viewport ranges when the alignment panel is resized
121         // in particular, this initialises the end residue value when Jalview
122         // is initialised
123         if (av.getWrapAlignment())
124         {
125           int widthInRes = seqPanel.seqCanvas
126                   .getWrappedCanvasWidth(seqPanel.seqCanvas.getWidth());
127           vpRanges.setViewportWidth(widthInRes);
128         }
129         else
130         {
131           int widthInRes = seqPanel.seqCanvas.getWidth()
132                   / av.getCharWidth();
133           int heightInSeq = seqPanel.seqCanvas.getHeight()
134                   / av.getCharHeight();
135
136           vpRanges.setViewportWidth(widthInRes);
137           vpRanges.setViewportHeight(heightInSeq);
138         }
139         // setScrollValues(vpRanges.getStartRes(), vpRanges.getStartSeq());
140         if (getSize().height > 0
141                 && annotationPanelHolder.getSize().height > 0)
142         {
143           validateAnnotationDimensions(false);
144         }
145         repaint();
146       }
147
148     });
149
150     Dimension d = calculateIdWidth();
151     idPanel.idCanvas.setSize(d);
152
153     hscrollFillerPanel.setSize(d.width, annotationPanel.getSize().height);
154
155     idPanel.idCanvas.setSize(d.width, seqPanel.seqCanvas.getSize().height);
156     annotationSpaceFillerHolder.setSize(d.width,
157             annotationPanel.getSize().height);
158     alabels.setSize(d.width, annotationPanel.getSize().height);
159     final AlignmentPanel ap = this;
160     av.addPropertyChangeListener(new java.beans.PropertyChangeListener()
161     {
162       @Override
163       public void propertyChange(java.beans.PropertyChangeEvent evt)
164       {
165         if (evt.getPropertyName().equals("alignment"))
166         {
167           PaintRefresher.Refresh(ap, av.getSequenceSetId(), true, true);
168           alignmentChanged();
169         }
170       }
171     });
172     av.getRanges().addPropertyChangeListener(this);
173   }
174
175   @Override
176   public AlignViewportI getAlignViewport()
177   {
178     return av;
179   }
180
181   public SequenceRendererI getSequenceRenderer()
182   {
183     return seqPanel.seqCanvas.sr;
184   }
185
186   @Override
187   public jalview.api.FeatureRenderer getFeatureRenderer()
188   {
189     return seqPanel.seqCanvas.fr;
190   }
191
192   @Override
193   public jalview.api.FeatureRenderer cloneFeatureRenderer()
194   {
195     FeatureRenderer nfr = new FeatureRenderer(av);
196     nfr.transferSettings(seqPanel.seqCanvas.fr);
197     return nfr;
198   }
199
200   public void alignmentChanged()
201   {
202     av.alignmentChanged(this);
203
204     if (overviewPanel != null)
205     {
206       overviewPanel.updateOverviewImage();
207     }
208
209     alignFrame.updateEditMenuBar();
210
211     repaint();
212   }
213
214   public void fontChanged()
215   {
216     // set idCanvas bufferedImage to null
217     // to prevent drawing old image
218     idPanel.idCanvas.image = null;
219     FontMetrics fm = getFontMetrics(av.getFont());
220
221     scalePanel.setSize(
222             new Dimension(10, av.getCharHeight() + fm.getDescent()));
223     idwidthAdjuster.setSize(
224             new Dimension(10, av.getCharHeight() + fm.getDescent()));
225     av.updateSequenceIdColours();
226     annotationPanel.image = null;
227     int ap = annotationPanel.adjustPanelHeight(false);
228     Dimension d = calculateIdWidth();
229     d.setSize(d.width + 4, seqPanel.seqCanvas.getSize().height);
230     alabels.setSize(d.width + 4, ap);
231
232     idPanel.idCanvas.setSize(d);
233     hscrollFillerPanel.setSize(d);
234
235     validateAnnotationDimensions(false);
236     annotationPanel.repaint();
237     validate();
238     repaint();
239   }
240
241   public void setIdWidth(int w, int h)
242   {
243     idPanel.idCanvas.setSize(w, h);
244     idPanelHolder.setSize(w, idPanelHolder.getSize().height);
245     annotationSpaceFillerHolder.setSize(w,
246             annotationSpaceFillerHolder.getSize().height);
247     alabels.setSize(w, alabels.getSize().height);
248     validate();
249   }
250
251   Dimension calculateIdWidth()
252   {
253     if (av.nullFrame == null)
254     {
255       av.nullFrame = new Frame();
256       av.nullFrame.addNotify();
257     }
258
259     Graphics g = av.nullFrame.getGraphics();
260
261     FontMetrics fm = g.getFontMetrics(av.font);
262     AlignmentI al = av.getAlignment();
263
264     int i = 0;
265     int idWidth = 0;
266     String id;
267     while (i < al.getHeight() && al.getSequenceAt(i) != null)
268     {
269       SequenceI s = al.getSequenceAt(i);
270       id = s.getDisplayId(av.getShowJVSuffix());
271
272       if (fm.stringWidth(id) > idWidth)
273       {
274         idWidth = fm.stringWidth(id);
275       }
276       i++;
277     }
278
279     // Also check annotation label widths
280     i = 0;
281     if (al.getAlignmentAnnotation() != null)
282     {
283       fm = g.getFontMetrics(av.nullFrame.getFont());
284       while (i < al.getAlignmentAnnotation().length)
285       {
286         String label = al.getAlignmentAnnotation()[i].label;
287         if (fm.stringWidth(label) > idWidth)
288         {
289           idWidth = fm.stringWidth(label);
290         }
291         i++;
292       }
293     }
294
295     return new Dimension(idWidth, idPanel.idCanvas.getSize().height);
296   }
297
298   /**
299    * Highlight the given results on the alignment.
300    * 
301    */
302   public void highlightSearchResults(SearchResultsI results)
303   {
304     scrollToPosition(results);
305     seqPanel.seqCanvas.highlightSearchResults(results);
306   }
307
308   /**
309    * scroll the view to show the position of the highlighted region in results
310    * (if any) and redraw the overview
311    * 
312    * @param results
313    * @return false if results were not found
314    */
315   public boolean scrollToPosition(SearchResultsI results)
316   {
317     return scrollToPosition(results, true);
318   }
319
320   /**
321    * scroll the view to show the position of the highlighted region in results
322    * (if any)
323    * 
324    * @param results
325    * @param redrawOverview
326    *          - when set, the overview will be recalculated (takes longer)
327    * @return false if results were not found
328    */
329   public boolean scrollToPosition(SearchResultsI results,
330           boolean redrawOverview)
331   {
332     return scrollToPosition(results, 0, redrawOverview, false);
333   }
334
335   /**
336    * scroll the view to show the position of the highlighted region in results
337    * (if any)
338    * 
339    * @param results
340    * @param redrawOverview
341    *          - when set, the overview will be recalculated (takes longer)
342    * @return false if results were not found
343    */
344   public boolean scrollToPosition(SearchResultsI results,
345           int verticalOffset, boolean redrawOverview, boolean centre)
346   {
347     // do we need to scroll the panel?
348     if (results != null && results.getSize() > 0)
349     {
350       AlignmentI alignment = av.getAlignment();
351       int seqIndex = alignment.findIndex(results);
352       if (seqIndex == -1)
353       {
354         return false;
355       }
356       /*
357        * allow for offset of target sequence (actually scroll to one above it)
358        */
359
360       SequenceI seq = alignment.getSequenceAt(seqIndex);
361       int[] r = results.getResults(seq, 0, alignment.getWidth());
362       if (r == null)
363       {
364         if (JalviewLite.debug)
365         {// DEBUG
366           System.out.println(
367                   "DEBUG: scroll didn't happen - results not within alignment : "
368                           + seq.getStart() + "," + seq.getEnd());
369         }
370         return false;
371       }
372       if (JalviewLite.debug)
373       {
374         // DEBUG
375         /*
376          * System.out.println("DEBUG: scroll: start=" + r[0] +
377          * " av.getStartRes()=" + av.getStartRes() + " end=" + r[1] +
378          * " seq.end=" + seq.getEnd() + " av.getEndRes()=" + av.getEndRes() +
379          * " hextent=" + hextent);
380          */
381       }
382       int start = r[0];
383       int end = r[1];
384
385       /*
386        * To centre results, scroll to positions half the visible width
387        * left/right of the start/end positions
388        */
389       if (centre)
390       {
391         int offset = (vpRanges.getEndRes() - vpRanges.getStartRes() + 1) / 2
392                 - 1;
393         start = Math.max(start - offset, 0);
394         end = end + offset - 1;
395         // end = Math.min(end + offset, seq.getEnd() - 1);
396       }
397
398       if (start < 0)
399       {
400         return false;
401       }
402       if (end == seq.getEnd())
403       {
404         return false;
405       }
406
407       /*
408        * allow for offset of target sequence (actually scroll to one above it)
409        */
410       seqIndex = Math.max(0, seqIndex - verticalOffset);
411       return scrollTo(start, end, seqIndex, false, redrawOverview);
412     }
413     return true;
414   }
415
416   public boolean scrollTo(int ostart, int end, int seqIndex,
417           boolean scrollToNearest, boolean redrawOverview)
418   {
419     int startv, endv, starts, ends, width;
420
421     int start = -1;
422     if (av.hasHiddenColumns())
423     {
424       AlignmentI al = av.getAlignment();
425       start = al.getHiddenColumns().absoluteToVisibleColumn(ostart);
426       end = al.getHiddenColumns().absoluteToVisibleColumn(end);
427       if (start == end)
428       {
429         if (!scrollToNearest && !al.getHiddenColumns().isVisible(ostart))
430         {
431           // don't scroll - position isn't visible
432           return false;
433         }
434       }
435     }
436     else
437     {
438       start = ostart;
439     }
440
441     if (!av.getWrapAlignment())
442     {
443       /*
444        * int spos=av.getStartRes(),sqpos=av.getStartSeq(); if ((startv =
445        * av.getStartRes()) >= start) { spos=start-1; // seqIn //
446        * setScrollValues(start - 1, seqIndex); } else if ((endv =
447        * av.getEndRes()) <= end) { // setScrollValues(spos=startv + 1 + end -
448        * endv, seqIndex); spos=startv + 1 + end - endv; } else if ((starts =
449        * av.getStartSeq()) > seqIndex) { setScrollValues(av.getStartRes(),
450        * seqIndex); } else if ((ends = av.getEndSeq()) <= seqIndex) {
451        * setScrollValues(av.getStartRes(), starts + seqIndex - ends + 1); }
452        */
453
454       // below is scrolling logic up to Jalview 2.8.2
455       // if ((av.getStartRes() > end)
456       // || (av.getEndRes() < start)
457       // || ((av.getStartSeq() > seqIndex) || (av.getEndSeq() < seqIndex)))
458       // {
459       // if (start > av.getAlignment().getWidth() - hextent)
460       // {
461       // start = av.getAlignment().getWidth() - hextent;
462       // if (start < 0)
463       // {
464       // start = 0;
465       // }
466       //
467       // }
468       // if (seqIndex > av.getAlignment().getHeight() - vextent)
469       // {
470       // seqIndex = av.getAlignment().getHeight() - vextent;
471       // if (seqIndex < 0)
472       // {
473       // seqIndex = 0;
474       // }
475       // }
476       // setScrollValues(start, seqIndex);
477       // }
478       // logic copied from jalview.gui.AlignmentPanel:
479       if ((startv = vpRanges.getStartRes()) >= start)
480       {
481         /*
482          * Scroll left to make start of search results visible
483          */
484         setScrollValues(start - 1, seqIndex);
485       }
486       else if ((endv = vpRanges.getEndRes()) <= end)
487       {
488         /*
489          * Scroll right to make end of search results visible
490          */
491         setScrollValues(startv + 1 + end - endv, seqIndex);
492       }
493       else if ((starts = vpRanges.getStartSeq()) > seqIndex)
494       {
495         /*
496          * Scroll up to make start of search results visible
497          */
498         setScrollValues(vpRanges.getStartRes(), seqIndex);
499       }
500       else if ((ends = vpRanges.getEndSeq()) <= seqIndex)
501       {
502         /*
503          * Scroll down to make end of search results visible
504          */
505         setScrollValues(vpRanges.getStartRes(),
506                 starts + seqIndex - ends + 1);
507       }
508       /*
509        * Else results are already visible - no need to scroll
510        */
511     }
512     else
513     {
514       vpRanges.scrollToWrappedVisible(start);
515     }
516
517     paintAlignment(redrawOverview, false);
518     return true;
519   }
520
521   public OverviewPanel getOverviewPanel()
522   {
523     return overviewPanel;
524   }
525
526   public void setOverviewPanel(OverviewPanel op)
527   {
528     overviewPanel = op;
529   }
530
531   public void setAnnotationVisible(boolean b)
532   {
533     if (!av.getWrapAlignment())
534     {
535       annotationSpaceFillerHolder.setVisible(b);
536       annotationPanelHolder.setVisible(b);
537     }
538     else
539     {
540       annotationSpaceFillerHolder.setVisible(false);
541       annotationPanelHolder.setVisible(false);
542     }
543     validate();
544     repaint();
545   }
546
547   /**
548    * automatically adjust annotation panel height for new annotation whilst
549    * ensuring the alignment is still visible.
550    */
551   @Override
552   public void adjustAnnotationHeight()
553   {
554     // TODO: display vertical annotation scrollbar if necessary
555     // this is called after loading new annotation onto alignment
556     if (alignFrame.getSize().height == 0)
557     {
558       System.out.println(
559               "adjustAnnotationHeight frame size zero NEEDS FIXING");
560     }
561     fontChanged();
562     validateAnnotationDimensions(true);
563     apvscroll.addNotify();
564     hscroll.addNotify();
565     validate();
566     paintAlignment(true, false);
567   }
568
569   /**
570    * Calculate the annotation dimensions and refresh slider values accordingly.
571    * Need to do repaints/notifys afterwards.
572    */
573   protected void validateAnnotationDimensions(boolean adjustPanelHeight)
574   {
575     int rowHeight = av.getCharHeight();
576     int alignmentHeight = rowHeight * av.getAlignment().getHeight();
577     int annotationHeight = av.calcPanelHeight();
578
579     int mheight = annotationHeight;
580     Dimension d = sequenceHolderPanel.getSize();
581
582     int availableHeight = d.height - scalePanelHolder.getHeight();
583
584     if (adjustPanelHeight)
585     {
586       /*
587        * If not enough vertical space, maximize annotation height while keeping
588        * at least two rows of alignment visible
589        */
590       if (annotationHeight + alignmentHeight > availableHeight)
591       {
592         annotationHeight = Math.min(annotationHeight,
593                 availableHeight - 2 * rowHeight);
594       }
595     }
596     else
597     {
598       // maintain same window layout whilst updating sliders
599       annotationHeight = annotationPanelHolder.getSize().height;
600     }
601
602     if (availableHeight - annotationHeight < 5)
603     {
604       annotationHeight = availableHeight;
605     }
606
607     annotationPanel.setSize(new Dimension(d.width, annotationHeight));
608     annotationPanelHolder.setSize(new Dimension(d.width, annotationHeight));
609     // seqPanelHolder.setSize(d.width, seqandannot - height);
610     seqPanel.seqCanvas.setSize(d.width,
611             seqPanel.seqCanvas.getSize().height);
612
613     Dimension e = idPanel.getSize();
614     alabels.setSize(new Dimension(e.width, annotationHeight));
615     annotationSpaceFillerHolder
616             .setSize(new Dimension(e.width, annotationHeight));
617
618     int s = apvscroll.getValue();
619     if (s > mheight - annotationHeight)
620     {
621       s = 0;
622     }
623     apvscroll.setValues(s, annotationHeight, 0, mheight);
624     annotationPanel.setScrollOffset(apvscroll.getValue(), false);
625     alabels.setScrollOffset(apvscroll.getValue(), false);
626   }
627
628   public void setWrapAlignment(boolean wrap)
629   {
630     vpRanges.setStartEndSeq(0, vpRanges.getVisibleAlignmentHeight());
631     vpRanges.setStartRes(0);
632     scalePanelHolder.setVisible(!wrap);
633
634     hscroll.setVisible(!wrap);
635     idwidthAdjuster.setVisible(!wrap);
636
637     if (wrap)
638     {
639       annotationPanelHolder.setVisible(false);
640       annotationSpaceFillerHolder.setVisible(false);
641     }
642     else if (av.isShowAnnotation())
643     {
644       annotationPanelHolder.setVisible(true);
645       annotationSpaceFillerHolder.setVisible(true);
646     }
647
648     idSpaceFillerPanel1.setVisible(!wrap);
649
650     fontChanged(); // This is so that the scalePanel is resized correctly
651
652     validate();
653     sequenceHolderPanel.validate();
654     repaint();
655
656   }
657
658   int hextent = 0;
659
660   int vextent = 0;
661
662   public void setScrollValues(int xpos, int ypos)
663   {
664     int x = xpos;
665     int y = ypos;
666
667     if (av.getWrapAlignment())
668     {
669       setScrollingForWrappedPanel(x);
670     }
671     else
672     {
673       int width = av.getAlignment().getWidth();
674       int height = av.getAlignment().getHeight();
675
676       if (av.hasHiddenColumns())
677       {
678         width = av.getAlignment().getHiddenColumns()
679                 .absoluteToVisibleColumn(width);
680       }
681       if (x < 0)
682       {
683         x = 0;
684       }
685
686       hextent = seqPanel.seqCanvas.getSize().width / av.getCharWidth();
687       vextent = seqPanel.seqCanvas.getSize().height / av.getCharHeight();
688
689       if (hextent > width)
690       {
691         hextent = width;
692       }
693
694       if (vextent > height)
695       {
696         vextent = height;
697       }
698
699       if ((hextent + x) > width)
700       {
701         System.err.println("hextent was " + hextent + " and x was " + x);
702
703         x = width - hextent;
704       }
705
706       if ((vextent + y) > height)
707       {
708         y = height - vextent;
709       }
710
711       if (y < 0)
712       {
713         y = 0;
714       }
715
716       if (x < 0)
717       {
718         System.err.println("x was " + x);
719         x = 0;
720       }
721
722       hscroll.setValues(x, hextent, 0, width);
723       vscroll.setValues(y, vextent, 0, height);
724
725       // AWT scrollbar does not fire adjustmentValueChanged for setValues
726       // so also call adjustment code!
727       adjustHorizontal(x);
728       adjustVertical(y);
729
730       sendViewPosition();
731     }
732   }
733
734   /**
735    * Respond to adjustment event when horizontal or vertical scrollbar is
736    * changed
737    * 
738    * @param evt
739    *          adjustment event encoding whether apvscroll, hscroll or vscroll
740    *          changed
741    */
742   @Override
743   public void adjustmentValueChanged(AdjustmentEvent evt)
744   {
745     // Note that this event is NOT fired by the AWT scrollbar when setValues is
746     // called. Instead manually call adjustHorizontal and adjustVertical
747     // directly.
748     if (evt == null || evt.getSource() == apvscroll)
749     {
750       annotationPanel.setScrollOffset(apvscroll.getValue(), false);
751       alabels.setScrollOffset(apvscroll.getValue(), false);
752     }
753     if (evt == null || evt.getSource() == hscroll)
754     {
755       int x = hscroll.getValue();
756       adjustHorizontal(x);
757     }
758
759     if (evt == null || evt.getSource() == vscroll)
760     {
761       int offy = vscroll.getValue();
762       adjustVertical(offy);
763     }
764
765   }
766
767   private void adjustHorizontal(int x)
768   {
769     int oldX = vpRanges.getStartRes();
770     int oldwidth = vpRanges.getViewportWidth();
771     int width = seqPanel.seqCanvas.getWidth() / av.getCharWidth();
772
773     // if we're scrolling to the position we're already at, stop
774     // this prevents infinite recursion of events when the scroll/viewport
775     // ranges values are the same
776     if ((x == oldX) && (width == oldwidth))
777     {
778       return;
779     }
780     vpRanges.setViewportStartAndWidth(x, width);
781
782     if (av.getWrapAlignment() || !fastPaint)
783     {
784       repaint();
785     }
786     sendViewPosition();
787   }
788
789   private void adjustVertical(int newY)
790   {
791     if (av.getWrapAlignment())
792     {
793       /*
794        * if we're scrolling to the position we're already at, stop
795        * this prevents infinite recursion of events when the scroll/viewport
796        * ranges values are the same
797        */
798       int oldX = vpRanges.getStartRes();
799       int oldY = vpRanges.getWrappedScrollPosition(oldX);
800       if (oldY == newY)
801       {
802         return;
803       }
804       if (newY > -1)
805       {
806         /*
807          * limit page up/down to one width's worth of positions
808          */
809         int rowSize = vpRanges.getViewportWidth();
810         int newX = newY > oldY ? oldX + rowSize : oldX - rowSize;
811         vpRanges.setViewportStartAndWidth(Math.max(0, newX), rowSize);
812       }
813     }
814     else
815     {
816       int height = seqPanel.seqCanvas.getHeight() / av.getCharHeight();
817       int oldY = vpRanges.getStartSeq();
818       int oldheight = vpRanges.getViewportHeight();
819
820       // if we're scrolling to the position we're already at, stop
821       // this prevents infinite recursion of events when the scroll/viewport
822       // ranges values are the same
823       if ((newY == oldY) && (height == oldheight))
824       {
825         return;
826       }
827       vpRanges.setViewportStartAndHeight(newY, height);
828     }
829     if (av.getWrapAlignment() || !fastPaint)
830     {
831       repaint();
832     }
833     sendViewPosition();
834   }
835
836   /**
837    * A helper method to return the AlignmentPanel in the other (complementary)
838    * half of a SplitFrame view. Returns null if not in a SplitFrame.
839    * 
840    * @return
841    */
842   private AlignmentPanel getComplementPanel()
843   {
844     AlignmentPanel ap = null;
845     if (alignFrame != null)
846     {
847       SplitFrame sf = alignFrame.getSplitFrame();
848       if (sf != null)
849       {
850         AlignFrame other = sf.getComplement(alignFrame);
851         if (other != null)
852         {
853           ap = other.alignPanel;
854         }
855       }
856     }
857     return ap;
858   }
859
860   /**
861    * Follow a scrolling change in the (cDNA/Protein) complementary alignment.
862    * The aim is to keep the two alignments 'lined up' on their centre columns.
863    * 
864    * @param sr
865    *          holds mapped region(s) of this alignment that we are scrolling
866    *          'to'; may be modified for sequence offset by this method
867    * @param seqOffset
868    *          the number of visible sequences to show above the mapped region
869    */
870   protected void scrollToCentre(SearchResultsI sr, int seqOffset)
871   {
872     /*
873      * To avoid jumpy vertical scrolling (if some sequences are gapped or not
874      * mapped), we can make the scroll-to location a sequence above the one
875      * actually mapped.
876      */
877     SequenceI mappedTo = sr.getResults().get(0).getSequence();
878     List<SequenceI> seqs = av.getAlignment().getSequences();
879
880     /*
881      * This is like AlignmentI.findIndex(seq) but here we are matching the
882      * dataset sequence not the aligned sequence
883      */
884     boolean matched = false;
885     for (SequenceI seq : seqs)
886     {
887       if (mappedTo == seq.getDatasetSequence())
888       {
889         matched = true;
890         break;
891       }
892     }
893     if (!matched)
894     {
895       return; // failsafe, shouldn't happen
896     }
897
898     /*
899      * Scroll to position but centring the target residue. Also set a state flag
900      * to prevent adjustmentValueChanged performing this recursively.
901      */
902     scrollToPosition(sr, seqOffset, true, true);
903   }
904
905   private void sendViewPosition()
906   {
907     StructureSelectionManager.getStructureSelectionManager(av.applet)
908             .sendViewPosition(this, vpRanges.getStartRes(),
909                     vpRanges.getEndRes(), vpRanges.getStartSeq(),
910                     vpRanges.getEndSeq());
911   }
912
913   /**
914    * Repaint the alignment and annotations, and, optionally, any overview window
915    */
916   @Override
917   public void paintAlignment(boolean updateOverview,
918           boolean updateStructures)
919   {
920     final AnnotationSorter sorter = new AnnotationSorter(getAlignment(),
921             av.isShowAutocalculatedAbove());
922     sorter.sort(getAlignment().getAlignmentAnnotation(),
923             av.getSortAnnotationsBy());
924     repaint();
925
926     if (updateStructures)
927     {
928       jalview.structure.StructureSelectionManager
929               .getStructureSelectionManager(av.applet)
930               .sequenceColoursChanged(this);
931     }
932     if (updateOverview)
933     {
934       if (overviewPanel != null)
935       {
936         overviewPanel.updateOverviewImage();
937       }
938     }
939   }
940
941   @Override
942   public void update(Graphics g)
943   {
944     paint(g);
945   }
946
947   @Override
948   public void paint(Graphics g)
949   {
950     invalidate();
951     Dimension d = idPanel.idCanvas.getSize();
952     final int canvasHeight = seqPanel.seqCanvas.getSize().height;
953     if (canvasHeight != d.height)
954     {
955       idPanel.idCanvas.setSize(d.width, canvasHeight);
956     }
957
958     setScrollValues(vpRanges.getStartRes(), vpRanges.getStartSeq());
959
960     seqPanel.seqCanvas.repaint();
961     idPanel.idCanvas.repaint();
962     if (!av.getWrapAlignment())
963     {
964       if (av.isShowAnnotation())
965       {
966         alabels.repaint();
967         annotationPanel.repaint();
968       }
969       scalePanel.repaint();
970     }
971
972   }
973
974   /**
975    * Set vertical scroll bar parameters for wrapped panel
976    * 
977    * @param topLeftColumn
978    *          the column position at top left (0..)
979    */
980   private void setScrollingForWrappedPanel(int topLeftColumn)
981   {
982     int scrollPosition = vpRanges.getWrappedScrollPosition(topLeftColumn);
983     int maxScroll = vpRanges.getWrappedMaxScroll(topLeftColumn);
984
985     /*
986      * a scrollbar's value can be set to at most (maximum-extent)
987      * so we add extent (1) to the maxScroll value
988      */
989     vscroll.setUnitIncrement(1);
990     vscroll.setValues(scrollPosition, 1, 0, maxScroll + 1);
991   }
992
993   protected Panel sequenceHolderPanel = new Panel();
994
995   protected Scrollbar vscroll = new Scrollbar();
996
997   protected Scrollbar hscroll = new Scrollbar();
998
999   protected Panel seqPanelHolder = new Panel();
1000
1001   protected Panel scalePanelHolder = new Panel();
1002
1003   protected Panel idPanelHolder = new Panel();
1004
1005   protected Panel idSpaceFillerPanel1 = new Panel();
1006
1007   public Panel annotationSpaceFillerHolder = new Panel();
1008
1009   protected Panel hscrollFillerPanel = new Panel();
1010
1011   Panel annotationPanelHolder = new Panel();
1012
1013   protected Scrollbar apvscroll = new Scrollbar();
1014
1015   /*
1016    * Flag set while scrolling to follow complementary cDNA/protein scroll. When
1017    * false, suppresses invoking the same method recursively.
1018    */
1019   private boolean scrollComplementaryPanel = true;
1020
1021   private void jbInit() throws Exception
1022   {
1023     // idPanelHolder.setPreferredSize(new Dimension(70, 10));
1024     this.setLayout(new BorderLayout());
1025
1026     // sequenceHolderPanel.setPreferredSize(new Dimension(150, 150));
1027     sequenceHolderPanel.setLayout(new BorderLayout());
1028     seqPanelHolder.setLayout(new BorderLayout());
1029     scalePanelHolder.setBackground(Color.white);
1030
1031     // scalePanelHolder.setPreferredSize(new Dimension(10, 30));
1032     scalePanelHolder.setLayout(new BorderLayout());
1033     idPanelHolder.setLayout(new BorderLayout());
1034     idSpaceFillerPanel1.setBackground(Color.white);
1035
1036     // idSpaceFillerPanel1.setPreferredSize(new Dimension(10, 30));
1037     idSpaceFillerPanel1.setLayout(new BorderLayout());
1038     annotationSpaceFillerHolder.setBackground(Color.white);
1039
1040     // annotationSpaceFillerHolder.setPreferredSize(new Dimension(10, 80));
1041     annotationSpaceFillerHolder.setLayout(new BorderLayout());
1042     hscroll.setOrientation(Scrollbar.HORIZONTAL);
1043
1044     Panel hscrollHolder = new Panel();
1045     hscrollHolder.setLayout(new BorderLayout());
1046     hscrollFillerPanel.setBackground(Color.white);
1047     apvscroll.setOrientation(Scrollbar.VERTICAL);
1048     apvscroll.setVisible(true);
1049     apvscroll.addAdjustmentListener(this);
1050
1051     annotationPanelHolder.setBackground(Color.white);
1052     annotationPanelHolder.setLayout(new BorderLayout());
1053     annotationPanelHolder.add(apvscroll, BorderLayout.EAST);
1054     // hscrollFillerPanel.setPreferredSize(new Dimension(70, 10));
1055     hscrollHolder.setBackground(Color.white);
1056
1057     // annotationScroller.setPreferredSize(new Dimension(10, 80));
1058     // this.setPreferredSize(new Dimension(220, 166));
1059     seqPanelHolder.setBackground(Color.white);
1060     idPanelHolder.setBackground(Color.white);
1061     sequenceHolderPanel.add(scalePanelHolder, BorderLayout.NORTH);
1062     sequenceHolderPanel.add(seqPanelHolder, BorderLayout.CENTER);
1063     seqPanelHolder.add(vscroll, BorderLayout.EAST);
1064
1065     // Panel3.add(secondaryPanelHolder, BorderLayout.SOUTH);
1066     this.add(idPanelHolder, BorderLayout.WEST);
1067     idPanelHolder.add(idSpaceFillerPanel1, BorderLayout.NORTH);
1068     idPanelHolder.add(annotationSpaceFillerHolder, BorderLayout.SOUTH);
1069     this.add(hscrollHolder, BorderLayout.SOUTH);
1070     hscrollHolder.add(hscroll, BorderLayout.CENTER);
1071     hscrollHolder.add(hscrollFillerPanel, BorderLayout.WEST);
1072     this.add(sequenceHolderPanel, BorderLayout.CENTER);
1073   }
1074
1075   /**
1076    * hides or shows dynamic annotation rows based on groups and av state flags
1077    */
1078   public void updateAnnotation()
1079   {
1080     updateAnnotation(false);
1081   }
1082
1083   public void updateAnnotation(boolean applyGlobalSettings)
1084   {
1085     updateAnnotation(applyGlobalSettings, false);
1086   }
1087
1088   public void updateAnnotation(boolean applyGlobalSettings,
1089           boolean preserveNewGroupSettings)
1090   {
1091     av.updateGroupAnnotationSettings(applyGlobalSettings,
1092             preserveNewGroupSettings);
1093     adjustAnnotationHeight();
1094   }
1095
1096   @Override
1097   public AlignmentI getAlignment()
1098   {
1099     return av.getAlignment();
1100   }
1101
1102   @Override
1103   public String getViewName()
1104   {
1105     return getName();
1106   }
1107
1108   @Override
1109   public StructureSelectionManager getStructureSelectionManager()
1110   {
1111     return StructureSelectionManager
1112             .getStructureSelectionManager(av.applet);
1113   }
1114
1115   @Override
1116   public void raiseOOMWarning(String string, OutOfMemoryError error)
1117   {
1118     // TODO: JAL-960
1119     System.err.println("Out of memory whilst '" + string + "'");
1120     error.printStackTrace();
1121   }
1122
1123   /**
1124    * Set a flag to say we are scrolling to follow a (cDNA/protein) complement.
1125    * 
1126    * @param b
1127    */
1128   protected void setToScrollComplementPanel(boolean b)
1129   {
1130     this.scrollComplementaryPanel = b;
1131   }
1132
1133   /**
1134    * Get whether to scroll complement panel
1135    * 
1136    * @return true if cDNA/protein complement panels should be scrolled
1137    */
1138   protected boolean isSetToScrollComplementPanel()
1139   {
1140     return this.scrollComplementaryPanel;
1141   }
1142
1143   @Override
1144   /**
1145    * Property change event fired when a change is made to the viewport ranges
1146    * object associated with this alignment panel's viewport
1147    */
1148   public void propertyChange(PropertyChangeEvent evt)
1149   {
1150     // update this panel's scroll values based on the new viewport ranges values
1151     int x = vpRanges.getStartRes();
1152     int y = vpRanges.getStartSeq();
1153     setScrollValues(x, y);
1154
1155     // now update any complementary alignment (its viewport ranges object
1156     // is different so does not get automatically updated)
1157     if (isSetToScrollComplementPanel())
1158     {
1159       setToScrollComplementPanel(false);
1160       av.scrollComplementaryAlignment(getComplementPanel());
1161       setToScrollComplementPanel(true);
1162     }
1163
1164   }
1165
1166 }