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