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