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