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