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