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