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