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