JAL-3186 reorder popup menu slightly
[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     JPopupMenu men = new JPopupMenu(MessageManager
446             .formatMessage("label.settings_for_param", new String[]
447             { type }));
448     final FeatureColourI featureColour = (FeatureColourI) typeCol;
449
450     /*
451      * menu option to select (or deselect) variable colour
452      */
453     final JCheckBoxMenuItem variableColourCB = new JCheckBoxMenuItem(
454             MessageManager.getString("label.variable_colour"));
455     variableColourCB.setSelected(!featureColour.isSimpleColour());
456     men.add(variableColourCB);
457     
458     /*
459      * checkbox action listener doubles up as listener to OK
460      * from the variable colour / filters dialog
461      */
462     variableColourCB.addActionListener(new ActionListener()
463     {
464       @Override
465       public void actionPerformed(ActionEvent e)
466       {
467         if (e.getSource() == variableColourCB)
468         {
469           men.setVisible(true); // BH 2018 for JavaScript because this is a checkbox
470           men.setVisible(false); // BH 2018 for JavaScript because this is a checkbox
471           if (featureColour.isSimpleColour())
472           {
473             /*
474              * toggle simple colour to variable colour - show dialog
475              */
476             FeatureTypeSettings fc = new FeatureTypeSettings(fr, type);
477             fc.addActionListener(this);
478           }
479           else
480           {
481             /*
482              * toggle variable to simple colour - show colour chooser
483              */
484             String title = MessageManager.formatMessage("label.select_colour_for", type);
485             ColourChooserListener listener = new ColourChooserListener()
486             {
487               @Override
488               public void colourSelected(Color c)
489               {
490                 table.setValueAt(new FeatureColour(c), rowSelected,
491                         COLOUR_COLUMN);
492                 table.validate();
493                 updateFeatureRenderer(
494                         ((FeatureTableModel) table.getModel()).getData(),
495                         false);
496               }
497             };
498             JalviewColourChooser.showColourChooser(FeatureSettings.this, title,
499                 featureColour.getMaxColour(), listener);
500           }
501         }
502         else    
503         {
504           if (e.getSource() instanceof FeatureTypeSettings)
505           {
506             /*
507              * update after OK in feature colour dialog; the updated
508              * colour will have already been set in the FeatureRenderer
509              */
510             FeatureColourI fci = fr.getFeatureColours().get(type);
511             table.setValueAt(fci, rowSelected, COLOUR_COLUMN);
512             // BH 2018 setting a table value does not invalidate it.
513 //                  System.out.println("FeatureSettings is valied" + table.isValid());
514 //                  table.validate();
515           }
516         }
517       }
518     });
519     
520     men.addSeparator();
521
522     JMenuItem scr = new JMenuItem(
523             MessageManager.getString("label.sort_by_score"));
524     men.add(scr);
525     scr.addActionListener(new ActionListener()
526     {
527
528       @Override
529       public void actionPerformed(ActionEvent e)
530       {
531         af.avc.sortAlignmentByFeatureScore(Arrays.asList(new String[]
532                 { type }));
533       }
534     });
535     JMenuItem dens = new JMenuItem(
536             MessageManager.getString("label.sort_by_density"));
537     dens.addActionListener(new ActionListener()
538     {
539
540       @Override
541       public void actionPerformed(ActionEvent e)
542       {
543         af.avc.sortAlignmentByFeatureDensity(Arrays.asList(new String[]
544                 { type }));
545       }
546     });
547     men.add(dens);
548
549     JMenuItem selCols = new JMenuItem(
550             MessageManager.getString("label.select_columns_containing"));
551     selCols.addActionListener(new ActionListener()
552     {
553       @Override
554       public void actionPerformed(ActionEvent arg0)
555       {
556         fr.ap.alignFrame.avc.markColumnsContainingFeatures(false, false,
557                 false, type);
558       }
559     });
560     JMenuItem clearCols = new JMenuItem(MessageManager
561             .getString("label.select_columns_not_containing"));
562     clearCols.addActionListener(new ActionListener()
563     {
564       @Override
565       public void actionPerformed(ActionEvent arg0)
566       {
567         fr.ap.alignFrame.avc.markColumnsContainingFeatures(true, false,
568                 false, type);
569       }
570     });
571     JMenuItem hideCols = new JMenuItem(
572             MessageManager.getString("label.hide_columns_containing"));
573     hideCols.addActionListener(new ActionListener()
574     {
575       @Override
576       public void actionPerformed(ActionEvent arg0)
577       {
578         fr.ap.alignFrame.hideFeatureColumns(type, true);
579       }
580     });
581     JMenuItem hideOtherCols = new JMenuItem(
582             MessageManager.getString("label.hide_columns_not_containing"));
583     hideOtherCols.addActionListener(new ActionListener()
584     {
585       @Override
586       public void actionPerformed(ActionEvent arg0)
587       {
588         fr.ap.alignFrame.hideFeatureColumns(type, false);
589       }
590     });
591     men.add(selCols);
592     men.add(clearCols);
593     men.add(hideCols);
594     men.add(hideOtherCols);
595     men.show(table, pt.x, pt.y);
596   }
597
598   @Override
599   synchronized public void discoverAllFeatureData()
600   {
601     Set<String> allGroups = new HashSet<>();
602     AlignmentI alignment = af.getViewport().getAlignment();
603
604     for (int i = 0; i < alignment.getHeight(); i++)
605     {
606       SequenceI seq = alignment.getSequenceAt(i);
607       for (String group : seq.getFeatures().getFeatureGroups(true))
608       {
609         if (group != null && !allGroups.contains(group))
610         {
611           allGroups.add(group);
612           checkGroupState(group);
613         }
614       }
615     }
616
617     resetTable(null);
618
619     validate();
620   }
621
622   /**
623    * Synchronise gui group list and check visibility of group
624    * 
625    * @param group
626    * @return true if group is visible
627    */
628   private boolean checkGroupState(String group)
629   {
630     boolean visible = fr.checkGroupVisibility(group, true);
631
632     for (int g = 0; g < groupPanel.getComponentCount(); g++)
633     {
634       if (((JCheckBox) groupPanel.getComponent(g)).getText().equals(group))
635       {
636         ((JCheckBox) groupPanel.getComponent(g)).setSelected(visible);
637         return visible;
638       }
639     }
640
641     final String grp = group;
642     final JCheckBox check = new JCheckBox(group, visible);
643     check.setFont(new Font("Serif", Font.BOLD, 12));
644     check.setToolTipText(group);
645     check.addItemListener(new ItemListener()
646     {
647       @Override
648       public void itemStateChanged(ItemEvent evt)
649       {
650         fr.setGroupVisibility(check.getText(), check.isSelected());
651         resetTable(new String[] { grp });
652         af.alignPanel.paintAlignment(true, true);
653       }
654     });
655     groupPanel.add(check);
656     return visible;
657   }
658
659   synchronized void resetTable(String[] groupChanged)
660   {
661     if (resettingTable)
662     {
663       return;
664     }
665     resettingTable = true;
666     typeWidth = new Hashtable<>();
667     // TODO: change avWidth calculation to 'per-sequence' average and use long
668     // rather than float
669
670     Set<String> displayableTypes = new HashSet<>();
671     Set<String> foundGroups = new HashSet<>();
672
673     /*
674      * determine which feature types may be visible depending on 
675      * which groups are selected, and recompute average width data
676      */
677     for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++)
678     {
679
680       SequenceI seq = af.getViewport().getAlignment().getSequenceAt(i);
681
682       /*
683        * get the sequence's groups for positional features
684        * and keep track of which groups are visible
685        */
686       Set<String> groups = seq.getFeatures().getFeatureGroups(true);
687       Set<String> visibleGroups = new HashSet<>();
688       for (String group : groups)
689       {
690         if (group == null || checkGroupState(group))
691         {
692           visibleGroups.add(group);
693         }
694       }
695       foundGroups.addAll(groups);
696
697       /*
698        * get distinct feature types for visible groups
699        * record distinct visible types, and their count and total length
700        */
701       Set<String> types = seq.getFeatures().getFeatureTypesForGroups(true,
702               visibleGroups.toArray(new String[visibleGroups.size()]));
703       for (String type : types)
704       {
705         displayableTypes.add(type);
706         float[] avWidth = typeWidth.get(type);
707         if (avWidth == null)
708         {
709           avWidth = new float[2];
710           typeWidth.put(type, avWidth);
711         }
712         // todo this could include features with a non-visible group
713         // - do we greatly care?
714         // todo should we include non-displayable features here, and only
715         // update when features are added?
716         avWidth[0] += seq.getFeatures().getFeatureCount(true, type);
717         avWidth[1] += seq.getFeatures().getTotalFeatureLength(type);
718       }
719     }
720
721     Object[][] data = new Object[displayableTypes.size()][COLUMN_COUNT];
722     int dataIndex = 0;
723
724     if (fr.hasRenderOrder())
725     {
726       if (!handlingUpdate)
727       {
728         fr.findAllFeatures(groupChanged != null); // prod to update
729         // colourschemes. but don't
730         // affect display
731         // First add the checks in the previous render order,
732         // in case the window has been closed and reopened
733       }
734       List<String> frl = fr.getRenderOrder();
735       for (int ro = frl.size() - 1; ro > -1; ro--)
736       {
737         String type = frl.get(ro);
738
739         if (!displayableTypes.contains(type))
740         {
741           continue;
742         }
743
744         data[dataIndex][TYPE_COLUMN] = type;
745         data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type);
746         FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type);
747         data[dataIndex][FILTER_COLUMN] = featureFilter == null
748                 ? new FeatureMatcherSet()
749                 : featureFilter;
750         data[dataIndex][SHOW_COLUMN] = new Boolean(
751                 af.getViewport().getFeaturesDisplayed().isVisible(type));
752         dataIndex++;
753         displayableTypes.remove(type);
754       }
755     }
756
757     /*
758      * process any extra features belonging only to 
759      * a group which was just selected
760      */
761     while (!displayableTypes.isEmpty())
762     {
763       String type = displayableTypes.iterator().next();
764       data[dataIndex][TYPE_COLUMN] = type;
765
766       data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type);
767       if (data[dataIndex][COLOUR_COLUMN] == null)
768       {
769         // "Colour has been updated in another view!!"
770         fr.clearRenderOrder();
771         return;
772       }
773       FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type);
774       data[dataIndex][FILTER_COLUMN] = featureFilter == null
775               ? new FeatureMatcherSet()
776               : featureFilter;
777       data[dataIndex][SHOW_COLUMN] = new Boolean(true);
778       dataIndex++;
779       displayableTypes.remove(type);
780     }
781
782     if (originalData == null)
783     {
784       originalData = new Object[data.length][COLUMN_COUNT];
785       for (int i = 0; i < data.length; i++)
786       {
787         System.arraycopy(data[i], 0, originalData[i], 0, COLUMN_COUNT);
788       }
789     }
790     else
791     {
792       updateOriginalData(data);
793     }
794
795     table.setModel(new FeatureTableModel(data));
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     chooser.setResponseHandler(0, new Runnable()
930     {
931           @Override
932           public void run() 
933           {
934             File file = chooser.getSelectedFile();
935                 load(file);
936           }
937         });
938     chooser.showOpenDialog(this);
939   }
940
941   /**
942    * Loads feature colours and filters from XML stored in the given file
943    * 
944    * @param file
945    */
946   void load(File file)
947   {
948     try
949     {
950       InputStreamReader in = new InputStreamReader(
951               new FileInputStream(file), "UTF-8");
952
953       JAXBContext jc = JAXBContext
954               .newInstance("jalview.xml.binding.jalview");
955       javax.xml.bind.Unmarshaller um = jc.createUnmarshaller();
956       XMLStreamReader streamReader = XMLInputFactory.newInstance()
957               .createXMLStreamReader(in);
958       JAXBElement<JalviewUserColours> jbe = um.unmarshal(streamReader,
959               JalviewUserColours.class);
960       JalviewUserColours jucs = jbe.getValue();
961
962       // JalviewUserColours jucs = JalviewUserColours.unmarshal(in);
963
964       /*
965        * load feature colours
966        */
967       for (int i = jucs.getColour().size() - 1; i >= 0; i--)
968       {
969         Colour newcol = jucs.getColour().get(i);
970         FeatureColourI colour = jalview.project.Jalview2XML
971                 .parseColour(newcol);
972         fr.setColour(newcol.getName(), colour);
973         fr.setOrder(newcol.getName(), i / (float) jucs.getColour().size());
974       }
975
976       /*
977        * load feature filters; loaded filters will replace any that are
978        * currently defined, other defined filters are left unchanged 
979        */
980       for (int i = 0; i < jucs.getFilter().size(); i++)
981       {
982         Filter filterModel = jucs.getFilter().get(i);
983         String featureType = filterModel.getFeatureType();
984         FeatureMatcherSetI filter = jalview.project.Jalview2XML
985                 .parseFilter(featureType, filterModel.getMatcherSet());
986         if (!filter.isEmpty())
987         {
988           fr.setFeatureFilter(featureType, filter);
989         }
990       }
991
992       /*
993        * update feature settings table
994        */
995       if (table != null)
996       {
997         resetTable(null);
998         Object[][] data = ((FeatureTableModel) table.getModel())
999                 .getData();
1000         ensureOrder(data);
1001         updateFeatureRenderer(data, false);
1002         table.repaint();
1003       }
1004     } catch (Exception ex)
1005     {
1006       System.out.println("Error loading User Colour File\n" + ex);
1007     }
1008   }
1009
1010   /**
1011    * Offers a file chooser dialog, and then saves the current feature colours
1012    * and any filters to the selected file in XML format
1013    */
1014   void save()
1015   {
1016     JalviewFileChooser chooser = new JalviewFileChooser("fc",
1017             SEQUENCE_FEATURE_COLOURS);
1018     chooser.setFileView(new JalviewFileView());
1019     chooser.setDialogTitle(
1020             MessageManager.getString("label.save_feature_colours"));
1021     chooser.setToolTipText(MessageManager.getString("action.save"));
1022     int option = chooser.showSaveDialog(this);
1023         if (option == JalviewFileChooser.APPROVE_OPTION) 
1024         {
1025           File file = chooser.getSelectedFile();
1026           save(file);
1027         }
1028   }
1029
1030   /**
1031    * Saves feature colours and filters to the given file
1032    * 
1033    * @param file
1034    */
1035   void save(File file)
1036   {
1037     JalviewUserColours ucs = new JalviewUserColours();
1038     ucs.setSchemeName("Sequence Features");
1039     try
1040     {
1041       PrintWriter out = new PrintWriter(new OutputStreamWriter(
1042               new FileOutputStream(file), "UTF-8"));
1043
1044       /*
1045        * sort feature types by colour order, from 0 (highest)
1046        * to 1 (lowest)
1047        */
1048       Set<String> fr_colours = fr.getAllFeatureColours();
1049       String[] sortedTypes = fr_colours
1050               .toArray(new String[fr_colours.size()]);
1051       Arrays.sort(sortedTypes, new Comparator<String>()
1052       {
1053         @Override
1054         public int compare(String type1, String type2)
1055         {
1056           return Float.compare(fr.getOrder(type1), fr.getOrder(type2));
1057         }
1058       });
1059
1060       /*
1061        * save feature colours
1062        */
1063       for (String featureType : sortedTypes)
1064       {
1065         FeatureColourI fcol = fr.getFeatureStyle(featureType);
1066         Colour col = jalview.project.Jalview2XML.marshalColour(featureType,
1067                 fcol);
1068         ucs.getColour().add(col);
1069       }
1070
1071       /*
1072        * save any feature filters
1073        */
1074       for (String featureType : sortedTypes)
1075       {
1076         FeatureMatcherSetI filter = fr.getFeatureFilter(featureType);
1077         if (filter != null && !filter.isEmpty())
1078         {
1079           Iterator<FeatureMatcherI> iterator = filter.getMatchers().iterator();
1080           FeatureMatcherI firstMatcher = iterator.next();
1081           jalview.xml.binding.jalview.FeatureMatcherSet ms = jalview.project.Jalview2XML
1082                   .marshalFilter(firstMatcher, iterator,
1083                   filter.isAnded());
1084           Filter filterModel = new Filter();
1085           filterModel.setFeatureType(featureType);
1086           filterModel.setMatcherSet(ms);
1087           ucs.getFilter().add(filterModel);
1088         }
1089       }
1090       JAXBContext jaxbContext = JAXBContext
1091               .newInstance(JalviewUserColours.class);
1092       Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
1093       jaxbMarshaller.marshal(
1094               new ObjectFactory().createJalviewUserColours(ucs), out);
1095
1096       // jaxbMarshaller.marshal(object, pout);
1097       // marshaller.marshal(object);
1098       out.flush();
1099
1100       // ucs.marshal(out);
1101       out.close();
1102     } catch (Exception ex)
1103     {
1104       ex.printStackTrace();
1105     }
1106   }
1107
1108   public void invertSelection()
1109   {
1110     Object[][] data = ((FeatureTableModel) table.getModel()).getData();
1111     for (int i = 0; i < data.length; i++)
1112     {
1113       data[i][SHOW_COLUMN] = !(Boolean) data[i][SHOW_COLUMN];
1114     }
1115     updateFeatureRenderer(data, true);
1116     table.repaint();
1117   }
1118
1119   public void orderByAvWidth()
1120   {
1121     if (table == null || table.getModel() == null)
1122     {
1123       return;
1124     }
1125     Object[][] data = ((FeatureTableModel) table.getModel()).getData();
1126     float[] width = new float[data.length];
1127     float[] awidth;
1128     float max = 0;
1129
1130     for (int i = 0; i < data.length; i++)
1131     {
1132       awidth = typeWidth.get(data[i][TYPE_COLUMN]);
1133       if (awidth[0] > 0)
1134       {
1135         width[i] = awidth[1] / awidth[0];// *awidth[0]*awidth[2]; - better
1136         // weight - but have to make per
1137         // sequence, too (awidth[2])
1138         // if (width[i]==1) // hack to distinguish single width sequences.
1139       }
1140       else
1141       {
1142         width[i] = 0;
1143       }
1144       if (max < width[i])
1145       {
1146         max = width[i];
1147       }
1148     }
1149     boolean sort = false;
1150     for (int i = 0; i < width.length; i++)
1151     {
1152       // awidth = (float[]) typeWidth.get(data[i][0]);
1153       if (width[i] == 0)
1154       {
1155         width[i] = fr.getOrder(data[i][TYPE_COLUMN].toString());
1156         if (width[i] < 0)
1157         {
1158           width[i] = fr.setOrder(data[i][TYPE_COLUMN].toString(),
1159                   i / data.length);
1160         }
1161       }
1162       else
1163       {
1164         width[i] /= max; // normalize
1165         fr.setOrder(data[i][TYPE_COLUMN].toString(), width[i]); // store for later
1166       }
1167       if (i > 0)
1168       {
1169         sort = sort || width[i - 1] > width[i];
1170       }
1171     }
1172     if (sort)
1173     {
1174       jalview.util.QuickSort.sort(width, data);
1175       // update global priority order
1176     }
1177
1178     updateFeatureRenderer(data, false);
1179     table.repaint();
1180   }
1181
1182   public void close()
1183   {
1184     try
1185     {
1186       frame.setClosed(true);
1187     } catch (Exception exe)
1188     {
1189     }
1190
1191   }
1192
1193   public void updateFeatureRenderer(Object[][] data)
1194   {
1195     updateFeatureRenderer(data, true);
1196   }
1197
1198   /**
1199    * Update the priority order of features; only repaint if this changed the order
1200    * of visible features
1201    * 
1202    * @param data
1203    * @param visibleNew
1204    */
1205   void updateFeatureRenderer(Object[][] data, boolean visibleNew)
1206   {
1207     FeatureSettingsBean[] rowData = getTableAsBeans(data);
1208
1209     if (fr.setFeaturePriority(rowData, visibleNew))
1210     {
1211       af.alignPanel.paintAlignment(true, true);
1212     }
1213   }
1214
1215   /**
1216    * Converts table data into an array of data beans
1217    */
1218   private FeatureSettingsBean[] getTableAsBeans(Object[][] data)
1219   {
1220     FeatureSettingsBean[] rowData = new FeatureSettingsBean[data.length];
1221     for (int i = 0; i < data.length; i++)
1222     {
1223       String type = (String) data[i][TYPE_COLUMN];
1224       FeatureColourI colour = (FeatureColourI) data[i][COLOUR_COLUMN];
1225       FeatureMatcherSetI theFilter = (FeatureMatcherSetI) data[i][FILTER_COLUMN];
1226       Boolean isShown = (Boolean) data[i][SHOW_COLUMN];
1227       rowData[i] = new FeatureSettingsBean(type, colour, theFilter,
1228               isShown);
1229     }
1230     return rowData;
1231   }
1232
1233   private void jbInit() throws Exception
1234   {
1235     this.setLayout(new BorderLayout());
1236
1237     JPanel settingsPane = new JPanel();
1238     settingsPane.setLayout(new BorderLayout());
1239
1240     JPanel bigPanel = new JPanel();
1241     bigPanel.setLayout(new BorderLayout());
1242
1243     groupPanel = new JPanel();
1244     bigPanel.add(groupPanel, BorderLayout.NORTH);
1245
1246     JButton invert = new JButton(
1247             MessageManager.getString("label.invert_selection"));
1248     invert.setFont(JvSwingUtils.getLabelFont());
1249     invert.addActionListener(new ActionListener()
1250     {
1251       @Override
1252       public void actionPerformed(ActionEvent e)
1253       {
1254         invertSelection();
1255       }
1256     });
1257
1258     JButton optimizeOrder = new JButton(
1259             MessageManager.getString("label.optimise_order"));
1260     optimizeOrder.setFont(JvSwingUtils.getLabelFont());
1261     optimizeOrder.addActionListener(new ActionListener()
1262     {
1263       @Override
1264       public void actionPerformed(ActionEvent e)
1265       {
1266         orderByAvWidth();
1267       }
1268     });
1269
1270     JButton sortByScore = new JButton(
1271             MessageManager.getString("label.seq_sort_by_score"));
1272     sortByScore.setFont(JvSwingUtils.getLabelFont());
1273     sortByScore.addActionListener(new ActionListener()
1274     {
1275       @Override
1276       public void actionPerformed(ActionEvent e)
1277       {
1278         af.avc.sortAlignmentByFeatureScore(null);
1279       }
1280     });
1281     JButton sortByDens = new JButton(
1282             MessageManager.getString("label.sequence_sort_by_density"));
1283     sortByDens.setFont(JvSwingUtils.getLabelFont());
1284     sortByDens.addActionListener(new ActionListener()
1285     {
1286       @Override
1287       public void actionPerformed(ActionEvent e)
1288       {
1289         af.avc.sortAlignmentByFeatureDensity(null);
1290       }
1291     });
1292
1293     JButton help = new JButton(MessageManager.getString("action.help"));
1294     help.setFont(JvSwingUtils.getLabelFont());
1295     help.addActionListener(new ActionListener()
1296     {
1297       @Override
1298       public void actionPerformed(ActionEvent e)
1299       {
1300         try
1301         {
1302           Help.showHelpWindow(HelpId.SequenceFeatureSettings);
1303         } catch (HelpSetException e1)
1304         {
1305           e1.printStackTrace();
1306         }
1307       }
1308     });
1309     help.setFont(JvSwingUtils.getLabelFont());
1310     help.setText(MessageManager.getString("action.help"));
1311     help.addActionListener(new ActionListener()
1312     {
1313       @Override
1314       public void actionPerformed(ActionEvent e)
1315       {
1316         try
1317         {
1318           Help.showHelpWindow(HelpId.SequenceFeatureSettings);
1319         } catch (HelpSetException e1)
1320         {
1321           e1.printStackTrace();
1322         }
1323       }
1324     });
1325
1326     JButton cancel = new JButton(MessageManager.getString("action.cancel"));
1327     cancel.setFont(JvSwingUtils.getLabelFont());
1328     cancel.addActionListener(new ActionListener()
1329     {
1330       @Override
1331       public void actionPerformed(ActionEvent e)
1332       {
1333         fr.setTransparency(originalTransparency);
1334         fr.setFeatureFilters(originalFilters);
1335         updateFeatureRenderer(originalData);
1336         close();
1337       }
1338     });
1339
1340     JButton ok = new JButton(MessageManager.getString("action.ok"));
1341     ok.setFont(JvSwingUtils.getLabelFont());
1342     ok.addActionListener(new ActionListener()
1343     {
1344       @Override
1345       public void actionPerformed(ActionEvent e)
1346       {
1347         close();
1348       }
1349     });
1350
1351     JButton loadColours = new JButton(
1352             MessageManager.getString("label.load_colours"));
1353     loadColours.setFont(JvSwingUtils.getLabelFont());
1354     loadColours.setToolTipText(
1355             MessageManager.getString("label.load_colours_tooltip"));
1356     loadColours.addActionListener(new ActionListener()
1357     {
1358       @Override
1359       public void actionPerformed(ActionEvent e)
1360       {
1361         load();
1362       }
1363     });
1364
1365     JButton saveColours = new JButton(
1366             MessageManager.getString("label.save_colours"));
1367     saveColours.setFont(JvSwingUtils.getLabelFont());
1368     saveColours.setToolTipText(
1369             MessageManager.getString("label.save_colours_tooltip"));
1370     saveColours.addActionListener(new ActionListener()
1371     {
1372       @Override
1373       public void actionPerformed(ActionEvent e)
1374       {
1375         save();
1376       }
1377     });
1378     transparency.addChangeListener(new ChangeListener()
1379     {
1380       @Override
1381       public void stateChanged(ChangeEvent evt)
1382       {
1383         if (!inConstruction)
1384         {
1385           fr.setTransparency((100 - transparency.getValue()) / 100f);
1386           af.alignPanel.paintAlignment(true, true);
1387         }
1388       }
1389     });
1390
1391     transparency.setMaximum(70);
1392     transparency.setToolTipText(
1393             MessageManager.getString("label.transparency_tip"));
1394
1395     JPanel transPanel = new JPanel(new GridLayout(1, 2));
1396     bigPanel.add(transPanel, BorderLayout.SOUTH);
1397
1398     JPanel transbuttons = new JPanel(new GridLayout(5, 1));
1399     transbuttons.add(optimizeOrder);
1400     transbuttons.add(invert);
1401     transbuttons.add(sortByScore);
1402     transbuttons.add(sortByDens);
1403     transbuttons.add(help);
1404     transPanel.add(transparency);
1405     transPanel.add(transbuttons);
1406
1407     JPanel buttonPanel = new JPanel();
1408     buttonPanel.add(ok);
1409     buttonPanel.add(cancel);
1410     buttonPanel.add(loadColours);
1411     buttonPanel.add(saveColours);
1412     bigPanel.add(scrollPane, BorderLayout.CENTER);
1413     settingsPane.add(bigPanel, BorderLayout.CENTER);
1414     settingsPane.add(buttonPanel, BorderLayout.SOUTH);
1415     this.add(settingsPane);
1416   }
1417
1418   /**
1419    * Answers a suitable tooltip to show on the colour cell of the table
1420    * 
1421    * @param fcol
1422    * @param withHint
1423    *          if true include 'click to edit' and similar text
1424    * @return
1425    */
1426   public static String getColorTooltip(FeatureColourI fcol,
1427           boolean withHint)
1428   {
1429     if (fcol == null)
1430     {
1431       return null;
1432     }
1433     if (fcol.isSimpleColour())
1434     {
1435       return withHint ? BASE_TOOLTIP : null;
1436     }
1437     String description = fcol.getDescription();
1438     description = description.replaceAll("<", "&lt;");
1439     description = description.replaceAll(">", "&gt;");
1440     StringBuilder tt = new StringBuilder(description);
1441     if (withHint)
1442     {
1443       tt.append("<br>").append(BASE_TOOLTIP).append("</br>");
1444     }
1445     return JvSwingUtils.wrapTooltip(true, tt.toString());
1446   }
1447
1448   public static void renderGraduatedColor(JLabel comp, FeatureColourI gcol,
1449           int w, int h)
1450   {
1451     boolean thr = false;
1452     StringBuilder tx = new StringBuilder();
1453   
1454     if (gcol.isColourByAttribute())
1455     {
1456       tx.append(FeatureMatcher
1457               .toAttributeDisplayName(gcol.getAttributeName()));
1458     }
1459     else if (!gcol.isColourByLabel())
1460     {
1461       tx.append(MessageManager.getString("label.score"));
1462     }
1463     tx.append(" ");
1464     if (gcol.isAboveThreshold())
1465     {
1466       thr = true;
1467       tx.append(">");
1468     }
1469     if (gcol.isBelowThreshold())
1470     {
1471       thr = true;
1472       tx.append("<");
1473     }
1474     if (gcol.isColourByLabel())
1475     {
1476       if (thr)
1477       {
1478         tx.append(" ");
1479       }
1480       if (!gcol.isColourByAttribute())
1481       {
1482         tx.append("Label");
1483       }
1484       comp.setIcon(null);
1485     }
1486     else
1487     {
1488       Color newColor = gcol.getMaxColour();
1489       comp.setBackground(newColor);
1490       // System.err.println("Width is " + w / 2);
1491       Icon ficon = new FeatureIcon(gcol, comp.getBackground(), w, h, thr);
1492       comp.setIcon(ficon);
1493       // tt+="RGB value: Max (" + newColor.getRed() + ", "
1494       // + newColor.getGreen() + ", " + newColor.getBlue()
1495       // + ")\nMin (" + minCol.getRed() + ", " + minCol.getGreen()
1496       // + ", " + minCol.getBlue() + ")");
1497     }
1498     comp.setHorizontalAlignment(SwingConstants.CENTER);
1499     comp.setText(tx.toString());
1500   }
1501
1502   // ///////////////////////////////////////////////////////////////////////
1503   // http://java.sun.com/docs/books/tutorial/uiswing/components/table.html
1504   // ///////////////////////////////////////////////////////////////////////
1505   class FeatureTableModel extends AbstractTableModel
1506   {
1507     private String[] columnNames = {
1508         MessageManager.getString("label.feature_type"),
1509         MessageManager.getString("action.colour"),
1510         MessageManager.getString("label.filter"),
1511         MessageManager.getString("label.show") };
1512
1513     private Object[][] data;
1514
1515     FeatureTableModel(Object[][] data)
1516     {
1517       this.data = data;
1518     }
1519
1520     public Object[][] getData()
1521     {
1522       return data;
1523     }
1524
1525     public void setData(Object[][] data)
1526     {
1527       this.data = data;
1528     }
1529
1530     @Override
1531     public int getColumnCount()
1532     {
1533       return columnNames.length;
1534     }
1535
1536     public Object[] getRow(int row)
1537     {
1538       return data[row];
1539     }
1540
1541     @Override
1542     public int getRowCount()
1543     {
1544       return data.length;
1545     }
1546
1547     @Override
1548     public String getColumnName(int col)
1549     {
1550       return columnNames[col];
1551     }
1552
1553     @Override
1554     public Object getValueAt(int row, int col)
1555     {
1556       return data[row][col];
1557     }
1558
1559     /**
1560      * Answers the class of the object in column c of the first row of the table
1561      */
1562     @Override
1563     public Class<?> getColumnClass(int c)
1564     {
1565       Object v = getValueAt(0, c);
1566       return v == null ? null : v.getClass();
1567     }
1568
1569     @Override
1570     public boolean isCellEditable(int row, int col)
1571     {
1572       return col == 0 ? false : true;
1573     }
1574
1575     @Override
1576     public void setValueAt(Object value, int row, int col)
1577     {
1578       data[row][col] = value;
1579       fireTableCellUpdated(row, col);
1580       updateFeatureRenderer(data);
1581     }
1582
1583   }
1584
1585   class ColorRenderer extends JLabel implements TableCellRenderer
1586   {
1587     Border unselectedBorder = null;
1588
1589     Border selectedBorder = null;
1590
1591     public ColorRenderer()
1592     {
1593       setOpaque(true); // MUST do this for background to show up.
1594       setHorizontalTextPosition(SwingConstants.CENTER);
1595       setVerticalTextPosition(SwingConstants.CENTER);
1596     }
1597
1598     @Override
1599     public Component getTableCellRendererComponent(JTable tbl, Object color,
1600             boolean isSelected, boolean hasFocus, int row, int column)
1601     {
1602       FeatureColourI cellColour = (FeatureColourI) color;
1603       setOpaque(true);
1604       setBackground(tbl.getBackground());
1605       if (!cellColour.isSimpleColour())
1606       {
1607         Rectangle cr = tbl.getCellRect(row, column, false);
1608         FeatureSettings.renderGraduatedColor(this, cellColour,
1609                 (int) cr.getWidth(), (int) cr.getHeight());
1610       }
1611       else
1612       {
1613         this.setText("");
1614         this.setIcon(null);
1615         setBackground(cellColour.getColour());
1616       }
1617       if (isSelected)
1618       {
1619         if (selectedBorder == null)
1620         {
1621           selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
1622                   tbl.getSelectionBackground());
1623         }
1624         setBorder(selectedBorder);
1625       }
1626       else
1627       {
1628         if (unselectedBorder == null)
1629         {
1630           unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
1631                   tbl.getBackground());
1632         }
1633         setBorder(unselectedBorder);
1634       }
1635
1636       return this;
1637     }
1638   }
1639
1640   class FilterRenderer extends JLabel implements TableCellRenderer
1641   {
1642     javax.swing.border.Border unselectedBorder = null;
1643
1644     javax.swing.border.Border selectedBorder = null;
1645
1646     public FilterRenderer()
1647     {
1648       setOpaque(true); // MUST do this for background to show up.
1649       setHorizontalTextPosition(SwingConstants.CENTER);
1650       setVerticalTextPosition(SwingConstants.CENTER);
1651     }
1652
1653     @Override
1654     public Component getTableCellRendererComponent(JTable tbl,
1655             Object filter, boolean isSelected, boolean hasFocus, int row,
1656             int column)
1657     {
1658       FeatureMatcherSetI theFilter = (FeatureMatcherSetI) filter;
1659       setOpaque(true);
1660       String asText = theFilter.toString();
1661       setBackground(tbl.getBackground());
1662       this.setText(asText);
1663       this.setIcon(null);
1664
1665       if (isSelected)
1666       {
1667         if (selectedBorder == null)
1668         {
1669           selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
1670                   tbl.getSelectionBackground());
1671         }
1672         setBorder(selectedBorder);
1673       }
1674       else
1675       {
1676         if (unselectedBorder == null)
1677         {
1678           unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
1679                   tbl.getBackground());
1680         }
1681         setBorder(unselectedBorder);
1682       }
1683
1684       return this;
1685     }
1686   }
1687
1688   /**
1689    * update comp using rendering settings from gcol
1690    * 
1691    * @param comp
1692    * @param gcol
1693    */
1694   public static void renderGraduatedColor(JLabel comp, FeatureColourI gcol)
1695   {
1696     int w = comp.getWidth(), h = comp.getHeight();
1697     if (w < 20)
1698     {
1699       w = (int) comp.getPreferredSize().getWidth();
1700       h = (int) comp.getPreferredSize().getHeight();
1701       if (w < 20)
1702       {
1703         w = 80;
1704         h = 12;
1705       }
1706     }
1707     renderGraduatedColor(comp, gcol, w, h);
1708   }
1709
1710   @SuppressWarnings("serial")
1711 class ColorEditor extends AbstractCellEditor
1712           implements TableCellEditor, ActionListener
1713   {
1714     FeatureColourI currentColor;
1715
1716     FeatureTypeSettings chooser;
1717
1718     String type;
1719
1720     JButton button;
1721
1722     protected static final String EDIT = "edit";
1723
1724     int rowSelected = 0;
1725
1726     public ColorEditor()
1727     {
1728       // Set up the editor (from the table's point of view),
1729       // which is a button.
1730       // This button brings up the color chooser dialog,
1731       // which is the editor from the user's point of view.
1732       button = new JButton();
1733       button.setActionCommand(EDIT);
1734       button.addActionListener(this);
1735       button.setBorderPainted(false);
1736     }
1737
1738     /**
1739      * Handles events from the editor button, and from the colour/filters
1740      * dialog's OK button
1741      */
1742     @Override
1743     public void actionPerformed(ActionEvent e)
1744     {
1745       if (button == e.getSource())
1746       {
1747         if (currentColor.isSimpleColour())
1748         {
1749           /*
1750            * simple colour chooser
1751            */
1752           String ttl = MessageManager.formatMessage("label.select_colour_for", type);
1753           ColourChooserListener listener = new ColourChooserListener() 
1754           {
1755             @Override
1756             public void colourSelected(Color c)
1757             {
1758               currentColor = new FeatureColour(c);
1759               table.setValueAt(currentColor, rowSelected, COLOUR_COLUMN);
1760               fireEditingStopped();
1761             }
1762                         @Override
1763                         public void cancel() 
1764                         {
1765                   fireEditingStopped();
1766                         }
1767           };
1768           JalviewColourChooser.showColourChooser(button,  ttl,  currentColor.getColour(), listener);
1769         }
1770         else
1771         {
1772           /*
1773            * variable colour and filters dialog
1774            */
1775           chooser = new FeatureTypeSettings(fr, type);
1776           if (!Jalview.isJS())
1777           {
1778             chooser.setRequestFocusEnabled(true);
1779             chooser.requestFocus();
1780           }
1781           chooser.addActionListener(this);
1782           fireEditingStopped();
1783         }
1784       }
1785       else
1786       {
1787         /*
1788          * after OK in variable colour dialog, any changes to colour 
1789          * (or filters!) are already set in FeatureRenderer, so just
1790          * update table data without triggering updateFeatureRenderer
1791          */
1792         currentColor = fr.getFeatureColours().get(type);
1793         FeatureMatcherSetI currentFilter = fr.getFeatureFilter(type);
1794         if (currentFilter == null)
1795         {
1796           currentFilter = new FeatureMatcherSet();
1797         }
1798         Object[] data = ((FeatureTableModel) table.getModel())
1799                 .getData()[rowSelected];
1800         data[COLOUR_COLUMN] = currentColor;
1801         data[FILTER_COLUMN] = currentFilter;
1802         fireEditingStopped();
1803         // SwingJS needs an explicit repaint() here, 
1804         // rather than relying upon no validation having
1805         // occurred since the stopEditing call was made.
1806         // Its laying out has not been stopped by the modal frame
1807         table.validate();
1808         table.repaint();
1809       }
1810     }
1811
1812     /**
1813      * Override allows access to this method from anonymous inner classes 
1814      */
1815     @Override
1816         protected void fireEditingStopped() 
1817     {
1818             super.fireEditingStopped();
1819         }
1820
1821         // Implement the one CellEditor method that AbstractCellEditor doesn't.
1822     @Override
1823     public Object getCellEditorValue()
1824     {
1825       return currentColor;
1826     }
1827
1828     // Implement the one method defined by TableCellEditor.
1829     @Override
1830     public Component getTableCellEditorComponent(JTable theTable, Object value,
1831             boolean isSelected, int row, int column)
1832     {
1833       currentColor = (FeatureColourI) value;
1834       this.rowSelected = row;
1835       type = table.getValueAt(row, TYPE_COLUMN).toString();
1836       button.setOpaque(true);
1837       button.setBackground(FeatureSettings.this.getBackground());
1838       if (!currentColor.isSimpleColour())
1839       {
1840         JLabel btn = new JLabel();
1841         btn.setSize(button.getSize());
1842         FeatureSettings.renderGraduatedColor(btn, currentColor);
1843         button.setBackground(btn.getBackground());
1844         button.setIcon(btn.getIcon());
1845         button.setText(btn.getText());
1846       }
1847       else
1848       {
1849         button.setText("");
1850         button.setIcon(null);
1851         button.setBackground(currentColor.getColour());
1852       }
1853       return button;
1854     }
1855   }
1856
1857   /**
1858    * The cell editor for the Filter column. It displays the text of any filters
1859    * for the feature type in that row (in full as a tooltip, possible abbreviated
1860    * as display text). On click in the cell, opens the Feature Display Settings
1861    * dialog at the Filters tab.
1862    */
1863   @SuppressWarnings("serial")
1864 class FilterEditor extends AbstractCellEditor
1865           implements TableCellEditor, ActionListener
1866   {
1867
1868     FeatureMatcherSetI currentFilter;
1869
1870     Point lastLocation;
1871
1872     String type;
1873
1874     JButton button;
1875
1876     protected static final String EDIT = "edit";
1877
1878     int rowSelected = 0;
1879
1880     public FilterEditor()
1881     {
1882       button = new JButton();
1883       button.setActionCommand(EDIT);
1884       button.addActionListener(this);
1885       button.setBorderPainted(false);
1886     }
1887
1888     /**
1889      * Handles events from the editor button
1890      */
1891     @Override
1892     public void actionPerformed(ActionEvent e)
1893     {
1894       if (button == e.getSource())
1895       {
1896         FeatureTypeSettings chooser = new FeatureTypeSettings(fr, type);
1897         chooser.addActionListener(this);
1898         chooser.setRequestFocusEnabled(true);
1899         chooser.requestFocus();
1900         if (lastLocation != null)
1901         {
1902           // todo open at its last position on screen
1903           chooser.setBounds(lastLocation.x, lastLocation.y,
1904                   chooser.getWidth(), chooser.getHeight());
1905           chooser.validate();
1906         }
1907         fireEditingStopped();
1908       }
1909       else if (e.getSource() instanceof Component)
1910       {
1911
1912         /*
1913          * after OK in variable colour dialog, any changes to filter
1914          * (or colours!) are already set in FeatureRenderer, so just
1915          * update table data without triggering updateFeatureRenderer
1916          */
1917         FeatureColourI currentColor = fr.getFeatureColours().get(type);
1918         currentFilter = fr.getFeatureFilter(type);
1919         if (currentFilter == null)
1920         {
1921           currentFilter = new FeatureMatcherSet();
1922         }
1923         
1924         Object[] data = ((FeatureTableModel) table.getModel())
1925                 .getData()[rowSelected];
1926         data[COLOUR_COLUMN] = currentColor;
1927         data[FILTER_COLUMN] = currentFilter;
1928         fireEditingStopped();
1929         // SwingJS needs an explicit repaint() here, 
1930         // rather than relying upon no validation having
1931         // occurred since the stopEditing call was made.
1932         // Its laying out has not been stopped by the modal frame
1933         table.validate();
1934         table.repaint();
1935       }
1936     }
1937
1938     @Override
1939     public Object getCellEditorValue()
1940     {
1941       return currentFilter;
1942     }
1943
1944     @Override
1945     public Component getTableCellEditorComponent(JTable theTable, Object value,
1946             boolean isSelected, int row, int column)
1947     {
1948       currentFilter = (FeatureMatcherSetI) value;
1949       this.rowSelected = row;
1950       type = table.getValueAt(row, TYPE_COLUMN).toString();
1951       button.setOpaque(true);
1952       button.setBackground(FeatureSettings.this.getBackground());
1953       button.setText(currentFilter.toString());
1954       button.setIcon(null);
1955       return button;
1956     }
1957   }
1958 }
1959
1960 class FeatureIcon implements Icon
1961 {
1962   FeatureColourI gcol;
1963
1964   Color backg;
1965
1966   boolean midspace = false;
1967
1968   int width = 50, height = 20;
1969
1970   int s1, e1; // start and end of midpoint band for thresholded symbol
1971
1972   Color mpcolour = Color.white;
1973
1974   FeatureIcon(FeatureColourI gfc, Color bg, int w, int h, boolean mspace)
1975   {
1976     gcol = gfc;
1977     backg = bg;
1978     width = w;
1979     height = h;
1980     midspace = mspace;
1981     if (midspace)
1982     {
1983       s1 = width / 3;
1984       e1 = s1 * 2;
1985     }
1986     else
1987     {
1988       s1 = width / 2;
1989       e1 = s1;
1990     }
1991   }
1992
1993   @Override
1994   public int getIconWidth()
1995   {
1996     return width;
1997   }
1998
1999   @Override
2000   public int getIconHeight()
2001   {
2002     return height;
2003   }
2004
2005   @Override
2006   public void paintIcon(Component c, Graphics g, int x, int y)
2007   {
2008
2009     if (gcol.isColourByLabel())
2010     {
2011       g.setColor(backg);
2012       g.fillRect(0, 0, width, height);
2013       // need an icon here.
2014       g.setColor(gcol.getMaxColour());
2015
2016       g.setFont(new Font("Verdana", Font.PLAIN, 9));
2017
2018       // g.setFont(g.getFont().deriveFont(
2019       // AffineTransform.getScaleInstance(
2020       // width/g.getFontMetrics().stringWidth("Label"),
2021       // height/g.getFontMetrics().getHeight())));
2022
2023       g.drawString(MessageManager.getString("label.label"), 0, 0);
2024
2025     }
2026     else
2027     {
2028       Color minCol = gcol.getMinColour();
2029       g.setColor(minCol);
2030       g.fillRect(0, 0, s1, height);
2031       if (midspace)
2032       {
2033         g.setColor(Color.white);
2034         g.fillRect(s1, 0, e1 - s1, height);
2035       }
2036       g.setColor(gcol.getMaxColour());
2037 //      g.fillRect(0, e1, width - e1, height);  // BH 2018
2038       g.fillRect(e1, 0, width - e1, height);
2039     }
2040   }
2041 }