JAL-3181 refactor: extracted buildLinkMenu() for testability
[jalview.git] / src / jalview / gui / PopupMenu.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.AAFrequency;
24 import jalview.analysis.AlignmentAnnotationUtils;
25 import jalview.analysis.AlignmentUtils;
26 import jalview.analysis.Conservation;
27 import jalview.bin.Cache;
28 import jalview.commands.ChangeCaseCommand;
29 import jalview.commands.EditCommand;
30 import jalview.commands.EditCommand.Action;
31 import jalview.datamodel.AlignmentAnnotation;
32 import jalview.datamodel.AlignmentI;
33 import jalview.datamodel.Annotation;
34 import jalview.datamodel.DBRefEntry;
35 import jalview.datamodel.HiddenColumns;
36 import jalview.datamodel.PDBEntry;
37 import jalview.datamodel.SequenceFeature;
38 import jalview.datamodel.SequenceGroup;
39 import jalview.datamodel.SequenceI;
40 import jalview.gui.ColourMenuHelper.ColourChangeListener;
41 import jalview.io.FileFormatI;
42 import jalview.io.FileFormats;
43 import jalview.io.FormatAdapter;
44 import jalview.io.SequenceAnnotationReport;
45 import jalview.schemes.Blosum62ColourScheme;
46 import jalview.schemes.ColourSchemeI;
47 import jalview.schemes.ColourSchemes;
48 import jalview.schemes.PIDColourScheme;
49 import jalview.util.GroupUrlLink;
50 import jalview.util.GroupUrlLink.UrlStringTooLongException;
51 import jalview.util.MessageManager;
52 import jalview.util.StringUtils;
53 import jalview.util.UrlLink;
54
55 import java.awt.Color;
56 import java.awt.event.ActionEvent;
57 import java.awt.event.ActionListener;
58 import java.util.ArrayList;
59 import java.util.Arrays;
60 import java.util.BitSet;
61 import java.util.Collection;
62 import java.util.Collections;
63 import java.util.Hashtable;
64 import java.util.LinkedHashMap;
65 import java.util.List;
66 import java.util.Map;
67 import java.util.SortedMap;
68 import java.util.TreeMap;
69 import java.util.Vector;
70
71 import javax.swing.JCheckBoxMenuItem;
72 import javax.swing.JColorChooser;
73 import javax.swing.JMenu;
74 import javax.swing.JMenuItem;
75 import javax.swing.JPopupMenu;
76
77 /**
78  * The popup menu that is displayed on right-click on a sequence id, or in the
79  * sequence alignment.
80  */
81 public class PopupMenu extends JPopupMenu implements ColourChangeListener
82 {
83   JMenu groupMenu = new JMenu();
84
85   JMenuItem groupName = new JMenuItem();
86
87   protected JCheckBoxMenuItem abovePIDColour = new JCheckBoxMenuItem();
88
89   protected JMenuItem modifyPID = new JMenuItem();
90
91   protected JCheckBoxMenuItem conservationMenuItem = new JCheckBoxMenuItem();
92
93   protected JMenuItem modifyConservation = new JMenuItem();
94
95   AlignmentPanel ap;
96
97   JMenu sequenceMenu = new JMenu();
98
99   JMenuItem sequenceName = new JMenuItem();
100
101   JMenuItem sequenceDetails = new JMenuItem();
102
103   JMenuItem sequenceSelDetails = new JMenuItem();
104
105   JMenuItem makeReferenceSeq = new JMenuItem();
106
107   JMenuItem chooseAnnotations = new JMenuItem();
108
109   SequenceI sequence;
110
111   JMenuItem createGroupMenuItem = new JMenuItem();
112
113   JMenuItem unGroupMenuItem = new JMenuItem();
114
115   JMenuItem outline = new JMenuItem();
116
117   JMenu colourMenu = new JMenu();
118
119   JCheckBoxMenuItem showBoxes = new JCheckBoxMenuItem();
120
121   JCheckBoxMenuItem showText = new JCheckBoxMenuItem();
122
123   JCheckBoxMenuItem showColourText = new JCheckBoxMenuItem();
124
125   JCheckBoxMenuItem displayNonconserved = new JCheckBoxMenuItem();
126
127   JMenu editMenu = new JMenu();
128
129   JMenuItem cut = new JMenuItem();
130
131   JMenuItem copy = new JMenuItem();
132
133   JMenuItem upperCase = new JMenuItem();
134
135   JMenuItem lowerCase = new JMenuItem();
136
137   JMenuItem toggle = new JMenuItem();
138
139   JMenu pdbMenu = new JMenu();
140
141   JMenu outputMenu = new JMenu();
142
143   JMenu seqShowAnnotationsMenu = new JMenu();
144
145   JMenu seqHideAnnotationsMenu = new JMenu();
146
147   JMenuItem seqAddReferenceAnnotations = new JMenuItem(
148           MessageManager.getString("label.add_reference_annotations"));
149
150   JMenu groupShowAnnotationsMenu = new JMenu();
151
152   JMenu groupHideAnnotationsMenu = new JMenu();
153
154   JMenuItem groupAddReferenceAnnotations = new JMenuItem(
155           MessageManager.getString("label.add_reference_annotations"));
156
157   JMenuItem sequenceFeature = new JMenuItem();
158
159   JMenuItem textColour = new JMenuItem();
160
161   JMenu jMenu1 = new JMenu();
162
163   JMenuItem pdbStructureDialog = new JMenuItem();
164
165   JMenu rnaStructureMenu = new JMenu();
166
167   JMenuItem editSequence = new JMenuItem();
168
169   JMenu groupLinksMenu;
170
171   JMenuItem hideInsertions = new JMenuItem();
172
173   /**
174    * Constructs a menu with sub-menu items for any hyperlinks for the sequence
175    * and/or features provided. Hyperlinks may include a lookup by sequence id,
176    * or database cross-references, depending on which links are enabled in user
177    * preferences.
178    * 
179    * @param seq
180    * @param features
181    * @return
182    */
183   static JMenu buildLinkMenu(final SequenceI seq,
184           List<SequenceFeature> features)
185   {
186     JMenu linkMenu = new JMenu(MessageManager.getString("action.link"));
187
188     List<String> nlinks = null;
189     if (seq != null)
190     {
191       nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
192       UrlLink.sort(nlinks);
193     }
194     else
195     {
196       nlinks = new ArrayList<>();
197     }
198
199     if (features != null)
200     {
201       for (SequenceFeature sf : features)
202       {
203         if (sf.links != null)
204         {
205           for (String link : sf.links)
206           {
207             nlinks.add(link);
208           }
209         }
210       }
211     }
212
213     /*
214      * instantiate the hyperlinklink templates from sequence data;
215      * note the order of the templates is preserved in the map
216      */
217     Map<String, List<String>> linkset = new LinkedHashMap<>();
218     for (String link : nlinks)
219     {
220       UrlLink urlLink = null;
221       try
222       {
223         urlLink = new UrlLink(link);
224       } catch (Exception foo)
225       {
226         Cache.log.error("Exception for URLLink '" + link + "'", foo);
227         continue;
228       }
229
230       if (!urlLink.isValid())
231       {
232         Cache.log.error(urlLink.getInvalidMessage());
233         continue;
234       }
235
236       urlLink.createLinksFromSeq(seq, linkset);
237     }
238
239     /*
240      * construct menu items for the hyperlinks (still preserving
241      * the order of the sorted templates)
242      */
243     addUrlLinks(linkMenu, linkset.values());
244
245     return linkMenu;
246   }
247
248   /**
249    * A helper method that builds menu items from the given links, with action
250    * handlers to open the link URL, and adds them to the linkMenu. Each provided
251    * link should be a list whose second item is the menu text, and whose fourth
252    * item is the URL to open when the menu item is selected.
253    * 
254    * @param linkMenu
255    * @param linkset
256    */
257   static private void addUrlLinks(JMenu linkMenu,
258           Collection<List<String>> linkset)
259   {
260     for (List<String> linkstrset : linkset)
261     {
262       final String url = linkstrset.get(3);
263       JMenuItem item = new JMenuItem(linkstrset.get(1));
264       item.setToolTipText(MessageManager
265               .formatMessage("label.open_url_param", new Object[]
266               { url }));
267       item.addActionListener(new ActionListener()
268       {
269         @Override
270         public void actionPerformed(ActionEvent e)
271         {
272           new Thread(new Runnable()
273           {
274             @Override
275             public void run()
276             {
277               showLink(url);
278             }
279           }).start();
280         }
281       });
282       linkMenu.add(item);
283     }
284   }
285
286   /**
287    * Opens the provided url in the default web browser, or shows an error
288    * message if this fails
289    * 
290    * @param url
291    */
292   static void showLink(String url)
293   {
294     try
295     {
296       jalview.util.BrowserLauncher.openURL(url);
297     } catch (Exception ex)
298     {
299       JvOptionPane.showInternalMessageDialog(Desktop.desktop,
300               MessageManager.getString("label.web_browser_not_found_unix"),
301               MessageManager.getString("label.web_browser_not_found"),
302               JvOptionPane.WARNING_MESSAGE);
303
304       ex.printStackTrace();
305     }
306   }
307
308   /**
309    * add a late bound groupURL item to the given linkMenu
310    * 
311    * @param linkMenu
312    * @param label
313    *          - menu label string
314    * @param urlgenerator
315    *          GroupURLLink used to generate URL
316    * @param urlstub
317    *          Object array returned from the makeUrlStubs function.
318    */
319   static void addshowLink(JMenu linkMenu, String label,
320           final GroupUrlLink urlgenerator, final Object[] urlstub)
321   {
322     JMenuItem item = new JMenuItem(label);
323     item.setToolTipText(MessageManager
324             .formatMessage("label.open_url_seqs_param", new Object[]
325             { urlgenerator.getUrl_prefix(),
326                 urlgenerator.getNumberInvolved(urlstub) }));
327     // TODO: put in info about what is being sent.
328     item.addActionListener(new ActionListener()
329     {
330       @Override
331       public void actionPerformed(ActionEvent e)
332       {
333         new Thread(new Runnable()
334         {
335
336           @Override
337           public void run()
338           {
339             try
340             {
341               showLink(urlgenerator.constructFrom(urlstub));
342             } catch (UrlStringTooLongException e2)
343             {
344             }
345           }
346
347         }).start();
348       }
349     });
350
351     linkMenu.add(item);
352   }
353
354   /**
355    * Creates a new PopupMenu object.
356    * 
357    * @param ap
358    * @param seq
359    * @param features
360    *          non-positional features (for seq not null), or positional features
361    *          at residue (for seq equal to null)
362    */
363   public PopupMenu(final AlignmentPanel ap, SequenceI seq,
364           List<SequenceFeature> features)
365   {
366     this(ap, seq, features, null);
367   }
368
369   /**
370    * Constructor
371    * 
372    * @param alignPanel
373    * @param seq
374    *          the sequence under the cursor if in the Id panel, null if in the
375    *          sequence panel
376    * @param features
377    *          non-positional features if in the Id panel, features at the
378    *          clicked residue if in the sequence panel
379    * @param groupLinks
380    */
381   public PopupMenu(final AlignmentPanel alignPanel, final SequenceI seq,
382           List<SequenceFeature> features, List<String> groupLinks)
383   {
384     // /////////////////////////////////////////////////////////
385     // If this is activated from the sequence panel, the user may want to
386     // edit or annotate a particular residue. Therefore display the residue menu
387     //
388     // If from the IDPanel, we must display the sequence menu
389     // ////////////////////////////////////////////////////////
390     this.ap = alignPanel;
391     sequence = seq;
392
393     for (String ff : FileFormats.getInstance().getWritableFormats(true))
394     {
395       JMenuItem item = new JMenuItem(ff);
396
397       item.addActionListener(new ActionListener()
398       {
399         @Override
400         public void actionPerformed(ActionEvent e)
401         {
402           outputText_actionPerformed(e);
403         }
404       });
405
406       outputMenu.add(item);
407     }
408
409     /*
410      * Build menus for annotation types that may be shown or hidden, and for
411      * 'reference annotations' that may be added to the alignment. First for the
412      * currently selected sequence (if there is one):
413      */
414     final List<SequenceI> selectedSequence = (seq == null
415             ? Collections.<SequenceI> emptyList()
416             : Arrays.asList(seq));
417     buildAnnotationTypesMenus(seqShowAnnotationsMenu,
418             seqHideAnnotationsMenu, selectedSequence);
419     configureReferenceAnnotationsMenu(seqAddReferenceAnnotations,
420             selectedSequence);
421
422     /*
423      * And repeat for the current selection group (if there is one):
424      */
425     final List<SequenceI> selectedGroup = (alignPanel.av.getSelectionGroup() == null
426             ? Collections.<SequenceI> emptyList()
427             : alignPanel.av.getSelectionGroup().getSequences());
428     buildAnnotationTypesMenus(groupShowAnnotationsMenu,
429             groupHideAnnotationsMenu, selectedGroup);
430     configureReferenceAnnotationsMenu(groupAddReferenceAnnotations,
431             selectedGroup);
432
433     try
434     {
435       jbInit();
436     } catch (Exception e)
437     {
438       e.printStackTrace();
439     }
440
441     JMenuItem menuItem;
442     if (seq != null)
443     {
444       sequenceMenu.setText(sequence.getName());
445       if (seq == alignPanel.av.getAlignment().getSeqrep())
446       {
447         makeReferenceSeq.setText(
448                 MessageManager.getString("action.unmark_as_reference"));
449       }
450       else
451       {
452         makeReferenceSeq.setText(
453                 MessageManager.getString("action.set_as_reference"));
454       }
455
456       if (!alignPanel.av.getAlignment().isNucleotide())
457       {
458         remove(rnaStructureMenu);
459       }
460       else
461       {
462         int origCount = rnaStructureMenu.getItemCount();
463         /*
464          * add menu items to 2D-render any alignment or sequence secondary
465          * structure annotation
466          */
467         AlignmentAnnotation[] aas = alignPanel.av.getAlignment()
468                 .getAlignmentAnnotation();
469         if (aas != null)
470         {
471           for (final AlignmentAnnotation aa : aas)
472           {
473             if (aa.isValidStruc() && aa.sequenceRef == null)
474             {
475               /*
476                * valid alignment RNA secondary structure annotation
477                */
478               menuItem = new JMenuItem();
479               menuItem.setText(MessageManager.formatMessage(
480                       "label.2d_rna_structure_line", new Object[]
481                       { aa.label }));
482               menuItem.addActionListener(new ActionListener()
483               {
484                 @Override
485                 public void actionPerformed(ActionEvent e)
486                 {
487                   new AppVarna(seq, aa, alignPanel);
488                 }
489               });
490               rnaStructureMenu.add(menuItem);
491             }
492           }
493         }
494
495         if (seq.getAnnotation() != null)
496         {
497           AlignmentAnnotation seqAnns[] = seq.getAnnotation();
498           for (final AlignmentAnnotation aa : seqAnns)
499           {
500             if (aa.isValidStruc())
501             {
502               /*
503                * valid sequence RNA secondary structure annotation
504                */
505               // TODO: make rnastrucF a bit more nice
506               menuItem = new JMenuItem();
507               menuItem.setText(MessageManager.formatMessage(
508                       "label.2d_rna_sequence_name", new Object[]
509                       { seq.getName() }));
510               menuItem.addActionListener(new ActionListener()
511               {
512                 @Override
513                 public void actionPerformed(ActionEvent e)
514                 {
515                   // TODO: VARNA does'nt print gaps in the sequence
516                   new AppVarna(seq, aa, alignPanel);
517                 }
518               });
519               rnaStructureMenu.add(menuItem);
520             }
521           }
522         }
523         if (rnaStructureMenu.getItemCount() == origCount)
524         {
525           remove(rnaStructureMenu);
526         }
527       }
528
529       menuItem = new JMenuItem(
530               MessageManager.getString("action.hide_sequences"));
531       menuItem.addActionListener(new ActionListener()
532       {
533         @Override
534         public void actionPerformed(ActionEvent e)
535         {
536           hideSequences(false);
537         }
538       });
539       add(menuItem);
540
541       if (alignPanel.av.getSelectionGroup() != null
542               && alignPanel.av.getSelectionGroup().getSize() > 1)
543       {
544         menuItem = new JMenuItem(MessageManager
545                 .formatMessage("label.represent_group_with", new Object[]
546                 { seq.getName() }));
547         menuItem.addActionListener(new ActionListener()
548         {
549           @Override
550           public void actionPerformed(ActionEvent e)
551           {
552             hideSequences(true);
553           }
554         });
555         sequenceMenu.add(menuItem);
556       }
557
558       if (alignPanel.av.hasHiddenRows())
559       {
560         final int index = alignPanel.av.getAlignment().findIndex(seq);
561
562         if (alignPanel.av.adjustForHiddenSeqs(index)
563                 - alignPanel.av.adjustForHiddenSeqs(index - 1) > 1)
564         {
565           menuItem = new JMenuItem(
566                   MessageManager.getString("action.reveal_sequences"));
567           menuItem.addActionListener(new ActionListener()
568           {
569             @Override
570             public void actionPerformed(ActionEvent e)
571             {
572               alignPanel.av.showSequence(index);
573               if (alignPanel.overviewPanel != null)
574               {
575                 alignPanel.overviewPanel.updateOverviewImage();
576               }
577             }
578           });
579           add(menuItem);
580         }
581       }
582     }
583     // for the case when no sequences are even visible
584     if (alignPanel.av.hasHiddenRows())
585     {
586       {
587         menuItem = new JMenuItem(
588                 MessageManager.getString("action.reveal_all"));
589         menuItem.addActionListener(new ActionListener()
590         {
591           @Override
592           public void actionPerformed(ActionEvent e)
593           {
594             alignPanel.av.showAllHiddenSeqs();
595             if (alignPanel.overviewPanel != null)
596             {
597               alignPanel.overviewPanel.updateOverviewImage();
598             }
599           }
600         });
601
602         add(menuItem);
603       }
604     }
605
606     SequenceGroup sg = alignPanel.av.getSelectionGroup();
607     boolean isDefinedGroup = (sg != null)
608             ? alignPanel.av.getAlignment().getGroups().contains(sg)
609             : false;
610
611     if (sg != null && sg.getSize() > 0)
612     {
613       groupName.setText(MessageManager
614               .getString("label.edit_name_and_description_current_group"));
615
616       ColourMenuHelper.setColourSelected(colourMenu, sg.getColourScheme());
617
618       conservationMenuItem.setEnabled(!sg.isNucleotide());
619
620       if (sg.cs != null)
621       {
622         if (sg.cs.conservationApplied())
623         {
624           conservationMenuItem.setSelected(true);
625         }
626         if (sg.cs.getThreshold() > 0)
627         {
628           abovePIDColour.setSelected(true);
629         }
630       }
631       modifyConservation.setEnabled(conservationMenuItem.isSelected());
632       modifyPID.setEnabled(abovePIDColour.isSelected());
633       displayNonconserved.setSelected(sg.getShowNonconserved());
634       showText.setSelected(sg.getDisplayText());
635       showColourText.setSelected(sg.getColourText());
636       showBoxes.setSelected(sg.getDisplayBoxes());
637       // add any groupURLs to the groupURL submenu and make it visible
638       if (groupLinks != null && groupLinks.size() > 0)
639       {
640         buildGroupURLMenu(sg, groupLinks);
641       }
642       // Add a 'show all structures' for the current selection
643       Hashtable<String, PDBEntry> pdbe = new Hashtable<>(), reppdb = new Hashtable<>();
644
645       SequenceI sqass = null;
646       for (SequenceI sq : alignPanel.av.getSequenceSelection())
647       {
648         Vector<PDBEntry> pes = sq.getDatasetSequence().getAllPDBEntries();
649         if (pes != null && pes.size() > 0)
650         {
651           reppdb.put(pes.get(0).getId(), pes.get(0));
652           for (PDBEntry pe : pes)
653           {
654             pdbe.put(pe.getId(), pe);
655             if (sqass == null)
656             {
657               sqass = sq;
658             }
659           }
660         }
661       }
662       if (pdbe.size() > 0)
663       {
664         final PDBEntry[] pe = pdbe.values()
665                 .toArray(new PDBEntry[pdbe.size()]),
666                 pr = reppdb.values().toArray(new PDBEntry[reppdb.size()]);
667         final JMenuItem gpdbview, rpdbview;
668       }
669     }
670     else
671     {
672       groupMenu.setVisible(false);
673       editMenu.setVisible(false);
674     }
675
676     if (!isDefinedGroup)
677     {
678       createGroupMenuItem.setVisible(true);
679       unGroupMenuItem.setVisible(false);
680       jMenu1.setText(MessageManager.getString("action.edit_new_group"));
681     }
682     else
683     {
684       createGroupMenuItem.setVisible(false);
685       unGroupMenuItem.setVisible(true);
686       jMenu1.setText(MessageManager.getString("action.edit_group"));
687     }
688
689     if (seq == null)
690     {
691       sequenceMenu.setVisible(false);
692       pdbStructureDialog.setVisible(false);
693       rnaStructureMenu.setVisible(false);
694     }
695
696     addLinks(seq, features);
697
698     if (seq == null)
699     {
700       addFeatureDetails(features);
701     }
702   }
703
704   /**
705    * Add a link to show feature details for each sequence feature
706    * 
707    * @param features
708    */
709   protected void addFeatureDetails(List<SequenceFeature> features)
710   {
711     if (features == null || features.isEmpty())
712     {
713       return;
714     }
715     JMenu details = new JMenu(
716             MessageManager.getString("label.feature_details"));
717     add(details);
718
719     for (final SequenceFeature sf : features)
720     {
721       int start = sf.getBegin();
722       int end = sf.getEnd();
723       String desc = null;
724       if (start == end)
725       {
726         desc = String.format("%s %d", sf.getType(), start);
727       }
728       else
729       {
730         desc = String.format("%s %d-%d", sf.getType(), start, end);
731       }
732       String tooltip = desc;
733       String description = sf.getDescription();
734       if (description != null)
735       {
736         description = StringUtils.stripHtmlTags(description);
737         if (description.length() > 12)
738         {
739           desc = desc + " " + description.substring(0, 12) + "..";
740         }
741         else
742         {
743           desc = desc + " " + description;
744         }
745         tooltip = tooltip + " " + description;
746       }
747       if (sf.getFeatureGroup() != null)
748       {
749         tooltip = tooltip + (" (" + sf.getFeatureGroup() + ")");
750       }
751       JMenuItem item = new JMenuItem(desc);
752       item.setToolTipText(tooltip);
753       item.addActionListener(new ActionListener()
754       {
755         @Override
756         public void actionPerformed(ActionEvent e)
757         {
758           showFeatureDetails(sf);
759         }
760       });
761       details.add(item);
762     }
763   }
764
765   /**
766    * Opens a panel showing a text report of feature dteails
767    * 
768    * @param sf
769    */
770   protected void showFeatureDetails(SequenceFeature sf)
771   {
772     CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer();
773     // it appears Java's CSS does not support border-collaps :-(
774     cap.addStylesheetRule("table { border-collapse: collapse;}");
775     cap.addStylesheetRule("table, td, th {border: 1px solid black;}");
776     cap.setText(sf.getDetailsReport());
777
778     Desktop.addInternalFrame(cap,
779             MessageManager.getString("label.feature_details"), 500, 500);
780   }
781
782   /**
783    * Adds a 'Link' menu item with a sub-menu item for each hyperlink provided.
784    * When seq is not null, these are links for the sequence id, which may be to
785    * external web sites for the sequence accession, and/or links embedded in
786    * non-positional features. When seq is null, only links embedded in the
787    * provided features are added. If no links are found, the menu is not added.
788    * 
789    * @param seq
790    * @param features
791    */
792   void addLinks(final SequenceI seq, List<SequenceFeature> features)
793   {
794     JMenu linkMenu = buildLinkMenu(seq, features);
795
796     // only add link menu if it has entries
797     if (linkMenu.getItemCount() > 0)
798     {
799       if (sequence != null)
800       {
801         sequenceMenu.add(linkMenu);
802       }
803       else
804       {
805         add(linkMenu);
806       }
807     }
808   }
809
810   /**
811    * Add annotation types to 'Show annotations' and/or 'Hide annotations' menus.
812    * "All" is added first, followed by a separator. Then add any annotation
813    * types associated with the current selection. Separate menus are built for
814    * the selected sequence group (if any), and the selected sequence.
815    * <p>
816    * Some annotation rows are always rendered together - these can be identified
817    * by a common graphGroup property > -1. Only one of each group will be marked
818    * as visible (to avoid duplication of the display). For such groups we add a
819    * composite type name, e.g.
820    * <p>
821    * IUPredWS (Long), IUPredWS (Short)
822    * 
823    * @param seq
824    */
825   protected void buildAnnotationTypesMenus(JMenu showMenu, JMenu hideMenu,
826           List<SequenceI> forSequences)
827   {
828     showMenu.removeAll();
829     hideMenu.removeAll();
830
831     final List<String> all = Arrays
832             .asList(new String[]
833             { MessageManager.getString("label.all") });
834     addAnnotationTypeToShowHide(showMenu, forSequences, "", all, true,
835             true);
836     addAnnotationTypeToShowHide(hideMenu, forSequences, "", all, true,
837             false);
838     showMenu.addSeparator();
839     hideMenu.addSeparator();
840
841     final AlignmentAnnotation[] annotations = ap.getAlignment()
842             .getAlignmentAnnotation();
843
844     /*
845      * Find shown/hidden annotations types, distinguished by source (calcId),
846      * and grouped by graphGroup. Using LinkedHashMap means we will retrieve in
847      * the insertion order, which is the order of the annotations on the
848      * alignment.
849      */
850     Map<String, List<List<String>>> shownTypes = new LinkedHashMap<>();
851     Map<String, List<List<String>>> hiddenTypes = new LinkedHashMap<>();
852     AlignmentAnnotationUtils.getShownHiddenTypes(shownTypes, hiddenTypes,
853             AlignmentAnnotationUtils.asList(annotations), forSequences);
854
855     for (String calcId : hiddenTypes.keySet())
856     {
857       for (List<String> type : hiddenTypes.get(calcId))
858       {
859         addAnnotationTypeToShowHide(showMenu, forSequences, calcId, type,
860                 false, true);
861       }
862     }
863     // grey out 'show annotations' if none are hidden
864     showMenu.setEnabled(!hiddenTypes.isEmpty());
865
866     for (String calcId : shownTypes.keySet())
867     {
868       for (List<String> type : shownTypes.get(calcId))
869       {
870         addAnnotationTypeToShowHide(hideMenu, forSequences, calcId, type,
871                 false, false);
872       }
873     }
874     // grey out 'hide annotations' if none are shown
875     hideMenu.setEnabled(!shownTypes.isEmpty());
876   }
877
878   /**
879    * Returns a list of sequences - either the current selection group (if there
880    * is one), else the specified single sequence.
881    * 
882    * @param seq
883    * @return
884    */
885   protected List<SequenceI> getSequenceScope(SequenceI seq)
886   {
887     List<SequenceI> forSequences = null;
888     final SequenceGroup selectionGroup = ap.av.getSelectionGroup();
889     if (selectionGroup != null && selectionGroup.getSize() > 0)
890     {
891       forSequences = selectionGroup.getSequences();
892     }
893     else
894     {
895       forSequences = seq == null ? Collections.<SequenceI> emptyList()
896               : Arrays.asList(seq);
897     }
898     return forSequences;
899   }
900
901   /**
902    * Add one annotation type to the 'Show Annotations' or 'Hide Annotations'
903    * menus.
904    * 
905    * @param showOrHideMenu
906    *          the menu to add to
907    * @param forSequences
908    *          the sequences whose annotations may be shown or hidden
909    * @param calcId
910    * @param types
911    *          the label to add
912    * @param allTypes
913    *          if true this is a special label meaning 'All'
914    * @param actionIsShow
915    *          if true, the select menu item action is to show the annotation
916    *          type, else hide
917    */
918   protected void addAnnotationTypeToShowHide(JMenu showOrHideMenu,
919           final List<SequenceI> forSequences, String calcId,
920           final List<String> types, final boolean allTypes,
921           final boolean actionIsShow)
922   {
923     String label = types.toString(); // [a, b, c]
924     label = label.substring(1, label.length() - 1); // a, b, c
925     final JMenuItem item = new JMenuItem(label);
926     item.setToolTipText(calcId);
927     item.addActionListener(new ActionListener()
928     {
929       @Override
930       public void actionPerformed(ActionEvent e)
931       {
932         AlignmentUtils.showOrHideSequenceAnnotations(ap.getAlignment(),
933                 types, forSequences, allTypes, actionIsShow);
934         refresh();
935       }
936     });
937     showOrHideMenu.add(item);
938   }
939
940   private void buildGroupURLMenu(SequenceGroup sg, List<String> groupLinks)
941   {
942
943     // TODO: usability: thread off the generation of group url content so root
944     // menu appears asap
945     // sequence only URLs
946     // ID/regex match URLs
947     groupLinksMenu = new JMenu(
948             MessageManager.getString("action.group_link"));
949     // three types of url that might be created.
950     JMenu[] linkMenus = new JMenu[] { null,
951         new JMenu(MessageManager.getString("action.ids")),
952         new JMenu(MessageManager.getString("action.sequences")),
953         new JMenu(MessageManager.getString("action.ids_sequences")) };
954
955     SequenceI[] seqs = ap.av.getSelectionAsNewSequence();
956     String[][] idandseqs = GroupUrlLink.formStrings(seqs);
957     Hashtable<String, Object[]> commonDbrefs = new Hashtable<>();
958     for (int sq = 0; sq < seqs.length; sq++)
959     {
960
961       int start = seqs[sq].findPosition(sg.getStartRes()),
962               end = seqs[sq].findPosition(sg.getEndRes());
963       // just collect ids from dataset sequence
964       // TODO: check if IDs collected from selecton group intersects with the
965       // current selection, too
966       SequenceI sqi = seqs[sq];
967       while (sqi.getDatasetSequence() != null)
968       {
969         sqi = sqi.getDatasetSequence();
970       }
971       DBRefEntry[] dbr = sqi.getDBRefs();
972       if (dbr != null && dbr.length > 0)
973       {
974         for (int d = 0; d < dbr.length; d++)
975         {
976           String src = dbr[d].getSource(); // jalview.util.DBRefUtils.getCanonicalName(dbr[d].getSource()).toUpperCase();
977           Object[] sarray = commonDbrefs.get(src);
978           if (sarray == null)
979           {
980             sarray = new Object[2];
981             sarray[0] = new int[] { 0 };
982             sarray[1] = new String[seqs.length];
983
984             commonDbrefs.put(src, sarray);
985           }
986
987           if (((String[]) sarray[1])[sq] == null)
988           {
989             if (!dbr[d].hasMap() || (dbr[d].getMap()
990                     .locateMappedRange(start, end) != null))
991             {
992               ((String[]) sarray[1])[sq] = dbr[d].getAccessionId();
993               ((int[]) sarray[0])[0]++;
994             }
995           }
996         }
997       }
998     }
999     // now create group links for all distinct ID/sequence sets.
1000     boolean addMenu = false; // indicates if there are any group links to give
1001                              // to user
1002     for (String link : groupLinks)
1003     {
1004       GroupUrlLink urlLink = null;
1005       try
1006       {
1007         urlLink = new GroupUrlLink(link);
1008       } catch (Exception foo)
1009       {
1010         Cache.log.error("Exception for GroupURLLink '" + link + "'", foo);
1011         continue;
1012       }
1013       ;
1014       if (!urlLink.isValid())
1015       {
1016         Cache.log.error(urlLink.getInvalidMessage());
1017         continue;
1018       }
1019       final String label = urlLink.getLabel();
1020       boolean usingNames = false;
1021       // Now see which parts of the group apply for this URL
1022       String ltarget = urlLink.getTarget(); // jalview.util.DBRefUtils.getCanonicalName(urlLink.getTarget());
1023       Object[] idset = commonDbrefs.get(ltarget.toUpperCase());
1024       String[] seqstr, ids; // input to makeUrl
1025       if (idset != null)
1026       {
1027         int numinput = ((int[]) idset[0])[0];
1028         String[] allids = ((String[]) idset[1]);
1029         seqstr = new String[numinput];
1030         ids = new String[numinput];
1031         for (int sq = 0, idcount = 0; sq < seqs.length; sq++)
1032         {
1033           if (allids[sq] != null)
1034           {
1035             ids[idcount] = allids[sq];
1036             seqstr[idcount++] = idandseqs[1][sq];
1037           }
1038         }
1039       }
1040       else
1041       {
1042         // just use the id/seq set
1043         seqstr = idandseqs[1];
1044         ids = idandseqs[0];
1045         usingNames = true;
1046       }
1047       // and try and make the groupURL!
1048
1049       Object[] urlset = null;
1050       try
1051       {
1052         urlset = urlLink.makeUrlStubs(ids, seqstr,
1053                 "FromJalview" + System.currentTimeMillis(), false);
1054       } catch (UrlStringTooLongException e)
1055       {
1056       }
1057       if (urlset != null)
1058       {
1059         int type = urlLink.getGroupURLType() & 3;
1060         // first two bits ofurlLink type bitfield are sequenceids and sequences
1061         // TODO: FUTURE: ensure the groupURL menu structure can be generalised
1062         addshowLink(linkMenus[type],
1063                 label + (((type & 1) == 1)
1064                         ? ("(" + (usingNames ? "Names" : ltarget) + ")")
1065                         : ""),
1066                 urlLink, urlset);
1067         addMenu = true;
1068       }
1069     }
1070     if (addMenu)
1071     {
1072       groupLinksMenu = new JMenu(
1073               MessageManager.getString("action.group_link"));
1074       for (int m = 0; m < linkMenus.length; m++)
1075       {
1076         if (linkMenus[m] != null
1077                 && linkMenus[m].getMenuComponentCount() > 0)
1078         {
1079           groupLinksMenu.add(linkMenus[m]);
1080         }
1081       }
1082
1083       groupMenu.add(groupLinksMenu);
1084     }
1085   }
1086
1087   /**
1088    * DOCUMENT ME!
1089    * 
1090    * @throws Exception
1091    *           DOCUMENT ME!
1092    */
1093   private void jbInit() throws Exception
1094   {
1095     groupMenu.setText(MessageManager.getString("label.selection"));
1096     groupName.setText(MessageManager.getString("label.name"));
1097     groupName.addActionListener(new ActionListener()
1098     {
1099       @Override
1100       public void actionPerformed(ActionEvent e)
1101       {
1102         groupName_actionPerformed();
1103       }
1104     });
1105     sequenceMenu.setText(MessageManager.getString("label.sequence"));
1106     sequenceName.setText(
1107             MessageManager.getString("label.edit_name_description"));
1108     sequenceName.addActionListener(new ActionListener()
1109     {
1110       @Override
1111       public void actionPerformed(ActionEvent e)
1112       {
1113         sequenceName_actionPerformed();
1114       }
1115     });
1116     chooseAnnotations
1117             .setText(MessageManager.getString("action.choose_annotations"));
1118     chooseAnnotations.addActionListener(new ActionListener()
1119     {
1120       @Override
1121       public void actionPerformed(ActionEvent e)
1122       {
1123         chooseAnnotations_actionPerformed(e);
1124       }
1125     });
1126     sequenceDetails
1127             .setText(MessageManager.getString("label.sequence_details"));
1128     sequenceDetails.addActionListener(new ActionListener()
1129     {
1130       @Override
1131       public void actionPerformed(ActionEvent e)
1132       {
1133         sequenceDetails_actionPerformed();
1134       }
1135     });
1136     sequenceSelDetails
1137             .setText(MessageManager.getString("label.sequence_details"));
1138     sequenceSelDetails.addActionListener(new ActionListener()
1139     {
1140       @Override
1141       public void actionPerformed(ActionEvent e)
1142       {
1143         sequenceSelectionDetails_actionPerformed();
1144       }
1145     });
1146
1147     unGroupMenuItem
1148             .setText(MessageManager.getString("action.remove_group"));
1149     unGroupMenuItem.addActionListener(new ActionListener()
1150     {
1151       @Override
1152       public void actionPerformed(ActionEvent e)
1153       {
1154         unGroupMenuItem_actionPerformed();
1155       }
1156     });
1157     createGroupMenuItem
1158             .setText(MessageManager.getString("action.create_group"));
1159     createGroupMenuItem.addActionListener(new ActionListener()
1160     {
1161       @Override
1162       public void actionPerformed(ActionEvent e)
1163       {
1164         createGroupMenuItem_actionPerformed();
1165       }
1166     });
1167
1168     outline.setText(MessageManager.getString("action.border_colour"));
1169     outline.addActionListener(new ActionListener()
1170     {
1171       @Override
1172       public void actionPerformed(ActionEvent e)
1173       {
1174         outline_actionPerformed();
1175       }
1176     });
1177     showBoxes.setText(MessageManager.getString("action.boxes"));
1178     showBoxes.setState(true);
1179     showBoxes.addActionListener(new ActionListener()
1180     {
1181       @Override
1182       public void actionPerformed(ActionEvent e)
1183       {
1184         showBoxes_actionPerformed();
1185       }
1186     });
1187     showText.setText(MessageManager.getString("action.text"));
1188     showText.setState(true);
1189     showText.addActionListener(new ActionListener()
1190     {
1191       @Override
1192       public void actionPerformed(ActionEvent e)
1193       {
1194         showText_actionPerformed();
1195       }
1196     });
1197     showColourText.setText(MessageManager.getString("label.colour_text"));
1198     showColourText.addActionListener(new ActionListener()
1199     {
1200       @Override
1201       public void actionPerformed(ActionEvent e)
1202       {
1203         showColourText_actionPerformed();
1204       }
1205     });
1206     displayNonconserved
1207             .setText(MessageManager.getString("label.show_non_conserved"));
1208     displayNonconserved.setState(true);
1209     displayNonconserved.addActionListener(new ActionListener()
1210     {
1211       @Override
1212       public void actionPerformed(ActionEvent e)
1213       {
1214         showNonconserved_actionPerformed();
1215       }
1216     });
1217     editMenu.setText(MessageManager.getString("action.edit"));
1218     cut.setText(MessageManager.getString("action.cut"));
1219     cut.addActionListener(new ActionListener()
1220     {
1221       @Override
1222       public void actionPerformed(ActionEvent e)
1223       {
1224         cut_actionPerformed();
1225       }
1226     });
1227     upperCase.setText(MessageManager.getString("label.to_upper_case"));
1228     upperCase.addActionListener(new ActionListener()
1229     {
1230       @Override
1231       public void actionPerformed(ActionEvent e)
1232       {
1233         changeCase(e);
1234       }
1235     });
1236     copy.setText(MessageManager.getString("action.copy"));
1237     copy.addActionListener(new ActionListener()
1238     {
1239       @Override
1240       public void actionPerformed(ActionEvent e)
1241       {
1242         copy_actionPerformed();
1243       }
1244     });
1245     lowerCase.setText(MessageManager.getString("label.to_lower_case"));
1246     lowerCase.addActionListener(new ActionListener()
1247     {
1248       @Override
1249       public void actionPerformed(ActionEvent e)
1250       {
1251         changeCase(e);
1252       }
1253     });
1254     toggle.setText(MessageManager.getString("label.toggle_case"));
1255     toggle.addActionListener(new ActionListener()
1256     {
1257       @Override
1258       public void actionPerformed(ActionEvent e)
1259       {
1260         changeCase(e);
1261       }
1262     });
1263     outputMenu.setText(
1264             MessageManager.getString("label.out_to_textbox") + "...");
1265     seqShowAnnotationsMenu
1266             .setText(MessageManager.getString("label.show_annotations"));
1267     seqHideAnnotationsMenu
1268             .setText(MessageManager.getString("label.hide_annotations"));
1269     groupShowAnnotationsMenu
1270             .setText(MessageManager.getString("label.show_annotations"));
1271     groupHideAnnotationsMenu
1272             .setText(MessageManager.getString("label.hide_annotations"));
1273     sequenceFeature.setText(
1274             MessageManager.getString("label.create_sequence_feature"));
1275     sequenceFeature.addActionListener(new ActionListener()
1276     {
1277       @Override
1278       public void actionPerformed(ActionEvent e)
1279       {
1280         sequenceFeature_actionPerformed();
1281       }
1282     });
1283     jMenu1.setText(MessageManager.getString("label.group"));
1284     pdbStructureDialog.setText(
1285             MessageManager.getString("label.show_pdbstruct_dialog"));
1286     pdbStructureDialog.addActionListener(new ActionListener()
1287     {
1288       @Override
1289       public void actionPerformed(ActionEvent actionEvent)
1290       {
1291         SequenceI[] selectedSeqs = new SequenceI[] { sequence };
1292         if (ap.av.getSelectionGroup() != null)
1293         {
1294           selectedSeqs = ap.av.getSequenceSelection();
1295         }
1296         new StructureChooser(selectedSeqs, sequence, ap);
1297       }
1298     });
1299
1300     rnaStructureMenu
1301             .setText(MessageManager.getString("label.view_rna_structure"));
1302
1303     // colStructureMenu.setText("Colour By Structure");
1304     editSequence.setText(
1305             MessageManager.getString("label.edit_sequence") + "...");
1306     editSequence.addActionListener(new ActionListener()
1307     {
1308       @Override
1309       public void actionPerformed(ActionEvent actionEvent)
1310       {
1311         editSequence_actionPerformed(actionEvent);
1312       }
1313     });
1314     makeReferenceSeq.setText(
1315             MessageManager.getString("label.mark_as_representative"));
1316     makeReferenceSeq.addActionListener(new ActionListener()
1317     {
1318
1319       @Override
1320       public void actionPerformed(ActionEvent actionEvent)
1321       {
1322         makeReferenceSeq_actionPerformed(actionEvent);
1323
1324       }
1325     });
1326     hideInsertions
1327             .setText(MessageManager.getString("label.hide_insertions"));
1328     hideInsertions.addActionListener(new ActionListener()
1329     {
1330
1331       @Override
1332       public void actionPerformed(ActionEvent e)
1333       {
1334         hideInsertions_actionPerformed(e);
1335       }
1336     });
1337
1338     groupMenu.add(sequenceSelDetails);
1339     add(groupMenu);
1340     add(sequenceMenu);
1341     add(rnaStructureMenu);
1342     add(pdbStructureDialog);
1343     if (sequence != null)
1344     {
1345       add(hideInsertions);
1346     }
1347     // annotations configuration panel suppressed for now
1348     // groupMenu.add(chooseAnnotations);
1349
1350     /*
1351      * Add show/hide annotations to the Sequence menu, and to the Selection menu
1352      * (if a selection group is in force).
1353      */
1354     sequenceMenu.add(seqShowAnnotationsMenu);
1355     sequenceMenu.add(seqHideAnnotationsMenu);
1356     sequenceMenu.add(seqAddReferenceAnnotations);
1357     groupMenu.add(groupShowAnnotationsMenu);
1358     groupMenu.add(groupHideAnnotationsMenu);
1359     groupMenu.add(groupAddReferenceAnnotations);
1360     groupMenu.add(editMenu);
1361     groupMenu.add(outputMenu);
1362     groupMenu.add(sequenceFeature);
1363     groupMenu.add(createGroupMenuItem);
1364     groupMenu.add(unGroupMenuItem);
1365     groupMenu.add(jMenu1);
1366     sequenceMenu.add(sequenceName);
1367     sequenceMenu.add(sequenceDetails);
1368     sequenceMenu.add(makeReferenceSeq);
1369
1370     initColourMenu();
1371     buildColourMenu();
1372
1373     editMenu.add(copy);
1374     editMenu.add(cut);
1375     editMenu.add(editSequence);
1376     editMenu.add(upperCase);
1377     editMenu.add(lowerCase);
1378     editMenu.add(toggle);
1379     // JBPNote: These shouldn't be added here - should appear in a generic
1380     // 'apply web service to this sequence menu'
1381     // pdbMenu.add(RNAFold);
1382     // pdbMenu.add(ContraFold);
1383     jMenu1.add(groupName);
1384     jMenu1.add(colourMenu);
1385     jMenu1.add(showBoxes);
1386     jMenu1.add(showText);
1387     jMenu1.add(showColourText);
1388     jMenu1.add(outline);
1389     jMenu1.add(displayNonconserved);
1390   }
1391
1392   /**
1393    * Constructs the entries for the colour menu
1394    */
1395   protected void initColourMenu()
1396   {
1397     colourMenu.setText(MessageManager.getString("label.group_colour"));
1398     textColour.setText(MessageManager.getString("label.text_colour"));
1399     textColour.addActionListener(new ActionListener()
1400     {
1401       @Override
1402       public void actionPerformed(ActionEvent e)
1403       {
1404         textColour_actionPerformed();
1405       }
1406     });
1407
1408     abovePIDColour.setText(
1409             MessageManager.getString("label.above_identity_threshold"));
1410     abovePIDColour.addActionListener(new ActionListener()
1411     {
1412       @Override
1413       public void actionPerformed(ActionEvent e)
1414       {
1415         abovePIDColour_actionPerformed(abovePIDColour.isSelected());
1416       }
1417     });
1418
1419     modifyPID.setText(
1420             MessageManager.getString("label.modify_identity_threshold"));
1421     modifyPID.addActionListener(new ActionListener()
1422     {
1423       @Override
1424       public void actionPerformed(ActionEvent e)
1425       {
1426         modifyPID_actionPerformed();
1427       }
1428     });
1429
1430     conservationMenuItem
1431             .setText(MessageManager.getString("action.by_conservation"));
1432     conservationMenuItem.addActionListener(new ActionListener()
1433     {
1434       @Override
1435       public void actionPerformed(ActionEvent e)
1436       {
1437         conservationMenuItem_actionPerformed(
1438                 conservationMenuItem.isSelected());
1439       }
1440     });
1441
1442     modifyConservation.setText(MessageManager
1443             .getString("label.modify_conservation_threshold"));
1444     modifyConservation.addActionListener(new ActionListener()
1445     {
1446       @Override
1447       public void actionPerformed(ActionEvent e)
1448       {
1449         modifyConservation_actionPerformed();
1450       }
1451     });
1452   }
1453
1454   /**
1455    * Builds the group colour sub-menu, including any user-defined colours which
1456    * were loaded at startup or during the Jalview session
1457    */
1458   protected void buildColourMenu()
1459   {
1460     SequenceGroup sg = ap.av.getSelectionGroup();
1461     if (sg == null)
1462     {
1463       /*
1464        * popup menu with no sequence group scope
1465        */
1466       return;
1467     }
1468     colourMenu.removeAll();
1469     colourMenu.add(textColour);
1470     colourMenu.addSeparator();
1471
1472     ColourMenuHelper.addMenuItems(colourMenu, this, sg, false);
1473
1474     colourMenu.addSeparator();
1475     colourMenu.add(conservationMenuItem);
1476     colourMenu.add(modifyConservation);
1477     colourMenu.add(abovePIDColour);
1478     colourMenu.add(modifyPID);
1479   }
1480
1481   protected void modifyConservation_actionPerformed()
1482   {
1483     SequenceGroup sg = getGroup();
1484     if (sg.cs != null)
1485     {
1486       SliderPanel.setConservationSlider(ap, sg.cs, sg.getName());
1487       SliderPanel.showConservationSlider();
1488     }
1489   }
1490
1491   protected void modifyPID_actionPerformed()
1492   {
1493     SequenceGroup sg = getGroup();
1494     if (sg.cs != null)
1495     {
1496       // int threshold = SliderPanel.setPIDSliderSource(ap, sg.cs, getGroup()
1497       // .getName());
1498       // sg.cs.setThreshold(threshold, ap.av.isIgnoreGapsConsensus());
1499       SliderPanel.setPIDSliderSource(ap, sg.cs, getGroup().getName());
1500       SliderPanel.showPIDSlider();
1501     }
1502   }
1503
1504   /**
1505    * Check for any annotations on the underlying dataset sequences (for the
1506    * current selection group) which are not 'on the alignment'.If any are found,
1507    * enable the option to add them to the alignment. The criteria for 'on the
1508    * alignment' is finding an alignment annotation on the alignment, matched on
1509    * calcId, label and sequenceRef.
1510    * 
1511    * A tooltip is also constructed that displays the source (calcId) and type
1512    * (label) of the annotations that can be added.
1513    * 
1514    * @param menuItem
1515    * @param forSequences
1516    */
1517   protected void configureReferenceAnnotationsMenu(JMenuItem menuItem,
1518           List<SequenceI> forSequences)
1519   {
1520     menuItem.setEnabled(false);
1521
1522     /*
1523      * Temporary store to hold distinct calcId / type pairs for the tooltip.
1524      * Using TreeMap means calcIds are shown in alphabetical order.
1525      */
1526     SortedMap<String, String> tipEntries = new TreeMap<>();
1527     final Map<SequenceI, List<AlignmentAnnotation>> candidates = new LinkedHashMap<>();
1528     AlignmentI al = this.ap.av.getAlignment();
1529     AlignmentUtils.findAddableReferenceAnnotations(forSequences, tipEntries,
1530             candidates, al);
1531     if (!candidates.isEmpty())
1532     {
1533       StringBuilder tooltip = new StringBuilder(64);
1534       tooltip.append(MessageManager.getString("label.add_annotations_for"));
1535
1536       /*
1537        * Found annotations that could be added. Enable the menu item, and
1538        * configure its tooltip and action.
1539        */
1540       menuItem.setEnabled(true);
1541       for (String calcId : tipEntries.keySet())
1542       {
1543         tooltip.append("<br/>" + calcId + "/" + tipEntries.get(calcId));
1544       }
1545       String tooltipText = JvSwingUtils.wrapTooltip(true,
1546               tooltip.toString());
1547       menuItem.setToolTipText(tooltipText);
1548
1549       menuItem.addActionListener(new ActionListener()
1550       {
1551         @Override
1552         public void actionPerformed(ActionEvent e)
1553         {
1554           addReferenceAnnotations_actionPerformed(candidates);
1555         }
1556       });
1557     }
1558   }
1559
1560   /**
1561    * Add annotations to the sequences and to the alignment.
1562    * 
1563    * @param candidates
1564    *          a map whose keys are sequences on the alignment, and values a list
1565    *          of annotations to add to each sequence
1566    */
1567   protected void addReferenceAnnotations_actionPerformed(
1568           Map<SequenceI, List<AlignmentAnnotation>> candidates)
1569   {
1570     final SequenceGroup selectionGroup = this.ap.av.getSelectionGroup();
1571     final AlignmentI alignment = this.ap.getAlignment();
1572     AlignmentUtils.addReferenceAnnotations(candidates, alignment,
1573             selectionGroup);
1574     refresh();
1575   }
1576
1577   protected void makeReferenceSeq_actionPerformed(ActionEvent actionEvent)
1578   {
1579     if (!ap.av.getAlignment().hasSeqrep())
1580     {
1581       // initialise the display flags so the user sees something happen
1582       ap.av.setDisplayReferenceSeq(true);
1583       ap.av.setColourByReferenceSeq(true);
1584       ap.av.getAlignment().setSeqrep(sequence);
1585     }
1586     else
1587     {
1588       if (ap.av.getAlignment().getSeqrep() == sequence)
1589       {
1590         ap.av.getAlignment().setSeqrep(null);
1591       }
1592       else
1593       {
1594         ap.av.getAlignment().setSeqrep(sequence);
1595       }
1596     }
1597     refresh();
1598   }
1599
1600   protected void hideInsertions_actionPerformed(ActionEvent actionEvent)
1601   {
1602     HiddenColumns hidden = ap.av.getAlignment().getHiddenColumns();
1603     BitSet inserts = new BitSet();
1604
1605     boolean markedPopup = false;
1606     // mark inserts in current selection
1607     if (ap.av.getSelectionGroup() != null)
1608     {
1609       // mark just the columns in the selection group to be hidden
1610       inserts.set(ap.av.getSelectionGroup().getStartRes(),
1611               ap.av.getSelectionGroup().getEndRes() + 1); // TODO why +1?
1612
1613       // now clear columns without gaps
1614       for (SequenceI sq : ap.av.getSelectionGroup().getSequences())
1615       {
1616         if (sq == sequence)
1617         {
1618           markedPopup = true;
1619         }
1620         inserts.and(sq.getInsertionsAsBits());
1621       }
1622       hidden.clearAndHideColumns(inserts, ap.av.getSelectionGroup().getStartRes(),
1623               ap.av.getSelectionGroup().getEndRes());
1624     }
1625
1626     // now mark for sequence under popup if we haven't already done it
1627     else if (!markedPopup && sequence != null)
1628     {
1629       inserts.or(sequence.getInsertionsAsBits());
1630
1631       // and set hidden columns accordingly
1632       hidden.hideColumns(inserts);
1633     }
1634     refresh();
1635   }
1636
1637   protected void sequenceSelectionDetails_actionPerformed()
1638   {
1639     createSequenceDetailsReport(ap.av.getSequenceSelection());
1640   }
1641
1642   protected void sequenceDetails_actionPerformed()
1643   {
1644     createSequenceDetailsReport(new SequenceI[] { sequence });
1645   }
1646
1647   public void createSequenceDetailsReport(SequenceI[] sequences)
1648   {
1649     CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer();
1650     StringBuilder contents = new StringBuilder(128);
1651     for (SequenceI seq : sequences)
1652     {
1653       contents.append("<p><h2>" + MessageManager.formatMessage(
1654               "label.create_sequence_details_report_annotation_for",
1655               new Object[]
1656               { seq.getDisplayId(true) }) + "</h2></p><p>");
1657       new SequenceAnnotationReport(null).createSequenceAnnotationReport(
1658               contents, seq, true, true, ap.getSeqPanel().seqCanvas.fr);
1659       contents.append("</p>");
1660     }
1661     cap.setText("<html>" + contents.toString() + "</html>");
1662
1663     Desktop.addInternalFrame(cap,
1664             MessageManager.formatMessage("label.sequence_details_for",
1665                     (sequences.length == 1 ? new Object[]
1666                     { sequences[0].getDisplayId(true) }
1667                             : new Object[]
1668                             { MessageManager
1669                                     .getString("label.selection") })),
1670             500, 400);
1671
1672   }
1673
1674   protected void showNonconserved_actionPerformed()
1675   {
1676     getGroup().setShowNonconserved(displayNonconserved.isSelected());
1677     refresh();
1678   }
1679
1680   /**
1681    * call to refresh view after settings change
1682    */
1683   void refresh()
1684   {
1685     ap.updateAnnotation();
1686     // removed paintAlignment(true) here:
1687     // updateAnnotation calls paintAlignment already, so don't need to call
1688     // again
1689
1690     PaintRefresher.Refresh(this, ap.av.getSequenceSetId());
1691   }
1692
1693   /*
1694    * protected void covariationColour_actionPerformed() { getGroup().cs = new
1695    * CovariationColourScheme(sequence.getAnnotation()[0]); refresh(); }
1696    */
1697   /**
1698    * DOCUMENT ME!
1699    * 
1700    * @param selected
1701    * 
1702    * @param e
1703    *          DOCUMENT ME!
1704    */
1705   public void abovePIDColour_actionPerformed(boolean selected)
1706   {
1707     SequenceGroup sg = getGroup();
1708     if (sg.cs == null)
1709     {
1710       return;
1711     }
1712
1713     if (selected)
1714     {
1715       sg.cs.setConsensus(AAFrequency.calculate(
1716               sg.getSequences(ap.av.getHiddenRepSequences()),
1717               sg.getStartRes(), sg.getEndRes() + 1));
1718
1719       int threshold = SliderPanel.setPIDSliderSource(ap,
1720               sg.getGroupColourScheme(), getGroup().getName());
1721
1722       sg.cs.setThreshold(threshold, ap.av.isIgnoreGapsConsensus());
1723
1724       SliderPanel.showPIDSlider();
1725     }
1726     else
1727     // remove PIDColouring
1728     {
1729       sg.cs.setThreshold(0, ap.av.isIgnoreGapsConsensus());
1730       SliderPanel.hidePIDSlider();
1731     }
1732     modifyPID.setEnabled(selected);
1733
1734     refresh();
1735   }
1736
1737   /**
1738    * Open a panel where the user can choose which types of sequence annotation
1739    * to show or hide.
1740    * 
1741    * @param e
1742    */
1743   protected void chooseAnnotations_actionPerformed(ActionEvent e)
1744   {
1745     // todo correct way to guard against opening a duplicate panel?
1746     new AnnotationChooser(ap);
1747   }
1748
1749   /**
1750    * DOCUMENT ME!
1751    * 
1752    * @param e
1753    *          DOCUMENT ME!
1754    */
1755   public void conservationMenuItem_actionPerformed(boolean selected)
1756   {
1757     SequenceGroup sg = getGroup();
1758     if (sg.cs == null)
1759     {
1760       return;
1761     }
1762
1763     if (selected)
1764     {
1765       // JBPNote: Conservation name shouldn't be i18n translated
1766       Conservation c = new Conservation("Group",
1767               sg.getSequences(ap.av.getHiddenRepSequences()),
1768               sg.getStartRes(), sg.getEndRes() + 1);
1769
1770       c.calculate();
1771       c.verdict(false, ap.av.getConsPercGaps());
1772       sg.cs.setConservation(c);
1773
1774       SliderPanel.setConservationSlider(ap, sg.getGroupColourScheme(),
1775               sg.getName());
1776       SliderPanel.showConservationSlider();
1777     }
1778     else
1779     // remove ConservationColouring
1780     {
1781       sg.cs.setConservation(null);
1782       SliderPanel.hideConservationSlider();
1783     }
1784     modifyConservation.setEnabled(selected);
1785
1786     refresh();
1787   }
1788
1789   /**
1790    * DOCUMENT ME!
1791    * 
1792    * @param e
1793    *          DOCUMENT ME!
1794    */
1795   protected void groupName_actionPerformed()
1796   {
1797
1798     SequenceGroup sg = getGroup();
1799     EditNameDialog dialog = new EditNameDialog(sg.getName(),
1800             sg.getDescription(),
1801             "       " + MessageManager.getString("label.group_name") + " ",
1802             MessageManager.getString("label.group_description") + " ",
1803             MessageManager.getString("label.edit_group_name_description"),
1804             ap.alignFrame);
1805
1806     if (!dialog.accept)
1807     {
1808       return;
1809     }
1810
1811     sg.setName(dialog.getName());
1812     sg.setDescription(dialog.getDescription());
1813     refresh();
1814   }
1815
1816   /**
1817    * Get selection group - adding it to the alignment if necessary.
1818    * 
1819    * @return sequence group to operate on
1820    */
1821   SequenceGroup getGroup()
1822   {
1823     SequenceGroup sg = ap.av.getSelectionGroup();
1824     // this method won't add a new group if it already exists
1825     if (sg != null)
1826     {
1827       ap.av.getAlignment().addGroup(sg);
1828     }
1829
1830     return sg;
1831   }
1832
1833   /**
1834    * DOCUMENT ME!
1835    * 
1836    * @param e
1837    *          DOCUMENT ME!
1838    */
1839   void sequenceName_actionPerformed()
1840   {
1841     EditNameDialog dialog = new EditNameDialog(sequence.getName(),
1842             sequence.getDescription(),
1843             "       " + MessageManager.getString("label.sequence_name")
1844                     + " ",
1845             MessageManager.getString("label.sequence_description") + " ",
1846             MessageManager.getString(
1847                     "label.edit_sequence_name_description"),
1848             ap.alignFrame);
1849
1850     if (!dialog.accept)
1851     {
1852       return;
1853     }
1854
1855     if (dialog.getName() != null)
1856     {
1857       if (dialog.getName().indexOf(" ") > -1)
1858       {
1859         JvOptionPane.showMessageDialog(ap,
1860                 MessageManager
1861                         .getString("label.spaces_converted_to_backslashes"),
1862                 MessageManager
1863                         .getString("label.no_spaces_allowed_sequence_name"),
1864                 JvOptionPane.WARNING_MESSAGE);
1865       }
1866
1867       sequence.setName(dialog.getName().replace(' ', '_'));
1868       ap.paintAlignment(false, false);
1869     }
1870
1871     sequence.setDescription(dialog.getDescription());
1872
1873     ap.av.firePropertyChange("alignment", null,
1874             ap.av.getAlignment().getSequences());
1875
1876   }
1877
1878   /**
1879    * DOCUMENT ME!
1880    * 
1881    * @param e
1882    *          DOCUMENT ME!
1883    */
1884   void unGroupMenuItem_actionPerformed()
1885   {
1886     SequenceGroup sg = ap.av.getSelectionGroup();
1887     ap.av.getAlignment().deleteGroup(sg);
1888     ap.av.setSelectionGroup(null);
1889     refresh();
1890   }
1891
1892   void createGroupMenuItem_actionPerformed()
1893   {
1894     getGroup(); // implicitly creates group - note - should apply defaults / use
1895                 // standard alignment window logic for this
1896     refresh();
1897   }
1898
1899   /**
1900    * DOCUMENT ME!
1901    * 
1902    * @param e
1903    *          DOCUMENT ME!
1904    */
1905   protected void outline_actionPerformed()
1906   {
1907     SequenceGroup sg = getGroup();
1908     Color col = JColorChooser.showDialog(this,
1909             MessageManager.getString("label.select_outline_colour"),
1910             Color.BLUE);
1911
1912     if (col != null)
1913     {
1914       sg.setOutlineColour(col);
1915     }
1916
1917     refresh();
1918   }
1919
1920   /**
1921    * DOCUMENT ME!
1922    * 
1923    * @param e
1924    *          DOCUMENT ME!
1925    */
1926   public void showBoxes_actionPerformed()
1927   {
1928     getGroup().setDisplayBoxes(showBoxes.isSelected());
1929     refresh();
1930   }
1931
1932   /**
1933    * DOCUMENT ME!
1934    * 
1935    * @param e
1936    *          DOCUMENT ME!
1937    */
1938   public void showText_actionPerformed()
1939   {
1940     getGroup().setDisplayText(showText.isSelected());
1941     refresh();
1942   }
1943
1944   /**
1945    * DOCUMENT ME!
1946    * 
1947    * @param e
1948    *          DOCUMENT ME!
1949    */
1950   public void showColourText_actionPerformed()
1951   {
1952     getGroup().setColourText(showColourText.isSelected());
1953     refresh();
1954   }
1955
1956   void hideSequences(boolean representGroup)
1957   {
1958     ap.av.hideSequences(sequence, representGroup);
1959   }
1960
1961   public void copy_actionPerformed()
1962   {
1963     ap.alignFrame.copy_actionPerformed(null);
1964   }
1965
1966   public void cut_actionPerformed()
1967   {
1968     ap.alignFrame.cut_actionPerformed(null);
1969   }
1970
1971   void changeCase(ActionEvent e)
1972   {
1973     Object source = e.getSource();
1974     SequenceGroup sg = ap.av.getSelectionGroup();
1975
1976     if (sg != null)
1977     {
1978       List<int[]> startEnd = ap.av.getVisibleRegionBoundaries(
1979               sg.getStartRes(), sg.getEndRes() + 1);
1980
1981       String description;
1982       int caseChange;
1983
1984       if (source == toggle)
1985       {
1986         description = MessageManager.getString("label.toggle_case");
1987         caseChange = ChangeCaseCommand.TOGGLE_CASE;
1988       }
1989       else if (source == upperCase)
1990       {
1991         description = MessageManager.getString("label.to_upper_case");
1992         caseChange = ChangeCaseCommand.TO_UPPER;
1993       }
1994       else
1995       {
1996         description = MessageManager.getString("label.to_lower_case");
1997         caseChange = ChangeCaseCommand.TO_LOWER;
1998       }
1999
2000       ChangeCaseCommand caseCommand = new ChangeCaseCommand(description,
2001               sg.getSequencesAsArray(ap.av.getHiddenRepSequences()),
2002               startEnd, caseChange);
2003
2004       ap.alignFrame.addHistoryItem(caseCommand);
2005
2006       ap.av.firePropertyChange("alignment", null,
2007               ap.av.getAlignment().getSequences());
2008
2009     }
2010   }
2011
2012   public void outputText_actionPerformed(ActionEvent e)
2013   {
2014     CutAndPasteTransfer cap = new CutAndPasteTransfer();
2015     cap.setForInput(null);
2016     Desktop.addInternalFrame(cap, MessageManager
2017             .formatMessage("label.alignment_output_command", new Object[]
2018             { e.getActionCommand() }), 600, 500);
2019
2020     String[] omitHidden = null;
2021
2022     System.out.println("PROMPT USER HERE"); // TODO: decide if a prompt happens
2023     // or we simply trust the user wants
2024     // wysiwig behaviour
2025
2026     FileFormatI fileFormat = FileFormats.getInstance()
2027             .forName(e.getActionCommand());
2028     cap.setText(
2029             new FormatAdapter(ap).formatSequences(fileFormat, ap, true));
2030   }
2031
2032   public void sequenceFeature_actionPerformed()
2033   {
2034     SequenceGroup sg = ap.av.getSelectionGroup();
2035     if (sg == null)
2036     {
2037       return;
2038     }
2039
2040     List<SequenceI> seqs = new ArrayList<>();
2041     List<SequenceFeature> features = new ArrayList<>();
2042
2043     /*
2044      * assemble dataset sequences, and template new sequence features,
2045      * for the amend features dialog
2046      */
2047     int gSize = sg.getSize();
2048     for (int i = 0; i < gSize; i++)
2049     {
2050       int start = sg.getSequenceAt(i).findPosition(sg.getStartRes());
2051       int end = sg.findEndRes(sg.getSequenceAt(i));
2052       if (start <= end)
2053       {
2054         seqs.add(sg.getSequenceAt(i).getDatasetSequence());
2055         features.add(new SequenceFeature(null, null, start, end, null));
2056       }
2057     }
2058
2059     /*
2060      * an entirely gapped region will generate empty lists of sequence / features
2061      */
2062     if (!seqs.isEmpty())
2063     {
2064       if (ap.getSeqPanel().seqCanvas.getFeatureRenderer()
2065               .amendFeatures(seqs, features, true, ap))
2066       {
2067         ap.alignFrame.setShowSeqFeatures(true);
2068         ap.av.setSearchResults(null); // clear highlighting
2069         ap.repaint(); // draw new/amended features
2070       }
2071     }
2072   }
2073
2074   public void textColour_actionPerformed()
2075   {
2076     SequenceGroup sg = getGroup();
2077     if (sg != null)
2078     {
2079       new TextColourChooser().chooseColour(ap, sg);
2080     }
2081   }
2082
2083   public void colourByStructure(String pdbid)
2084   {
2085     Annotation[] anots = ap.av.getStructureSelectionManager()
2086             .colourSequenceFromStructure(sequence, pdbid);
2087
2088     AlignmentAnnotation an = new AlignmentAnnotation("Structure",
2089             "Coloured by " + pdbid, anots);
2090
2091     ap.av.getAlignment().addAnnotation(an);
2092     an.createSequenceMapping(sequence, 0, true);
2093     // an.adjustForAlignment();
2094     ap.av.getAlignment().setAnnotationIndex(an, 0);
2095
2096     ap.adjustAnnotationHeight();
2097
2098     sequence.addAlignmentAnnotation(an);
2099
2100   }
2101
2102   public void editSequence_actionPerformed(ActionEvent actionEvent)
2103   {
2104     SequenceGroup sg = ap.av.getSelectionGroup();
2105
2106     if (sg != null)
2107     {
2108       if (sequence == null)
2109       {
2110         sequence = sg.getSequenceAt(0);
2111       }
2112
2113       EditNameDialog dialog = new EditNameDialog(
2114               sequence.getSequenceAsString(sg.getStartRes(),
2115                       sg.getEndRes() + 1),
2116               null, MessageManager.getString("label.edit_sequence"), null,
2117               MessageManager.getString("label.edit_sequence"),
2118               ap.alignFrame);
2119
2120       if (dialog.accept)
2121       {
2122         EditCommand editCommand = new EditCommand(
2123                 MessageManager.getString("label.edit_sequences"),
2124                 Action.REPLACE,
2125                 dialog.getName().replace(' ', ap.av.getGapCharacter()),
2126                 sg.getSequencesAsArray(ap.av.getHiddenRepSequences()),
2127                 sg.getStartRes(), sg.getEndRes() + 1, ap.av.getAlignment());
2128
2129         ap.alignFrame.addHistoryItem(editCommand);
2130
2131         ap.av.firePropertyChange("alignment", null,
2132                 ap.av.getAlignment().getSequences());
2133       }
2134     }
2135   }
2136
2137   /**
2138    * Action on user selecting an item from the colour menu (that does not have
2139    * its bespoke action handler)
2140    * 
2141    * @return
2142    */
2143   @Override
2144   public void changeColour_actionPerformed(String colourSchemeName)
2145   {
2146     SequenceGroup sg = getGroup();
2147     /*
2148      * switch to the chosen colour scheme (or null for None)
2149      */
2150     ColourSchemeI colourScheme = ColourSchemes.getInstance()
2151             .getColourScheme(colourSchemeName, sg,
2152                     ap.av.getHiddenRepSequences());
2153     sg.setColourScheme(colourScheme);
2154     if (colourScheme instanceof Blosum62ColourScheme
2155             || colourScheme instanceof PIDColourScheme)
2156     {
2157       sg.cs.setConsensus(AAFrequency.calculate(
2158               sg.getSequences(ap.av.getHiddenRepSequences()),
2159               sg.getStartRes(), sg.getEndRes() + 1));
2160     }
2161
2162     refresh();
2163   }
2164
2165 }