JAL-1481 wrap tooltip in html
[jalview.git] / src / jalview / gui / PopupMenu.java
index 6042ab8..78c1592 100644 (file)
@@ -62,8 +62,8 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.BitSet;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.Hashtable;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Vector;
@@ -192,6 +192,8 @@ public class PopupMenu extends JPopupMenu
 
   JMenu hideAnnotationsMenu = new JMenu();
 
+  JMenuItem addDatasequenceAnnotations = new JMenuItem();
+
   JMenuItem sequenceFeature = new JMenuItem();
 
   JMenuItem textColour = new JMenuItem();
@@ -306,18 +308,18 @@ public class PopupMenu extends JPopupMenu
 
           menuItem = new JMenuItem();
           menuItem.setText(pdb.getId());
-          menuItem.addActionListener(new java.awt.event.ActionListener()
+          menuItem.addActionListener(new ActionListener() 
           {
-            public void actionPerformed(ActionEvent e)
-            {
-              // TODO re JAL-860: optionally open dialog or provide a menu entry
-              // allowing user to open just one structure per sequence
-              // new AppJmol(pdb, ap.av.collateForPDB(new PDBEntry[]
-              // { pdb })[0], null, ap);
-              new StructureViewer(ap.getStructureSelectionManager())
-                      .viewStructures(pdb,
-                              ap.av.collateForPDB(new PDBEntry[]
-                              { pdb })[0], null, ap);
+            @Override
+            public void actionPerformed(ActionEvent e) {
+            // TODO re JAL-860: optionally open dialog or provide a menu entry
+            // allowing user to open just one structure per sequence
+            // new AppJmol(pdb, ap.av.collateForPDB(new PDBEntry[]
+            // { pdb })[0], null, ap);
+           new StructureViewer(ap.getStructureSelectionManager())
+                    .viewStructures(pdb,
+                            ap.av.collateForPDB(new PDBEntry[]
+                            { pdb })[0], null, ap);
             }
           });
           viewStructureMenu.add(menuItem);
@@ -355,7 +357,6 @@ public class PopupMenu extends JPopupMenu
                     "label.2d_rna_structure_line", new String[]
                     { structureLine }));
             menuItem.addActionListener(new java.awt.event.ActionListener()
-
             {
               @Override
               public void actionPerformed(ActionEvent e)
@@ -809,14 +810,18 @@ public class PopupMenu extends JPopupMenu
    */
   protected void buildAnnotationTypesMenus()
   {
+    final SequenceGroup selectionGroup = ap.av.getSelectionGroup();
+    if (selectionGroup == null)
+    {
+      // this menu option is only for a selection
+      return;
+    }
+
     showAnnotationsMenu.removeAll();
     hideAnnotationsMenu.removeAll();
     final List<String> all = Arrays.asList(ALL_ANNOTATIONS);
-    addAnnotationTypeToShowHide(showAnnotationsMenu,
-            all, true,
- true);
-    addAnnotationTypeToShowHide(hideAnnotationsMenu, all, true,
-            false);
+    addAnnotationTypeToShowHide(showAnnotationsMenu, "", all, true, true);
+    addAnnotationTypeToShowHide(hideAnnotationsMenu, "", all, true, false);
     showAnnotationsMenu.addSeparator();
     hideAnnotationsMenu.addSeparator();
 
@@ -825,19 +830,31 @@ public class PopupMenu extends JPopupMenu
     BitSet visibleGraphGroups = PopupMenu
             .getVisibleLineGraphGroups(annotations);
 
-    List<List<String>> shownTypes = new ArrayList<List<String>>();
-    List<List<String>> hiddenTypes = new ArrayList<List<String>>();
+    /*
+     * Find shown/hidden annotations types, distinguished by source (calcId),
+     * and grouped by graphGroup.
+     */
+    Map<String, List<List<String>>> shownTypes = new HashMap<String, List<List<String>>>();
+    Map<String, List<List<String>>> hiddenTypes = new HashMap<String, List<List<String>>>();
     PopupMenu.getAnnotationTypesForShowHide(shownTypes, hiddenTypes,
-            visibleGraphGroups, annotations, ap.av.getSelectionGroup());
+            visibleGraphGroups, annotations, selectionGroup);
 
-    for (List<String> types : hiddenTypes)
+    for (String calcId : hiddenTypes.keySet())
     {
-      addAnnotationTypeToShowHide(showAnnotationsMenu, types, false, true);
+      for (List<String> type : hiddenTypes.get(calcId))
+      {
+        addAnnotationTypeToShowHide(showAnnotationsMenu, calcId, type,
+                false, true);
+      }
     }
 
-    for (List<String> types : shownTypes)
+    for (String calcId : shownTypes.keySet())
     {
-      addAnnotationTypeToShowHide(hideAnnotationsMenu, types, false, false);
+      for (List<String> type : shownTypes.get(calcId))
+      {
+        addAnnotationTypeToShowHide(hideAnnotationsMenu, calcId, type,
+                false, false);
+      }
     }
   }
 
@@ -859,19 +876,31 @@ public class PopupMenu extends JPopupMenu
    * </ul>
    * 
    * @param shownTypes
+   *          a map, keyed by calcId (annotation source), whose entries are the
+   *          lists of annotation types found for the calcId; each annotation
+   *          type in turn may be a list (in the case of grouped annotations)
    * @param hiddenTypes
+   *          a map, similar to shownTypes, but for hidden annotation types
    * @param visibleGraphGroups
+   *          a lookup keyed by graphGroup identifier
    * @param annotations
+   *          the annotations on the alignment to scan
    * @param sequenceGroup
+   *          the sequence group to restrict search to
    */
   public static void getAnnotationTypesForShowHide(
-          List<List<String>> shownTypes, List<List<String>> hiddenTypes,
-          BitSet visibleGraphGroups,
-          AlignmentAnnotation[] annotations, SequenceGroup sequenceGroup)
+          Map<String, List<List<String>>> shownTypes,
+          Map<String, List<List<String>>> hiddenTypes,
+          BitSet visibleGraphGroups, AlignmentAnnotation[] annotations,
+          SequenceGroup sequenceGroup)
   {
-    // lookup table, key = graph group, value = list of types in the group
-    Map<Integer, List<String>> groupLabels = new LinkedHashMap<Integer, List<String>>();
+    /*
+     * Build a lookup, by calcId (annotation source), of all annotation types in
+     * each graph group.
+     */
+    Map<String, Map<Integer, List<String>>> groupLabels = new HashMap<String, Map<Integer, List<String>>>();
 
+    // trackers for which calcId!label combinations we have dealt with
     List<String> addedToShown = new ArrayList<String>();
     List<String> addedToHidden = new ArrayList<String>();
 
@@ -882,37 +911,63 @@ public class PopupMenu extends JPopupMenu
               || (aa.sequenceRef != null && sequenceGroup.getSequences()
                       .contains(aa.sequenceRef)))
       {
+        String calcId = aa.getCalcId();
+
         /*
          * Build a 'composite label' for types in line graph groups.
          */
         final List<String> labelAsList = new ArrayList<String>();
-        labelAsList.add(aa.label);
+        final String displayLabel = aa.label;
+        labelAsList.add(displayLabel);
         if (aa.graph == AlignmentAnnotation.LINE_GRAPH
                 && aa.graphGroup > -1)
         {
-          if (groupLabels.containsKey(aa.graphGroup))
+          if (!groupLabels.containsKey(calcId))
           {
-            if (!groupLabels.get(aa.graphGroup).contains(aa.label))
+            groupLabels.put(calcId, new HashMap<Integer, List<String>>());
+          }
+          Map<Integer, List<String>> groupLabelsForCalcId = groupLabels
+                  .get(calcId);
+          if (groupLabelsForCalcId.containsKey(aa.graphGroup))
+          {
+            if (!groupLabelsForCalcId.get(aa.graphGroup).contains(
+                    displayLabel))
             {
-              groupLabels.get(aa.graphGroup).add(aa.label);
+              groupLabelsForCalcId.get(aa.graphGroup).add(displayLabel);
             }
           }
           else
           {
-            groupLabels.put(aa.graphGroup, labelAsList);
+            groupLabelsForCalcId.put(aa.graphGroup, labelAsList);
           }
         }
-        else if (aa.visible && !addedToShown.contains(aa.label))
-        {
-          shownTypes.add(labelAsList);
-          addedToShown.add(aa.label);
-        }
         else
+        /*
+         * 'Simple case' - not a grouped annotation type - list of one label
+         * only
+         */
         {
-          if (!aa.visible && !addedToHidden.contains(aa.label))
+          String rememberAs = calcId + "!" + displayLabel;
+          if (aa.visible && !addedToShown.contains(rememberAs))
+          {
+            if (!shownTypes.containsKey(calcId))
+            {
+              shownTypes.put(calcId, new ArrayList<List<String>>());
+            }
+            shownTypes.get(calcId).add(labelAsList);
+            addedToShown.add(rememberAs);
+          }
+          else
           {
-            hiddenTypes.add(labelAsList);
-            addedToHidden.add(aa.label);
+            if (!aa.visible && !addedToHidden.contains(rememberAs))
+            {
+              if (!hiddenTypes.containsKey(calcId))
+              {
+                hiddenTypes.put(calcId, new ArrayList<List<String>>());
+              }
+              hiddenTypes.get(calcId).add(labelAsList);
+              addedToHidden.add(rememberAs);
+            }
           }
         }
       }
