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