JAL-2349 store/restore mappable contact matrix in project and fix up interactive...
[jalview.git] / src / jalview / gui / AnnotationPanel.java
index 7b0fa67..2177a26 100755 (executable)
-package jalview.gui;\r
-\r
-import jalview.datamodel.*;\r
-\r
-import javax.swing.*;\r
-import java.awt.*;\r
-import java.awt.event.*;\r
-import java.util.*;\r
-import java.awt.image.*;\r
-\r
-public class AnnotationPanel extends JPanel implements MouseListener, MouseMotionListener, ActionListener, AdjustmentListener\r
-{\r
-  AlignViewport av;\r
-  AlignmentPanel ap;\r
-  int activeRow =-1;\r
-\r
-  ArrayList activeRes;\r
-  static String HELIX ="Helix";\r
-  static String SHEET ="Sheet";\r
-  static String LABEL ="Label";\r
-  static String REMOVE="Remove Annotation";\r
-  static String COLOUR="Colour";\r
-  static Color HELIX_COLOUR = Color.red.darker();\r
-  static Color SHEET_COLOUR = Color.green.darker().darker();\r
-\r
-\r
-  BufferedImage image;\r
-  Graphics2D gg;\r
-  FontMetrics fm;\r
-  int imgWidth=0;\r
-\r
-  boolean fastPaint = false;\r
-\r
-  public static int GRAPH_HEIGHT = 40;\r
-\r
-\r
-\r
-  public AnnotationPanel(AlignmentPanel ap)\r
-  {\r
-    this.ap = ap;\r
-    av = ap.av;\r
-    this.setLayout(null);\r
-    addMouseListener(this);\r
-    addMouseMotionListener(this);\r
-    adjustPanelHeight();\r
-\r
-\r
-    ap.annotationScroller.getVerticalScrollBar().addAdjustmentListener( this );\r
-  }\r
-\r
-  public void adjustmentValueChanged(AdjustmentEvent evt)\r
-  {\r
-    ap.alabels.setScrollOffset( -evt.getValue() );\r
-  }\r
-\r
-  public void adjustPanelHeight()\r
-  {\r
-    // setHeight of panels\r
-    AlignmentAnnotation[] aa = av.alignment.getAlignmentAnnotation();\r
-    int height = 0;\r
-    if(aa!=null)\r
-    for (int i = 0; i < aa.length; i++)\r
-    {\r
-      if(!aa[i].visible)\r
-        continue;\r
-\r
-      aa[i].height = 0;\r
-\r
-      if(aa[i].hasText)\r
-        aa[i].height += av.charHeight;\r
-      if (aa[i].hasIcons)\r
-        aa[i].height += 16;\r
-\r
-      if (aa[i].isGraph)\r
-        aa[i].height += GRAPH_HEIGHT;\r
-\r
-      if(aa[i].height==0)\r
-        aa[i].height = 20;\r
-      height += aa[i].height;\r
-    }\r
-  else height=20;\r
-    this.setPreferredSize(new Dimension(1, height));\r
-\r
-  }\r
-\r
-  public void addEditableColumn(int i)\r
-  {\r
-    if(activeRow==-1)\r
-    {\r
-      AlignmentAnnotation [] aa = av.alignment.getAlignmentAnnotation();\r
-      for(int j=0; j<aa.length; j++)\r
-        if(aa[j].editable)\r
-        {\r
-          activeRow = j;\r
-          break;\r
-        }\r
-    }\r
-\r
-    if(activeRes==null)\r
-    {\r
-      activeRes = new ArrayList();\r
-      activeRes.add(String.valueOf(i));\r
-      return;\r
-    }\r
-\r
-    activeRes.add(String.valueOf(i));\r
-\r
-  }\r
-\r
-\r
-  public void actionPerformed(ActionEvent evt)\r
-  {\r
-\r
-    AlignmentAnnotation [] aa = av.alignment.getAlignmentAnnotation();\r
-    Annotation [] anot = aa[activeRow].annotations;\r
-\r
-    if(evt.getActionCommand().equals(REMOVE))\r
-    {\r
-      for(int i=0; i<activeRes.size(); i++)\r
-      {\r
-        anot[Integer.parseInt(activeRes.get(i).toString())] = null;\r
-        anot[Integer.parseInt(activeRes.get(i).toString())] = null;\r
-      }\r
-    }\r
-    else if(evt.getActionCommand().equals(LABEL))\r
-    {\r
-      String label = JOptionPane.showInputDialog(this, "Enter Label ", "Enter label", JOptionPane.QUESTION_MESSAGE );\r
-      if(label==null)\r
-        label = "";\r
-\r
-      if(label.length()>0 && !aa[activeRow].hasText)\r
-       aa[activeRow].hasText = true;\r
-\r
-      for(int i=0; i<activeRes.size(); i++)\r
-      {\r
-        int index = Integer.parseInt(activeRes.get(i).toString());\r
-        if(anot[index]==null)\r
-          anot[index] = new Annotation(label, "", ' ',0);\r
-        anot[index].displayCharacter = label;\r
-      }\r
-    }\r
-    else if(evt.getActionCommand().equals(COLOUR))\r
-    {\r
-     Color col = JColorChooser.showDialog(this, "Choose foreground colour", Color.black);\r
-     for (int i = 0; i < activeRes.size(); i++)\r
-     {\r
-       int index = Integer.parseInt(activeRes.get(i).toString());\r
-       if (anot[index] == null)\r
-         anot[index] = new Annotation("", "", ' ', 0);\r
-       anot[index].colour = col;\r
-     }\r
-    }\r
-    else // HELIX OR SHEET\r
-    {\r
-    char type = 0;\r
-    String symbol = "\u03B1";\r
-    if(evt.getActionCommand().equals(HELIX))\r
-       type = 'H';\r
-    else if(evt.getActionCommand().equals(SHEET))\r
-    {\r
-      type = 'E';\r
-      symbol = "\u03B2";\r
-    }\r
-\r
-    if(!aa[activeRow].hasIcons)\r
-      aa[activeRow].hasIcons = true;\r
-\r
-\r
-    String label = JOptionPane.showInputDialog("Enter a label for the structure?", symbol );\r
-    if(label==null)\r
-       label="";\r
-\r
-    if(label.length()>0 && !aa[activeRow].hasText)\r
-       aa[activeRow].hasText = true;\r
-\r
-    for(int i=0; i<activeRes.size(); i++)\r
-    {\r
-      int index = Integer.parseInt(activeRes.get(i).toString());\r
-      if (anot[index] == null)\r
-      {\r
-        anot[index] = new Annotation(label, "", type, 0);\r
-      }\r
-\r
-        anot[ index ].secondaryStructure = type;\r
-        anot[ index ].displayCharacter = label;\r
-    }\r
-    }\r
-\r
-    adjustPanelHeight();\r
-    activeRes = null;\r
-    repaint();\r
-    return;\r
-\r
-\r
-  }\r
-\r
-  public void mousePressed(MouseEvent evt)\r
-  {\r
-    if (SwingUtilities.isRightMouseButton(evt))\r
-    {\r
-      if(activeRes==null)\r
-        return;\r
-\r
-      JPopupMenu pop = new JPopupMenu("Structure type");\r
-      JMenuItem item = new JMenuItem(HELIX);\r
-      item.addActionListener(this);\r
-      pop.add(item);\r
-      item = new JMenuItem(SHEET);\r
-      item.addActionListener(this);\r
-      pop.add(item);\r
-      item = new JMenuItem(LABEL);\r
-      item.addActionListener(this);\r
-      pop.add(item);\r
-      item = new JMenuItem(COLOUR);\r
-      item.addActionListener(this);\r
-      pop.add(item);\r
-      item = new JMenuItem(REMOVE);\r
-      item.addActionListener(this);\r
-      pop.add(item);\r
-      pop.show(this, evt.getX(), evt.getY());\r
-      return;\r
-    }\r
-\r
-\r
-    AlignmentAnnotation [] aa = av.alignment.getAlignmentAnnotation();\r
-    if(aa==null)\r
-      return;\r
-\r
-    int height = 0;\r
-    activeRow = -1;\r
-    for(int i=0; i<aa.length; i++)\r
-    {\r
-      height+= aa[i].height;\r
-\r
-      if(evt.getY()<height)\r
-      {\r
-        if(!aa[i].editable)\r
-        {\r
-          activeRes = null;\r
-          continue;\r
-        }\r
-\r
-        activeRow = i;\r
-        break;\r
-      }\r
-    }\r
-\r
-\r
-    int res = evt.getX() / av.getCharWidth() + av.getStartRes();\r
-\r
-    if(evt.isControlDown() || evt.isAltDown())\r
-      addEditableColumn(res);\r
-\r
-    else if(evt.isShiftDown())\r
-    {\r
-\r
-      if(activeRes==null)\r
-        activeRes=new ArrayList();\r
-      else\r
-      {\r
-          int start =  Integer.parseInt( activeRes.get( activeRes.size()-1 ).toString() );\r
-          int end = res;\r
-          if(end<start)\r
-          {\r
-            int temp = end;\r
-            end = start;\r
-            start = temp;\r
-          }\r
-          for(int n=start; n<=end; n++)\r
-            addEditableColumn(n);\r
-\r
-      }\r
-    }\r
-    else\r
-    {\r
-        activeRes = new ArrayList();\r
-        activeRes.add( String.valueOf(res) );\r
-    }\r
-\r
-    repaint();\r
-\r
-  }\r
-  public void mouseReleased(MouseEvent evt)\r
-  {  }\r
-  public void mouseEntered(MouseEvent evt)\r
-  {  }\r
-  public void mouseExited(MouseEvent evt)\r
-  {  }\r
-  public void mouseDragged(MouseEvent evt)\r
-  {  }\r
-  public void mouseMoved(MouseEvent evt)\r
-  {\r
-    ToolTipManager.sharedInstance().registerComponent(this);\r
-    AlignmentAnnotation [] aa = av.alignment.getAlignmentAnnotation();\r
-    if(aa==null)\r
-      return;\r
-\r
-    int row = -1;\r
-    int height=0;\r
-    for(int i=0; i<aa.length; i++)\r
-    {\r
-\r
-      if( aa[i].visible )\r
-        height += aa[i].height;\r
-\r
-      if(evt.getY()<height)\r
-      {\r
-        row = i;\r
-        break;\r
-      }\r
-    }\r
-\r
-    int res = evt.getX() / av.getCharWidth() + av.getStartRes();\r
-    if(row>-1 && res<aa[row].annotations.length && aa[row].annotations[res]!=null)\r
-    {\r
-      this.setToolTipText(aa[row].annotations[res].description);\r
-      StringBuffer text = new StringBuffer("Sequence position " + (res + 1) +\r
-                                           "  " +\r
-                                           aa[row].annotations[res].description);\r
-      ap.alignFrame.statusBar.setText(text.toString());\r
-    }\r
-\r
-\r
-  }\r
-  public void mouseClicked(MouseEvent evt) {}\r
-\r
-\r
-  public void paintComponent(Graphics g)\r
-  {\r
-    g.setColor(Color.white);\r
-    g.fillRect(0,0,getWidth(), getHeight());\r
-\r
-    if(fastPaint)\r
-    {\r
-      g.drawImage(image, 0, 0, this);\r
-      fastPaint = false;\r
-      return;\r
-    }\r
-\r
-    imgWidth = (av.endRes-av.startRes+1) *av.charWidth;\r
-\r
-    image = new BufferedImage(imgWidth,\r
-                                ap.annotationPanel.getHeight(),\r
-                                BufferedImage.TYPE_INT_RGB);\r
-      gg = (Graphics2D) image.getGraphics();\r
-      gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,\r
-                          RenderingHints.VALUE_ANTIALIAS_ON);\r
-      fm = gg.getFontMetrics();\r
-      gg.setFont(av.getFont());\r
-\r
-    drawComponent( gg, av.startRes, av.endRes+1);\r
-    g.drawImage( image, 0, 0, this);\r
-\r
-  }\r
-\r
-  public void fastPaint(int horizontal)\r
-{\r
-  if( horizontal == 0\r
-     || av.alignment.getAlignmentAnnotation()==null\r
-     || av.alignment.getAlignmentAnnotation().length<1\r
-    )\r
-  {\r
-    repaint();\r
-    return;\r
-  }\r
-\r
-  gg.copyArea( 0,0, imgWidth, getHeight(), -horizontal*av.charWidth, 0 );\r
-  int sr=av.startRes, er=av.endRes+1, transX=0;\r
-\r
-  if(horizontal>0) // scrollbar pulled right, image to the left\r
-  {\r
-    transX =  (er-sr-horizontal)*av.charWidth;\r
-    sr = er - horizontal ;\r
-  }\r
-  else if(horizontal<0)\r
-  {\r
-    er = sr-horizontal;\r
-  }\r
-\r
-\r
-  gg.translate(transX, 0);\r
-\r
-  drawComponent(gg, sr, er);\r
-\r
-  gg.translate( -transX, 0 );\r
-\r
-  fastPaint = true;\r
-  repaint();\r
-}\r
-\r
-\r
-  public void drawComponent(Graphics2D g, int startRes, int endRes)\r
-  {\r
-    g.setColor(Color.white);\r
-    g.fillRect(0,0,(endRes-startRes) *av.charWidth, getHeight());\r
-    if(av.alignment.getAlignmentAnnotation()==null || av.alignment.getAlignmentAnnotation().length<1)\r
-    {\r
-      g.setColor(Color.white);\r
-      g.fillRect(0,0,getWidth(), getHeight());\r
-      g.setColor(Color.black);\r
-      g.drawString("Alignment has no annotations",20,15);\r
-      return;\r
-    }\r
-\r
-    AlignmentAnnotation [] aa = av.alignment.getAlignmentAnnotation();\r
-\r
-    int j, x=0, y=0;\r
-    char [] lastSS = new char[aa.length];\r
-    int  [] lastSSX= new int[aa.length] ;\r
-    int iconOffset = av.charHeight/2;\r
-    boolean validRes = false;\r
-    //\u03B2 \u03B1\r
-\r
-    for(int i=0; i<aa.length; i++)\r
-    {\r
-      AlignmentAnnotation row = aa[i];\r
-      if(!row.visible)\r
-        continue;\r
-\r
-      if(row.isGraph)\r
-      {\r
-        // this is so that we draw the characters below the graph\r
-        y += row.height;\r
-        if(row.hasText)\r
-          y -= av.charHeight;\r
-      }\r
-      if(row.hasText)\r
-        iconOffset = av.charHeight/2;\r
-      else\r
-        iconOffset = 0;\r
-\r
-      for(j=startRes; j<endRes; j++)\r
-      {\r
-        validRes = row.annotations[j]==null?false:true;\r
-\r
-       x = (j-startRes)*av.charWidth;\r
-\r
-\r
-       if(activeRow==i)\r
-       {\r
-\r
-         g.setColor(Color.red);\r
-\r
-         if(activeRes!=null)\r
-           for (int n = 0; n < activeRes.size(); n++)\r
-           {\r
-             int v = Integer.parseInt(activeRes.get(n).toString()) ;\r
-             if (v == j)\r
-               g.fillRect( (j-startRes) * av.charWidth, y, av.charWidth, row.height);\r
-           }\r
-       }\r
-\r
-\r
-\r
-       if(validRes && row.annotations[j].displayCharacter.length()>0)\r
-       {\r
-         int charOffset = (av.charWidth -\r
-                            fm.charWidth(row.annotations[j].displayCharacter.\r
-                                         charAt(0))) / 2;\r
-         g.setColor( row.annotations[j].colour);\r
-          if(j==0)\r
-          {\r
-            if (row.annotations[0].secondaryStructure == 'H'\r
-                || row.annotations[0].secondaryStructure == 'E')\r
-              g.drawString(row.annotations[j].displayCharacter, x,\r
-                           y + iconOffset + 2);\r
-          }\r
-         else if( (row.annotations[j].secondaryStructure=='H'\r
-               || row.annotations[j].secondaryStructure=='E') &&\r
-               (row.annotations[j-1]==null ||\r
-                row.annotations[j].secondaryStructure!=row.annotations[j-1].secondaryStructure))\r
-\r
-        g.drawString(row.annotations[j].displayCharacter, x, y + iconOffset + 2);\r
-\r
-        if(!row.hasIcons)\r
-            g.drawString(row.annotations[j].displayCharacter, x + charOffset,\r
-                         y + iconOffset + 2);\r
-       }\r
-\r
-       if(row.hasIcons)\r
-       if(!validRes || row.annotations[j].secondaryStructure!=lastSS[i])\r
-       {\r
-         switch (lastSS[i])\r
-         {\r
-           case 'H':\r
-             g.setColor(HELIX_COLOUR);\r
-             g.fillRoundRect(lastSSX[i], y+4 + iconOffset, x-lastSSX[i], 7, 8, 8);\r
-             break;\r
-           case 'E':\r
-             g.setColor(SHEET_COLOUR);\r
-             g.fillRect(lastSSX[i], y + 4 + iconOffset, x-lastSSX[i]-4, 7);\r
-             g.fillPolygon(new int[] {x - 4, x- 4, x }\r
-                           , new int[]{y+ iconOffset, y + 14+ iconOffset, y + 8+ iconOffset}, 3);\r
-             break;\r
-           case 'C':\r
-             break;\r
-           default :\r
-             g.setColor(Color.gray);\r
-             g.fillRect(lastSSX[i], y+6+ iconOffset, x-lastSSX[i], 2);\r
-             break;\r
-         }\r
-\r
-         if(validRes)\r
-           lastSS[i] = row.annotations[j].secondaryStructure;\r
-          else\r
-            lastSS[i] = ' ';\r
-         lastSSX[i] = x;\r
-       }\r
-\r
-       if (validRes && row.isGraph)\r
-       {\r
-         g.setColor(new Color(0,0,180));\r
-         int height = (int)((row.annotations[j].value / row.graphMax)*GRAPH_HEIGHT);\r
-\r
-         if(row.windowLength>1)\r
-         {\r
-           int total =0;\r
-           for(int i2=j- (row.windowLength/2); i2<j+(row.windowLength/2); i2++)\r
-           {\r
-             if(i2<0 || i2>=av.alignment.getWidth())\r
-               continue;\r
-\r
-             total += row.annotations[i2].value;\r
-           }\r
-\r
-           total/=row.windowLength;\r
-           height = (int)( (total / row.graphMax) *GRAPH_HEIGHT);\r
-\r
-         }\r
-         g.setColor(row.annotations[j].colour);\r
-         g.fillRect(x, y-height, av.charWidth, height );\r
-       }\r
-\r
-\r
-      }\r
-\r
-      x+=av.charWidth;\r
-\r
-      if(row.hasIcons)\r
-      switch (lastSS[i])\r
-      {\r
-        case 'H':\r
-          g.setColor(HELIX_COLOUR);\r
-          g.fillRoundRect(lastSSX[i], y+4+ iconOffset, x - lastSSX[i], 7, 8, 8);\r
-          break;\r
-        case 'E':\r
-          g.setColor(SHEET_COLOUR);\r
-          g.fillRect(lastSSX[i], y + 4+ iconOffset, x - lastSSX[i] - 4, 7);\r
-          g.fillPolygon(new int[]\r
-                        {x - 4, x - 4, x}\r
-                        , new int[]\r
-                        {y + iconOffset, y + 14+ iconOffset, y + 7+ iconOffset}\r
-                        , 3);\r
-          break;\r
-        case 'C':\r
-          break;\r
-        default:\r
-          g.setColor(Color.gray);\r
-          g.fillRect(lastSSX[i], y+6+ iconOffset, x-lastSSX[i], 2);\r
-          break;\r
-\r
-      }\r
-\r
-       if(row.isGraph && row.hasText)\r
-         y+=av.charHeight;\r
-       if(!row.isGraph)\r
-          y+=aa[i].height;\r
-    }\r
-  }\r
-\r
-  // used by overview window\r
-  public void drawGraph(Graphics g, AlignmentAnnotation aa,int width, int y)\r
-  {\r
-    g.setColor(Color.white);\r
-    g.fillRect(0,0,width, y);\r
-    g.setColor(new Color(0,0,180));\r
-    int x = 0;\r
-    for(int j=0; j<aa.annotations.length; j++)\r
-    {\r
-      g.setColor(new Color(0, 0, 180));\r
-      int height = (int) ( (aa.annotations[j].value / aa.graphMax) * GRAPH_HEIGHT );\r
-      g.fillRect(x, y - height, av.charWidth, height);\r
-      x+=av.charWidth;\r
-    }\r
-  }\r
-}\r
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.gui;
+
+import java.awt.AlphaComposite;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.AdjustmentEvent;
+import java.awt.event.AdjustmentListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.awt.event.MouseWheelEvent;
+import java.awt.event.MouseWheelListener;
+import java.awt.image.BufferedImage;
+import java.beans.PropertyChangeEvent;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+
+import javax.swing.JMenuItem;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.Scrollable;
+import javax.swing.ToolTipManager;
+
+import jalview.api.AlignViewportI;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Annotation;
+import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.ContactListI;
+import jalview.datamodel.ContactMatrixI;
+import jalview.datamodel.ContactRange;
+import jalview.datamodel.GraphLine;
+import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.SequenceI;
+import jalview.gui.JalviewColourChooser.ColourChooserListener;
+import jalview.renderer.AnnotationRenderer;
+import jalview.renderer.AwtRenderPanelI;
+import jalview.renderer.ContactGeometry;
+import jalview.schemes.ResidueProperties;
+import jalview.util.Comparison;
+import jalview.util.MessageManager;
+import jalview.util.Platform;
+import jalview.viewmodel.ViewportListenerI;
+import jalview.viewmodel.ViewportRanges;
+import jalview.ws.datamodel.alphafold.PAEContactMatrix;
+
+/**
+ * AnnotationPanel displays visible portion of annotation rows below unwrapped
+ * alignment
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
+        MouseListener, MouseWheelListener, MouseMotionListener,
+        ActionListener, AdjustmentListener, Scrollable, ViewportListenerI
+{
+  enum DragMode
+  {
+    Select, Resize, Undefined, MatrixSelect
+  };
+
+  String HELIX = MessageManager.getString("label.helix");
+
+  String SHEET = MessageManager.getString("label.sheet");
+
+  /**
+   * For RNA secondary structure "stems" aka helices
+   */
+  String STEM = MessageManager.getString("label.rna_helix");
+
+  String LABEL = MessageManager.getString("label.label");
+
+  String REMOVE = MessageManager.getString("label.remove_annotation");
+
+  String COLOUR = MessageManager.getString("action.colour");
+
+  public final Color HELIX_COLOUR = Color.red.darker();
+
+  public final Color SHEET_COLOUR = Color.green.darker().darker();
+
+  public final Color STEM_COLOUR = Color.blue.darker();
+
+  /** DOCUMENT ME!! */
+  public AlignViewport av;
+
+  AlignmentPanel ap;
+
+  public int activeRow = -1;
+
+  public BufferedImage image;
+
+  public volatile BufferedImage fadedImage;
+
+  // private Graphics2D gg;
+
+  public FontMetrics fm;
+
+  public int imgWidth = 0;
+
+  boolean fastPaint = false;
+
+  // Used For mouse Dragging and resizing graphs
+  int graphStretch = -1;
+
+  int mouseDragLastX = -1;
+
+  int mouseDragLastY = -1;
+
+  int firstDragX = -1;
+
+  int firstDragY = -1;
+
+  DragMode dragMode = DragMode.Undefined;
+
+  boolean mouseDragging = false;
+
+  // for editing cursor
+  int cursorX = 0;
+
+  int cursorY = 0;
+
+  public final AnnotationRenderer renderer;
+
+  private MouseWheelListener[] _mwl;
+
+  private boolean notJustOne;
+
+  /**
+   * Creates a new AnnotationPanel object.
+   * 
+   * @param ap
+   *          DOCUMENT ME!
+   */
+  public AnnotationPanel(AlignmentPanel ap)
+  {
+    ToolTipManager.sharedInstance().registerComponent(this);
+    ToolTipManager.sharedInstance().setInitialDelay(0);
+    ToolTipManager.sharedInstance().setDismissDelay(10000);
+    this.ap = ap;
+    av = ap.av;
+    this.setLayout(null);
+    addMouseListener(this);
+    addMouseMotionListener(this);
+    ap.annotationScroller.getVerticalScrollBar()
+            .addAdjustmentListener(this);
+    // save any wheel listeners on the scroller, so we can propagate scroll
+    // events to them.
+    _mwl = ap.annotationScroller.getMouseWheelListeners();
+    // and then set our own listener to consume all mousewheel events
+    ap.annotationScroller.addMouseWheelListener(this);
+    renderer = new AnnotationRenderer();
+
+    av.getRanges().addPropertyChangeListener(this);
+  }
+
+  public AnnotationPanel(AlignViewport av)
+  {
+    this.av = av;
+    renderer = new AnnotationRenderer();
+  }
+
+  @Override
+  public void mouseWheelMoved(MouseWheelEvent e)
+  {
+    if (e.isShiftDown())
+    {
+      e.consume();
+      double wheelRotation = e.getPreciseWheelRotation();
+      if (wheelRotation > 0)
+      {
+        av.getRanges().scrollRight(true);
+      }
+      else if (wheelRotation < 0)
+      {
+        av.getRanges().scrollRight(false);
+      }
+    }
+    else
+    {
+      // TODO: find the correct way to let the event bubble up to
+      // ap.annotationScroller
+      for (MouseWheelListener mwl : _mwl)
+      {
+        if (mwl != null)
+        {
+          mwl.mouseWheelMoved(e);
+        }
+        if (e.isConsumed())
+        {
+          break;
+        }
+      }
+    }
+  }
+
+  @Override
+  public Dimension getPreferredScrollableViewportSize()
+  {
+    Dimension ps = getPreferredSize();
+    return new Dimension(ps.width, adjustForAlignFrame(false, ps.height));
+  }
+
+  @Override
+  public int getScrollableBlockIncrement(Rectangle visibleRect,
+          int orientation, int direction)
+  {
+    return 30;
+  }
+
+  @Override
+  public boolean getScrollableTracksViewportHeight()
+  {
+    return false;
+  }
+
+  @Override
+  public boolean getScrollableTracksViewportWidth()
+  {
+    return true;
+  }
+
+  @Override
+  public int getScrollableUnitIncrement(Rectangle visibleRect,
+          int orientation, int direction)
+  {
+    return 30;
+  }
+
+  /*
+   * (non-Javadoc)
+   * 
+   * @see
+   * java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event
+   * .AdjustmentEvent)
+   */
+  @Override
+  public void adjustmentValueChanged(AdjustmentEvent evt)
+  {
+    // update annotation label display
+    ap.getAlabels().setScrollOffset(-evt.getValue());
+  }
+
+  /**
+   * Calculates the height of the annotation displayed in the annotation panel.
+   * Callers should normally call the ap.adjustAnnotationHeight method to ensure
+   * all annotation associated components are updated correctly.
+   * 
+   */
+  public int adjustPanelHeight()
+  {
+    int height = av.calcPanelHeight();
+    this.setPreferredSize(new Dimension(1, height));
+    if (ap != null)
+    {
+      // revalidate only when the alignment panel is fully constructed
+      ap.validate();
+    }
+
+    return height;
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param evt
+   *          DOCUMENT ME!
+   */
+  @Override
+  public void actionPerformed(ActionEvent evt)
+  {
+    AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
+    if (aa == null)
+    {
+      return;
+    }
+    Annotation[] anot = aa[activeRow].annotations;
+
+    if (anot.length < av.getColumnSelection().getMax())
+    {
+      Annotation[] temp = new Annotation[av.getColumnSelection().getMax()
+              + 2];
+      System.arraycopy(anot, 0, temp, 0, anot.length);
+      anot = temp;
+      aa[activeRow].annotations = anot;
+    }
+
+    String action = evt.getActionCommand();
+    if (action.equals(REMOVE))
+    {
+      for (int index : av.getColumnSelection().getSelected())
+      {
+        if (av.getAlignment().getHiddenColumns().isVisible(index))
+        {
+          anot[index] = null;
+        }
+      }
+    }
+    else if (action.equals(LABEL))
+    {
+      String exMesg = collectAnnotVals(anot, LABEL);
+      String label = JvOptionPane.showInputDialog(
+              MessageManager.getString("label.enter_label"), exMesg);
+
+      if (label == null)
+      {
+        return;
+      }
+
+      if ((label.length() > 0) && !aa[activeRow].hasText)
+      {
+        aa[activeRow].hasText = true;
+      }
+
+      for (int index : av.getColumnSelection().getSelected())
+      {
+        if (!av.getAlignment().getHiddenColumns().isVisible(index))
+        {
+          continue;
+        }
+
+        if (anot[index] == null)
+        {
+          anot[index] = new Annotation(label, "", ' ', 0);
+        }
+        else
+        {
+          anot[index].displayCharacter = label;
+        }
+      }
+    }
+    else if (action.equals(COLOUR))
+    {
+      final Annotation[] fAnot = anot;
+      String title = MessageManager
+              .getString("label.select_foreground_colour");
+      ColourChooserListener listener = new ColourChooserListener()
+      {
+        @Override
+        public void colourSelected(Color c)
+        {
+          HiddenColumns hiddenColumns = av.getAlignment()
+                  .getHiddenColumns();
+          for (int index : av.getColumnSelection().getSelected())
+          {
+            if (hiddenColumns.isVisible(index))
+            {
+              if (fAnot[index] == null)
+              {
+                fAnot[index] = new Annotation("", "", ' ', 0);
+              }
+              fAnot[index].colour = c;
+            }
+          }
+        };
+      };
+      JalviewColourChooser.showColourChooser(this, title, Color.black,
+              listener);
+    }
+    else
+    // HELIX, SHEET or STEM
+    {
+      char type = 0;
+      String symbol = "\u03B1"; // alpha
+
+      if (action.equals(HELIX))
+      {
+        type = 'H';
+      }
+      else if (action.equals(SHEET))
+      {
+        type = 'E';
+        symbol = "\u03B2"; // beta
+      }
+
+      // Added by LML to color stems
+      else if (action.equals(STEM))
+      {
+        type = 'S';
+        int column = av.getColumnSelection().getSelectedRanges().get(0)[0];
+        symbol = aa[activeRow].getDefaultRnaHelixSymbol(column);
+      }
+
+      if (!aa[activeRow].hasIcons)
+      {
+        aa[activeRow].hasIcons = true;
+      }
+
+      String label = JvOptionPane.showInputDialog(MessageManager
+              .getString("label.enter_label_for_the_structure"), symbol);
+
+      if (label == null)
+      {
+        return;
+      }
+
+      if ((label.length() > 0) && !aa[activeRow].hasText)
+      {
+        aa[activeRow].hasText = true;
+        if (action.equals(STEM))
+        {
+          aa[activeRow].showAllColLabels = true;
+        }
+      }
+      for (int index : av.getColumnSelection().getSelected())
+      {
+        if (!av.getAlignment().getHiddenColumns().isVisible(index))
+        {
+          continue;
+        }
+
+        if (anot[index] == null)
+        {
+          anot[index] = new Annotation(label, "", type, 0);
+        }
+
+        anot[index].secondaryStructure = type != 'S' ? type
+                : label.length() == 0 ? ' ' : label.charAt(0);
+        anot[index].displayCharacter = label;
+
+      }
+    }
+
+    av.getAlignment().validateAnnotation(aa[activeRow]);
+    ap.alignmentChanged();
+    ap.alignFrame.setMenusForViewport();
+    adjustPanelHeight();
+    repaint();
+
+    return;
+  }
+
+  /**
+   * Returns any existing annotation concatenated as a string. For each
+   * annotation, takes the description, if any, else the secondary structure
+   * character (if type is HELIX, SHEET or STEM), else the display character (if
+   * type is LABEL).
+   * 
+   * @param anots
+   * @param type
+   * @return
+   */
+  private String collectAnnotVals(Annotation[] anots, String type)
+  {
+    // TODO is this method wanted? why? 'last' is not used
+
+    StringBuilder collatedInput = new StringBuilder(64);
+    String last = "";
+    ColumnSelection viscols = av.getColumnSelection();
+    HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+
+    /*
+     * the selection list (read-only view) is in selection order, not
+     * column order; make a copy so we can sort it
+     */
+    List<Integer> selected = new ArrayList<>(viscols.getSelected());
+    Collections.sort(selected);
+    for (int index : selected)
+    {
+      // always check for current display state - just in case
+      if (!hidden.isVisible(index))
+      {
+        continue;
+      }
+      String tlabel = null;
+      if (anots[index] != null)
+      { // LML added stem code
+        if (type.equals(HELIX) || type.equals(SHEET) || type.equals(STEM)
+                || type.equals(LABEL))
+        {
+          tlabel = anots[index].description;
+          if (tlabel == null || tlabel.length() < 1)
+          {
+            if (type.equals(HELIX) || type.equals(SHEET)
+                    || type.equals(STEM))
+            {
+              tlabel = "" + anots[index].secondaryStructure;
+            }
+            else
+            {
+              tlabel = "" + anots[index].displayCharacter;
+            }
+          }
+        }
+        if (tlabel != null && !tlabel.equals(last))
+        {
+          if (last.length() > 0)
+          {
+            collatedInput.append(" ");
+          }
+          collatedInput.append(tlabel);
+        }
+      }
+    }
+    return collatedInput.toString();
+  }
+
+  /**
+   * Action on right mouse pressed on Mac is to show a pop-up menu for the
+   * annotation. Action on left mouse pressed is to find which annotation is
+   * pressed and mark the start of a column selection or graph resize operation.
+   * 
+   * @param evt
+   */
+  @Override
+  public void mousePressed(MouseEvent evt)
+  {
+
+    AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
+    if (aa == null)
+    {
+      return;
+    }
+    mouseDragLastX = evt.getX();
+    mouseDragLastY = evt.getY();
+
+    /*
+     * add visible annotation heights until we reach the y
+     * position, to find which annotation it is in
+     */
+    int height = 0;
+    activeRow = -1;
+    int yOffset = 0;
+    // todo could reuse getRowIndexAndOffset ?
+    final int y = evt.getY();
+
+    for (int i = 0; i < aa.length; i++)
+    {
+      if (aa[i].visible)
+      {
+        height += aa[i].height;
+      }
+
+      if (y < height)
+      {
+        if (aa[i].editable)
+        {
+          activeRow = i;
+        }
+        else if (aa[i].graph != 0)
+        {
+          /*
+           * we have clicked on a resizable graph annotation
+           */
+          graphStretch = i;
+          yOffset = height - y;
+        }
+        break;
+      }
+    }
+
+    /*
+     * isPopupTrigger fires in mousePressed on Mac,
+     * not until mouseRelease on Windows
+     */
+    if (evt.isPopupTrigger() && activeRow != -1)
+    {
+      showPopupMenu(y, evt.getX());
+      return;
+    }
+
+    if (graphStretch != -1)
+    {
+
+      if (aa[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP)
+      {
+        // data in row has position on y as well as x axis
+        if (evt.isAltDown() || evt.isAltGraphDown())
+        {
+          dragMode = DragMode.MatrixSelect;
+          firstDragX = mouseDragLastX;
+          firstDragY = mouseDragLastY;
+        }
+      }
+    }
+    else
+    {
+      // no row (or row that can be adjusted) was pressed. Simulate a ruler click
+      ap.getScalePanel().mousePressed(evt);
+    }
+  }
+
+  /**
+   * checks whether the annotation row under the mouse click evt's handles the
+   * event
+   * 
+   * @param evt
+   * @return false if evt was not handled
+   */
+  boolean matrix_clicked(MouseEvent evt)
+  {
+    int[] rowIndex = getRowIndexAndOffset(evt.getY(),
+            av.getAlignment().getAlignmentAnnotation());
+    if (rowIndex == null)
+    {
+      jalview.bin.Console
+              .error("IMPLEMENTATION ERROR: matrix click out of range.");
+      return false;
+    }
+    int yOffset = rowIndex[1];
+
+    AlignmentAnnotation clicked = av.getAlignment()
+            .getAlignmentAnnotation()[rowIndex[0]];
+    if (clicked.graph != AlignmentAnnotation.CONTACT_MAP)
+    {
+      return false;
+    }
+
+    // TODO - use existing threshold to select related sections of matrix
+    GraphLine thr = clicked.getThreshold();
+
+    int currentX = getColumnForXPos(evt.getX());
+    ContactListI forCurrentX = av.getContactList(clicked, currentX);
+    if (forCurrentX != null)
+    {
+      ContactGeometry cXcgeom = new ContactGeometry(forCurrentX,
+              clicked.graphHeight);
+      ContactGeometry.contactInterval cXci = cXcgeom.mapFor(yOffset,
+              yOffset);
+      /**
+       * start and end range corresponding to the row range under the mouse at
+       * column currentX
+       */
+      int fr, to;
+      fr = Math.min(cXci.cStart, cXci.cEnd);
+      to = Math.max(cXci.cStart, cXci.cEnd);
+
+      // double click selects the whole group
+      if (evt.getClickCount() == 2)
+      {
+        ContactMatrixI matrix = av.getContactMatrix(clicked);
+
+        if (matrix != null)
+        {
+          // simplest approach is to select all group containing column
+          if (matrix.hasGroups())
+          {
+            SequenceI rseq = clicked.sequenceRef;
+            BitSet grp = matrix.getGroupsFor(currentX);
+            // TODO: cXci needs to be mapped to real groups
+            for (int c = fr; c <= to; c++)
+            {
+              BitSet additionalGrp = matrix.getGroupsFor(c);
+              grp.or(additionalGrp);
+            }
+            HiddenColumns hc = av.getAlignment().getHiddenColumns();
+            for (int p = grp.nextSetBit(0); p >= 0; p = grp
+                    .nextSetBit(p + 1))
+            {
+              int offp = (rseq != null)
+                      ? rseq.findIndex(rseq.getStart() - 1 + p)
+                      : p;
+
+              if (!av.hasHiddenColumns() || hc.isVisible(offp))
+              {
+                av.getColumnSelection().addElement(offp);
+              }
+            }
+          }
+          // possible alternative for interactive selection - threshold
+          // gives 'ceiling' for forming a cluster
+          // when a row+column is selected, farthest common ancestor less
+          // than thr is used to compute cluster
+
+        }
+      }
+      else
+      {
+        // select corresponding range in segment under mouse
+        {
+          int[] rng = forCurrentX.getMappedPositionsFor(fr, to);
+          if (rng != null)
+          {
+            av.getColumnSelection().addRangeOfElements(rng, true);
+          }
+          av.getColumnSelection().addElement(currentX);
+        }
+        // PAE SPECIFIC
+        // and also select everything lower than the max range adjacent
+        // (kind of works)
+        if (evt.isControlDown()
+                && PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
+        {
+          int c = fr - 1;
+          ContactRange cr = forCurrentX.getRangeFor(fr, to);
+          double cval;
+          // TODO: could use GraphLine instead of arbitrary picking
+          // TODO: could report mean/median/variance for partitions
+          // (contiguous selected vs unselected regions and inter-contig
+          // regions)
+          // controls feathering - what other elements in row/column
+          // should we select
+          double thresh = cr.getMean() + (cr.getMax() - cr.getMean()) * .15;
+          while (c > 0)
+          {
+            cval = forCurrentX.getContactAt(c);
+            if (// cr.getMin() <= cval &&
+            cval <= thresh)
+            {
+              int[] cols = forCurrentX.getMappedPositionsFor(c, c);
+              if (cols != null)
+              {
+                av.getColumnSelection().addRangeOfElements(cols, true);
+              }
+              else
+              {
+                break;
+              }
+            }
+            c--;
+          }
+            c = to;
+            while (c < forCurrentX.getContactHeight())
+            {
+              cval = forCurrentX.getContactAt(c);
+              if (// cr.getMin() <= cval &&
+              cval <= thresh)
+              {
+                int[] cols = forCurrentX.getMappedPositionsFor(c, c);
+                if (cols != null)
+                {
+                  av.getColumnSelection().addRangeOfElements(cols, true);
+                }
+              }
+              else
+              {
+                break;
+              }
+              c++;
+
+            }
+          }
+      }
+    }
+    ap.paintAlignment(false, false);
+    PaintRefresher.Refresh(ap, av.getSequenceSetId());
+    av.sendSelection();
+    return true;
+  }
+  /**
+   * Construct and display a context menu at the right-click position
+   * 
+   * @param y
+   * @param x
+   */
+  void showPopupMenu(final int y, int x)
+  {
+    if (av.getColumnSelection() == null
+            || av.getColumnSelection().isEmpty())
+    {
+      return;
+    }
+
+    JPopupMenu pop = new JPopupMenu(
+            MessageManager.getString("label.structure_type"));
+    JMenuItem item;
+    /*
+     * Just display the needed structure options
+     */
+    if (av.getAlignment().isNucleotide())
+    {
+      item = new JMenuItem(STEM);
+      item.addActionListener(this);
+      pop.add(item);
+    }
+    else
+    {
+      item = new JMenuItem(HELIX);
+      item.addActionListener(this);
+      pop.add(item);
+      item = new JMenuItem(SHEET);
+      item.addActionListener(this);
+      pop.add(item);
+    }
+    item = new JMenuItem(LABEL);
+    item.addActionListener(this);
+    pop.add(item);
+    item = new JMenuItem(COLOUR);
+    item.addActionListener(this);
+    pop.add(item);
+    item = new JMenuItem(REMOVE);
+    item.addActionListener(this);
+    pop.add(item);
+    pop.show(this, x, y);
+  }
+
+  /**
+   * Action on mouse up is to clear mouse drag data and call mouseReleased on
+   * ScalePanel, to deal with defining the selection group (if any) defined by
+   * the mouse drag
+   * 
+   * @param evt
+   */
+  @Override
+  public void mouseReleased(MouseEvent evt)
+  {
+    if (dragMode == DragMode.MatrixSelect)
+    {
+      matrixSelectRange(evt);
+    }
+    graphStretch = -1;
+    mouseDragLastX = -1;
+    mouseDragLastY = -1;
+    firstDragX = -1;
+    firstDragY = -1;
+    mouseDragging = false;
+    if (dragMode == DragMode.Resize)
+    {
+      ap.adjustAnnotationHeight();
+    }
+    dragMode = DragMode.Undefined;
+    if (!matrix_clicked(evt))
+    {
+      ap.getScalePanel().mouseReleased(evt);
+    }
+
+    /*
+     * isPopupTrigger is set in mouseReleased on Windows
+     * (in mousePressed on Mac)
+     */
+    if (evt.isPopupTrigger() && activeRow != -1)
+    {
+      showPopupMenu(evt.getY(), evt.getX());
+    }
+
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param evt
+   *          DOCUMENT ME!
+   */
+  @Override
+  public void mouseEntered(MouseEvent evt)
+  {
+    this.mouseDragging = false;
+    ap.getScalePanel().mouseEntered(evt);
+  }
+
+  /**
+   * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
+   * with column selection on a mouse drag
+   * 
+   * @param evt
+   */
+  @Override
+  public void mouseExited(MouseEvent evt)
+  {
+    ap.getScalePanel().mouseExited(evt);
+  }
+
+  /**
+   * Action on starting or continuing a mouse drag. There are two possible
+   * actions:
+   * <ul>
+   * <li>drag up or down on a graphed annotation increases or decreases the
+   * height of the graph</li>
+   * <li>dragging left or right selects the columns dragged across</li>
+   * </ul>
+   * A drag on a graph annotation is treated as column selection if it starts
+   * with more horizontal than vertical movement, and as resize if it starts
+   * with more vertical than horizontal movement. Once started, the drag does
+   * not change mode.
+   * 
+   * @param evt
+   */
+  @Override
+  public void mouseDragged(MouseEvent evt)
+  {
+    /*
+     * if dragMode is Undefined:
+     * - set to Select if dx > dy
+     * - set to Resize if dy > dx
+     * - do nothing if dx == dy
+     */
+    final int x = evt.getX();
+    final int y = evt.getY();
+    if (dragMode == DragMode.Undefined)
+    {
+      int dx = Math.abs(x - mouseDragLastX);
+      int dy = Math.abs(y - mouseDragLastY);
+      if (graphStretch == -1 || dx > dy)
+      {
+        /*
+         * mostly horizontal drag, or not a graph annotation
+         */
+        dragMode = DragMode.Select;
+      }
+      else if (dy > dx)
+      {
+        /*
+         * mostly vertical drag
+         */
+        dragMode = DragMode.Resize;
+        notJustOne = evt.isShiftDown();
+
+        /*
+         * but could also be a matrix drag
+         */
+        if ((evt.isAltDown() || evt.isAltGraphDown()) && (av.getAlignment()
+                .getAlignmentAnnotation()[graphStretch].graph == AlignmentAnnotation.CONTACT_MAP))
+        {
+          /*
+           * dragging in a matrix
+           */
+          dragMode = DragMode.MatrixSelect;
+          firstDragX = mouseDragLastX;
+          firstDragY = mouseDragLastY;
+        }
+      }
+    }
+
+    if (dragMode == DragMode.Undefined)
+
+    {
+      /*
+       * drag is diagonal - defer deciding whether to
+       * treat as up/down or left/right
+       */
+      return;
+    }
+
+    try
+    {
+      if (dragMode == DragMode.Resize)
+      {
+        /*
+         * resize graph annotation if mouse was dragged up or down
+         */
+        int deltaY = mouseDragLastY - evt.getY();
+        if (deltaY != 0)
+        {
+          AlignmentAnnotation graphAnnotation = av.getAlignment()
+                  .getAlignmentAnnotation()[graphStretch];
+          int newHeight = Math.max(0, graphAnnotation.graphHeight + deltaY);
+          if (notJustOne)
+          {
+            for (AlignmentAnnotation similar : av.getAlignment()
+                    .findAnnotations(null, graphAnnotation.getCalcId(),
+                            graphAnnotation.label))
+            {
+              similar.graphHeight = newHeight;
+            }
+
+          }
+          else
+          {
+            graphAnnotation.graphHeight = newHeight;
+          }
+          adjustPanelHeight();
+          ap.paintAlignment(false, false);
+        }
+      }
+      else if (dragMode == DragMode.MatrixSelect)
+      {
+        /*
+         * TODO draw a rubber band for range
+         */
+        mouseDragLastX = x;
+        mouseDragLastY = y;
+        ap.paintAlignment(false, false);
+      }
+      else
+      {
+        /*
+         * for mouse drag left or right, delegate to 
+         * ScalePanel to adjust the column selection
+         */
+        ap.getScalePanel().mouseDragged(evt);
+      }
+    } finally
+    {
+      mouseDragLastX = x;
+      mouseDragLastY = y;
+    }
+  }
+
+  public void matrixSelectRange(MouseEvent evt)
+  {
+    /*
+     * get geometry of drag
+     */
+    int fromY = Math.min(firstDragY, evt.getY());
+    int toY = Math.max(firstDragY, evt.getY());
+    int fromX = Math.min(firstDragX, evt.getX());
+    int toX = Math.max(firstDragX, evt.getX());
+
+    int deltaY = toY - fromY;
+    int deltaX = toX - fromX;
+
+    int[] rowIndex = getRowIndexAndOffset(fromY,
+            av.getAlignment().getAlignmentAnnotation());
+    int[] toRowIndex = getRowIndexAndOffset(toY,
+            av.getAlignment().getAlignmentAnnotation());
+
+    if (rowIndex == null || toRowIndex == null)
+    {
+      jalview.bin.Console.trace("Drag out of range. needs to be clipped");
+
+    }
+    if (rowIndex[0] != toRowIndex[0])
+    {
+      jalview.bin.Console.trace("Drag went to another row. needs to be clipped");
+    }
+
+    // rectangular selection on matrix style annotation
+    AlignmentAnnotation cma = av.getAlignment()
+            .getAlignmentAnnotation()[rowIndex[0]];
+
+    int lastX = getColumnForXPos(fromX);
+    int currentX = getColumnForXPos(toX);
+    int fromXc = Math.min(lastX, currentX);
+    int toXc = Math.max(lastX, currentX);
+    ContactListI forFromX = av.getContactList(cma, fromXc);
+    ContactListI forToX = av.getContactList(cma, toXc);
+
+    if (forFromX != null && forToX != null)
+    {
+      ContactGeometry lastXcgeom = new ContactGeometry(forFromX,
+              cma.graphHeight);
+      ContactGeometry.contactInterval lastXci = lastXcgeom
+              .mapFor(rowIndex[1], rowIndex[1] - deltaY);
+
+      ContactGeometry cXcgeom = new ContactGeometry(forToX,
+              cma.graphHeight);
+      ContactGeometry.contactInterval cXci = cXcgeom.mapFor(rowIndex[1],
+              rowIndex[1] - deltaY);
+
+      // mark rectangular region formed by drag
+      jalview.bin.Console.trace("Matrix Selection from last(" + fromXc
+              + ",[" + lastXci.cStart + "," + lastXci.cEnd + "]) to cur("
+              + toXc + ",[" + cXci.cStart + "," + cXci.cEnd + "])");
+      int fr, to;
+      fr = Math.min(lastXci.cStart, lastXci.cEnd);
+      to = Math.max(lastXci.cStart, lastXci.cEnd);
+      int[] mappedPos = forFromX.getMappedPositionsFor(fr, to);
+      if (mappedPos != null)
+      {
+        jalview.bin.Console.trace("Marking " + fr + " to " + to
+                + " mapping to sequence positions " + mappedPos[0] + " to "
+                + mappedPos[1]);
+        for (int pair = 0; pair < mappedPos.length; pair += 2)
+        {
+          for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
+//          {
+//            if (cma.sequenceRef != null)
+//            {
+//              int col = cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
+//              av.getColumnSelection().addElement(col);
+//            }
+//            else
+            {
+              av.getColumnSelection().addElement(c);
+            }
+        }
+      }
+      // and again for most recent corner of drag
+      fr = Math.min(cXci.cStart, cXci.cEnd);
+      to = Math.max(cXci.cStart, cXci.cEnd);
+      mappedPos = forFromX.getMappedPositionsFor(fr, to);
+      if (mappedPos != null)
+      {
+        for (int pair = 0; pair < mappedPos.length; pair += 2)
+        {
+          jalview.bin.Console.trace("Marking " + fr + " to " + to
+                  + " mapping to sequence positions " + mappedPos[pair] + " to "
+                  + mappedPos[pair+1]);
+          for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
+          {
+//            if (cma.sequenceRef != null)
+//            {
+//              int col = cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
+//              av.getColumnSelection().addElement(col);
+//            }
+//            else
+            {
+              av.getColumnSelection().addElement(c);
+            }
+          }
+        }
+      }
+      fr = Math.min(lastX, currentX);
+      to = Math.max(lastX, currentX);
+
+      jalview.bin.Console.trace("Marking " + fr + " to " + to);
+      for (int c = fr; c <= to; c++)
+      {
+        av.getColumnSelection().addElement(c);
+      }
+    }
+
+  }
+
+  /**
+   * Constructs the tooltip, and constructs and displays a status message, for
+   * the current mouse position
+   * 
+   * @param evt
+   */
+  @Override
+  public void mouseMoved(MouseEvent evt)
+  {
+    int yPos = evt.getY();
+    AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
+    int rowAndOffset[] = getRowIndexAndOffset(yPos, aa);
+    int row = rowAndOffset[0];
+
+    if (row == -1)
+    {
+      this.setToolTipText(null);
+      return;
+    }
+
+    int column = getColumnForXPos(evt.getX());
+
+    AlignmentAnnotation ann = aa[row];
+    if (row > -1 && ann.annotations != null
+            && column < ann.annotations.length)
+    {
+      String toolTip = buildToolTip(ann, column, aa, rowAndOffset[1], av,
+              ap);
+      setToolTipText(toolTip == null ? null
+              : JvSwingUtils.wrapTooltip(true, toolTip));
+      String msg = getStatusMessage(av.getAlignment(), column, ann,
+              rowAndOffset[1], av);
+      ap.alignFrame.setStatus(msg);
+    }
+    else
+    {
+      this.setToolTipText(null);
+      ap.alignFrame.setStatus(" ");
+    }
+  }
+
+  private int getColumnForXPos(int x)
+  {
+    int column = (x / av.getCharWidth()) + av.getRanges().getStartRes();
+    column = Math.min(column, av.getRanges().getEndRes());
+
+    if (av.hasHiddenColumns())
+    {
+      column = av.getAlignment().getHiddenColumns()
+              .visibleToAbsoluteColumn(column);
+    }
+    return column;
+  }
+
+  /**
+   * Answers the index in the annotations array of the visible annotation at the
+   * given y position. This is done by adding the heights of visible annotations
+   * until the y position has been exceeded. Answers -1 if no annotations are
+   * visible, or the y position is below all annotations.
+   * 
+   * @param yPos
+   * @param aa
+   * @return
+   */
+  static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
+  {
+    if (aa == null)
+    {
+      return -1;
+    }
+    return getRowIndexAndOffset(yPos, aa)[0];
+  }
+
+  static int[] getRowIndexAndOffset(int yPos, AlignmentAnnotation[] aa)
+  {
+    int[] res = new int[2];
+    res[0] = -1;
+    res[1] = 0;
+    if (aa == null)
+    {
+      return res;
+    }
+    int row = -1;
+    int height = 0, lheight = 0;
+    for (int i = 0; i < aa.length; i++)
+    {
+      if (aa[i].visible)
+      {
+        lheight = height;
+        height += aa[i].height;
+      }
+
+      if (height > yPos)
+      {
+        row = i;
+        res[0] = row;
+        res[1] = height - yPos;
+        break;
+      }
+    }
+    return res;
+  }
+
+  /**
+   * Answers a tooltip for the annotation at the current mouse position, not
+   * wrapped in &lt;html&gt; tags (apply if wanted). Answers null if there is no
+   * tooltip to show.
+   * 
+   * @param ann
+   * @param column
+   * @param anns
+   * @param rowAndOffset
+   */
+  static String buildToolTip(AlignmentAnnotation ann, int column,
+          AlignmentAnnotation[] anns, int rowAndOffset, AlignViewportI av,
+          AlignmentPanel ap)
+  {
+    String tooltip = null;
+    if (ann.graphGroup > -1)
+    {
+      StringBuilder tip = new StringBuilder(32);
+      boolean first = true;
+      for (int i = 0; i < anns.length; i++)
+      {
+        if (anns[i].graphGroup == ann.graphGroup
+                && anns[i].annotations[column] != null)
+        {
+          if (!first)
+          {
+            tip.append("<br>");
+          }
+          first = false;
+          tip.append(anns[i].label);
+          String description = anns[i].annotations[column].description;
+          if (description != null && description.length() > 0)
+          {
+            tip.append(" ").append(description);
+          }
+        }
+      }
+      tooltip = first ? null : tip.toString();
+    }
+    else if (column < ann.annotations.length
+            && ann.annotations[column] != null)
+    {
+      tooltip = ann.annotations[column].description;
+    }
+    // TODO abstract tooltip generator so different implementations can be built
+    if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
+    {
+      ContactListI clist = av.getContactList(ann, column);
+      if (clist != null)
+      {
+        ContactGeometry cgeom = new ContactGeometry(clist, ann.graphHeight);
+        ContactGeometry.contactInterval ci = cgeom.mapFor(rowAndOffset);
+        ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
+        tooltip = "Contact from " + clist.getPosition() + ", [" + ci.cStart
+                + " - " + ci.cEnd + "]" + "<br/>Mean:" + cr.getMean();
+        
+        int col = ann.sequenceRef.findPosition(column);
+        int[][] highlightPos;
+        int[] mappedPos = clist.getMappedPositionsFor(ci.cStart, ci.cEnd);
+        if (mappedPos != null)
+        {
+          highlightPos = new int[1 + mappedPos.length][2];
+          highlightPos[0] = new int[] { col, col };
+          for (int p = 0, h = 0; p < mappedPos.length; h++, p += 2)
+          {
+            highlightPos[h][0] = ann.sequenceRef
+                    .findPosition(mappedPos[p] - 1);
+            highlightPos[h][1] = ann.sequenceRef
+                    .findPosition(mappedPos[p + 1] - 1);
+          }
+        }
+        else
+        {
+          highlightPos = new int[][] { new int[] { col, col } };
+        }
+        ap.getStructureSelectionManager()
+                .highlightPositionsOn(ann.sequenceRef, highlightPos, null);
+      }
+    }
+    return tooltip;
+  }
+
+  /**
+   * Constructs and returns the status bar message
+   * 
+   * @param al
+   * @param column
+   * @param ann
+   * @param rowAndOffset
+   */
+  static String getStatusMessage(AlignmentI al, int column,
+          AlignmentAnnotation ann, int rowAndOffset, AlignViewportI av)
+  {
+    /*
+     * show alignment column and annotation description if any
+     */
+    StringBuilder text = new StringBuilder(32);
+    text.append(MessageManager.getString("label.column")).append(" ")
+            .append(column + 1);
+
+    if (column < ann.annotations.length && ann.annotations[column] != null)
+    {
+      String description = ann.annotations[column].description;
+      if (description != null && description.trim().length() > 0)
+      {
+        text.append("  ").append(description);
+      }
+    }
+
+    /*
+     * if the annotation is sequence-specific, show the sequence number
+     * in the alignment, and (if not a gap) the residue and position
+     */
+    SequenceI seqref = ann.sequenceRef;
+    if (seqref != null)
+    {
+      int seqIndex = al.findIndex(seqref);
+      if (seqIndex != -1)
+      {
+        text.append(", ").append(MessageManager.getString("label.sequence"))
+                .append(" ").append(seqIndex + 1);
+        char residue = seqref.getCharAt(column);
+        if (!Comparison.isGap(residue))
+        {
+          text.append(" ");
+          String name;
+          if (al.isNucleotide())
+          {
+            name = ResidueProperties.nucleotideName
+                    .get(String.valueOf(residue));
+            text.append(" Nucleotide: ")
+                    .append(name != null ? name : residue);
+          }
+          else
+          {
+            name = 'X' == residue ? "X"
+                    : ('*' == residue ? "STOP"
+                            : ResidueProperties.aa2Triplet
+                                    .get(String.valueOf(residue)));
+            text.append(" Residue: ").append(name != null ? name : residue);
+          }
+          int residuePos = seqref.findPosition(column);
+          text.append(" (").append(residuePos).append(")");
+        }
+      }
+    }
+
+    return text.toString();
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param evt
+   *          DOCUMENT ME!
+   */
+  @Override
+  public void mouseClicked(MouseEvent evt)
+  {
+    // if (activeRow != -1)
+    // {
+    // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
+    // AlignmentAnnotation anot = aa[activeRow];
+    // }
+  }
+
+  // TODO mouseClicked-content and drawCursor are quite experimental!
+  public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
+          int y1)
+  {
+    int pady = av.getCharHeight() / 5;
+    int charOffset = 0;
+    graphics.setColor(Color.black);
+    graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
+
+    if (av.validCharWidth)
+    {
+      graphics.setColor(Color.white);
+
+      char s = seq.getCharAt(res);
+
+      charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
+      graphics.drawString(String.valueOf(s), charOffset + x1,
+              (y1 + av.getCharHeight()) - pady);
+    }
+
+  }
+
+  private volatile boolean imageFresh = false;
+
+  private Rectangle visibleRect = new Rectangle(),
+          clipBounds = new Rectangle();
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param g
+   *          DOCUMENT ME!
+   */
+  @Override
+  public void paintComponent(Graphics g)
+  {
+
+    // BH: note that this method is generally recommended to
+    // call super.paintComponent(g). Otherwise, the children of this
+    // component will not be rendered. That is not needed here
+    // because AnnotationPanel does not have any children. It is
+    // just a JPanel contained in a JViewPort.
+
+    computeVisibleRect(visibleRect);
+
+    g.setColor(Color.white);
+    g.fillRect(0, 0, visibleRect.width, visibleRect.height);
+
+    if (image != null)
+    {
+      // BH 2018 optimizing generation of new Rectangle().
+      if (fastPaint
+              || (visibleRect.width != (clipBounds = g
+                      .getClipBounds(clipBounds)).width)
+              || (visibleRect.height != clipBounds.height))
+      {
+
+        g.drawImage(image, 0, 0, this);
+        fastPaint = false;
+        return;
+      }
+    }
+    imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
+            + 1) * av.getCharWidth();
+    if (imgWidth < 1)
+    {
+      return;
+    }
+    Graphics2D gg;
+    if (image == null || imgWidth != image.getWidth(this)
+            || image.getHeight(this) != getHeight())
+    {
+      boolean tried = false;
+      image = null;
+      while (image == null && !tried)
+      {
+        try
+        {
+          image = new BufferedImage(imgWidth,
+                  ap.getAnnotationPanel().getHeight(),
+                  BufferedImage.TYPE_INT_RGB);
+          tried = true;
+        } catch (IllegalArgumentException exc)
+        {
+          System.err.println(
+                  "Serious issue with viewport geometry imgWidth requested was "
+                          + imgWidth);
+          return;
+        } catch (OutOfMemoryError oom)
+        {
+          try
+          {
+            System.gc();
+          } catch (Exception x)
+          {
+          }
+          ;
+          new OOMWarning(
+                  "Couldn't allocate memory to redraw screen. Please restart Jalview",
+                  oom);
+          return;
+        }
+
+      }
+      gg = (Graphics2D) image.getGraphics();
+
+      if (av.antiAlias)
+      {
+        gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                RenderingHints.VALUE_ANTIALIAS_ON);
+      }
+
+      gg.setFont(av.getFont());
+      fm = gg.getFontMetrics();
+      gg.setColor(Color.white);
+      gg.fillRect(0, 0, imgWidth, image.getHeight());
+      imageFresh = true;
+    }
+    else
+    {
+      gg = (Graphics2D) image.getGraphics();
+
+    }
+
+    drawComponent(gg, av.getRanges().getStartRes(),
+            av.getRanges().getEndRes() + 1);
+    gg.dispose();
+    imageFresh = false;
+    g.drawImage(image, 0, 0, this);
+  }
+
+  /**
+   * set true to enable redraw timing debug output on stderr
+   */
+  private final boolean debugRedraw = false;
+
+  /**
+   * non-Thread safe repaint
+   * 
+   * @param horizontal
+   *          repaint with horizontal shift in alignment
+   */
+  public void fastPaint(int horizontal)
+  {
+    if ((horizontal == 0) || image == null
+            || av.getAlignment().getAlignmentAnnotation() == null
+            || av.getAlignment().getAlignmentAnnotation().length < 1
+            || av.isCalcInProgress())
+    {
+      repaint();
+      return;
+    }
+
+    int sr = av.getRanges().getStartRes();
+    int er = av.getRanges().getEndRes() + 1;
+    int transX = 0;
+
+    Graphics2D gg = (Graphics2D) image.getGraphics();
+
+    if (imgWidth > Math.abs(horizontal * av.getCharWidth()))
+    {
+      // scroll is less than imgWidth away so can re-use buffered graphics
+      gg.copyArea(0, 0, imgWidth, getHeight(),
+              -horizontal * av.getCharWidth(), 0);
+
+      if (horizontal > 0) // scrollbar pulled right, image to the left
+      {
+        transX = (er - sr - horizontal) * av.getCharWidth();
+        sr = er - horizontal;
+      }
+      else if (horizontal < 0)
+      {
+        er = sr - horizontal;
+      }
+    }
+    gg.translate(transX, 0);
+
+    drawComponent(gg, sr, er);
+
+    gg.translate(-transX, 0);
+
+    gg.dispose();
+
+    fastPaint = true;
+
+    // Call repaint on alignment panel so that repaints from other alignment
+    // panel components can be aggregated. Otherwise performance of the overview
+    // window and others may be adversely affected.
+    av.getAlignPanel().repaint();
+  }
+
+  private volatile boolean lastImageGood = false;
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param g
+   *          DOCUMENT ME!
+   * @param startRes
+   *          DOCUMENT ME!
+   * @param endRes
+   *          DOCUMENT ME!
+   */
+  public void drawComponent(Graphics g, int startRes, int endRes)
+  {
+    BufferedImage oldFaded = fadedImage;
+    if (av.isCalcInProgress())
+    {
+      if (image == null)
+      {
+        lastImageGood = false;
+        return;
+      }
+      // We'll keep a record of the old image,
+      // and draw a faded image until the calculation
+      // has completed
+      if (lastImageGood
+              && (fadedImage == null || fadedImage.getWidth() != imgWidth
+                      || fadedImage.getHeight() != image.getHeight()))
+      {
+        // System.err.println("redraw faded image ("+(fadedImage==null ?
+        // "null image" : "") + " lastGood="+lastImageGood+")");
+        fadedImage = new BufferedImage(imgWidth, image.getHeight(),
+                BufferedImage.TYPE_INT_RGB);
+
+        Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
+
+        fadedG.setColor(Color.white);
+        fadedG.fillRect(0, 0, imgWidth, image.getHeight());
+
+        fadedG.setComposite(
+                AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
+        fadedG.drawImage(image, 0, 0, this);
+
+      }
+      // make sure we don't overwrite the last good faded image until all
+      // calculations have finished
+      lastImageGood = false;
+
+    }
+    else
+    {
+      if (fadedImage != null)
+      {
+        oldFaded = fadedImage;
+      }
+      fadedImage = null;
+    }
+
+    g.setColor(Color.white);
+    g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
+
+    g.setFont(av.getFont());
+    if (fm == null)
+    {
+      fm = g.getFontMetrics();
+    }
+
+    if ((av.getAlignment().getAlignmentAnnotation() == null)
+            || (av.getAlignment().getAlignmentAnnotation().length < 1))
+    {
+      g.setColor(Color.white);
+      g.fillRect(0, 0, getWidth(), getHeight());
+      g.setColor(Color.black);
+      if (av.validCharWidth)
+      {
+        g.drawString(MessageManager
+                .getString("label.alignment_has_no_annotations"), 20, 15);
+      }
+
+      return;
+    }
+    lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
+            endRes);
+    if (!lastImageGood && fadedImage == null)
+    {
+      fadedImage = oldFaded;
+    }
+    if (dragMode == DragMode.MatrixSelect)
+    {
+      g.setColor(Color.yellow);
+      g.drawRect(Math.min(firstDragX, mouseDragLastX),
+              Math.min(firstDragY, mouseDragLastY),
+              Math.max(firstDragX, mouseDragLastX)
+                      - Math.min(firstDragX, mouseDragLastX),
+              Math.max(firstDragY, mouseDragLastY)
+                      - Math.min(firstDragY, mouseDragLastY));
+
+    }
+  }
+
+  @Override
+  public FontMetrics getFontMetrics()
+  {
+    return fm;
+  }
+
+  @Override
+  public Image getFadedImage()
+  {
+    return fadedImage;
+  }
+
+  @Override
+  public int getFadedImageWidth()
+  {
+    return imgWidth;
+  }
+
+  private int[] bounds = new int[2];
+
+  @Override
+  public int[] getVisibleVRange()
+  {
+    if (ap != null && ap.getAlabels() != null)
+    {
+      int sOffset = -ap.getAlabels().getScrollOffset();
+      int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
+      bounds[0] = sOffset;
+      bounds[1] = visHeight;
+      return bounds;
+    }
+    else
+    {
+      return null;
+    }
+  }
+
+  /**
+   * Try to ensure any references held are nulled
+   */
+  public void dispose()
+  {
+    av = null;
+    ap = null;
+    image = null;
+    fadedImage = null;
+    // gg = null;
+    _mwl = null;
+
+    /*
+     * I created the renderer so I will dispose of it
+     */
+    if (renderer != null)
+    {
+      renderer.dispose();
+    }
+  }
+
+  @Override
+  public void propertyChange(PropertyChangeEvent evt)
+  {
+    // Respond to viewport range changes (e.g. alignment panel was scrolled)
+    // Both scrolling and resizing change viewport ranges: scrolling changes
+    // both start and end points, but resize only changes end values.
+    // Here we only want to fastpaint on a scroll, with resize using a normal
+    // paint, so scroll events are identified as changes to the horizontal or
+    // vertical start value.
+    if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
+    {
+      fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
+    }
+    else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
+    {
+      fastPaint(((int[]) evt.getNewValue())[0]
+              - ((int[]) evt.getOldValue())[0]);
+    }
+    else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
+    {
+      repaint();
+    }
+  }
+
+  /**
+   * computes the visible height of the annotation panel
+   * 
+   * @param adjustPanelHeight
+   *          - when false, just adjust existing height according to other
+   *          windows
+   * @param annotationHeight
+   * @return height to use for the ScrollerPreferredVisibleSize
+   */
+  public int adjustForAlignFrame(boolean adjustPanelHeight,
+          int annotationHeight)
+  {
+    /*
+     * Estimate available height in the AlignFrame for alignment +
+     * annotations. Deduct an estimate for title bar, menu bar, scale panel,
+     * hscroll, status bar, insets. 
+     */
+    int stuff = (ap.getViewName() != null ? 30 : 0)
+            + (Platform.isAMacAndNotJS() ? 120 : 140);
+    int availableHeight = ap.alignFrame.getHeight() - stuff;
+    int rowHeight = av.getCharHeight();
+
+    if (adjustPanelHeight)
+    {
+      int alignmentHeight = rowHeight * av.getAlignment().getHeight();
+
+      /*
+       * If not enough vertical space, maximize annotation height while keeping
+       * at least two rows of alignment visible
+       */
+      if (annotationHeight + alignmentHeight > availableHeight)
+      {
+        annotationHeight = Math.min(annotationHeight,
+                availableHeight - 2 * rowHeight);
+      }
+    }
+    else
+    {
+      // maintain same window layout whilst updating sliders
+      annotationHeight = Math.min(ap.annotationScroller.getSize().height,
+              availableHeight - 2 * rowHeight);
+    }
+    return annotationHeight;
+  }
+}