@@ -921,19 +976,27 @@ public class PopupMenu extends JPopupMenu
      * finally add the 'composite group labels' to the appropriate lists,
      * depending on whether the group is identified as visible or hidden
      */
-    for (int group : groupLabels.keySet())
+    for (String calcId : groupLabels.keySet())
     {
-      final List<String> groupLabel = groupLabels.get(group);
-      if (visibleGraphGroups.get(group))
+      for (int group : groupLabels.get(calcId).keySet())
       {
-        if (!shownTypes.contains(groupLabel))
+        final List<String> groupLabel = groupLabels.get(calcId).get(group);
+        if (visibleGraphGroups.get(group))
         {
-          shownTypes.add(groupLabel);
+          if (!shownTypes.containsKey(calcId))
+          {
+            shownTypes.put(calcId, new ArrayList<List<String>>());
+          }
+          shownTypes.get(calcId).add(groupLabel);
+        }
+        else
+        {
+          if (!hiddenTypes.containsKey(calcId))
+          {
+            hiddenTypes.put(calcId, new ArrayList<List<String>>());
+          }
+          hiddenTypes.get(calcId).add(groupLabel);
         }
-      }
-      else if (!hiddenTypes.contains(groupLabel))
-      {
-        hiddenTypes.add(groupLabel);
       }
     }
   }
