JAL-3691 automatic insertion of Locale.ROOT to toUpperCase() and toLowerCase() and...
[jalview.git] / src / jalview / gui / FeatureSettings.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.util.Locale;
24
25 import java.awt.BorderLayout;
26 import java.awt.Color;
27 import java.awt.Component;
28 import java.awt.Dimension;
29 import java.awt.FlowLayout;
30 import java.awt.Font;
31 import java.awt.Graphics;
32 import java.awt.GridLayout;
33 import java.awt.Point;
34 import java.awt.Rectangle;
35 import java.awt.event.ActionEvent;
36 import java.awt.event.ActionListener;
37 import java.awt.event.ItemEvent;
38 import java.awt.event.ItemListener;
39 import java.awt.event.MouseAdapter;
40 import java.awt.event.MouseEvent;
41 import java.awt.event.MouseMotionAdapter;
42 import java.beans.PropertyChangeEvent;
43 import java.beans.PropertyChangeListener;
44 import java.io.File;
45 import java.io.FileInputStream;
46 import java.io.FileOutputStream;
47 import java.io.InputStreamReader;
48 import java.io.OutputStreamWriter;
49 import java.io.PrintWriter;
50 import java.util.Arrays;
51 import java.util.Comparator;
52 import java.util.HashMap;
53 import java.util.HashSet;
54 import java.util.Hashtable;
55 import java.util.Iterator;
56 import java.util.List;
57 import java.util.Map;
58 import java.util.Set;
59
60 import javax.help.HelpSetException;
61 import javax.swing.AbstractCellEditor;
62 import javax.swing.BorderFactory;
63 import javax.swing.Icon;
64 import javax.swing.JButton;
65 import javax.swing.JCheckBox;
66 import javax.swing.JCheckBoxMenuItem;
67 import javax.swing.JInternalFrame;
68 import javax.swing.JLabel;
69 import javax.swing.JLayeredPane;
70 import javax.swing.JMenuItem;
71 import javax.swing.JPanel;
72 import javax.swing.JPopupMenu;
73 import javax.swing.JScrollPane;
74 import javax.swing.JSlider;
75 import javax.swing.JTable;
76 import javax.swing.ListSelectionModel;
77 import javax.swing.SwingConstants;
78 import javax.swing.ToolTipManager;
79 import javax.swing.border.Border;
80 import javax.swing.event.ChangeEvent;
81 import javax.swing.event.ChangeListener;
82 import javax.swing.table.AbstractTableModel;
83 import javax.swing.table.JTableHeader;
84 import javax.swing.table.TableCellEditor;
85 import javax.swing.table.TableCellRenderer;
86 import javax.swing.table.TableColumn;
87 import javax.xml.bind.JAXBContext;
88 import javax.xml.bind.JAXBElement;
89 import javax.xml.bind.Marshaller;
90 import javax.xml.stream.XMLInputFactory;
91 import javax.xml.stream.XMLStreamReader;
92
93 import jalview.api.AlignViewControllerGuiI;
94 import jalview.api.AlignViewportI;
95 import jalview.api.FeatureColourI;
96 import jalview.api.FeatureSettingsControllerI;
97 import jalview.api.SplitContainerI;
98 import jalview.api.ViewStyleI;
99 import jalview.controller.FeatureSettingsControllerGuiI;
100 import jalview.datamodel.AlignmentI;
101 import jalview.datamodel.SequenceI;
102 import jalview.datamodel.features.FeatureMatcher;
103 import jalview.datamodel.features.FeatureMatcherI;
104 import jalview.datamodel.features.FeatureMatcherSet;
105 import jalview.datamodel.features.FeatureMatcherSetI;
106 import jalview.gui.Help.HelpId;
107 import jalview.gui.JalviewColourChooser.ColourChooserListener;
108 import jalview.io.JalviewFileChooser;
109 import jalview.io.JalviewFileView;
110 import jalview.schemes.FeatureColour;
111 import jalview.util.MessageManager;
112 import jalview.util.Platform;
113 import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
114 import jalview.viewmodel.styles.ViewStyle;
115 import jalview.xml.binding.jalview.JalviewUserColours;
116 import jalview.xml.binding.jalview.JalviewUserColours.Colour;
117 import jalview.xml.binding.jalview.JalviewUserColours.Filter;
118 import jalview.xml.binding.jalview.ObjectFactory;
119
120 public class FeatureSettings extends JPanel
121         implements FeatureSettingsControllerI, FeatureSettingsControllerGuiI
122 {
123   private static final String SEQUENCE_FEATURE_COLOURS = MessageManager
124           .getString("label.sequence_feature_colours");
125
126   /*
127    * column indices of fields in Feature Settings table
128    */
129   static final int TYPE_COLUMN = 0;
130
131   static final int COLOUR_COLUMN = 1;
132
133   static final int FILTER_COLUMN = 2;
134
135   static final int SHOW_COLUMN = 3;
136
137   private static final int COLUMN_COUNT = 4;
138
139   private static final int MIN_WIDTH = 400;
140
141   private static final int MIN_HEIGHT = 400;
142
143   private final static String BASE_TOOLTIP = MessageManager
144           .getString("label.click_to_edit");
145
146   final FeatureRenderer fr;
147
148   public final AlignFrame af;
149
150   /*
151    * 'original' fields hold settings to restore on Cancel
152    */
153   Object[][] originalData;
154
155   private float originalTransparency;
156
157   private ViewStyleI originalViewStyle;
158
159   private Map<String, FeatureMatcherSetI> originalFilters;
160
161   final JInternalFrame frame;
162
163   JScrollPane scrollPane = new JScrollPane();
164
165   JTable table;
166
167   JPanel groupPanel;
168
169   JSlider transparency = new JSlider();
170
171   private JCheckBox showComplementOnTop;
172
173   private JCheckBox showComplement;
174
175   /*
176    * when true, constructor is still executing - so ignore UI events
177    */
178   protected volatile boolean inConstruction = true;
179
180   int selectedRow = -1;
181
182   boolean resettingTable = false;
183
184   /*
185    * true when Feature Settings are updating from feature renderer
186    */
187   private boolean handlingUpdate = false;
188
189   /*
190    * a change listener to ensure the dialog is updated if
191    * FeatureRenderer discovers new features
192    */
193   private PropertyChangeListener change;
194
195   /*
196    * holds {featureCount, totalExtent} for each feature type
197    */
198   Map<String, float[]> typeWidth = null;
199
200   private void storeOriginalSettings()
201   {
202     // save transparency for restore on Cancel
203     originalTransparency = fr.getTransparency();
204
205     updateTransparencySliderFromFR();
206
207     originalFilters = new HashMap<>(fr.getFeatureFilters()); // shallow copy
208     originalViewStyle = new ViewStyle(af.viewport.getViewStyle());
209   }
210
211   private void updateTransparencySliderFromFR()
212   {
213     boolean incon = inConstruction;
214     inConstruction = true;
215
216     int transparencyAsPercent = (int) (fr.getTransparency() * 100);
217     transparency.setValue(100 - transparencyAsPercent);
218     inConstruction = incon;
219   }
220   /**
221    * Constructor
222    * 
223    * @param af
224    */
225   public FeatureSettings(AlignFrame alignFrame)
226   {
227     this.af = alignFrame;
228     fr = af.getFeatureRenderer();
229
230     storeOriginalSettings();
231
232     try
233     {
234       jbInit();
235     } catch (Exception ex)
236     {
237       ex.printStackTrace();
238     }
239
240     table = new JTable()
241     {
242       @Override
243       public String getToolTipText(MouseEvent e)
244       {
245         String tip = null;
246         int column = table.columnAtPoint(e.getPoint());
247         int row = table.rowAtPoint(e.getPoint());
248
249         switch (column)
250         {
251         case TYPE_COLUMN:
252           tip = JvSwingUtils.wrapTooltip(true, MessageManager
253                   .getString("label.feature_settings_click_drag"));
254           break;
255         case COLOUR_COLUMN:
256           FeatureColourI colour = (FeatureColourI) table.getValueAt(row,
257                   column);
258           tip = getColorTooltip(colour, true);
259           break;
260         case FILTER_COLUMN:
261           FeatureMatcherSet o = (FeatureMatcherSet) table.getValueAt(row,
262                   column);
263           tip = o.isEmpty()
264                   ? MessageManager
265                           .getString("label.configure_feature_tooltip")
266                   : o.toString();
267           break;
268         default:
269           break;
270         }
271
272         return tip;
273       }
274
275       /**
276        * Position the tooltip near the bottom edge of, and half way across, the
277        * current cell
278        */
279       @Override
280       public Point getToolTipLocation(MouseEvent e)
281       {
282         Point point = e.getPoint();
283         int column = table.columnAtPoint(point);
284         int row = table.rowAtPoint(point);
285         Rectangle r = getCellRect(row, column, false);
286         Point loc = new Point(r.x + r.width / 2, r.y + r.height - 3);
287         return loc;
288       }
289     };
290     JTableHeader tableHeader = table.getTableHeader();
291     tableHeader.setFont(new Font("Verdana", Font.PLAIN, 12));
292     tableHeader.setReorderingAllowed(false);
293     table.setFont(new Font("Verdana", Font.PLAIN, 12));
294     ToolTipManager.sharedInstance().registerComponent(table);
295     table.setDefaultEditor(FeatureColour.class, new ColorEditor());
296     table.setDefaultRenderer(FeatureColour.class, new ColorRenderer());
297
298     table.setDefaultEditor(FeatureMatcherSet.class, new FilterEditor());
299     table.setDefaultRenderer(FeatureMatcherSet.class, new FilterRenderer());
300
301     TableColumn colourColumn = new TableColumn(COLOUR_COLUMN, 75,
302             new ColorRenderer(), new ColorEditor());
303     table.addColumn(colourColumn);
304
305     TableColumn filterColumn = new TableColumn(FILTER_COLUMN, 75,
306             new FilterRenderer(), new FilterEditor());
307     table.addColumn(filterColumn);
308
309     table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
310
311     table.addMouseListener(new MouseAdapter()
312     {
313       @Override
314       public void mousePressed(MouseEvent evt)
315       {
316         Point pt = evt.getPoint();
317         selectedRow = table.rowAtPoint(pt);
318         String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN);
319         if (evt.isPopupTrigger())
320         {
321           Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN);
322           showPopupMenu(selectedRow, type, colour, evt.getPoint());
323         }
324         else if (evt.getClickCount() == 2
325                 && table.columnAtPoint(pt) == TYPE_COLUMN)
326         {
327           boolean invertSelection = evt.isAltDown();
328           boolean toggleSelection = Platform.isControlDown(evt);
329           boolean extendSelection = evt.isShiftDown();
330           fr.ap.alignFrame.avc.markColumnsContainingFeatures(
331                   invertSelection, extendSelection, toggleSelection, type);
332           fr.ap.av.sendSelection();
333         }
334       }
335
336       // isPopupTrigger fires on mouseReleased on Windows
337       @Override
338       public void mouseReleased(MouseEvent evt)
339       {
340         selectedRow = table.rowAtPoint(evt.getPoint());
341         if (evt.isPopupTrigger())
342         {
343           String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN);
344           Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN);
345           showPopupMenu(selectedRow, type, colour, evt.getPoint());
346         }
347       }
348     });
349
350     table.addMouseMotionListener(new MouseMotionAdapter()
351     {
352       @Override
353       public void mouseDragged(MouseEvent evt)
354       {
355         int newRow = table.rowAtPoint(evt.getPoint());
356         if (newRow != selectedRow && selectedRow != -1 && newRow != -1)
357         {
358           /*
359            * reposition 'selectedRow' to 'newRow' (the dragged to location)
360            * this could be more than one row away for a very fast drag action
361            * so just swap it with adjacent rows until we get it there
362            */
363           Object[][] data = ((FeatureTableModel) table.getModel())
364                   .getData();
365           int direction = newRow < selectedRow ? -1 : 1;
366           for (int i = selectedRow; i != newRow; i += direction)
367           {
368             Object[] temp = data[i];
369             data[i] = data[i + direction];
370             data[i + direction] = temp;
371           }
372           updateFeatureRenderer(data);
373           table.repaint();
374           selectedRow = newRow;
375         }
376       }
377     });
378     // table.setToolTipText(JvSwingUtils.wrapTooltip(true,
379     // MessageManager.getString("label.feature_settings_click_drag")));
380     scrollPane.setViewportView(table);
381
382     if (af.getViewport().isShowSequenceFeatures() || !fr.hasRenderOrder())
383     {
384       fr.findAllFeatures(true); // display everything!
385     }
386
387     discoverAllFeatureData();
388     final FeatureSettings fs = this;
389     fr.addPropertyChangeListener(change = new PropertyChangeListener()
390     {
391       @Override
392       public void propertyChange(PropertyChangeEvent evt)
393       {
394         if (!fs.resettingTable && !fs.handlingUpdate)
395         {
396           fs.handlingUpdate = true;
397           fs.resetTable(null);
398           // new groups may be added with new sequence feature types only
399           fs.handlingUpdate = false;
400         }
401       }
402
403     });
404
405     SplitContainerI splitframe = af.getSplitViewContainer();
406     if (splitframe != null)
407     {
408       frame = null; // keeps eclipse happy
409       splitframe.addFeatureSettingsUI(this);
410     }
411     else
412     {
413       frame = new JInternalFrame();
414       frame.setContentPane(this);
415       Rectangle bounds = af.getFeatureSettingsGeometry();
416       String title;
417       if (af.getAlignPanels().size() > 1 || Desktop.getAlignmentPanels(
418               af.alignPanel.av.getSequenceSetId()).length > 1)
419       {
420         title = MessageManager.formatMessage(
421                 "label.sequence_feature_settings_for_view",
422                 af.alignPanel.getViewName());
423       }
424       else
425       {
426         title = MessageManager.getString("label.sequence_feature_settings");
427       }
428       if (bounds == null)
429       {
430         if (Platform.isAMacAndNotJS())
431         {
432           Desktop.addInternalFrame(frame, title, 600, 480);
433         }
434         else
435         {
436           Desktop.addInternalFrame(frame, title, 600, 450);
437         }
438       }
439       else
440       {
441         Desktop.addInternalFrame(frame, title, false, bounds.width,
442                 bounds.height);
443         frame.setBounds(bounds);
444         frame.setVisible(true);
445       }
446       frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
447
448       frame.addInternalFrameListener(
449               new javax.swing.event.InternalFrameAdapter()
450               {
451                 @Override
452                 public void internalFrameClosed(
453                         javax.swing.event.InternalFrameEvent evt)
454                 {
455                   featureSettings_isClosed();
456                 };
457               });
458       frame.setLayer(JLayeredPane.PALETTE_LAYER);
459     }
460                 inConstruction = false;
461         }
462
463   /**
464    * Sets the state of buttons to show complement features from viewport
465    * settings
466    */
467   private void updateComplementButtons()
468   {
469     showComplement.setSelected(af.getViewport().isShowComplementFeatures());
470     showComplementOnTop
471             .setSelected(af.getViewport().isShowComplementFeaturesOnTop());
472   }
473
474   @Override
475   public AlignViewControllerGuiI getAlignframe()
476   {
477     return af;
478   }
479
480   @Override
481   public void featureSettings_isClosed()
482   {
483     fr.removePropertyChangeListener(change);
484     change = null;
485   }
486
487         /**
488          * Constructs and shows a popup menu of possible actions on the selected row and
489          * feature type
490          * 
491          * @param rowSelected
492          * @param type
493          * @param typeCol
494          * @param pt
495          */
496         protected void showPopupMenu(final int rowSelected, final String type, final Object typeCol, final Point pt)
497   {
498     JPopupMenu men = new JPopupMenu(MessageManager
499             .formatMessage("label.settings_for_param", new String[]
500             { type }));
501     final FeatureColourI featureColour = (FeatureColourI) typeCol;
502
503     /*
504      * menu option to select (or deselect) variable colour
505      */
506     final JCheckBoxMenuItem variableColourCB = new JCheckBoxMenuItem(
507             MessageManager.getString("label.variable_colour"));
508     variableColourCB.setSelected(!featureColour.isSimpleColour());
509     men.add(variableColourCB);
510
511     /*
512      * checkbox action listener doubles up as listener to OK
513      * from the variable colour / filters dialog
514      */
515     variableColourCB.addActionListener(new ActionListener()
516     {
517       @Override
518       public void actionPerformed(ActionEvent e)
519       {
520         if (e.getSource() == variableColourCB)
521         {
522                                         // BH 2018 for JavaScript because this is a checkbox
523                                         men.setVisible(true);
524           men.setVisible(false);
525           if (featureColour.isSimpleColour())
526           {
527             /*
528              * toggle simple colour to variable colour - show dialog
529              */
530             FeatureTypeSettings fc = new FeatureTypeSettings(fr, type);
531             fc.addActionListener(this);
532           }
533           else
534           {
535             /*
536              * toggle variable to simple colour - show colour chooser
537              */
538             String title = MessageManager
539                     .formatMessage("label.select_colour_for", type);
540             ColourChooserListener listener = new ColourChooserListener()
541             {
542               @Override
543               public void colourSelected(Color c)
544               {
545                 table.setValueAt(new FeatureColour(c), rowSelected,
546                         COLOUR_COLUMN);
547                 table.validate();
548                 updateFeatureRenderer(
549                         ((FeatureTableModel) table.getModel()).getData(),
550                         false);
551               }
552             };
553             JalviewColourChooser.showColourChooser(FeatureSettings.this,
554                     title, featureColour.getMaxColour(), listener);
555           }
556         }
557         else
558         {
559           if (e.getSource() instanceof FeatureTypeSettings)
560           {
561             /*
562              * update after OK in feature colour dialog; the updated
563              * colour will have already been set in the FeatureRenderer
564              */
565             FeatureColourI fci = fr.getFeatureColours().get(type);
566             table.setValueAt(fci, rowSelected, COLOUR_COLUMN);
567             // BH 2018 setting a table value does not invalidate it.
568             // System.out.println("FeatureSettings is valied" +
569             // table.validate();
570           }
571         }
572       }
573     });
574
575     men.addSeparator();
576
577     JMenuItem scr = new JMenuItem(
578             MessageManager.getString("label.sort_by_score"));
579     men.add(scr);
580     scr.addActionListener(new ActionListener()
581     {
582       @Override
583       public void actionPerformed(ActionEvent e)
584       {
585         sortByScore(Arrays.asList(new String[] { type }));
586       }
587     });
588     JMenuItem dens = new JMenuItem(
589             MessageManager.getString("label.sort_by_density"));
590     dens.addActionListener(new ActionListener()
591     {
592       @Override
593       public void actionPerformed(ActionEvent e)
594       {
595         sortByDensity(Arrays.asList(new String[] { type }));
596       }
597     });
598     men.add(dens);
599
600     JMenuItem selCols = new JMenuItem(
601             MessageManager.getString("label.select_columns_containing"));
602     selCols.addActionListener(new ActionListener()
603     {
604       @Override
605       public void actionPerformed(ActionEvent arg0)
606       {
607         fr.ap.alignFrame.avc.markColumnsContainingFeatures(false, false,
608                 false, type);
609         fr.ap.av.sendSelection();
610       }
611     });
612     JMenuItem clearCols = new JMenuItem(MessageManager
613             .getString("label.select_columns_not_containing"));
614     clearCols.addActionListener(new ActionListener()
615     {
616       @Override
617       public void actionPerformed(ActionEvent arg0)
618       {
619         fr.ap.alignFrame.avc.markColumnsContainingFeatures(true, false,
620                 false, type);
621         fr.ap.av.sendSelection();
622       }
623     });
624     JMenuItem hideCols = new JMenuItem(
625             MessageManager.getString("label.hide_columns_containing"));
626     hideCols.addActionListener(new ActionListener()
627     {
628       @Override
629       public void actionPerformed(ActionEvent arg0)
630       {
631         fr.ap.alignFrame.hideFeatureColumns(type, true);
632         fr.ap.av.sendSelection();
633       }
634     });
635     JMenuItem hideOtherCols = new JMenuItem(
636             MessageManager.getString("label.hide_columns_not_containing"));
637     hideOtherCols.addActionListener(new ActionListener()
638     {
639       @Override
640       public void actionPerformed(ActionEvent arg0)
641       {
642         fr.ap.alignFrame.hideFeatureColumns(type, false);
643         fr.ap.av.sendSelection();
644       }
645     });
646     men.add(selCols);
647     men.add(clearCols);
648     men.add(hideCols);
649     men.add(hideOtherCols);
650     men.show(table, pt.x, pt.y);
651   }
652
653   /**
654    * Sort the sequences in the alignment by the number of features for the given
655    * feature types (or all features if null)
656    * 
657    * @param featureTypes
658    */
659   protected void sortByDensity(List<String> featureTypes)
660   {
661     af.avc.sortAlignmentByFeatureDensity(featureTypes);
662   }
663
664   /**
665    * Sort the sequences in the alignment by average score for the given feature
666    * types (or all features if null)
667    * 
668    * @param featureTypes
669    */
670   protected void sortByScore(List<String> featureTypes)
671   {
672     af.avc.sortAlignmentByFeatureScore(featureTypes);
673   }
674
675   /**
676    * Returns true if at least one feature type is visible. Else shows a warning
677    * dialog and returns false.
678    * 
679    * @param title
680    * @return
681    */
682   private boolean canSortBy(String title)
683   {
684     if (fr.getDisplayedFeatureTypes().isEmpty())
685     {
686       JvOptionPane.showMessageDialog(this,
687               MessageManager.getString("label.no_features_to_sort_by"),
688               title, JvOptionPane.OK_OPTION);
689       return false;
690     }
691     return true;
692   }
693
694   @Override
695   synchronized public void discoverAllFeatureData()
696   {
697     Set<String> allGroups = new HashSet<>();
698     AlignmentI alignment = af.getViewport().getAlignment();
699
700     for (int i = 0; i < alignment.getHeight(); i++)
701     {
702       SequenceI seq = alignment.getSequenceAt(i);
703       for (String group : seq.getFeatures().getFeatureGroups(true))
704       {
705         if (group != null && !allGroups.contains(group))
706         {
707           allGroups.add(group);
708           checkGroupState(group);
709         }
710       }
711     }
712
713     resetTable(null);
714
715     validate();
716   }
717
718   /**
719    * Synchronise gui group list and check visibility of group
720    * 
721    * @param group
722    * @return true if group is visible
723    */
724   private boolean checkGroupState(String group)
725   {
726     boolean visible = fr.checkGroupVisibility(group, true);
727
728     for (int g = 0; g < groupPanel.getComponentCount(); g++)
729     {
730       if (((JCheckBox) groupPanel.getComponent(g)).getText().equals(group))
731       {
732         ((JCheckBox) groupPanel.getComponent(g)).setSelected(visible);
733         return visible;
734       }
735     }
736
737     final String grp = group;
738     final JCheckBox check = new JCheckBox(group, visible);
739     check.setFont(new Font("Serif", Font.BOLD, 12));
740     check.setToolTipText(group);
741     check.addItemListener(new ItemListener()
742     {
743       @Override
744       public void itemStateChanged(ItemEvent evt)
745       {
746         fr.setGroupVisibility(check.getText(), check.isSelected());
747         resetTable(new String[] { grp });
748         refreshDisplay();
749       }
750     });
751     groupPanel.add(check);
752     return visible;
753   }
754
755   synchronized void resetTable(String[] groupChanged)
756   {
757     if (resettingTable)
758     {
759       return;
760     }
761     resettingTable = true;
762     typeWidth = new Hashtable<>();
763     // TODO: change avWidth calculation to 'per-sequence' average and use long
764     // rather than float
765
766     Set<String> displayableTypes = new HashSet<>();
767     Set<String> foundGroups = new HashSet<>();
768
769     /*
770      * determine which feature types may be visible depending on 
771      * which groups are selected, and recompute average width data
772      */
773     for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++)
774     {
775
776       SequenceI seq = af.getViewport().getAlignment().getSequenceAt(i);
777
778       /*
779        * get the sequence's groups for positional features
780        * and keep track of which groups are visible
781        */
782       Set<String> groups = seq.getFeatures().getFeatureGroups(true);
783       Set<String> visibleGroups = new HashSet<>();
784       for (String group : groups)
785       {
786         if (group == null || checkGroupState(group))
787         {
788           visibleGroups.add(group);
789         }
790       }
791       foundGroups.addAll(groups);
792
793       /*
794        * get distinct feature types for visible groups
795        * record distinct visible types, and their count and total length
796        */
797       Set<String> types = seq.getFeatures().getFeatureTypesForGroups(true,
798               visibleGroups.toArray(new String[visibleGroups.size()]));
799       for (String type : types)
800       {
801         displayableTypes.add(type);
802         float[] avWidth = typeWidth.get(type);
803         if (avWidth == null)
804         {
805           avWidth = new float[2];
806           typeWidth.put(type, avWidth);
807         }
808         // todo this could include features with a non-visible group
809         // - do we greatly care?
810         // todo should we include non-displayable features here, and only
811         // update when features are added?
812         avWidth[0] += seq.getFeatures().getFeatureCount(true, type);
813         avWidth[1] += seq.getFeatures().getTotalFeatureLength(type);
814       }
815     }
816
817     Object[][] data = new Object[displayableTypes.size()][COLUMN_COUNT];
818     int dataIndex = 0;
819
820     if (fr.hasRenderOrder())
821     {
822       if (!handlingUpdate)
823       {
824         fr.findAllFeatures(groupChanged != null); // prod to update
825         // colourschemes. but don't
826         // affect display
827         // First add the checks in the previous render order,
828         // in case the window has been closed and reopened
829       }
830       List<String> frl = fr.getRenderOrder();
831       for (int ro = frl.size() - 1; ro > -1; ro--)
832       {
833         String type = frl.get(ro);
834
835         if (!displayableTypes.contains(type))
836         {
837           continue;
838         }
839
840         data[dataIndex][TYPE_COLUMN] = type;
841         data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type);
842         FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type);
843         data[dataIndex][FILTER_COLUMN] = featureFilter == null
844                 ? new FeatureMatcherSet()
845                 : featureFilter;
846         data[dataIndex][SHOW_COLUMN] = Boolean.valueOf(
847                 af.getViewport().getFeaturesDisplayed().isVisible(type));
848         dataIndex++;
849         displayableTypes.remove(type);
850       }
851     }
852
853     /*
854      * process any extra features belonging only to 
855      * a group which was just selected
856      */
857     while (!displayableTypes.isEmpty())
858     {
859       String type = displayableTypes.iterator().next();
860       data[dataIndex][TYPE_COLUMN] = type;
861
862       data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type);
863       if (data[dataIndex][COLOUR_COLUMN] == null)
864       {
865         // "Colour has been updated in another view!!"
866         fr.clearRenderOrder();
867         return;
868       }
869       FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type);
870       data[dataIndex][FILTER_COLUMN] = featureFilter == null
871               ? new FeatureMatcherSet()
872               : featureFilter;
873       data[dataIndex][SHOW_COLUMN] = Boolean.valueOf(true);
874       dataIndex++;
875       displayableTypes.remove(type);
876     }
877
878     if (originalData == null)
879     {
880       originalData = new Object[data.length][COLUMN_COUNT];
881       for (int i = 0; i < data.length; i++)
882       {
883         System.arraycopy(data[i], 0, originalData[i], 0, COLUMN_COUNT);
884       }
885     }
886     else
887     {
888       updateOriginalData(data);
889     }
890
891     table.setModel(new FeatureTableModel(data));
892     table.getColumnModel().getColumn(0).setPreferredWidth(200);
893
894     groupPanel.setLayout(
895             new GridLayout(fr.getFeatureGroupsSize() / 4 + 1, 4));
896     pruneGroups(foundGroups);
897     groupPanel.validate();
898
899     updateFeatureRenderer(data, groupChanged != null);
900     resettingTable = false;
901   }
902
903   /**
904    * Updates 'originalData' (used for restore on Cancel) if we detect that
905    * changes have been made outwith this dialog
906    * <ul>
907    * <li>a new feature type added (and made visible)</li>
908    * <li>a feature colour changed (in the Amend Features dialog)</li>
909    * </ul>
910    * 
911    * @param foundData
912    */
913   protected void updateOriginalData(Object[][] foundData)
914   {
915     // todo LinkedHashMap instead of Object[][] would be nice
916
917     Object[][] currentData = ((FeatureTableModel) table.getModel())
918             .getData();
919     for (Object[] row : foundData)
920     {
921       String type = (String) row[TYPE_COLUMN];
922       boolean found = false;
923       for (Object[] current : currentData)
924       {
925         if (type.equals(current[TYPE_COLUMN]))
926         {
927           found = true;
928           /*
929            * currently dependent on object equality here;
930            * really need an equals method on FeatureColour
931            */
932           if (!row[COLOUR_COLUMN].equals(current[COLOUR_COLUMN]))
933           {
934             /*
935              * feature colour has changed externally - update originalData
936              */
937             for (Object[] original : originalData)
938             {
939               if (type.equals(original[TYPE_COLUMN]))
940               {
941                 original[COLOUR_COLUMN] = row[COLOUR_COLUMN];
942                 break;
943               }
944             }
945           }
946           break;
947         }
948       }
949       if (!found)
950       {
951         /*
952          * new feature detected - add to original data (on top)
953          */
954         Object[][] newData = new Object[originalData.length
955                 + 1][COLUMN_COUNT];
956         for (int i = 0; i < originalData.length; i++)
957         {
958           System.arraycopy(originalData[i], 0, newData[i + 1], 0,
959                   COLUMN_COUNT);
960         }
961         newData[0] = row;
962         originalData = newData;
963       }
964     }
965   }
966
967   /**
968    * Remove from the groups panel any checkboxes for groups that are not in the
969    * foundGroups set. This enables removing a group from the display when the
970    * last feature in that group is deleted.
971    * 
972    * @param foundGroups
973    */
974   protected void pruneGroups(Set<String> foundGroups)
975   {
976     for (int g = 0; g < groupPanel.getComponentCount(); g++)
977     {
978       JCheckBox checkbox = (JCheckBox) groupPanel.getComponent(g);
979       if (!foundGroups.contains(checkbox.getText()))
980       {
981         groupPanel.remove(checkbox);
982       }
983     }
984   }
985
986   /**
987    * reorder data based on the featureRenderers global priority list.
988    * 
989    * @param data
990    */
991   private void ensureOrder(Object[][] data)
992   {
993     boolean sort = false;
994     float[] order = new float[data.length];
995     for (int i = 0; i < order.length; i++)
996     {
997       order[i] = fr.getOrder(data[i][0].toString());
998       if (order[i] < 0)
999       {
1000         order[i] = fr.setOrder(data[i][0].toString(), i / order.length);
1001       }
1002       if (i > 1)
1003       {
1004         sort = sort || order[i - 1] > order[i];
1005       }
1006     }
1007     if (sort)
1008     {
1009       jalview.util.QuickSort.sort(order, data);
1010     }
1011   }
1012
1013   /**
1014    * Offers a file chooser dialog, and then loads the feature colours and
1015    * filters from file in XML format and unmarshals to Jalview feature settings
1016    */
1017   void load()
1018   {
1019     JalviewFileChooser chooser = new JalviewFileChooser("fc",
1020             SEQUENCE_FEATURE_COLOURS);
1021     chooser.setFileView(new JalviewFileView());
1022     chooser.setDialogTitle(
1023             MessageManager.getString("label.load_feature_colours"));
1024     chooser.setToolTipText(MessageManager.getString("action.load"));
1025     chooser.setResponseHandler(0, new Runnable()
1026     {
1027       @Override
1028       public void run()
1029       {
1030         File file = chooser.getSelectedFile();
1031         load(file);
1032       }
1033     });
1034     chooser.showOpenDialog(this);
1035   }
1036
1037   /**
1038    * Loads feature colours and filters from XML stored in the given file
1039    * 
1040    * @param file
1041    */
1042   void load(File file)
1043   {
1044     try
1045     {
1046       InputStreamReader in = new InputStreamReader(
1047               new FileInputStream(file), "UTF-8");
1048
1049       JAXBContext jc = JAXBContext
1050               .newInstance("jalview.xml.binding.jalview");
1051       javax.xml.bind.Unmarshaller um = jc.createUnmarshaller();
1052       XMLStreamReader streamReader = XMLInputFactory.newInstance()
1053               .createXMLStreamReader(in);
1054       JAXBElement<JalviewUserColours> jbe = um.unmarshal(streamReader,
1055               JalviewUserColours.class);
1056       JalviewUserColours jucs = jbe.getValue();
1057
1058       // JalviewUserColours jucs = JalviewUserColours.unmarshal(in);
1059
1060       /*
1061        * load feature colours
1062        */
1063       for (int i = jucs.getColour().size() - 1; i >= 0; i--)
1064       {
1065         Colour newcol = jucs.getColour().get(i);
1066         FeatureColourI colour = jalview.project.Jalview2XML
1067                 .parseColour(newcol);
1068         fr.setColour(newcol.getName(), colour);
1069         fr.setOrder(newcol.getName(), i / (float) jucs.getColour().size());
1070       }
1071
1072       /*
1073        * load feature filters; loaded filters will replace any that are
1074        * currently defined, other defined filters are left unchanged 
1075        */
1076       for (int i = 0; i < jucs.getFilter().size(); i++)
1077       {
1078         Filter filterModel = jucs.getFilter().get(i);
1079         String featureType = filterModel.getFeatureType();
1080         FeatureMatcherSetI filter = jalview.project.Jalview2XML
1081                 .parseFilter(featureType, filterModel.getMatcherSet());
1082         if (!filter.isEmpty())
1083         {
1084           fr.setFeatureFilter(featureType, filter);
1085         }
1086       }
1087
1088       /*
1089        * update feature settings table
1090        */
1091       if (table != null)
1092       {
1093         resetTable(null);
1094         Object[][] data = ((FeatureTableModel) table.getModel()).getData();
1095         ensureOrder(data);
1096         updateFeatureRenderer(data, false);
1097         table.repaint();
1098       }
1099     } catch (Exception ex)
1100     {
1101       System.out.println("Error loading User Colour File\n" + ex);
1102     }
1103   }
1104
1105   /**
1106    * Offers a file chooser dialog, and then saves the current feature colours
1107    * and any filters to the selected file in XML format
1108    */
1109   void save()
1110   {
1111     JalviewFileChooser chooser = new JalviewFileChooser("fc",
1112             SEQUENCE_FEATURE_COLOURS);
1113     chooser.setFileView(new JalviewFileView());
1114     chooser.setDialogTitle(
1115             MessageManager.getString("label.save_feature_colours"));
1116     chooser.setToolTipText(MessageManager.getString("action.save"));
1117     int option = chooser.showSaveDialog(this);
1118     if (option == JalviewFileChooser.APPROVE_OPTION)
1119     {
1120       File file = chooser.getSelectedFile();
1121       save(file);
1122     }
1123   }
1124
1125   /**
1126    * Saves feature colours and filters to the given file
1127    * 
1128    * @param file
1129    */
1130   void save(File file)
1131   {
1132     JalviewUserColours ucs = new JalviewUserColours();
1133     ucs.setSchemeName("Sequence Features");
1134     try
1135     {
1136       PrintWriter out = new PrintWriter(
1137               new OutputStreamWriter(new FileOutputStream(file), "UTF-8"));
1138
1139       /*
1140        * sort feature types by colour order, from 0 (highest)
1141        * to 1 (lowest)
1142        */
1143       Set<String> fr_colours = fr.getAllFeatureColours();
1144       String[] sortedTypes = fr_colours
1145               .toArray(new String[fr_colours.size()]);
1146       Arrays.sort(sortedTypes, new Comparator<String>()
1147       {
1148         @Override
1149         public int compare(String type1, String type2)
1150         {
1151           return Float.compare(fr.getOrder(type1), fr.getOrder(type2));
1152         }
1153       });
1154
1155       /*
1156        * save feature colours
1157        */
1158       for (String featureType : sortedTypes)
1159       {
1160         FeatureColourI fcol = fr.getFeatureStyle(featureType);
1161         Colour col = jalview.project.Jalview2XML.marshalColour(featureType,
1162                 fcol);
1163         ucs.getColour().add(col);
1164       }
1165
1166       /*
1167        * save any feature filters
1168        */
1169       for (String featureType : sortedTypes)
1170       {
1171         FeatureMatcherSetI filter = fr.getFeatureFilter(featureType);
1172         if (filter != null && !filter.isEmpty())
1173         {
1174           Iterator<FeatureMatcherI> iterator = filter.getMatchers()
1175                   .iterator();
1176           FeatureMatcherI firstMatcher = iterator.next();
1177           jalview.xml.binding.jalview.FeatureMatcherSet ms = jalview.project.Jalview2XML
1178                   .marshalFilter(firstMatcher, iterator, filter.isAnded());
1179           Filter filterModel = new Filter();
1180           filterModel.setFeatureType(featureType);
1181           filterModel.setMatcherSet(ms);
1182           ucs.getFilter().add(filterModel);
1183         }
1184       }
1185       JAXBContext jaxbContext = JAXBContext
1186               .newInstance(JalviewUserColours.class);
1187       Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
1188       jaxbMarshaller.marshal(
1189               new ObjectFactory().createJalviewUserColours(ucs), out);
1190
1191       // jaxbMarshaller.marshal(object, pout);
1192       // marshaller.marshal(object);
1193       out.flush();
1194
1195       // ucs.marshal(out);
1196       out.close();
1197     } catch (Exception ex)
1198     {
1199       ex.printStackTrace();
1200     }
1201   }
1202
1203   public void invertSelection()
1204   {
1205     Object[][] data = ((FeatureTableModel) table.getModel()).getData();
1206     for (int i = 0; i < data.length; i++)
1207     {
1208       data[i][SHOW_COLUMN] = !(Boolean) data[i][SHOW_COLUMN];
1209     }
1210     updateFeatureRenderer(data, true);
1211     table.repaint();
1212   }
1213
1214   public void orderByAvWidth()
1215   {
1216     if (table == null || table.getModel() == null)
1217     {
1218       return;
1219     }
1220     Object[][] data = ((FeatureTableModel) table.getModel()).getData();
1221     float[] width = new float[data.length];
1222     float[] awidth;
1223     float max = 0;
1224
1225     for (int i = 0; i < data.length; i++)
1226     {
1227       awidth = typeWidth.get(data[i][TYPE_COLUMN]);
1228       if (awidth[0] > 0)
1229       {
1230         width[i] = awidth[1] / awidth[0];// *awidth[0]*awidth[2]; - better
1231         // weight - but have to make per
1232         // sequence, too (awidth[2])
1233         // if (width[i]==1) // hack to distinguish single width sequences.
1234       }
1235       else
1236       {
1237         width[i] = 0;
1238       }
1239       if (max < width[i])
1240       {
1241         max = width[i];
1242       }
1243     }
1244     boolean sort = false;
1245     for (int i = 0; i < width.length; i++)
1246     {
1247       // awidth = (float[]) typeWidth.get(data[i][0]);
1248       if (width[i] == 0)
1249       {
1250         width[i] = fr.getOrder(data[i][TYPE_COLUMN].toString());
1251         if (width[i] < 0)
1252         {
1253           width[i] = fr.setOrder(data[i][TYPE_COLUMN].toString(),
1254                   i / data.length);
1255         }
1256       }
1257       else
1258       {
1259         width[i] /= max; // normalize
1260         fr.setOrder(data[i][TYPE_COLUMN].toString(), width[i]); // store for
1261                                                                 // later
1262       }
1263       if (i > 0)
1264       {
1265         sort = sort || width[i - 1] > width[i];
1266       }
1267     }
1268     if (sort)
1269     {
1270       jalview.util.QuickSort.sort(width, data);
1271       // update global priority order
1272     }
1273
1274     updateFeatureRenderer(data, false);
1275     table.repaint();
1276   }
1277
1278   /**
1279    * close ourselves but leave any existing UI handlers (e.g a CDS/Protein tabbed
1280    * feature settings dialog) intact
1281    */
1282   public void closeOldSettings()
1283   {
1284     closeDialog(false);
1285   }
1286
1287   /**
1288    * close the feature settings dialog (and any containing frame)
1289    */
1290   public void close()
1291   {
1292     closeDialog(true);
1293   }
1294
1295   private void closeDialog(boolean closeContainingFrame)
1296   {
1297     try
1298     {
1299       if (frame != null)
1300       {
1301         af.setFeatureSettingsGeometry(frame.getBounds());
1302         frame.setClosed(true);
1303       }
1304       else
1305       {
1306         SplitContainerI sc = af.getSplitViewContainer();
1307         sc.closeFeatureSettings(this, closeContainingFrame);
1308         af.featureSettings = null;
1309       }
1310     } catch (Exception exe)
1311     {
1312     }
1313
1314   }
1315
1316   public void updateFeatureRenderer(Object[][] data)
1317   {
1318     updateFeatureRenderer(data, true);
1319   }
1320
1321   /**
1322    * Update the priority order of features; only repaint if this changed the
1323    * order of visible features
1324    * 
1325    * @param data
1326    * @param visibleNew
1327    */
1328   void updateFeatureRenderer(Object[][] data, boolean visibleNew)
1329   {
1330     FeatureSettingsBean[] rowData = getTableAsBeans(data);
1331
1332     if (fr.setFeaturePriority(rowData, visibleNew))
1333     {
1334       refreshDisplay();
1335     }
1336   }
1337
1338   /**
1339    * Converts table data into an array of data beans
1340    */
1341   private FeatureSettingsBean[] getTableAsBeans(Object[][] data)
1342   {
1343     FeatureSettingsBean[] rowData = new FeatureSettingsBean[data.length];
1344     for (int i = 0; i < data.length; i++)
1345     {
1346       String type = (String) data[i][TYPE_COLUMN];
1347       FeatureColourI colour = (FeatureColourI) data[i][COLOUR_COLUMN];
1348       FeatureMatcherSetI theFilter = (FeatureMatcherSetI) data[i][FILTER_COLUMN];
1349       Boolean isShown = (Boolean) data[i][SHOW_COLUMN];
1350       rowData[i] = new FeatureSettingsBean(type, colour, theFilter,
1351               isShown);
1352     }
1353     return rowData;
1354   }
1355
1356   private void jbInit() throws Exception
1357   {
1358     this.setLayout(new BorderLayout());
1359
1360     final boolean hasComplement = af.getViewport()
1361             .getCodingComplement() != null;
1362
1363     JPanel settingsPane = new JPanel();
1364     settingsPane.setLayout(new BorderLayout());
1365
1366     JPanel bigPanel = new JPanel();
1367     bigPanel.setLayout(new BorderLayout());
1368
1369     groupPanel = new JPanel();
1370     bigPanel.add(groupPanel, BorderLayout.NORTH);
1371
1372     JButton invert = new JButton(
1373             MessageManager.getString("label.invert_selection"));
1374     invert.setFont(JvSwingUtils.getLabelFont());
1375     invert.addActionListener(new ActionListener()
1376     {
1377       @Override
1378       public void actionPerformed(ActionEvent e)
1379       {
1380         invertSelection();
1381       }
1382     });
1383
1384     JButton optimizeOrder = new JButton(
1385             MessageManager.getString("label.optimise_order"));
1386     optimizeOrder.setFont(JvSwingUtils.getLabelFont());
1387     optimizeOrder.addActionListener(new ActionListener()
1388     {
1389       @Override
1390       public void actionPerformed(ActionEvent e)
1391       {
1392         orderByAvWidth();
1393       }
1394     });
1395
1396     final String byScoreLabel = MessageManager.getString("label.seq_sort_by_score");
1397     JButton sortByScore = new JButton(byScoreLabel);
1398     sortByScore.setFont(JvSwingUtils.getLabelFont());
1399     sortByScore.addActionListener(new ActionListener()
1400     {
1401       @Override
1402       public void actionPerformed(ActionEvent e)
1403       {
1404         if (canSortBy(byScoreLabel))
1405         {
1406           sortByScore(null);
1407         }
1408       }
1409     });
1410     final String byDensityLabel = MessageManager.getString("label.sequence_sort_by_density");
1411     JButton sortByDens = new JButton(byDensityLabel);
1412     sortByDens.setFont(JvSwingUtils.getLabelFont());
1413     sortByDens.addActionListener(new ActionListener()
1414     {
1415       @Override
1416       public void actionPerformed(ActionEvent e)
1417       {
1418         if (canSortBy(byDensityLabel))
1419         {
1420           sortByDensity(null);
1421         }
1422       }
1423     });
1424
1425     JButton help = new JButton(MessageManager.getString("action.help"));
1426     help.setFont(JvSwingUtils.getLabelFont());
1427     help.addActionListener(new ActionListener()
1428     {
1429       @Override
1430       public void actionPerformed(ActionEvent e)
1431       {
1432         try
1433         {
1434           Help.showHelpWindow(HelpId.SequenceFeatureSettings);
1435         } catch (HelpSetException e1)
1436         {
1437           e1.printStackTrace();
1438         }
1439       }
1440     });
1441     // Cancel for a SplitFrame should just revert changes to the currently displayed
1442     // settings. May want to do this for either or both - so need a splitview
1443     // feature settings cancel/OK.
1444     JButton cancel = new JButton(MessageManager
1445             .getString(hasComplement ? "action.revert" : "action.cancel"));
1446     cancel.setToolTipText(MessageManager.getString(hasComplement
1447             ? "action.undo_changes_to_feature_settings"
1448             : "action.undo_changes_to_feature_settings_and_close_the_dialog"));
1449     cancel.setFont(JvSwingUtils.getLabelFont());
1450     // TODO: disable cancel (and apply!) until current settings are different
1451     cancel.addActionListener(new ActionListener()
1452     {
1453       @Override
1454       public void actionPerformed(ActionEvent e)
1455       {
1456         revert();
1457         refreshDisplay();
1458         if (!hasComplement)
1459         {
1460           close();
1461         }
1462       }
1463     });
1464     // Cancel for the whole dialog should cancel both CDS and Protein.
1465     // OK for an individual feature settings just applies changes, but dialog
1466     // remains open
1467     JButton ok = new JButton(MessageManager
1468             .getString(hasComplement ? "action.apply" : "action.ok"));
1469     ok.setFont(JvSwingUtils.getLabelFont());
1470     ok.addActionListener(new ActionListener()
1471     {
1472       @Override
1473       public void actionPerformed(ActionEvent e)
1474       {
1475         if (!hasComplement)
1476         {
1477           close();
1478         }
1479         else
1480         {
1481           storeOriginalSettings();
1482         }
1483       }
1484     });
1485
1486     JButton loadColours = new JButton(
1487             MessageManager.getString("label.load_colours"));
1488     loadColours.setFont(JvSwingUtils.getLabelFont());
1489     loadColours.setToolTipText(
1490             MessageManager.getString("label.load_colours_tooltip"));
1491     loadColours.addActionListener(new ActionListener()
1492     {
1493       @Override
1494       public void actionPerformed(ActionEvent e)
1495       {
1496         load();
1497       }
1498     });
1499
1500     JButton saveColours = new JButton(
1501             MessageManager.getString("label.save_colours"));
1502     saveColours.setFont(JvSwingUtils.getLabelFont());
1503     saveColours.setToolTipText(
1504             MessageManager.getString("label.save_colours_tooltip"));
1505     saveColours.addActionListener(new ActionListener()
1506     {
1507       @Override
1508       public void actionPerformed(ActionEvent e)
1509       {
1510         save();
1511       }
1512     });
1513     transparency.addChangeListener(new ChangeListener()
1514     {
1515       @Override
1516       public void stateChanged(ChangeEvent evt)
1517       {
1518         if (!inConstruction)
1519         {
1520           fr.setTransparency((100 - transparency.getValue()) / 100f);
1521           refreshDisplay();
1522         }
1523       }
1524     });
1525
1526     transparency.setMaximum(70);
1527     transparency.setToolTipText(
1528             MessageManager.getString("label.transparency_tip"));
1529
1530     boolean nucleotide = af.getViewport().getAlignment().isNucleotide();
1531     String text = MessageManager.formatMessage("label.show_linked_features",
1532             nucleotide
1533                     ? MessageManager.getString("label.protein")
1534                             .toLowerCase(Locale.ROOT)
1535                     : "CDS");
1536     showComplement = new JCheckBox(text);
1537     showComplement.addActionListener(new ActionListener()
1538     {
1539       @Override
1540       public void actionPerformed(ActionEvent e)
1541       {
1542         af.getViewport()
1543                 .setShowComplementFeatures(showComplement.isSelected());
1544         refreshDisplay();
1545       }
1546     });
1547
1548     showComplementOnTop = new JCheckBox(
1549             MessageManager.getString("label.on_top"));
1550     showComplementOnTop.addActionListener(new ActionListener()
1551     {
1552       @Override
1553       public void actionPerformed(ActionEvent e)
1554       {
1555         af.getViewport().setShowComplementFeaturesOnTop(
1556                 showComplementOnTop.isSelected());
1557         refreshDisplay();
1558       }
1559     });
1560
1561     updateComplementButtons();
1562
1563     JPanel lowerPanel = new JPanel(new GridLayout(1, 2));
1564     bigPanel.add(lowerPanel, BorderLayout.SOUTH);
1565
1566     JPanel transbuttons = new JPanel(new GridLayout(5, 1));
1567     transbuttons.add(optimizeOrder);
1568     transbuttons.add(invert);
1569     transbuttons.add(sortByScore);
1570     transbuttons.add(sortByDens);
1571     transbuttons.add(help);
1572
1573     JPanel transPanelLeft = new JPanel(
1574             new GridLayout(hasComplement ? 4 : 2, 1));
1575     transPanelLeft.add(new JLabel(" Colour transparency" + ":"));
1576     transPanelLeft.add(transparency);
1577     if (hasComplement)
1578     {
1579       JPanel cp = new JPanel(new FlowLayout(FlowLayout.LEFT));
1580       cp.add(showComplement);
1581       cp.add(showComplementOnTop);
1582       transPanelLeft.add(cp);
1583     }
1584     lowerPanel.add(transPanelLeft);
1585     lowerPanel.add(transbuttons);
1586
1587     JPanel buttonPanel = new JPanel();
1588     buttonPanel.add(ok);
1589     buttonPanel.add(cancel);
1590     buttonPanel.add(loadColours);
1591     buttonPanel.add(saveColours);
1592     bigPanel.add(scrollPane, BorderLayout.CENTER);
1593     settingsPane.add(bigPanel, BorderLayout.CENTER);
1594     settingsPane.add(buttonPanel, BorderLayout.SOUTH);
1595     this.add(settingsPane);
1596   }
1597
1598   /**
1599    * Repaints alignment, structure and overview (if shown). If there is a
1600    * complementary view which is showing this view's features, then also
1601    * repaints that.
1602    */
1603   void refreshDisplay()
1604   {
1605     af.alignPanel.paintAlignment(true, true);
1606     AlignViewportI complement = af.getViewport().getCodingComplement();
1607     if (complement != null && complement.isShowComplementFeatures())
1608     {
1609       AlignFrame af2 = Desktop.getAlignFrameFor(complement);
1610       af2.alignPanel.paintAlignment(true, true);
1611     }
1612   }
1613
1614   /**
1615    * Answers a suitable tooltip to show on the colour cell of the table
1616    * 
1617    * @param fcol
1618    * @param withHint
1619    *                   if true include 'click to edit' and similar text
1620    * @return
1621    */
1622   public static String getColorTooltip(FeatureColourI fcol,
1623           boolean withHint)
1624   {
1625     if (fcol == null)
1626     {
1627       return null;
1628     }
1629     if (fcol.isSimpleColour())
1630     {
1631       return withHint ? BASE_TOOLTIP : null;
1632     }
1633     String description = fcol.getDescription();
1634     description = description.replaceAll("<", "&lt;");
1635     description = description.replaceAll(">", "&gt;");
1636     StringBuilder tt = new StringBuilder(description);
1637     if (withHint)
1638     {
1639       tt.append("<br>").append(BASE_TOOLTIP).append("</br>");
1640     }
1641     return JvSwingUtils.wrapTooltip(true, tt.toString());
1642   }
1643
1644   public static void renderGraduatedColor(JLabel comp, FeatureColourI gcol,
1645           int w, int h)
1646   {
1647     boolean thr = false;
1648     StringBuilder tx = new StringBuilder();
1649
1650     if (gcol.isColourByAttribute())
1651     {
1652       tx.append(FeatureMatcher
1653               .toAttributeDisplayName(gcol.getAttributeName()));
1654     }
1655     else if (!gcol.isColourByLabel())
1656     {
1657       tx.append(MessageManager.getString("label.score"));
1658     }
1659     tx.append(" ");
1660     if (gcol.isAboveThreshold())
1661     {
1662       thr = true;
1663       tx.append(">");
1664     }
1665     if (gcol.isBelowThreshold())
1666     {
1667       thr = true;
1668       tx.append("<");
1669     }
1670     if (gcol.isColourByLabel())
1671     {
1672       if (thr)
1673       {
1674         tx.append(" ");
1675       }
1676       if (!gcol.isColourByAttribute())
1677       {
1678         tx.append("Label");
1679       }
1680       comp.setIcon(null);
1681     }
1682     else
1683     {
1684       Color newColor = gcol.getMaxColour();
1685       comp.setBackground(newColor);
1686       // System.err.println("Width is " + w / 2);
1687       Icon ficon = new FeatureIcon(gcol, comp.getBackground(), w, h, thr);
1688       comp.setIcon(ficon);
1689       // tt+="RGB value: Max (" + newColor.getRed() + ", "
1690       // + newColor.getGreen() + ", " + newColor.getBlue()
1691       // + ")\nMin (" + minCol.getRed() + ", " + minCol.getGreen()
1692       // + ", " + minCol.getBlue() + ")");
1693     }
1694     comp.setHorizontalAlignment(SwingConstants.CENTER);
1695     comp.setText(tx.toString());
1696   }
1697
1698   // ///////////////////////////////////////////////////////////////////////
1699   // http://java.sun.com/docs/books/tutorial/uiswing/components/table.html
1700   // ///////////////////////////////////////////////////////////////////////
1701   class FeatureTableModel extends AbstractTableModel
1702   {
1703     private String[] columnNames = {
1704         MessageManager.getString("label.feature_type"),
1705         MessageManager.getString("action.colour"),
1706         MessageManager.getString("label.configuration"),
1707         MessageManager.getString("label.show") };
1708
1709     private Object[][] data;
1710
1711     FeatureTableModel(Object[][] data)
1712     {
1713       this.data = data;
1714     }
1715
1716     public Object[][] getData()
1717     {
1718       return data;
1719     }
1720
1721     public void setData(Object[][] data)
1722     {
1723       this.data = data;
1724     }
1725
1726     @Override
1727     public int getColumnCount()
1728     {
1729       return columnNames.length;
1730     }
1731
1732     public Object[] getRow(int row)
1733     {
1734       return data[row];
1735     }
1736
1737     @Override
1738     public int getRowCount()
1739     {
1740       return data.length;
1741     }
1742
1743     @Override
1744     public String getColumnName(int col)
1745     {
1746       return columnNames[col];
1747     }
1748
1749     @Override
1750     public Object getValueAt(int row, int col)
1751     {
1752       return data[row][col];
1753     }
1754
1755     /**
1756      * Answers the class of column c of the table
1757      */
1758     @Override
1759     public Class<?> getColumnClass(int c)
1760     {
1761       switch (c)
1762       {
1763       case TYPE_COLUMN:
1764         return String.class;
1765       case COLOUR_COLUMN:
1766         return FeatureColour.class;
1767       case FILTER_COLUMN:
1768         return FeatureMatcherSet.class;
1769       default:
1770         return Boolean.class;
1771       }
1772     }
1773
1774     @Override
1775     public boolean isCellEditable(int row, int col)
1776     {
1777       return col == 0 ? false : true;
1778     }
1779
1780     @Override
1781     public void setValueAt(Object value, int row, int col)
1782     {
1783       data[row][col] = value;
1784       fireTableCellUpdated(row, col);
1785       updateFeatureRenderer(data);
1786     }
1787
1788   }
1789
1790   class ColorRenderer extends JLabel implements TableCellRenderer
1791   {
1792     Border unselectedBorder = null;
1793
1794     Border selectedBorder = null;
1795
1796     public ColorRenderer()
1797     {
1798       setOpaque(true); // MUST do this for background to show up.
1799       setHorizontalTextPosition(SwingConstants.CENTER);
1800       setVerticalTextPosition(SwingConstants.CENTER);
1801     }
1802
1803     @Override
1804     public Component getTableCellRendererComponent(JTable tbl, Object color,
1805             boolean isSelected, boolean hasFocus, int row, int column)
1806     {
1807       FeatureColourI cellColour = (FeatureColourI) color;
1808       setOpaque(true);
1809       setBackground(tbl.getBackground());
1810       if (!cellColour.isSimpleColour())
1811       {
1812         Rectangle cr = tbl.getCellRect(row, column, false);
1813         FeatureSettings.renderGraduatedColor(this, cellColour,
1814                 (int) cr.getWidth(), (int) cr.getHeight());
1815       }
1816       else
1817       {
1818         this.setText("");
1819         this.setIcon(null);
1820         setBackground(cellColour.getColour());
1821       }
1822       if (isSelected)
1823       {
1824         if (selectedBorder == null)
1825         {
1826           selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
1827                   tbl.getSelectionBackground());
1828         }
1829         setBorder(selectedBorder);
1830       }
1831       else
1832       {
1833         if (unselectedBorder == null)
1834         {
1835           unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
1836                   tbl.getBackground());
1837         }
1838         setBorder(unselectedBorder);
1839       }
1840
1841       return this;
1842     }
1843   }
1844
1845   class FilterRenderer extends JLabel implements TableCellRenderer
1846   {
1847     javax.swing.border.Border unselectedBorder = null;
1848
1849     javax.swing.border.Border selectedBorder = null;
1850
1851     public FilterRenderer()
1852     {
1853       setOpaque(true); // MUST do this for background to show up.
1854       setHorizontalTextPosition(SwingConstants.CENTER);
1855       setVerticalTextPosition(SwingConstants.CENTER);
1856     }
1857
1858     @Override
1859     public Component getTableCellRendererComponent(JTable tbl,
1860             Object filter, boolean isSelected, boolean hasFocus, int row,
1861             int column)
1862     {
1863       FeatureMatcherSetI theFilter = (FeatureMatcherSetI) filter;
1864       setOpaque(true);
1865       String asText = theFilter.toString();
1866       setBackground(tbl.getBackground());
1867       this.setText(asText);
1868       this.setIcon(null);
1869
1870       if (isSelected)
1871       {
1872         if (selectedBorder == null)
1873         {
1874           selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
1875                   tbl.getSelectionBackground());
1876         }
1877         setBorder(selectedBorder);
1878       }
1879       else
1880       {
1881         if (unselectedBorder == null)
1882         {
1883           unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
1884                   tbl.getBackground());
1885         }
1886         setBorder(unselectedBorder);
1887       }
1888
1889       return this;
1890     }
1891   }
1892
1893   /**
1894    * update comp using rendering settings from gcol
1895    * 
1896    * @param comp
1897    * @param gcol
1898    */
1899   public static void renderGraduatedColor(JLabel comp, FeatureColourI gcol)
1900   {
1901     int w = comp.getWidth(), h = comp.getHeight();
1902     if (w < 20)
1903     {
1904       w = (int) comp.getPreferredSize().getWidth();
1905       h = (int) comp.getPreferredSize().getHeight();
1906       if (w < 20)
1907       {
1908         w = 80;
1909         h = 12;
1910       }
1911     }
1912     renderGraduatedColor(comp, gcol, w, h);
1913   }
1914
1915   @SuppressWarnings("serial")
1916   class ColorEditor extends AbstractCellEditor
1917           implements TableCellEditor, ActionListener
1918   {
1919     FeatureColourI currentColor;
1920
1921     FeatureTypeSettings chooser;
1922
1923     String type;
1924
1925     JButton button;
1926
1927     protected static final String EDIT = "edit";
1928
1929     int rowSelected = 0;
1930
1931     public ColorEditor()
1932     {
1933       // Set up the editor (from the table's point of view),
1934       // which is a button.
1935       // This button brings up the color chooser dialog,
1936       // which is the editor from the user's point of view.
1937       button = new JButton();
1938       button.setActionCommand(EDIT);
1939       button.addActionListener(this);
1940       button.setBorderPainted(false);
1941     }
1942
1943     /**
1944      * Handles events from the editor button, and from the colour/filters
1945      * dialog's OK button
1946      */
1947     @Override
1948     public void actionPerformed(ActionEvent e)
1949     {
1950       if (button == e.getSource())
1951       {
1952         if (currentColor.isSimpleColour())
1953         {
1954           /*
1955            * simple colour chooser
1956            */
1957           String ttl = MessageManager
1958                   .formatMessage("label.select_colour_for", type);
1959           ColourChooserListener listener = new ColourChooserListener()
1960           {
1961             @Override
1962             public void colourSelected(Color c)
1963             {
1964               currentColor = new FeatureColour(c);
1965               table.setValueAt(currentColor, rowSelected, COLOUR_COLUMN);
1966               fireEditingStopped();
1967             }
1968
1969             @Override
1970             public void cancel()
1971             {
1972               fireEditingStopped();
1973             }
1974           };
1975           JalviewColourChooser.showColourChooser(button, ttl,
1976                   currentColor.getColour(), listener);
1977         }
1978         else
1979         {
1980           /*
1981            * variable colour and filters dialog
1982            */
1983           chooser = new FeatureTypeSettings(fr, type);
1984           if (!Platform.isJS())
1985           /**
1986            * Java only
1987            * 
1988            * @j2sIgnore
1989            */
1990           {
1991             chooser.setRequestFocusEnabled(true);
1992             chooser.requestFocus();
1993           }
1994           chooser.addActionListener(this);
1995           fireEditingStopped();
1996         }
1997       }
1998       else
1999       {
2000         /*
2001          * after OK in variable colour dialog, any changes to colour 
2002          * (or filters!) are already set in FeatureRenderer, so just
2003          * update table data without triggering updateFeatureRenderer
2004          */
2005         currentColor = fr.getFeatureColours().get(type);
2006         FeatureMatcherSetI currentFilter = fr.getFeatureFilter(type);
2007         if (currentFilter == null)
2008         {
2009           currentFilter = new FeatureMatcherSet();
2010         }
2011         Object[] data = ((FeatureTableModel) table.getModel())
2012                 .getData()[rowSelected];
2013         data[COLOUR_COLUMN] = currentColor;
2014         data[FILTER_COLUMN] = currentFilter;
2015         fireEditingStopped();
2016         // SwingJS needs an explicit repaint() here,
2017         // rather than relying upon no validation having
2018         // occurred since the stopEditing call was made.
2019         // Its laying out has not been stopped by the modal frame
2020         table.validate();
2021         table.repaint();
2022       }
2023     }
2024
2025     /**
2026      * Override allows access to this method from anonymous inner classes
2027      */
2028     @Override
2029     protected void fireEditingStopped()
2030     {
2031       super.fireEditingStopped();
2032     }
2033
2034     // Implement the one CellEditor method that AbstractCellEditor doesn't.
2035     @Override
2036     public Object getCellEditorValue()
2037     {
2038       return currentColor;
2039     }
2040
2041     // Implement the one method defined by TableCellEditor.
2042     @Override
2043     public Component getTableCellEditorComponent(JTable theTable,
2044             Object value, boolean isSelected, int row, int column)
2045     {
2046       currentColor = (FeatureColourI) value;
2047       this.rowSelected = row;
2048       type = table.getValueAt(row, TYPE_COLUMN).toString();
2049       button.setOpaque(true);
2050       button.setBackground(FeatureSettings.this.getBackground());
2051       if (!currentColor.isSimpleColour())
2052       {
2053         JLabel btn = new JLabel();
2054         btn.setSize(button.getSize());
2055         FeatureSettings.renderGraduatedColor(btn, currentColor);
2056         button.setBackground(btn.getBackground());
2057         button.setIcon(btn.getIcon());
2058         button.setText(btn.getText());
2059       }
2060       else
2061       {
2062         button.setText("");
2063         button.setIcon(null);
2064         button.setBackground(currentColor.getColour());
2065       }
2066       return button;
2067     }
2068   }
2069
2070   /**
2071    * The cell editor for the Filter column. It displays the text of any filters
2072    * for the feature type in that row (in full as a tooltip, possible
2073    * abbreviated as display text). On click in the cell, opens the Feature
2074    * Display Settings dialog at the Filters tab.
2075    */
2076   @SuppressWarnings("serial")
2077   class FilterEditor extends AbstractCellEditor
2078           implements TableCellEditor, ActionListener
2079   {
2080
2081     FeatureMatcherSetI currentFilter;
2082
2083     Point lastLocation;
2084
2085     String type;
2086
2087     JButton button;
2088
2089     protected static final String EDIT = "edit";
2090
2091     int rowSelected = 0;
2092
2093     public FilterEditor()
2094     {
2095       button = new JButton();
2096       button.setActionCommand(EDIT);
2097       button.addActionListener(this);
2098       button.setBorderPainted(false);
2099     }
2100
2101     /**
2102      * Handles events from the editor button
2103      */
2104     @Override
2105     public void actionPerformed(ActionEvent e)
2106     {
2107       if (button == e.getSource())
2108       {
2109         FeatureTypeSettings chooser = new FeatureTypeSettings(fr, type);
2110         chooser.addActionListener(this);
2111         chooser.setRequestFocusEnabled(true);
2112         chooser.requestFocus();
2113         if (lastLocation != null)
2114         {
2115           // todo open at its last position on screen
2116           chooser.setBounds(lastLocation.x, lastLocation.y,
2117                   chooser.getWidth(), chooser.getHeight());
2118           chooser.validate();
2119         }
2120         fireEditingStopped();
2121       }
2122       else if (e.getSource() instanceof Component)
2123       {
2124
2125         /*
2126          * after OK in variable colour dialog, any changes to filter
2127          * (or colours!) are already set in FeatureRenderer, so just
2128          * update table data without triggering updateFeatureRenderer
2129          */
2130         FeatureColourI currentColor = fr.getFeatureColours().get(type);
2131         currentFilter = fr.getFeatureFilter(type);
2132         if (currentFilter == null)
2133         {
2134           currentFilter = new FeatureMatcherSet();
2135         }
2136
2137         Object[] data = ((FeatureTableModel) table.getModel())
2138                 .getData()[rowSelected];
2139         data[COLOUR_COLUMN] = currentColor;
2140         data[FILTER_COLUMN] = currentFilter;
2141         fireEditingStopped();
2142         // SwingJS needs an explicit repaint() here,
2143         // rather than relying upon no validation having
2144         // occurred since the stopEditing call was made.
2145         // Its laying out has not been stopped by the modal frame
2146         table.validate();
2147         table.repaint();
2148       }
2149     }
2150
2151     @Override
2152     public Object getCellEditorValue()
2153     {
2154       return currentFilter;
2155     }
2156
2157     @Override
2158     public Component getTableCellEditorComponent(JTable theTable,
2159             Object value, boolean isSelected, int row, int column)
2160     {
2161       currentFilter = (FeatureMatcherSetI) value;
2162       this.rowSelected = row;
2163       type = table.getValueAt(row, TYPE_COLUMN).toString();
2164       button.setOpaque(true);
2165       button.setBackground(FeatureSettings.this.getBackground());
2166       button.setText(currentFilter.toString());
2167       button.setIcon(null);
2168       return button;
2169     }
2170   }
2171
2172   public boolean isOpen()
2173   {
2174     if (af.getSplitViewContainer() != null)
2175     {
2176       return af.getSplitViewContainer().isFeatureSettingsOpen();
2177     }
2178     return frame != null && !frame.isClosed();
2179   }
2180
2181   @Override
2182   public void revert()
2183   {
2184     fr.setTransparency(originalTransparency);
2185     fr.setFeatureFilters(originalFilters);
2186     updateFeatureRenderer(originalData);
2187     af.getViewport().setViewStyle(originalViewStyle);
2188     updateTransparencySliderFromFR();
2189     updateComplementButtons();
2190     refreshDisplay();
2191   }
2192 }
2193
2194 class FeatureIcon implements Icon
2195 {
2196   FeatureColourI gcol;
2197
2198   Color backg;
2199
2200   boolean midspace = false;
2201
2202   int width = 50, height = 20;
2203
2204   int s1, e1; // start and end of midpoint band for thresholded symbol
2205
2206   Color mpcolour = Color.white;
2207
2208   FeatureIcon(FeatureColourI gfc, Color bg, int w, int h, boolean mspace)
2209   {
2210     gcol = gfc;
2211     backg = bg;
2212     width = w;
2213     height = h;
2214     midspace = mspace;
2215     if (midspace)
2216     {
2217       s1 = width / 3;
2218       e1 = s1 * 2;
2219     }
2220     else
2221     {
2222       s1 = width / 2;
2223       e1 = s1;
2224     }
2225   }
2226
2227   @Override
2228   public int getIconWidth()
2229   {
2230     return width;
2231   }
2232
2233   @Override
2234   public int getIconHeight()
2235   {
2236     return height;
2237   }
2238
2239   @Override
2240   public void paintIcon(Component c, Graphics g, int x, int y)
2241   {
2242
2243     if (gcol.isColourByLabel())
2244     {
2245       g.setColor(backg);
2246       g.fillRect(0, 0, width, height);
2247       // need an icon here.
2248       g.setColor(gcol.getMaxColour());
2249
2250       g.setFont(new Font("Verdana", Font.PLAIN, 9));
2251
2252       // g.setFont(g.getFont().deriveFont(
2253       // AffineTransform.getScaleInstance(
2254       // width/g.getFontMetrics().stringWidth("Label"),
2255       // height/g.getFontMetrics().getHeight())));
2256
2257       g.drawString(MessageManager.getString("label.label"), 0, 0);
2258
2259     }
2260     else
2261     {
2262       Color minCol = gcol.getMinColour();
2263       g.setColor(minCol);
2264       g.fillRect(0, 0, s1, height);
2265       if (midspace)
2266       {
2267         g.setColor(Color.white);
2268         g.fillRect(s1, 0, e1 - s1, height);
2269       }
2270       g.setColor(gcol.getMaxColour());
2271       // g.fillRect(0, e1, width - e1, height); // BH 2018
2272       g.fillRect(e1, 0, width - e1, height);
2273     }
2274   }
2275 }