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