@@ -976,6 +1039,7 @@ public class PopupMenu extends JPopupMenu
    * 
    * @param showOrHideMenu
    *          the menu to add to
+   * @param calcId
    * @param types
    *          the label to add
    * @param allTypes
@@ -985,12 +1049,13 @@ public class PopupMenu extends JPopupMenu
    *          type, else hide
    */
   protected void addAnnotationTypeToShowHide(JMenu showOrHideMenu,
-          final Collection<String> types, final boolean allTypes,
+          String calcId, final List<String> types, final boolean allTypes,
           final boolean actionIsShow)
   {
     String label = types.toString(); // [a, b, c]
     label = label.substring(1, label.length() - 1);
     final JMenuItem item = new JMenuItem(label);
+    item.setToolTipText(calcId);
     item.addActionListener(new java.awt.event.ActionListener()
     {
       @Override
@@ -1011,12 +1076,14 @@ public class PopupMenu extends JPopupMenu
    * @param doShow
    */
   protected void showHideAnnotation_actionPerformed(
-          Collection<String> types,
-          boolean anyType, boolean doShow)
+          Collection<String> types, boolean anyType, boolean doShow)
   {
     for (AlignmentAnnotation aa : ap.getAlignment()
             .getAlignmentAnnotation())
     {
+      // TODO: select by calcId (source of annotation) as well as label
+      // e.g. by refactoring of buildAnnotationTypeMenus to as
+      // to construct the actionPerformed methods as the calcId/labels are found
       if (anyType || types.contains(aa.label))
       {
         if ((aa.sequenceRef != null)
@@ -1519,6 +1586,7 @@ public class PopupMenu extends JPopupMenu
             .getString("label.show_annotations"));
     hideAnnotationsMenu.setText(MessageManager
             .getString("label.hide_annotations"));
+    configureReferenceAnnotationsMenu();
     sequenceFeature.setText(MessageManager
             .getString("label.create_sequence_feature"));
     sequenceFeature.addActionListener(new ActionListener()
@@ -1567,6 +1635,7 @@ public class PopupMenu extends JPopupMenu
     // groupMenu.add(chooseAnnotations);
     groupMenu.add(showAnnotationsMenu);
     groupMenu.add(hideAnnotationsMenu);
+    groupMenu.add(addDatasequenceAnnotations);
     groupMenu.add(editMenu);
     groupMenu.add(outputMenu);
     groupMenu.add(sequenceFeature);
@@ -1800,6 +1869,138 @@ public class PopupMenu extends JPopupMenu
             });
   }
 
+  /**
+   * Check for any annotations on the underlying dataset sequences (for the
+   * current selection group) which are not on the alignment. If any are found,
+   * enable the option to add them to the alignment. The criteria for 'on the
+   * alignment' is finding an annotation that matches on
+   * sequenceRef.datasetSequence, calcId and label.
+   */
+  protected void configureReferenceAnnotationsMenu()
+  {
+    addDatasequenceAnnotations.setText(MessageManager
+            .getString("label.add_reference_annotations"));
+    addDatasequenceAnnotations.setEnabled(false);
+
+    /*
+     * Temporary store so we can write distinct calcId / type pairs on the
+     * tooltip.
+     */
+    Map<String, String> tipEntries = new HashMap<String, String>();
+    StringBuilder tooltip = new StringBuilder(64);
+    tooltip.append(MessageManager.getString("label.add_annotations_for"));
+
+    // this menu option only applies for a Selection
+    if (this.ap.av.getSelectionGroup() == null)
+    {
+      return;
+    }
+
+    /*
+     * For each sequence selected in the alignment, make a list of any
+     * annotations on the underlying dataset sequence which are not already on
+     * the sequence in the alignment.
+     * 
+     * Build a map of { alignmentSequence, <List of annotations to add> }
+     */
+    final Map<SequenceI, List<AlignmentAnnotation>> candidates = new HashMap<SequenceI, List<AlignmentAnnotation>>();
+    for (SequenceI seq : this.ap.av.getSelectionGroup().getSequences())
+    {
+      SequenceI dataset = seq.getDatasetSequence();
+      if (dataset == null)
+      {
+        continue;
+      }
+      AlignmentAnnotation[] datasetAnnotations = dataset.getAnnotation();
+      if (datasetAnnotations == null)
+      {
+        continue;
+      }
+      final List<AlignmentAnnotation> result = new ArrayList<AlignmentAnnotation>();
+      for (AlignmentAnnotation dsann : datasetAnnotations)
+      {
+        /*
+         * If the sequence has no annotation that matches this one, then add
+         * this one to the results list.
+         */
+        if (seq.getAlignmentAnnotations(dsann.getCalcId(), dsann.label)
+                .isEmpty())
+        {
+          result.add(dsann);
+          tipEntries.put(dsann.getCalcId(), dsann.label);
+        }
+      }
+      /*
+       * Save any addable annotations for this sequence
+       */
+      if (!result.isEmpty())
+      {
+        candidates.put(seq, result);
+      }
+    }
+    if (!candidates.isEmpty())
+    {
+      /*
+       * Found annotations that could be added. Enable the menu item, and
+       * configure its tooltip and action.
+       */
+      addDatasequenceAnnotations.setEnabled(true);
+      for (String calcId : tipEntries.keySet())
+      {
+        tooltip.append("<br/>" + calcId + "/" + tipEntries.get(calcId));
+      }
+      String tooltipText = JvSwingUtils.wrapTooltip(true,
+              tooltip.toString());
+      addDatasequenceAnnotations.setToolTipText(tooltipText);
+
+      addDatasequenceAnnotations.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          addReferenceAnnotations_actionPerformed(candidates);
+        }
+      });
+    }
+  }
+
+  /**
+   * Add annotations to the sequences and to the alignment.
+   * 
+   * @param candidates
+   *          a map whose keys are sequences on the alignment, and values a list
+   *          of annotations to add to each sequence
+   */
+  protected void addReferenceAnnotations_actionPerformed(
+          Map<SequenceI, List<AlignmentAnnotation>> candidates)
+  {
+    for (SequenceI seq : candidates.keySet())
+    {
+      for (AlignmentAnnotation ann : candidates.get(seq))
+      {
+        AlignmentAnnotation copyAnn = new AlignmentAnnotation(ann);
+        int startRes = 0;
+        int endRes = ann.annotations.length;
+        final SequenceGroup selectionGroup = this.ap.av.getSelectionGroup();
+        if (selectionGroup != null)
+        {
+          startRes = selectionGroup.getStartRes();
+          endRes = selectionGroup.getEndRes();
+        }
+        copyAnn.restrict(startRes, endRes);
+
+        // add to the sequence (sets correct copyAnn.datasetSequence)
+        seq.addAlignmentAnnotation(copyAnn);
+        // adjust for gaps
+        copyAnn.adjustForAlignment();
+        // add to the alignment and set visible
+        this.ap.getAlignment().addAnnotation(copyAnn);
+        copyAnn.visible = true;
+      }
+    }
+    refresh();
+  }
+
   protected void sequenceSelectionDetails_actionPerformed()
   {
     createSequenceDetailsReport(ap.av.getSequenceSelection());