JAL-3253 JAL-3260 SwingJS-3.2.9.v1e and tests
[jalview.git] / src / jalview / gui / 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.gui;
22
23 import jalview.analysis.AnnotationSorter;
24 import jalview.api.AlignViewportI;
25 import jalview.api.AlignmentViewPanel;
26 import jalview.api.SequenceRenderer;
27 import jalview.bin.Cache;
28 import jalview.bin.Jalview;
29 import jalview.datamodel.AlignmentI;
30 import jalview.datamodel.HiddenColumns;
31 import jalview.datamodel.SearchResultsI;
32 import jalview.datamodel.SequenceFeature;
33 import jalview.datamodel.SequenceGroup;
34 import jalview.datamodel.SequenceI;
35 import jalview.gui.ImageExporter.ImageWriterI;
36 import jalview.io.HTMLOutput;
37 import jalview.jbgui.GAlignmentPanel;
38 import jalview.math.AlignmentDimension;
39 import jalview.schemes.ResidueProperties;
40 import jalview.structure.StructureSelectionManager;
41 import jalview.util.Comparison;
42 import jalview.util.ImageMaker;
43 import jalview.util.MessageManager;
44 import jalview.viewmodel.ViewportListenerI;
45 import jalview.viewmodel.ViewportRanges;
46
47 import java.awt.BorderLayout;
48 import java.awt.Color;
49 import java.awt.Container;
50 import java.awt.Dimension;
51 import java.awt.Font;
52 import java.awt.FontMetrics;
53 import java.awt.Graphics;
54 import java.awt.Graphics2D;
55 import java.awt.event.AdjustmentEvent;
56 import java.awt.event.AdjustmentListener;
57 import java.awt.event.ComponentAdapter;
58 import java.awt.event.ComponentEvent;
59 import java.awt.image.BufferedImage;
60 import java.awt.print.PageFormat;
61 import java.awt.print.Printable;
62 import java.awt.print.PrinterException;
63 import java.beans.PropertyChangeEvent;
64 import java.beans.PropertyChangeListener;
65 import java.io.File;
66 import java.io.FileWriter;
67 import java.io.PrintWriter;
68 import java.util.List;
69
70 /**
71  * The main panel of an AlignFrame, containing holders for the IdPanel,
72  * SeqPanel, AnnotationLabels (a JPanel), and AnnotationPanel.
73  * 
74  * Additional holders contain an IdPanelWidthAdjuster space above the idPanel,
75  * AnnotationScroller (JScrollPane for AnnotationPanel), and vertical and
76  * horizontal scrollbars.
77  * 
78  * 
79  * 
80  * @author $author$
81  * @version $Revision: 1.161 $
82  */
83 @SuppressWarnings("serial")
84 public class AlignmentPanel extends GAlignmentPanel implements
85         AdjustmentListener, Printable, AlignmentViewPanel, ViewportListenerI
86 {
87   public AlignViewport av;
88
89   OverviewPanel overviewPanel;
90
91   private SeqPanel seqPanel;
92
93   private IdPanel idPanel;
94
95   IdwidthAdjuster idwidthAdjuster;
96
97   public AlignFrame alignFrame;
98
99   private ScalePanel scalePanel;
100
101   private AnnotationPanel annotationPanel;
102
103   private AnnotationLabels alabels;
104
105   private int hextent = 0;
106
107   private int vextent = 0;
108
109   /*
110    * Flag set while scrolling to follow complementary cDNA/protein scroll. When
111    * false, suppresses invoking the same method recursively.
112    */
113   private boolean scrollComplementaryPanel = true;
114
115   private PropertyChangeListener propertyChangeListener;
116
117   private CalculationChooser calculationDialog;
118
119   /**
120    * Creates a new AlignmentPanel object.
121    * 
122    * @param af
123    * @param av
124    */
125   public AlignmentPanel(AlignFrame af, final AlignViewport av)
126   {
127     setBackground(Color.white); // BH 2019
128     setOpaque(true);
129     alignFrame = af;
130     this.av = av;
131     setSeqPanel(new SeqPanel(av, this));
132     setIdPanel(new IdPanel(av, this));
133
134     setScalePanel(new ScalePanel(av, this));
135
136     idPanelHolder.add(getIdPanel(), BorderLayout.CENTER);
137     idwidthAdjuster = new IdwidthAdjuster(this);
138     idSpaceFillerPanel1.add(idwidthAdjuster, BorderLayout.CENTER);
139
140     setAnnotationPanel(new AnnotationPanel(this));
141     setAlabels(new AnnotationLabels(this));
142
143     annotationScroller.setViewportView(getAnnotationPanel());
144     annotationSpaceFillerHolder.add(getAlabels(), BorderLayout.CENTER);
145     if (!av.isShowAnnotation())
146     {
147       annotationScroller.setVisible(false);
148       annotationSpaceFillerHolder.setVisible(false);
149     }
150     scalePanelHolder.add(getScalePanel(), BorderLayout.CENTER);
151     seqPanelHolder.add(getSeqPanel(), BorderLayout.CENTER);
152
153     setScrollValues(0, 0);
154
155     hscroll.addAdjustmentListener(this);
156     vscroll.addAdjustmentListener(this);
157
158     addComponentListener(new ComponentAdapter()
159     {
160       @Override
161       public void componentResized(ComponentEvent evt)
162       {
163         // reset the viewport ranges when the alignment panel is resized
164         // in particular, this initialises the end residue value when Jalview
165         // is initialised
166         ViewportRanges ranges = av.getRanges();
167         if (av.getWrapAlignment())
168         {
169           int widthInRes = getSeqPanel().seqCanvas.getWrappedCanvasWidth(
170                   getSeqPanel().seqCanvas.getWidth());
171           ranges.setViewportWidth(widthInRes);
172         }
173         else
174         {
175           int widthInRes = getSeqPanel().seqCanvas.getWidth()
176                   / av.getCharWidth();
177           int heightInSeq = getSeqPanel().seqCanvas.getHeight()
178                   / av.getCharHeight();
179
180           ranges.setViewportWidth(widthInRes);
181           ranges.setViewportHeight(heightInSeq);
182         }
183       }
184
185     });
186
187     final AlignmentPanel ap = this;
188     propertyChangeListener = new PropertyChangeListener()
189     {
190       @Override
191       public void propertyChange(PropertyChangeEvent evt)
192       {
193         if (evt.getPropertyName().equals("alignment"))
194         {
195           PaintRefresher.Refresh(ap, av.getSequenceSetId(), true, true);
196           alignmentChanged();
197         }
198       }
199     };
200     av.addPropertyChangeListener(propertyChangeListener);
201
202     av.getRanges().addPropertyChangeListener(this);
203     fontChanged();
204     adjustAnnotationHeight();
205     updateLayout();
206   }
207
208   @Override
209   public AlignViewportI getAlignViewport()
210   {
211     return av;
212   }
213
214   public void alignmentChanged()
215   {
216     av.alignmentChanged(this);
217
218     if (getCalculationDialog() != null)
219     {
220       getCalculationDialog().validateCalcTypes();
221     }
222
223     alignFrame.updateEditMenuBar();
224
225     // no idea if we need to update structure
226     paintAlignment(true, true);
227
228   }
229
230   /**
231    * DOCUMENT ME!
232    */
233   public void fontChanged()
234   {
235     // set idCanvas bufferedImage to null
236     // to prevent drawing old image
237     FontMetrics fm = getFontMetrics(av.getFont());
238
239     scalePanelHolder.setPreferredSize(
240             new Dimension(10, av.getCharHeight() + fm.getDescent()));
241     idSpaceFillerPanel1.setPreferredSize(
242             new Dimension(10, av.getCharHeight() + fm.getDescent()));
243     idwidthAdjuster.invalidate();
244     scalePanelHolder.invalidate();
245     // BH 2018 getIdPanel().getIdCanvas().gg = null;
246     getSeqPanel().seqCanvas.img = null;
247     getAnnotationPanel().adjustPanelHeight();
248
249     Dimension d = calculateIdWidth();
250
251     d.setSize(d.width + 4, d.height);
252     getIdPanel().getIdCanvas().setPreferredSize(d);
253     hscrollFillerPanel.setPreferredSize(d);
254
255     repaint();
256   }
257
258   /**
259    * Calculate the width of the alignment labels based on the displayed names
260    * and any bounds on label width set in preferences.
261    * 
262    * @return Dimension giving the maximum width of the alignment label panel
263    *         that should be used.
264    */
265   public Dimension calculateIdWidth()
266   {
267     // calculate sensible default width when no preference is available
268     Dimension r = null;
269     if (av.getIdWidth() < 0)
270     {
271       int afwidth = (alignFrame != null ? alignFrame.getWidth() : 300);
272       int maxwidth = Math.max(20, Math.min(afwidth - 200, 2 * afwidth / 3));
273       r = calculateIdWidth(maxwidth);
274       av.setIdWidth(r.width);
275     }
276     else
277     {
278       r = new Dimension();
279       r.width = av.getIdWidth();
280       r.height = 0;
281     }
282     return r;
283   }
284
285   /**
286    * Calculate the width of the alignment labels based on the displayed names
287    * and any bounds on label width set in preferences.
288    * 
289    * @param maxwidth
290    *          -1 or maximum width allowed for IdWidth
291    * @return Dimension giving the maximum width of the alignment label panel
292    *         that should be used.
293    */
294   public Dimension calculateIdWidth(int maxwidth)
295   {
296     Container c = new Container();
297
298     FontMetrics fm = c.getFontMetrics(
299             new Font(av.font.getName(), Font.ITALIC, av.font.getSize()));
300
301     AlignmentI al = av.getAlignment();
302     int i = 0;
303     int idWidth = 0;
304     String id;
305
306     while ((i < al.getHeight()) && (al.getSequenceAt(i) != null))
307     {
308       SequenceI s = al.getSequenceAt(i);
309
310       id = s.getDisplayId(av.getShowJVSuffix());
311
312       if (fm.stringWidth(id) > idWidth)
313       {
314         idWidth = fm.stringWidth(id);
315       }
316
317       i++;
318     }
319
320     // Also check annotation label widths
321     i = 0;
322
323     if (al.getAlignmentAnnotation() != null)
324     {
325       fm = c.getFontMetrics(getAlabels().getFont());
326
327       while (i < al.getAlignmentAnnotation().length)
328       {
329         String label = al.getAlignmentAnnotation()[i].label;
330
331         if (fm.stringWidth(label) > idWidth)
332         {
333           idWidth = fm.stringWidth(label);
334         }
335
336         i++;
337       }
338     }
339
340     return new Dimension(
341             maxwidth < 0 ? idWidth : Math.min(maxwidth, idWidth), 12);
342   }
343
344   /**
345    * Highlight the given results on the alignment
346    * 
347    */
348   public void highlightSearchResults(SearchResultsI results)
349   {
350     boolean scrolled = scrollToPosition(results, 0, false);
351
352     boolean fastPaint = !(scrolled && av.getWrapAlignment());
353
354     getSeqPanel().seqCanvas.highlightSearchResults(results, fastPaint);
355   }
356
357   /**
358    * Scroll the view to show the position of the highlighted region in results
359    * (if any)
360    * 
361    * @param searchResults
362    * @return
363    */
364   public boolean scrollToPosition(SearchResultsI searchResults)
365   {
366     return scrollToPosition(searchResults, 0, false);
367   }
368
369   /**
370    * Scrolls the view (if necessary) to show the position of the first
371    * highlighted region in results (if any). Answers true if the view was
372    * scrolled, or false if no matched region was found, or it is already
373    * visible.
374    * 
375    * @param results
376    * @param verticalOffset
377    *          if greater than zero, allows scrolling to a position below the
378    *          first displayed sequence
379    * @param centre
380    *          if true, try to centre the search results horizontally in the view
381    * @return
382    */
383   protected boolean scrollToPosition(SearchResultsI results,
384           int verticalOffset, boolean centre)
385   {
386     int startv, endv, starts, ends;
387     ViewportRanges ranges = av.getRanges();
388
389     if (results == null || results.isEmpty() || av == null
390             || av.getAlignment() == null)
391     {
392       return false;
393     }
394     int seqIndex = av.getAlignment().findIndex(results);
395     if (seqIndex == -1)
396     {
397       return false;
398     }
399     SequenceI seq = av.getAlignment().getSequenceAt(seqIndex);
400
401     int[] r = results.getResults(seq, 0, av.getAlignment().getWidth());
402     if (r == null)
403     {
404       return false;
405     }
406     int start = r[0];
407     int end = r[1];
408
409     /*
410      * To centre results, scroll to positions half the visible width
411      * left/right of the start/end positions
412      */
413     if (centre)
414     {
415       int offset = (ranges.getEndRes() - ranges.getStartRes() + 1) / 2 - 1;
416       start = Math.max(start - offset, 0);
417       end = end + offset - 1;
418     }
419     if (start < 0)
420     {
421       return false;
422     }
423     if (end == seq.getEnd())
424     {
425       return false;
426     }
427
428     if (av.hasHiddenColumns())
429     {
430       HiddenColumns hidden = av.getAlignment().getHiddenColumns();
431       start = hidden.absoluteToVisibleColumn(start);
432       end = hidden.absoluteToVisibleColumn(end);
433       if (start == end)
434       {
435         if (!hidden.isVisible(r[0]))
436         {
437           // don't scroll - position isn't visible
438           return false;
439         }
440       }
441     }
442
443     /*
444      * allow for offset of target sequence (actually scroll to one above it)
445      */
446     seqIndex = Math.max(0, seqIndex - verticalOffset);
447     boolean scrollNeeded = true;
448
449     if (!av.getWrapAlignment())
450     {
451       if ((startv = ranges.getStartRes()) >= start)
452       {
453         /*
454          * Scroll left to make start of search results visible
455          */
456         setScrollValues(start, seqIndex);
457       }
458       else if ((endv = ranges.getEndRes()) <= end)
459       {
460         /*
461          * Scroll right to make end of search results visible
462          */
463         setScrollValues(startv + end - endv, seqIndex);
464       }
465       else if ((starts = ranges.getStartSeq()) > seqIndex)
466       {
467         /*
468          * Scroll up to make start of search results visible
469          */
470         setScrollValues(ranges.getStartRes(), seqIndex);
471       }
472       else if ((ends = ranges.getEndSeq()) <= seqIndex)
473       {
474         /*
475          * Scroll down to make end of search results visible
476          */
477         setScrollValues(ranges.getStartRes(), starts + seqIndex - ends
478                 + 1);
479       }
480       /*
481        * Else results are already visible - no need to scroll
482        */
483       scrollNeeded = false;
484     }
485     else
486     {
487       scrollNeeded = ranges.scrollToWrappedVisible(start);
488     }
489
490     paintAlignment(false, false);
491
492     return scrollNeeded;
493   }
494
495   /**
496    * DOCUMENT ME!
497    * 
498    * @return DOCUMENT ME!
499    */
500   public OverviewPanel getOverviewPanel()
501   {
502     return overviewPanel;
503   }
504
505   /**
506    * DOCUMENT ME!
507    * 
508    * @param op
509    *          DOCUMENT ME!
510    */
511   public void setOverviewPanel(OverviewPanel op)
512   {
513     overviewPanel = op;
514   }
515
516   /**
517    * 
518    * @param b
519    *          Hide or show annotation panel
520    * 
521    */
522   public void setAnnotationVisible(boolean b)
523   {
524     if (!av.getWrapAlignment())
525     {
526       annotationSpaceFillerHolder.setVisible(b);
527       annotationScroller.setVisible(b);
528     }
529     repaint();
530   }
531
532   /**
533    * automatically adjust annotation panel height for new annotation whilst
534    * ensuring the alignment is still visible.
535    */
536   @Override
537   public void adjustAnnotationHeight()
538   {
539     // TODO: display vertical annotation scrollbar if necessary
540     // this is called after loading new annotation onto alignment
541     if (alignFrame.getHeight() == 0)
542     {
543       System.out.println("NEEDS FIXING");
544     }
545     validateAnnotationDimensions(true);
546     addNotify();
547     // TODO: many places call this method and also paintAlignment with various
548     // different settings. this means multiple redraws are triggered...
549     paintAlignment(true, av.needToUpdateStructureViews());
550   }
551
552   /**
553    * calculate the annotation dimensions and refresh slider values accordingly.
554    * need to do repaints/notifys afterwards.
555    */
556   protected void validateAnnotationDimensions(boolean adjustPanelHeight)
557   {
558     // BH 2018.04.18 comment: addNotify() is not appropriate here. We
559     // are not changing ancestors, and keyboard action listeners do
560     // not need to be reset. addNotify() is a very expensive operation,
561     // requiring a full re-layout of all parents and children.
562     // Note in JComponent:
563     // This method is called by the toolkit internally and should
564     // not be called directly by programs.
565     // I note that addNotify() is called in several areas of Jalview.
566
567     int annotationHeight = getAnnotationPanel().adjustPanelHeight();
568     annotationHeight = getAnnotationPanel()
569             .adjustForAlignFrame(adjustPanelHeight, annotationHeight);
570
571     hscroll.addNotify();
572     annotationScroller.setPreferredSize(
573             new Dimension(annotationScroller.getWidth(), annotationHeight));
574
575     Dimension e = idPanel.getSize();
576     alabels.setSize(new Dimension(e.width, annotationHeight));
577
578
579     annotationSpaceFillerHolder.setPreferredSize(new Dimension(
580             annotationSpaceFillerHolder.getWidth(), annotationHeight));
581     annotationScroller.validate();
582     annotationScroller.addNotify();
583   }
584
585   /**
586    * update alignment layout for viewport settings
587    * 
588    * @param wrap
589    *          DOCUMENT ME!
590    */
591   public void updateLayout()
592   {
593     fontChanged(); // fires repaint
594     setAnnotationVisible(av.isShowAnnotation());
595     boolean wrap = av.getWrapAlignment();
596     ViewportRanges ranges = av.getRanges();
597     ranges.setStartSeq(0);
598     scalePanelHolder.setVisible(!wrap);
599     hscroll.setVisible(!wrap);
600     idwidthAdjuster.setVisible(!wrap);
601
602     if (wrap)
603     {
604       annotationScroller.setVisible(false);
605       annotationSpaceFillerHolder.setVisible(false);
606     }
607     else if (av.isShowAnnotation())
608     {
609       annotationScroller.setVisible(true);
610       annotationSpaceFillerHolder.setVisible(true);
611       validateAnnotationDimensions(false);
612     }
613
614     int canvasWidth = getSeqPanel().seqCanvas.getWidth();
615     if (canvasWidth > 0)
616     { // may not yet be laid out
617       if (wrap)
618       {
619         int widthInRes = getSeqPanel().seqCanvas
620                 .getWrappedCanvasWidth(canvasWidth);
621         ranges.setViewportWidth(widthInRes);
622       }
623       else
624       {
625         int widthInRes = (canvasWidth / av.getCharWidth());
626         int heightInSeq = (getSeqPanel().seqCanvas.getHeight()
627                 / av.getCharHeight());
628
629         ranges.setViewportWidth(widthInRes);
630         ranges.setViewportHeight(heightInSeq);
631       }
632     }
633
634     idSpaceFillerPanel1.setVisible(!wrap);
635
636     repaint();
637   }
638
639   /**
640    * Adjust row/column scrollers to show a visible position in the alignment.
641    * 
642    * @param x
643    *          visible column to scroll to
644    * @param y
645    *          visible row to scroll to
646    * 
647    */
648   public void setScrollValues(int xpos, int ypos)
649   {
650     int x = xpos;
651     int y = ypos;
652
653     if (av == null || av.getAlignment() == null)
654     {
655       return;
656     }
657
658     if (av.getWrapAlignment())
659     {
660       setScrollingForWrappedPanel(x);
661     }
662     else
663     {
664       int width = av.getAlignment().getVisibleWidth();
665       int height = av.getAlignment().getHeight();
666
667       hextent = getSeqPanel().seqCanvas.getWidth() / av.getCharWidth();
668       vextent = getSeqPanel().seqCanvas.getHeight() / av.getCharHeight();
669
670       if (hextent > width)
671       {
672         hextent = width;
673       }
674
675       if (vextent > height)
676       {
677         vextent = height;
678       }
679
680       if ((hextent + x) > width)
681       {
682         x = width - hextent;
683       }
684
685       if ((vextent + y) > height)
686       {
687         y = height - vextent;
688       }
689
690       if (y < 0)
691       {
692         y = 0;
693       }
694
695       if (x < 0)
696       {
697         x = 0;
698       }
699
700       // update the scroll values
701       hscroll.setValues(x, hextent, 0, width);
702       vscroll.setValues(y, vextent, 0, height);
703     }
704   }
705
706   /**
707    * Respond to adjustment event when horizontal or vertical scrollbar is
708    * changed
709    * 
710    * @param evt
711    *          adjustment event encoding whether hscroll or vscroll changed
712    */
713   @Override
714   public void adjustmentValueChanged(AdjustmentEvent evt)
715   {
716     if (av.getWrapAlignment())
717     {
718       adjustScrollingWrapped(evt);
719       return;
720     }
721
722     ViewportRanges ranges = av.getRanges();
723
724     if (evt.getSource() == hscroll)
725     {
726       int oldX = ranges.getStartRes();
727       int oldwidth = ranges.getViewportWidth();
728       int x = hscroll.getValue();
729       int width = getSeqPanel().seqCanvas.getWidth() / av.getCharWidth();
730
731       // if we're scrolling to the position we're already at, stop
732       // this prevents infinite recursion of events when the scroll/viewport
733       // ranges values are the same
734       if ((x == oldX) && (width == oldwidth))
735       {
736         return;
737       }
738       ranges.setViewportStartAndWidth(x, width);
739     }
740     else if (evt.getSource() == vscroll)
741     {
742       int oldY = ranges.getStartSeq();
743       int oldheight = ranges.getViewportHeight();
744       int y = vscroll.getValue();
745       int height = getSeqPanel().seqCanvas.getHeight() / av.getCharHeight();
746
747       // if we're scrolling to the position we're already at, stop
748       // this prevents infinite recursion of events when the scroll/viewport
749       // ranges values are the same
750       if ((y == oldY) && (height == oldheight))
751       {
752         return;
753       }
754       ranges.setViewportStartAndHeight(y, height);
755     }
756     repaint();
757   }
758
759   /**
760    * Responds to a scroll change by setting the start position of the viewport.
761    * Does
762    * 
763    * @param evt
764    */
765   protected void adjustScrollingWrapped(AdjustmentEvent evt)
766   {
767     if (evt.getSource() == hscroll)
768     {
769       return; // no horizontal scroll when wrapped
770     }
771     final ViewportRanges ranges = av.getRanges();
772
773     if (evt.getSource() == vscroll)
774     {
775       int newY = vscroll.getValue();
776
777       /*
778        * if we're scrolling to the position we're already at, stop
779        * this prevents infinite recursion of events when the scroll/viewport
780        * ranges values are the same
781        */
782       int oldX = ranges.getStartRes();
783       int oldY = ranges.getWrappedScrollPosition(oldX);
784       if (oldY == newY)
785       {
786         return;
787       }
788       if (newY > -1)
789       {
790         /*
791          * limit page up/down to one width's worth of positions
792          */
793         int rowSize = ranges.getViewportWidth();
794         int newX = newY > oldY ? oldX + rowSize : oldX - rowSize;
795         ranges.setViewportStartAndWidth(Math.max(0, newX), rowSize);
796       }
797     }
798     else
799     {
800       // This is only called if file loaded is a jar file that
801       // was wrapped when saved and user has wrap alignment true
802       // as preference setting
803       Jalview.execRunnable(new Runnable()
804       {
805         @Override
806         public void run()
807         {
808           // When updating scrolling to use ViewportChange events, this code
809           // could not be validated and it is not clear if it is now being
810           // called. Log warning here in case it is called and unforeseen
811           // problems occur
812           Cache.log.warn(
813                   "Unexpected path through code: Wrapped jar file opened with wrap alignment set in preferences");
814
815           // scroll to start of panel
816           ranges.setStartRes(0);
817           ranges.setStartSeq(0);
818         }
819       });
820     }
821     repaint();
822   }
823
824   /* (non-Javadoc)
825    * @see jalview.api.AlignmentViewPanel#paintAlignment(boolean)
826    */
827   @Override
828   public void paintAlignment(boolean updateOverview,
829           boolean updateStructures)
830   {
831     final AnnotationSorter sorter = new AnnotationSorter(getAlignment(),
832             av.isShowAutocalculatedAbove());
833     sorter.sort(getAlignment().getAlignmentAnnotation(),
834             av.getSortAnnotationsBy());
835     repaint();
836
837     if (updateStructures)
838     {
839       av.getStructureSelectionManager().sequenceColoursChanged(this);
840     }
841     if (updateOverview)
842     {
843       if (overviewPanel != null)
844       {
845         overviewPanel.updateOverviewImage();
846       }
847     }
848   }
849
850   /**
851    * DOCUMENT ME!
852    * 
853    * @param g
854    *          DOCUMENT ME!
855    */
856   @Override
857   public void paintComponent(Graphics g)
858   {
859
860     invalidate(); // needed so that the id width adjuster works correctly
861
862     Dimension d = getIdPanel().getIdCanvas().getPreferredSize();
863     idPanelHolder.setPreferredSize(d);
864     hscrollFillerPanel.setPreferredSize(new Dimension(d.width, 12));
865
866     validate(); // needed so that the id width adjuster works correctly
867
868     /*
869      * set scroll bar positions - tried to remove but necessary for split panel to resize correctly
870      * though I still think this call should be elsewhere.
871      */
872     ViewportRanges ranges = av.getRanges();
873     setScrollValues(ranges.getStartRes(), ranges.getStartSeq());
874     super.paintComponent(g);
875   }
876
877   /**
878    * Set vertical scroll bar position, and number of increments, for wrapped
879    * panel
880    * 
881    * @param topLeftColumn
882    *          the column position at top left (0..)
883    */
884   private void setScrollingForWrappedPanel(int topLeftColumn)
885   {
886     ViewportRanges ranges = av.getRanges();
887     int scrollPosition = ranges.getWrappedScrollPosition(topLeftColumn);
888     int maxScroll = ranges.getWrappedMaxScroll(topLeftColumn);
889
890     /*
891      * a scrollbar's value can be set to at most (maximum-extent)
892      * so we add extent (1) to the maxScroll value
893      */
894     vscroll.setUnitIncrement(1);
895     vscroll.setValues(scrollPosition, 1, 0, maxScroll + 1);
896   }
897
898   /**
899    * DOCUMENT ME!
900    * 
901    * @param pg
902    *          DOCUMENT ME!
903    * @param pf
904    *          DOCUMENT ME!
905    * @param pi
906    *          DOCUMENT ME!
907    * 
908    * @return DOCUMENT ME!
909    * 
910    * @throws PrinterException
911    *           DOCUMENT ME!
912    */
913   @Override
914   public int print(Graphics pg, PageFormat pf, int pi)
915           throws PrinterException
916   {
917     pg.translate((int) pf.getImageableX(), (int) pf.getImageableY());
918
919     int pwidth = (int) pf.getImageableWidth();
920     int pheight = (int) pf.getImageableHeight();
921
922     if (av.getWrapAlignment())
923     {
924       return printWrappedAlignment(pwidth, pheight, pi, pg);
925     }
926     else
927     {
928       return printUnwrapped(pwidth, pheight, pi, pg, pg);
929     }
930   }
931
932   /**
933    * Draws the alignment image, including sequence ids, sequences, and
934    * annotation labels and annotations if shown, on either one or two Graphics
935    * contexts.
936    * 
937    * @param pageWidth
938    *          in pixels
939    * @param pageHeight
940    *          in pixels
941    * @param pageIndex
942    *          (0, 1, ...)
943    * @param idGraphics
944    *          the graphics context for sequence ids and annotation labels
945    * @param alignmentGraphics
946    *          the graphics context for sequences and annotations (may or may not
947    *          be the same context as idGraphics)
948    * @return
949    * @throws PrinterException
950    */
951   public int printUnwrapped(int pageWidth, int pageHeight, int pageIndex,
952           Graphics idGraphics, Graphics alignmentGraphics)
953           throws PrinterException
954   {
955     final int idWidth = getVisibleIdWidth(false);
956
957     /*
958      * Get the horizontal offset to where we draw the sequences.
959      * This is idWidth if using a single Graphics context, else zero.
960      */
961     final int alignmentGraphicsOffset = idGraphics != alignmentGraphics ? 0
962             : idWidth;
963
964     FontMetrics fm = getFontMetrics(av.getFont());
965     final int charHeight = av.getCharHeight();
966     final int scaleHeight = charHeight + fm.getDescent();
967
968     idGraphics.setColor(Color.white);
969     idGraphics.fillRect(0, 0, pageWidth, pageHeight);
970     idGraphics.setFont(av.getFont());
971
972     /*
973      * How many sequences and residues can we fit on a printable page?
974      */
975     final int totalRes = (pageWidth - idWidth) / av.getCharWidth();
976
977     final int totalSeq = (pageHeight - scaleHeight) / charHeight - 1;
978
979     final int alignmentWidth = av.getAlignment().getVisibleWidth();
980     int pagesWide = (alignmentWidth / totalRes) + 1;
981
982     final int startRes = (pageIndex % pagesWide) * totalRes;
983     final int endRes = Math.min(startRes + totalRes - 1,
984             alignmentWidth - 1);
985
986     final int startSeq = (pageIndex / pagesWide) * totalSeq;
987     final int alignmentHeight = av.getAlignment().getHeight();
988     final int endSeq = Math.min(startSeq + totalSeq, alignmentHeight);
989
990     int pagesHigh = ((alignmentHeight / totalSeq) + 1) * pageHeight;
991
992     if (av.isShowAnnotation())
993     {
994       pagesHigh += getAnnotationPanel().adjustPanelHeight() + 3;
995     }
996
997     pagesHigh /= pageHeight;
998
999     if (pageIndex >= (pagesWide * pagesHigh))
1000     {
1001       return Printable.NO_SUCH_PAGE;
1002     }
1003     final int alignmentDrawnHeight = (endSeq - startSeq) * charHeight + 3;
1004
1005     /*
1006      * draw the Scale at horizontal offset, then reset to top left (0, 0)
1007      */
1008     alignmentGraphics.translate(alignmentGraphicsOffset, 0);
1009     getScalePanel().drawScale(alignmentGraphics, startRes, endRes,
1010             pageWidth - idWidth, scaleHeight);
1011     alignmentGraphics.translate(-alignmentGraphicsOffset, 0);
1012
1013     /*
1014      * Draw the sequence ids, offset for scale height,
1015      * then reset to top left (0, 0)
1016      */
1017     idGraphics.translate(0, scaleHeight);
1018     IdCanvas idCanvas = getIdPanel().getIdCanvas();
1019     List<SequenceI> selection = av.getSelectionGroup() == null ? null
1020             : av.getSelectionGroup().getSequences(null);
1021     idCanvas.drawIds((Graphics2D) idGraphics, av, startSeq, endSeq - 1,
1022             selection);
1023
1024     idGraphics.setFont(av.getFont());
1025     idGraphics.translate(0, -scaleHeight);
1026
1027     /*
1028      * draw the sequences, offset for scale height, and id width (if using a
1029      * single graphics context), then reset to (0, scale height)
1030      */
1031     alignmentGraphics.translate(alignmentGraphicsOffset, scaleHeight);
1032     getSeqPanel().seqCanvas.drawPanelForPrinting(alignmentGraphics, startRes,
1033             endRes, startSeq, endSeq - 1);
1034     alignmentGraphics.translate(-alignmentGraphicsOffset, 0);
1035
1036     if (av.isShowAnnotation() && (endSeq == alignmentHeight))
1037     {
1038       /*
1039        * draw annotation labels; drawComponent() translates by
1040        * getScrollOffset(), so compensate for that first;
1041        * then reset to (0, scale height)
1042        */
1043       int offset = getAlabels().getScrollOffset();
1044       idGraphics.translate(0, -offset);
1045       idGraphics.translate(0, alignmentDrawnHeight);
1046       getAlabels().drawComponent(idGraphics, idWidth);
1047       idGraphics.translate(0, -alignmentDrawnHeight);
1048
1049       /*
1050        * draw the annotations starting at 
1051        * (idOffset, alignmentHeight) from (0, scaleHeight)
1052        */
1053       alignmentGraphics.translate(alignmentGraphicsOffset,
1054               alignmentDrawnHeight);
1055       getAnnotationPanel().renderer.drawComponent(getAnnotationPanel(), av,
1056               alignmentGraphics, -1, startRes, endRes + 1);
1057     }
1058
1059     return Printable.PAGE_EXISTS;
1060   }
1061
1062   /**
1063    * Prints one page of an alignment in wrapped mode. Returns
1064    * Printable.PAGE_EXISTS (0) if a page was drawn, or Printable.NO_SUCH_PAGE if
1065    * no page could be drawn (page number out of range).
1066    * 
1067    * @param pageWidth
1068    * @param pageHeight
1069    * @param pageNumber
1070    *          (0, 1, ...)
1071    * @param g
1072    * 
1073    * @return
1074    * 
1075    * @throws PrinterException
1076    */
1077   public int printWrappedAlignment(int pageWidth, int pageHeight, int pageNumber,
1078           Graphics g) throws PrinterException
1079   {
1080
1081     int annotationHeight = 0;
1082     if (av.isShowAnnotation())
1083     {
1084       annotationHeight = getAnnotationPanel().adjustPanelHeight();
1085     }
1086
1087     int hgap = av.getCharHeight();
1088     if (av.getScaleAboveWrapped())
1089     {
1090       hgap += av.getCharHeight();
1091     }
1092
1093     int cHeight = av.getAlignment().getHeight() * av.getCharHeight() + hgap
1094             + annotationHeight;
1095
1096     int idWidth = getVisibleIdWidth(false);
1097
1098     int maxwidth = av.getAlignment().getVisibleWidth();
1099
1100     int resWidth = getSeqPanel().seqCanvas
1101             .getWrappedCanvasWidth(pageWidth - idWidth);
1102
1103     int totalHeight = cHeight * (maxwidth / resWidth + 1);
1104
1105     g = g.create();
1106
1107     g.setColor(Color.white);
1108     g.fillRect(0, 0, pageWidth, pageHeight);
1109     g.setFont(av.getFont());
1110     g.setColor(Color.black);
1111
1112     /*
1113      * method: print the whole wrapped alignment, but with a clip region that
1114      * is restricted to the requested page; this supports selective print of 
1115      * single  pages or ranges, (at the cost of some repeated processing in 
1116      * the 'normal' case, when all pages are printed)
1117      */
1118     g.translate(0, -pageNumber * pageHeight);
1119
1120     // BH 2020.03.19 avoiding g.setClip
1121     g.clipRect(0, pageNumber * pageHeight, pageWidth, pageHeight);
1122
1123     /*
1124      * draw sequence ids and annotation labels (if shown)
1125      */
1126     IdCanvas idCanvas = getIdPanel().getIdCanvas();
1127     idCanvas.drawIdsWrapped((Graphics2D) g, av, 0, totalHeight);
1128
1129     g.translate(idWidth, 0);
1130
1131     getSeqPanel().seqCanvas.drawWrappedPanelForPrinting(g, pageWidth - idWidth,
1132             totalHeight, 0);
1133
1134     g.dispose();
1135     if ((pageNumber * pageHeight) < totalHeight)
1136     {
1137       return Printable.PAGE_EXISTS;
1138     }
1139     else
1140     {
1141       return Printable.NO_SUCH_PAGE;
1142     }
1143   }
1144
1145   /**
1146    * get current sequence ID panel width, or nominal value if panel were to be
1147    * displayed using default settings
1148    * 
1149    * @return
1150    */
1151   public int getVisibleIdWidth()
1152   {
1153     return getVisibleIdWidth(true);
1154   }
1155
1156   /**
1157    * get current sequence ID panel width, or nominal value if panel were to be
1158    * displayed using default settings
1159    * 
1160    * @param onscreen
1161    *          indicate if the Id width for onscreen or offscreen display should
1162    *          be returned
1163    * @return
1164    */
1165   public int getVisibleIdWidth(boolean onscreen)
1166   {
1167     // see if rendering offscreen - check preferences and calc width accordingly
1168     if (!onscreen && Cache.getDefault(Preferences.FIGURE_AUTOIDWIDTH, false))
1169     {
1170       return calculateIdWidth(-1).width + 4;
1171     }
1172     Integer idwidth = null;
1173     if (onscreen || (idwidth = Cache
1174             .getIntegerProperty(Preferences.FIGURE_FIXEDIDWIDTH)) == null)
1175     {
1176       int w = getIdPanel().getWidth();
1177       return (w > 0 ? w : calculateIdWidth().width + 4);
1178     }
1179     return idwidth.intValue() + 4;
1180   }
1181
1182   /**
1183    * Builds an image of the alignment of the specified type (EPS/PNG/SVG) and
1184    * writes it to the specified file
1185    * 
1186    * @param type
1187    * @param file
1188    */
1189   void makeAlignmentImage(ImageMaker.TYPE type, File file)
1190   {
1191     final int borderBottomOffset = 5;
1192
1193     AlignmentDimension aDimension = getAlignmentDimension();
1194     // todo use a lambda function in place of callback here?
1195     ImageWriterI writer = new ImageWriterI()
1196     {
1197       @Override
1198       public void exportImage(Graphics graphics) throws Exception
1199       {
1200         if (av.getWrapAlignment())
1201         {
1202           printWrappedAlignment(aDimension.getWidth(),
1203                   aDimension.getHeight() + borderBottomOffset, 0, graphics);
1204         }
1205         else
1206         {
1207           printUnwrapped(aDimension.getWidth(), aDimension.getHeight(), 0,
1208                   graphics, graphics);
1209         }
1210       }
1211     };
1212
1213     String fileTitle = alignFrame.getTitle();
1214     ImageExporter exporter = new ImageExporter(writer, alignFrame, type,
1215             fileTitle);
1216     int imageWidth = aDimension.getWidth();
1217     int imageHeight = aDimension.getHeight() + borderBottomOffset;
1218     String of = MessageManager.getString("label.alignment");
1219     exporter.doExport(file, this, imageWidth, imageHeight, of);
1220   }
1221
1222   /**
1223    * Calculates and returns a suitable width and height (in pixels) for an
1224    * exported image
1225    * 
1226    * @return
1227    */
1228   public AlignmentDimension getAlignmentDimension()
1229   {
1230     int maxwidth = av.getAlignment().getVisibleWidth();
1231
1232     int height = ((av.getAlignment().getHeight() + 1) * av.getCharHeight())
1233             + getScalePanel().getHeight();
1234     int width = getVisibleIdWidth(false) + (maxwidth * av.getCharWidth());
1235
1236     if (av.getWrapAlignment())
1237     {
1238       height = getWrappedHeight();
1239       if (Jalview.isHeadlessMode())
1240       {
1241         // need to obtain default alignment width and then add in any
1242         // additional allowance for id margin
1243         // this duplicates the calculation in getWrappedHeight but adjusts for
1244         // offscreen idWith
1245         width = alignFrame.getWidth() - vscroll.getPreferredSize().width
1246                 - alignFrame.getInsets().left - alignFrame.getInsets().right
1247                 - getVisibleIdWidth() + getVisibleIdWidth(false);
1248       }
1249       else
1250       {
1251         width = getSeqPanel().getWidth() + getVisibleIdWidth(false);
1252       }
1253
1254     }
1255     else if (av.isShowAnnotation())
1256     {
1257       height += getAnnotationPanel().adjustPanelHeight() + 3;
1258     }
1259     return new AlignmentDimension(width, height);
1260
1261   }
1262
1263   public void makePNGImageMap(File imgMapFile, String imageName)
1264   {
1265     // /////ONLY WORKS WITH NON WRAPPED ALIGNMENTS
1266     // ////////////////////////////////////////////
1267     int idWidth = getVisibleIdWidth(false);
1268     FontMetrics fm = getFontMetrics(av.getFont());
1269     int scaleHeight = av.getCharHeight() + fm.getDescent();
1270
1271     // Gen image map
1272     // ////////////////////////////////
1273     if (imgMapFile != null)
1274     {
1275       try
1276       {
1277         int sSize = av.getAlignment().getHeight();
1278         int alwidth = av.getAlignment().getWidth();
1279         PrintWriter out = new PrintWriter(new FileWriter(imgMapFile));
1280         out.println(HTMLOutput.getImageMapHTML());
1281         out.println("<img src=\"" + imageName
1282                 + "\" border=\"0\" usemap=\"#Map\" >"
1283                 + "<map name=\"Map\">");
1284
1285         for (int s = 0; s < sSize; s++)
1286         {
1287           int sy = s * av.getCharHeight() + scaleHeight;
1288
1289           SequenceI seq = av.getAlignment().getSequenceAt(s);
1290           SequenceGroup[] groups = av.getAlignment().findAllGroups(seq);
1291           for (int column = 0; column < alwidth; column++)
1292           {
1293             StringBuilder text = new StringBuilder(512);
1294             String triplet = null;
1295             if (av.getAlignment().isNucleotide())
1296             {
1297               triplet = ResidueProperties.nucleotideName.get(seq
1298                       .getCharAt(column) + "");
1299             }
1300             else
1301             {
1302               triplet = ResidueProperties.aa2Triplet.get(seq.getCharAt(column)
1303                       + "");
1304             }
1305
1306             if (triplet == null)
1307             {
1308               continue;
1309             }
1310
1311             int seqPos = seq.findPosition(column);
1312             int gSize = groups.length;
1313             for (int g = 0; g < gSize; g++)
1314             {
1315               if (text.length() < 1)
1316               {
1317                 text.append("<area shape=\"rect\" coords=\"")
1318                         .append((idWidth + column * av.getCharWidth()))
1319                         .append(",").append(sy).append(",")
1320                         .append((idWidth + (column + 1) * av.getCharWidth()))
1321                         .append(",").append((av.getCharHeight() + sy))
1322                         .append("\"").append(" onMouseOver=\"toolTip('")
1323                         .append(seqPos).append(" ").append(triplet);
1324               }
1325
1326               if (groups[g].getStartRes() < column
1327                       && groups[g].getEndRes() > column)
1328               {
1329                 text.append("<br><em>").append(groups[g].getName())
1330                         .append("</em>");
1331               }
1332             }
1333
1334             if (text.length() < 1)
1335             {
1336               text.append("<area shape=\"rect\" coords=\"")
1337                       .append((idWidth + column * av.getCharWidth()))
1338                       .append(",").append(sy).append(",")
1339                       .append((idWidth + (column + 1) * av.getCharWidth()))
1340                       .append(",").append((av.getCharHeight() + sy))
1341                       .append("\"").append(" onMouseOver=\"toolTip('")
1342                       .append(seqPos).append(" ").append(triplet);
1343             }
1344             if (!Comparison.isGap(seq.getCharAt(column)))
1345             {
1346               List<SequenceFeature> features = seq.findFeatures(column, column);
1347               for (SequenceFeature sf : features)
1348               {
1349                 if (sf.isContactFeature())
1350                 {
1351                   text.append("<br>").append(sf.getType()).append(" ")
1352                           .append(sf.getBegin()).append(":")
1353                           .append(sf.getEnd());
1354                 }
1355                 else
1356                 {
1357                   text.append("<br>");
1358                   text.append(sf.getType());
1359                   String description = sf.getDescription();
1360                   if (description != null
1361                           && !sf.getType().equals(description))
1362                   {
1363                     description = description.replace("\"", "&quot;");
1364                     text.append(" ").append(description);
1365                   }
1366                 }
1367                 String status = sf.getStatus();
1368                 if (status != null && !"".equals(status))
1369                 {
1370                   text.append(" (").append(status).append(")");
1371                 }
1372               }
1373               if (text.length() > 1)
1374               {
1375                 text.append("')\"; onMouseOut=\"toolTip()\";  href=\"#\">");
1376                 out.println(text.toString());
1377               }
1378             }
1379           }
1380         }
1381         out.println("</map></body></html>");
1382         out.close();
1383
1384       } catch (Exception ex)
1385       {
1386         ex.printStackTrace();
1387       }
1388     } // /////////END OF IMAGE MAP
1389
1390   }
1391
1392   int getWrappedHeight()
1393   {
1394     int seqPanelWidth = getSeqPanel().seqCanvas.getWidth();
1395
1396     if (Jalview.isHeadlessMode())
1397     {
1398       seqPanelWidth = alignFrame.getWidth() - getVisibleIdWidth()
1399               - vscroll.getPreferredSize().width
1400               - alignFrame.getInsets().left - alignFrame.getInsets().right;
1401     }
1402
1403     int chunkWidth = getSeqPanel().seqCanvas
1404             .getWrappedCanvasWidth(seqPanelWidth);
1405
1406     int hgap = av.getCharHeight();
1407     if (av.getScaleAboveWrapped())
1408     {
1409       hgap += av.getCharHeight();
1410     }
1411
1412     int annotationHeight = 0;
1413     if (av.isShowAnnotation())
1414     {
1415       annotationHeight = getAnnotationPanel().adjustPanelHeight();
1416     }
1417
1418     int cHeight = av.getAlignment().getHeight() * av.getCharHeight() + hgap
1419             + annotationHeight;
1420
1421     int maxwidth = av.getAlignment().getWidth();
1422     if (av.hasHiddenColumns())
1423     {
1424       maxwidth = av.getAlignment().getHiddenColumns()
1425               .absoluteToVisibleColumn(maxwidth) - 1;
1426     }
1427
1428     int height = ((maxwidth / chunkWidth) + 1) * cHeight;
1429
1430     return height;
1431   }
1432
1433   /**
1434    * close the panel - deregisters all listeners and nulls any references to
1435    * alignment data.
1436    */
1437   public void closePanel()
1438   {
1439     PaintRefresher.RemoveComponent(getSeqPanel().seqCanvas);
1440     PaintRefresher.RemoveComponent(getIdPanel().getIdCanvas());
1441     PaintRefresher.RemoveComponent(this);
1442
1443     closeChildFrames();
1444
1445     /*
1446      * try to ensure references are nulled
1447      */
1448     if (annotationPanel != null)
1449     {
1450       annotationPanel.dispose();
1451       annotationPanel = null;
1452     }
1453
1454     if (av != null)
1455     {
1456       av.removePropertyChangeListener(propertyChangeListener);
1457       propertyChangeListener = null;
1458       StructureSelectionManager ssm = av.getStructureSelectionManager();
1459       ssm.removeStructureViewerListener(getSeqPanel(), null);
1460       ssm.removeSelectionListener(getSeqPanel());
1461       ssm.removeCommandListener(av);
1462       ssm.removeStructureViewerListener(getSeqPanel(), null);
1463       ssm.removeSelectionListener(getSeqPanel());
1464       av.dispose();
1465       av = null;
1466     }
1467     else
1468     {
1469       if (Cache.log.isDebugEnabled())
1470       {
1471         Cache.log.warn("Closing alignment panel which is already closed.");
1472       }
1473     }
1474   }
1475
1476   /**
1477    * Close any open dialogs that would be orphaned when this one is closed
1478    */
1479   protected void closeChildFrames()
1480   {
1481     if (overviewPanel != null)
1482     {
1483       overviewPanel.dispose();
1484       overviewPanel = null;
1485     }
1486     if (calculationDialog != null)
1487     {
1488       calculationDialog.closeFrame();
1489       calculationDialog = null;
1490     }
1491   }
1492
1493   /**
1494    * hides or shows dynamic annotation rows based on groups and av state flags
1495    */
1496   public void updateAnnotation()
1497   {
1498     updateAnnotation(false, false);
1499   }
1500
1501   public void updateAnnotation(boolean applyGlobalSettings)
1502   {
1503     updateAnnotation(applyGlobalSettings, false);
1504   }
1505
1506   public void updateAnnotation(boolean applyGlobalSettings,
1507           boolean preserveNewGroupSettings)
1508   {
1509     av.updateGroupAnnotationSettings(applyGlobalSettings,
1510             preserveNewGroupSettings);
1511     adjustAnnotationHeight();
1512   }
1513
1514   @Override
1515   public AlignmentI getAlignment()
1516   {
1517     return av == null ? null : av.getAlignment();
1518   }
1519
1520   @Override
1521   public String getViewName()
1522   {
1523     return av.getViewName();
1524   }
1525
1526   /**
1527    * Make/Unmake this alignment panel the current input focus
1528    * 
1529    * @param b
1530    */
1531   public void setSelected(boolean b)
1532   {
1533     try
1534     {
1535       if (alignFrame.getSplitViewContainer() != null)
1536       {
1537         /*
1538          * bring enclosing SplitFrame to front first if there is one
1539          */
1540         ((SplitFrame) alignFrame.getSplitViewContainer()).setSelected(b);
1541       }
1542       alignFrame.setSelected(b);
1543     } catch (Exception ex)
1544     {
1545     }
1546
1547     if (b)
1548     {
1549       alignFrame.setDisplayedView(this);
1550     }
1551   }
1552
1553   @Override
1554   public StructureSelectionManager getStructureSelectionManager()
1555   {
1556     return av.getStructureSelectionManager();
1557   }
1558
1559   @Override
1560   public void raiseOOMWarning(String string, OutOfMemoryError error)
1561   {
1562     new OOMWarning(string, error, this);
1563   }
1564
1565   @Override
1566   public jalview.api.FeatureRenderer cloneFeatureRenderer()
1567   {
1568
1569     return new FeatureRenderer(this);
1570   }
1571
1572   @Override
1573   public jalview.api.FeatureRenderer getFeatureRenderer()
1574   {
1575     return seqPanel.seqCanvas.getFeatureRenderer();
1576   }
1577
1578   public void updateFeatureRenderer(
1579           jalview.renderer.seqfeatures.FeatureRenderer fr)
1580   {
1581     fr.transferSettings(getSeqPanel().seqCanvas.getFeatureRenderer());
1582   }
1583
1584   public void updateFeatureRendererFrom(jalview.api.FeatureRenderer fr)
1585   {
1586     if (getSeqPanel().seqCanvas.getFeatureRenderer() != null)
1587     {
1588       getSeqPanel().seqCanvas.getFeatureRenderer().transferSettings(fr);
1589     }
1590   }
1591
1592   public ScalePanel getScalePanel()
1593   {
1594     return scalePanel;
1595   }
1596
1597   public void setScalePanel(ScalePanel scalePanel)
1598   {
1599     this.scalePanel = scalePanel;
1600   }
1601
1602   public SeqPanel getSeqPanel()
1603   {
1604     return seqPanel;
1605   }
1606
1607   public void setSeqPanel(SeqPanel seqPanel)
1608   {
1609     this.seqPanel = seqPanel;
1610   }
1611
1612   public AnnotationPanel getAnnotationPanel()
1613   {
1614     return annotationPanel;
1615   }
1616
1617   public void setAnnotationPanel(AnnotationPanel annotationPanel)
1618   {
1619     this.annotationPanel = annotationPanel;
1620   }
1621
1622   public AnnotationLabels getAlabels()
1623   {
1624     return alabels;
1625   }
1626
1627   public void setAlabels(AnnotationLabels alabels)
1628   {
1629     this.alabels = alabels;
1630   }
1631
1632   public IdPanel getIdPanel()
1633   {
1634     return idPanel;
1635   }
1636
1637   public void setIdPanel(IdPanel idPanel)
1638   {
1639     this.idPanel = idPanel;
1640   }
1641
1642   /**
1643    * Follow a scrolling change in the (cDNA/Protein) complementary alignment.
1644    * The aim is to keep the two alignments 'lined up' on their centre columns.
1645    * 
1646    * @param sr
1647    *          holds mapped region(s) of this alignment that we are scrolling
1648    *          'to'; may be modified for sequence offset by this method
1649    * @param verticalOffset
1650    *          the number of visible sequences to show above the mapped region
1651    */
1652   protected void scrollToCentre(SearchResultsI sr, int verticalOffset)
1653   {
1654     scrollToPosition(sr, verticalOffset, true);
1655   }
1656
1657   /**
1658    * Set a flag to say do not scroll any (cDNA/protein) complement.
1659    * 
1660    * @param b
1661    */
1662   protected void setToScrollComplementPanel(boolean b)
1663   {
1664     this.scrollComplementaryPanel = b;
1665   }
1666
1667   /**
1668    * Get whether to scroll complement panel
1669    * 
1670    * @return true if cDNA/protein complement panels should be scrolled
1671    */
1672   protected boolean isSetToScrollComplementPanel()
1673   {
1674     return this.scrollComplementaryPanel;
1675   }
1676
1677   /**
1678    * Redraw sensibly.
1679    * 
1680    * @adjustHeight if true, try to recalculate panel height for visible
1681    *               annotations
1682    */
1683   protected void refresh(boolean adjustHeight)
1684   {
1685     validateAnnotationDimensions(adjustHeight);
1686     addNotify();
1687     if (adjustHeight)
1688     {
1689       // sort, repaint, update overview
1690       paintAlignment(true, false);
1691     }
1692     else
1693     {
1694       // lightweight repaint
1695       repaint();
1696     }
1697   }
1698
1699   @Override
1700   /**
1701    * Property change event fired when a change is made to the viewport ranges
1702    * object associated with this alignment panel's viewport
1703    */
1704   public void propertyChange(PropertyChangeEvent evt)
1705   {
1706     // update this panel's scroll values based on the new viewport ranges values
1707     ViewportRanges ranges = av.getRanges();
1708     int x = ranges.getStartRes();
1709     int y = ranges.getStartSeq();
1710     setScrollValues(x, y);
1711
1712     // now update any complementary alignment (its viewport ranges object
1713     // is different so does not get automatically updated)
1714     if (isSetToScrollComplementPanel())
1715     {
1716       setToScrollComplementPanel(false);
1717       av.scrollComplementaryAlignment();
1718       setToScrollComplementPanel(true);
1719     }
1720   }
1721
1722   /**
1723    * Set the reference to the PCA/Tree chooser dialog for this panel. This
1724    * reference should be nulled when the dialog is closed.
1725    * 
1726    * @param calculationChooser
1727    */
1728   public void setCalculationDialog(CalculationChooser calculationChooser)
1729   {
1730     calculationDialog = calculationChooser;
1731   }
1732
1733   /**
1734    * Returns the reference to the PCA/Tree chooser dialog for this panel (null
1735    * if none is open)
1736    */
1737   public CalculationChooser getCalculationDialog()
1738   {
1739     return calculationDialog;
1740   }
1741
1742   @Override
1743   public SequenceRenderer getSequenceRenderer()
1744   {
1745     return seqPanel.seqCanvas.getSequenceRenderer();
1746   }
1747
1748   public boolean scrollTo(int ostart, int end, int seqIndex,
1749           boolean scrollToNearest, boolean redrawOverview)
1750   {
1751     int startv, endv, starts, ends;// , width;
1752
1753     int start = -1;
1754     if (av.hasHiddenColumns())
1755     {
1756       AlignmentI al = av.getAlignment();
1757       start = al.getHiddenColumns().absoluteToVisibleColumn(ostart);
1758       end = al.getHiddenColumns().absoluteToVisibleColumn(end);
1759       if (start == end)
1760       {
1761         if (!scrollToNearest && !al.getHiddenColumns().isVisible(ostart))
1762         {
1763           // don't scroll - position isn't visible
1764           return false;
1765         }
1766       }
1767     }
1768     else
1769     {
1770       start = ostart;
1771     }
1772
1773     ViewportRanges ranges = av.getRanges();
1774     if (!av.getWrapAlignment())
1775     {
1776       /*
1777        * int spos=av.getStartRes(),sqpos=av.getStartSeq(); if ((startv =
1778        * av.getStartRes()) >= start) { spos=start-1; // seqIn //
1779        * setScrollValues(start - 1, seqIndex); } else if ((endv =
1780        * av.getEndRes()) <= end) { // setScrollValues(spos=startv + 1 + end -
1781        * endv, seqIndex); spos=startv + 1 + end - endv; } else if ((starts =
1782        * av.getStartSeq()) > seqIndex) { setScrollValues(av.getStartRes(),
1783        * seqIndex); } else if ((ends = av.getEndSeq()) <= seqIndex) {
1784        * setScrollValues(av.getStartRes(), starts + seqIndex - ends + 1); }
1785        */
1786
1787       // below is scrolling logic up to Jalview 2.8.2
1788       // if ((av.getStartRes() > end)
1789       // || (av.getEndRes() < start)
1790       // || ((av.getStartSeq() > seqIndex) || (av.getEndSeq() < seqIndex)))
1791       // {
1792       // if (start > av.getAlignment().getWidth() - hextent)
1793       // {
1794       // start = av.getAlignment().getWidth() - hextent;
1795       // if (start < 0)
1796       // {
1797       // start = 0;
1798       // }
1799       //
1800       // }
1801       // if (seqIndex > av.getAlignment().getHeight() - vextent)
1802       // {
1803       // seqIndex = av.getAlignment().getHeight() - vextent;
1804       // if (seqIndex < 0)
1805       // {
1806       // seqIndex = 0;
1807       // }
1808       // }
1809       // setScrollValues(start, seqIndex);
1810       // }
1811       // logic copied from jalview.gui.AlignmentPanel:
1812       if ((startv = ranges.getStartRes()) >= start)
1813       {
1814         /*
1815          * Scroll left to make start of search results visible
1816          */
1817         setScrollValues(start - 1, seqIndex);
1818       }
1819       else if ((endv = ranges.getEndRes()) <= end)
1820       {
1821         /*
1822          * Scroll right to make end of search results visible
1823          */
1824         setScrollValues(startv + 1 + end - endv, seqIndex);
1825       }
1826       else if ((starts = ranges.getStartSeq()) > seqIndex)
1827       {
1828         /*
1829          * Scroll up to make start of search results visible
1830          */
1831         setScrollValues(ranges.getStartRes(), seqIndex);
1832       }
1833       else if ((ends = ranges.getEndSeq()) <= seqIndex)
1834       {
1835         /*
1836          * Scroll down to make end of search results visible
1837          */
1838         setScrollValues(ranges.getStartRes(), starts + seqIndex - ends + 1);
1839       }
1840       /*
1841        * Else results are already visible - no need to scroll
1842        */
1843     }
1844     else
1845     {
1846       ranges.scrollToWrappedVisible(start);
1847     }
1848
1849     paintAlignment(redrawOverview, false);
1850     return true;
1851   }
1852
1853   @Override
1854   public void overviewDone(BufferedImage miniMe)
1855   {
1856     overviewPanel.canvas.finalizeDraw(miniMe);
1857   }
1858
1859
1860   private boolean holdRepaint = false;
1861
1862   public boolean getHoldRepaint()
1863   {
1864     return holdRepaint;
1865   }
1866
1867   public void setHoldRepaint(boolean b)
1868   {
1869     if (holdRepaint == b)
1870     {
1871       return;
1872     }
1873     holdRepaint = b;
1874     if (!b)
1875     {
1876       repaint();
1877     }
1878   }
1879
1880   @Override
1881   public void repaint()
1882   {
1883     if (holdRepaint)
1884     {
1885       // System.out.println("AP repaint holding");
1886       // Platform.stackTrace();
1887       return;
1888     }
1889     super.repaint();
1890   }
1891
1892 }