JAL-2808 attribute description as tooltip in dropdown if known
[jalview.git] / src / jalview / gui / FeatureSettings.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import jalview.api.FeatureColourI;
24 import jalview.api.FeatureSettingsControllerI;
25 import jalview.bin.Cache;
26 import jalview.datamodel.AlignmentI;
27 import jalview.datamodel.SequenceI;
28 import jalview.datamodel.features.FeatureAttributes;
29 import jalview.gui.Help.HelpId;
30 import jalview.io.JalviewFileChooser;
31 import jalview.io.JalviewFileView;
32 import jalview.schemabinding.version2.JalviewUserColours;
33 import jalview.schemes.FeatureColour;
34 import jalview.util.Format;
35 import jalview.util.MessageManager;
36 import jalview.util.Platform;
37 import jalview.util.QuickSort;
38 import jalview.util.ReverseListIterator;
39 import jalview.util.matcher.Condition;
40 import jalview.util.matcher.KeyedMatcher;
41 import jalview.util.matcher.KeyedMatcherI;
42 import jalview.util.matcher.KeyedMatcherSet;
43 import jalview.util.matcher.KeyedMatcherSetI;
44 import jalview.viewmodel.AlignmentViewport;
45 import jalview.ws.DasSequenceFeatureFetcher;
46 import jalview.ws.dbsources.das.api.jalviewSourceI;
47
48 import java.awt.BorderLayout;
49 import java.awt.Color;
50 import java.awt.Component;
51 import java.awt.Dimension;
52 import java.awt.FlowLayout;
53 import java.awt.Font;
54 import java.awt.Graphics;
55 import java.awt.GridLayout;
56 import java.awt.Rectangle;
57 import java.awt.event.ActionEvent;
58 import java.awt.event.ActionListener;
59 import java.awt.event.FocusAdapter;
60 import java.awt.event.FocusEvent;
61 import java.awt.event.ItemEvent;
62 import java.awt.event.ItemListener;
63 import java.awt.event.MouseAdapter;
64 import java.awt.event.MouseEvent;
65 import java.awt.event.MouseMotionAdapter;
66 import java.beans.PropertyChangeEvent;
67 import java.beans.PropertyChangeListener;
68 import java.io.File;
69 import java.io.FileInputStream;
70 import java.io.FileOutputStream;
71 import java.io.InputStreamReader;
72 import java.io.OutputStreamWriter;
73 import java.io.PrintWriter;
74 import java.util.ArrayList;
75 import java.util.Arrays;
76 import java.util.HashSet;
77 import java.util.Hashtable;
78 import java.util.Iterator;
79 import java.util.List;
80 import java.util.Map;
81 import java.util.Set;
82 import java.util.Vector;
83
84 import javax.help.HelpSetException;
85 import javax.swing.AbstractCellEditor;
86 import javax.swing.BorderFactory;
87 import javax.swing.ButtonGroup;
88 import javax.swing.Icon;
89 import javax.swing.JButton;
90 import javax.swing.JCheckBox;
91 import javax.swing.JCheckBoxMenuItem;
92 import javax.swing.JColorChooser;
93 import javax.swing.JComboBox;
94 import javax.swing.JDialog;
95 import javax.swing.JInternalFrame;
96 import javax.swing.JLabel;
97 import javax.swing.JLayeredPane;
98 import javax.swing.JMenuItem;
99 import javax.swing.JPanel;
100 import javax.swing.JPopupMenu;
101 import javax.swing.JRadioButton;
102 import javax.swing.JScrollPane;
103 import javax.swing.JSlider;
104 import javax.swing.JTabbedPane;
105 import javax.swing.JTable;
106 import javax.swing.JTextArea;
107 import javax.swing.JTextField;
108 import javax.swing.ListSelectionModel;
109 import javax.swing.SwingConstants;
110 import javax.swing.SwingUtilities;
111 import javax.swing.event.ChangeEvent;
112 import javax.swing.event.ChangeListener;
113 import javax.swing.plaf.basic.BasicArrowButton;
114 import javax.swing.table.AbstractTableModel;
115 import javax.swing.table.TableCellEditor;
116 import javax.swing.table.TableCellRenderer;
117
118 public class FeatureSettings extends JPanel
119         implements FeatureSettingsControllerI
120 {
121   private static final int MIN_WIDTH = 400;
122
123   private static final int MIN_HEIGHT = 400;
124
125   DasSourceBrowser dassourceBrowser;
126
127   DasSequenceFeatureFetcher dasFeatureFetcher;
128
129   JPanel dasSettingsPane = new JPanel();
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, KeyedMatcherSetI> originalFilters;
143
144   final JInternalFrame frame;
145
146   JScrollPane scrollPane = new JScrollPane();
147
148   JTable table;
149
150   JPanel groupPanel;
151
152   JSlider transparency = new JSlider();
153   
154   /*
155    * when true, constructor is still executing - so ignore UI events
156    */
157   protected volatile boolean inConstruction = true;
158
159   int selectedRow = -1;
160
161   JButton fetchDAS = new JButton();
162
163   JButton saveDAS = new JButton();
164
165   JButton cancelDAS = new JButton();
166
167   boolean resettingTable = false;
168
169   /*
170    * true when Feature Settings are updating from feature renderer
171    */
172   private boolean handlingUpdate = false;
173
174   /*
175    * holds {featureCount, totalExtent} for each feature type
176    */
177   Map<String, float[]> typeWidth = null;
178
179   /*
180    * fields of the feature filters tab
181    */
182   private JPanel filtersPane;
183
184   private JPanel chooseFiltersPanel;
185
186   private JComboBox<String> filteredFeatureChoice;
187
188   private JRadioButton andFilters;
189
190   private JRadioButton orFilters;
191
192   /*
193    * filters for the currently selected feature type
194    */
195   private List<KeyedMatcherI> filters;
196
197   private JTextArea filtersAsText;
198
199   /**
200    * Constructor
201    * 
202    * @param af
203    */
204   public FeatureSettings(AlignFrame alignFrame)
205   {
206     this.af = alignFrame;
207     fr = af.getFeatureRenderer();
208
209     // save transparency for restore on Cancel
210     originalTransparency = fr.getTransparency();
211     int originalTransparencyAsPercent = (int) (originalTransparency * 100);
212     transparency.setMaximum(100 - originalTransparencyAsPercent);
213
214     originalFilters = fr.getFeatureFilters();
215
216     try
217     {
218       jbInit();
219     } catch (Exception ex)
220     {
221       ex.printStackTrace();
222     }
223
224     table = new JTable()
225     {
226       @Override
227       public String getToolTipText(MouseEvent e)
228       {
229         if (table.columnAtPoint(e.getPoint()) == 0)
230         {
231           /*
232            * Tooltip for feature name only
233            */
234           return JvSwingUtils.wrapTooltip(true, MessageManager
235                   .getString("label.feature_settings_click_drag"));
236         }
237         return null;
238       }
239     };
240     table.getTableHeader().setFont(new Font("Verdana", Font.PLAIN, 12));
241     table.setFont(new Font("Verdana", Font.PLAIN, 12));
242     table.setDefaultRenderer(Color.class, new ColorRenderer());
243
244     table.setDefaultEditor(Color.class, new ColorEditor(this));
245
246     table.setDefaultEditor(FeatureColour.class, new ColorEditor(this));
247     table.setDefaultRenderer(FeatureColour.class, new ColorRenderer());
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         if (evt.isPopupTrigger())
257         {
258           popupSort(selectedRow, (String) table.getValueAt(selectedRow, 0),
259                   table.getValueAt(selectedRow, 1), fr.getMinMax(),
260                   evt.getX(), evt.getY());
261         }
262         else if (evt.getClickCount() == 2)
263         {
264           boolean invertSelection = evt.isAltDown();
265           boolean toggleSelection = Platform.isControlDown(evt);
266           boolean extendSelection = evt.isShiftDown();
267           fr.ap.alignFrame.avc.markColumnsContainingFeatures(
268                   invertSelection, extendSelection, toggleSelection,
269                   (String) table.getValueAt(selectedRow, 0));
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           popupSort(selectedRow, (String) table.getValueAt(selectedRow, 0),
281                   table.getValueAt(selectedRow, 1), fr.getMinMax(),
282                   evt.getX(), evt.getY());
283         }
284       }
285     });
286
287     table.addMouseMotionListener(new MouseMotionAdapter()
288     {
289       @Override
290       public void mouseDragged(MouseEvent evt)
291       {
292         int newRow = table.rowAtPoint(evt.getPoint());
293         if (newRow != selectedRow && selectedRow != -1 && newRow != -1)
294         {
295           /*
296            * reposition 'selectedRow' to 'newRow' (the dragged to location)
297            * this could be more than one row away for a very fast drag action
298            * so just swap it with adjacent rows until we get it there
299            */
300           Object[][] data = ((FeatureTableModel) table.getModel())
301                   .getData();
302           int direction = newRow < selectedRow ? -1 : 1;
303           for (int i = selectedRow; i != newRow; i += direction)
304           {
305             Object[] temp = data[i];
306             data[i] = data[i + direction];
307             data[i + direction] = temp;
308           }
309           updateFeatureRenderer(data);
310           table.repaint();
311           selectedRow = newRow;
312         }
313       }
314     });
315     // table.setToolTipText(JvSwingUtils.wrapTooltip(true,
316     // MessageManager.getString("label.feature_settings_click_drag")));
317     scrollPane.setViewportView(table);
318
319     dassourceBrowser = new DasSourceBrowser(this);
320     dasSettingsPane.add(dassourceBrowser, BorderLayout.CENTER);
321
322     if (af.getViewport().isShowSequenceFeatures() || !fr.hasRenderOrder())
323     {
324       fr.findAllFeatures(true); // display everything!
325     }
326
327     discoverAllFeatureData();
328     final PropertyChangeListener change;
329     final FeatureSettings fs = this;
330     fr.addPropertyChangeListener(change = new PropertyChangeListener()
331     {
332       @Override
333       public void propertyChange(PropertyChangeEvent evt)
334       {
335         if (!fs.resettingTable && !fs.handlingUpdate)
336         {
337           fs.handlingUpdate = true;
338           fs.resetTable(null); // new groups may be added with new seuqence
339           // feature types only
340           fs.handlingUpdate = false;
341         }
342       }
343
344     });
345
346     frame = new JInternalFrame();
347     frame.setContentPane(this);
348     if (Platform.isAMac())
349     {
350       Desktop.addInternalFrame(frame,
351               MessageManager.getString("label.sequence_feature_settings"),
352               600, 480);
353     }
354     else
355     {
356       Desktop.addInternalFrame(frame,
357               MessageManager.getString("label.sequence_feature_settings"),
358               600, 450);
359     }
360     frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
361
362     frame.addInternalFrameListener(
363             new javax.swing.event.InternalFrameAdapter()
364             {
365               @Override
366               public void internalFrameClosed(
367                       javax.swing.event.InternalFrameEvent evt)
368               {
369                 fr.removePropertyChangeListener(change);
370                 dassourceBrowser.fs = null;
371               };
372             });
373     frame.setLayer(JLayeredPane.PALETTE_LAYER);
374     inConstruction = false;
375   }
376
377   protected void popupSort(final int selectedRow, final String type,
378           final Object typeCol, final Map<String, float[][]> minmax, int x,
379           int y)
380   {
381     final FeatureColourI featureColour = (FeatureColourI) typeCol;
382
383     JPopupMenu men = new JPopupMenu(MessageManager
384             .formatMessage("label.settings_for_param", new String[]
385             { type }));
386     JMenuItem scr = new JMenuItem(
387             MessageManager.getString("label.sort_by_score"));
388     men.add(scr);
389     final FeatureSettings me = this;
390     scr.addActionListener(new ActionListener()
391     {
392
393       @Override
394       public void actionPerformed(ActionEvent e)
395       {
396         me.af.avc
397                 .sortAlignmentByFeatureScore(Arrays.asList(new String[]
398                 { type }));
399       }
400
401     });
402     JMenuItem dens = new JMenuItem(
403             MessageManager.getString("label.sort_by_density"));
404     dens.addActionListener(new ActionListener()
405     {
406
407       @Override
408       public void actionPerformed(ActionEvent e)
409       {
410         me.af.avc
411                 .sortAlignmentByFeatureDensity(Arrays.asList(new String[]
412                 { type }));
413       }
414
415     });
416     men.add(dens);
417     if (minmax != null)
418     {
419       final float[][] typeMinMax = minmax.get(type);
420       /*
421        * final JCheckBoxMenuItem chb = new JCheckBoxMenuItem("Vary Height"); //
422        * this is broken at the moment and isn't that useful anyway!
423        * chb.setSelected(minmax.get(type) != null); chb.addActionListener(new
424        * ActionListener() {
425        * 
426        * public void actionPerformed(ActionEvent e) {
427        * chb.setState(chb.getState()); if (chb.getState()) { minmax.put(type,
428        * null); } else { minmax.put(type, typeMinMax); } }
429        * 
430        * });
431        * 
432        * men.add(chb);
433        */
434       if (typeMinMax != null && typeMinMax[0] != null)
435       {
436         // if (table.getValueAt(row, column));
437         // graduated colourschemes for those where minmax exists for the
438         // positional features
439         final JCheckBoxMenuItem mxcol = new JCheckBoxMenuItem(
440                 "Graduated Colour");
441         mxcol.setSelected(!featureColour.isSimpleColour());
442         men.add(mxcol);
443         mxcol.addActionListener(new ActionListener()
444         {
445           JColorChooser colorChooser;
446
447           @Override
448           public void actionPerformed(ActionEvent e)
449           {
450             if (e.getSource() == mxcol)
451             {
452               if (featureColour.isSimpleColour())
453               {
454                 FeatureColourChooser fc = new FeatureColourChooser(me.fr,
455                         type);
456                 fc.addActionListener(this);
457               }
458               else
459               {
460                 // bring up simple color chooser
461                 colorChooser = new JColorChooser();
462                 JDialog dialog = JColorChooser.createDialog(me,
463                         "Select new Colour", true, // modal
464                         colorChooser, this, // OK button handler
465                         null); // no CANCEL button handler
466                 colorChooser.setColor(featureColour.getMaxColour());
467                 dialog.setVisible(true);
468               }
469             }
470             else
471             {
472               if (e.getSource() instanceof FeatureColourChooser)
473               {
474                 FeatureColourChooser fc = (FeatureColourChooser) e
475                         .getSource();
476                 table.setValueAt(fc.getLastColour(), selectedRow, 1);
477                 table.validate();
478               }
479               else
480               {
481                 // probably the color chooser!
482                 table.setValueAt(new FeatureColour(colorChooser.getColor()),
483                         selectedRow, 1);
484                 table.validate();
485                 me.updateFeatureRenderer(
486                         ((FeatureTableModel) table.getModel()).getData(),
487                         false);
488               }
489             }
490           }
491
492         });
493       }
494     }
495     JMenuItem selCols = new JMenuItem(
496             MessageManager.getString("label.select_columns_containing"));
497     selCols.addActionListener(new ActionListener()
498     {
499       @Override
500       public void actionPerformed(ActionEvent arg0)
501       {
502         fr.ap.alignFrame.avc.markColumnsContainingFeatures(false, false,
503                 false, type);
504       }
505     });
506     JMenuItem clearCols = new JMenuItem(MessageManager
507             .getString("label.select_columns_not_containing"));
508     clearCols.addActionListener(new ActionListener()
509     {
510       @Override
511       public void actionPerformed(ActionEvent arg0)
512       {
513         fr.ap.alignFrame.avc.markColumnsContainingFeatures(true, false,
514                 false, type);
515       }
516     });
517     JMenuItem hideCols = new JMenuItem(
518             MessageManager.getString("label.hide_columns_containing"));
519     hideCols.addActionListener(new ActionListener()
520     {
521       @Override
522       public void actionPerformed(ActionEvent arg0)
523       {
524         fr.ap.alignFrame.hideFeatureColumns(type, true);
525       }
526     });
527     JMenuItem hideOtherCols = new JMenuItem(
528             MessageManager.getString("label.hide_columns_not_containing"));
529     hideOtherCols.addActionListener(new ActionListener()
530     {
531       @Override
532       public void actionPerformed(ActionEvent arg0)
533       {
534         fr.ap.alignFrame.hideFeatureColumns(type, false);
535       }
536     });
537     men.add(selCols);
538     men.add(clearCols);
539     men.add(hideCols);
540     men.add(hideOtherCols);
541     men.show(table, x, y);
542   }
543
544   @Override
545   synchronized public void discoverAllFeatureData()
546   {
547     Set<String> allGroups = new HashSet<>();
548     AlignmentI alignment = af.getViewport().getAlignment();
549
550     for (int i = 0; i < alignment.getHeight(); i++)
551     {
552       SequenceI seq = alignment.getSequenceAt(i);
553       for (String group : seq.getFeatures().getFeatureGroups(true))
554       {
555         if (group != null && !allGroups.contains(group))
556         {
557           allGroups.add(group);
558           checkGroupState(group);
559         }
560       }
561     }
562
563     populateFilterableFeatures();
564
565     resetTable(null);
566
567     validate();
568   }
569
570   /**
571    * Synchronise gui group list and check visibility of group
572    * 
573    * @param group
574    * @return true if group is visible
575    */
576   private boolean checkGroupState(String group)
577   {
578     boolean visible = fr.checkGroupVisibility(group, true);
579
580     for (int g = 0; g < groupPanel.getComponentCount(); g++)
581     {
582       if (((JCheckBox) groupPanel.getComponent(g)).getText().equals(group))
583       {
584         ((JCheckBox) groupPanel.getComponent(g)).setSelected(visible);
585         return visible;
586       }
587     }
588
589     final String grp = group;
590     final JCheckBox check = new JCheckBox(group, visible);
591     check.setFont(new Font("Serif", Font.BOLD, 12));
592     check.setToolTipText(group);
593     check.addItemListener(new ItemListener()
594     {
595       @Override
596       public void itemStateChanged(ItemEvent evt)
597       {
598         fr.setGroupVisibility(check.getText(), check.isSelected());
599         resetTable(new String[] { grp });
600         af.alignPanel.paintAlignment(true, true);
601       }
602     });
603     groupPanel.add(check);
604     return visible;
605   }
606
607   synchronized void resetTable(String[] groupChanged)
608   {
609     if (resettingTable)
610     {
611       return;
612     }
613     resettingTable = true;
614     typeWidth = new Hashtable<>();
615     // TODO: change avWidth calculation to 'per-sequence' average and use long
616     // rather than float
617
618     Set<String> displayableTypes = new HashSet<>();
619     Set<String> foundGroups = new HashSet<>();
620
621     /*
622      * determine which feature types may be visible depending on 
623      * which groups are selected, and recompute average width data
624      */
625     for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++)
626     {
627
628       SequenceI seq = af.getViewport().getAlignment().getSequenceAt(i);
629
630       /*
631        * get the sequence's groups for positional features
632        * and keep track of which groups are visible
633        */
634       Set<String> groups = seq.getFeatures().getFeatureGroups(true);
635       Set<String> visibleGroups = new HashSet<>();
636       for (String group : groups)
637       {
638         if (group == null || checkGroupState(group))
639         {
640           visibleGroups.add(group);
641         }
642       }
643       foundGroups.addAll(groups);
644
645       /*
646        * get distinct feature types for visible groups
647        * record distinct visible types, and their count and total length
648        */
649       Set<String> types = seq.getFeatures().getFeatureTypesForGroups(true,
650               visibleGroups.toArray(new String[visibleGroups.size()]));
651       for (String type : types)
652       {
653         displayableTypes.add(type);
654         float[] avWidth = typeWidth.get(type);
655         if (avWidth == null)
656         {
657           avWidth = new float[2];
658           typeWidth.put(type, avWidth);
659         }
660         // todo this could include features with a non-visible group
661         // - do we greatly care?
662         // todo should we include non-displayable features here, and only
663         // update when features are added?
664         avWidth[0] += seq.getFeatures().getFeatureCount(true, type);
665         avWidth[1] += seq.getFeatures().getTotalFeatureLength(type);
666       }
667     }
668
669     Object[][] data = new Object[displayableTypes.size()][3];
670     int dataIndex = 0;
671
672     if (fr.hasRenderOrder())
673     {
674       if (!handlingUpdate)
675       {
676         fr.findAllFeatures(groupChanged != null); // prod to update
677         // colourschemes. but don't
678         // affect display
679         // First add the checks in the previous render order,
680         // in case the window has been closed and reopened
681       }
682       List<String> frl = fr.getRenderOrder();
683       for (int ro = frl.size() - 1; ro > -1; ro--)
684       {
685         String type = frl.get(ro);
686
687         if (!displayableTypes.contains(type))
688         {
689           continue;
690         }
691
692         data[dataIndex][0] = type;
693         data[dataIndex][1] = fr.getFeatureStyle(type);
694         data[dataIndex][2] = new Boolean(
695                 af.getViewport().getFeaturesDisplayed().isVisible(type));
696         dataIndex++;
697         displayableTypes.remove(type);
698       }
699     }
700
701     /*
702      * process any extra features belonging only to 
703      * a group which was just selected
704      */
705     while (!displayableTypes.isEmpty())
706     {
707       String type = displayableTypes.iterator().next();
708       data[dataIndex][0] = type;
709
710       data[dataIndex][1] = fr.getFeatureStyle(type);
711       if (data[dataIndex][1] == null)
712       {
713         // "Colour has been updated in another view!!"
714         fr.clearRenderOrder();
715         return;
716       }
717
718       data[dataIndex][2] = new Boolean(true);
719       dataIndex++;
720       displayableTypes.remove(type);
721     }
722
723     if (originalData == null)
724     {
725       originalData = new Object[data.length][3];
726       for (int i = 0; i < data.length; i++)
727       {
728         System.arraycopy(data[i], 0, originalData[i], 0, 3);
729       }
730     }
731     else
732     {
733       updateOriginalData(data);
734     }
735
736     table.setModel(new FeatureTableModel(data));
737     table.getColumnModel().getColumn(0).setPreferredWidth(200);
738
739     groupPanel.setLayout(
740             new GridLayout(fr.getFeatureGroupsSize() / 4 + 1, 4));
741     pruneGroups(foundGroups);
742     groupPanel.validate();
743
744     updateFeatureRenderer(data, groupChanged != null);
745     resettingTable = false;
746   }
747
748   /**
749    * Updates 'originalData' (used for restore on Cancel) if we detect that
750    * changes have been made outwith this dialog
751    * <ul>
752    * <li>a new feature type added (and made visible)</li>
753    * <li>a feature colour changed (in the Amend Features dialog)</li>
754    * </ul>
755    * 
756    * @param foundData
757    */
758   protected void updateOriginalData(Object[][] foundData)
759   {
760     // todo LinkedHashMap instead of Object[][] would be nice
761
762     Object[][] currentData = ((FeatureTableModel) table.getModel())
763             .getData();
764     for (Object[] row : foundData)
765     {
766       String type = (String) row[0];
767       boolean found = false;
768       for (Object[] current : currentData)
769       {
770         if (type.equals(current[0]))
771         {
772           found = true;
773           /*
774            * currently dependent on object equality here;
775            * really need an equals method on FeatureColour
776            */
777           if (!row[1].equals(current[1]))
778           {
779             /*
780              * feature colour has changed externally - update originalData
781              */
782             for (Object[] original : originalData)
783             {
784               if (type.equals(original[0]))
785               {
786                 original[1] = row[1];
787                 break;
788               }
789             }
790           }
791           break;
792         }
793       }
794       if (!found)
795       {
796         /*
797          * new feature detected - add to original data (on top)
798          */
799         Object[][] newData = new Object[originalData.length + 1][3];
800         for (int i = 0; i < originalData.length; i++)
801         {
802           System.arraycopy(originalData[i], 0, newData[i + 1], 0, 3);
803         }
804         newData[0] = row;
805         originalData = newData;
806       }
807     }
808   }
809
810   /**
811    * Remove from the groups panel any checkboxes for groups that are not in the
812    * foundGroups set. This enables removing a group from the display when the
813    * last feature in that group is deleted.
814    * 
815    * @param foundGroups
816    */
817   protected void pruneGroups(Set<String> foundGroups)
818   {
819     for (int g = 0; g < groupPanel.getComponentCount(); g++)
820     {
821       JCheckBox checkbox = (JCheckBox) groupPanel.getComponent(g);
822       if (!foundGroups.contains(checkbox.getText()))
823       {
824         groupPanel.remove(checkbox);
825       }
826     }
827   }
828
829   /**
830    * reorder data based on the featureRenderers global priority list.
831    * 
832    * @param data
833    */
834   private void ensureOrder(Object[][] data)
835   {
836     boolean sort = false;
837     float[] order = new float[data.length];
838     for (int i = 0; i < order.length; i++)
839     {
840       order[i] = fr.getOrder(data[i][0].toString());
841       if (order[i] < 0)
842       {
843         order[i] = fr.setOrder(data[i][0].toString(), i / order.length);
844       }
845       if (i > 1)
846       {
847         sort = sort || order[i - 1] > order[i];
848       }
849     }
850     if (sort)
851     {
852       jalview.util.QuickSort.sort(order, data);
853     }
854   }
855
856   void load()
857   {
858     JalviewFileChooser chooser = new JalviewFileChooser("fc",
859             "Sequence Feature Colours");
860     chooser.setFileView(new JalviewFileView());
861     chooser.setDialogTitle(
862             MessageManager.getString("label.load_feature_colours"));
863     chooser.setToolTipText(MessageManager.getString("action.load"));
864
865     int value = chooser.showOpenDialog(this);
866
867     if (value == JalviewFileChooser.APPROVE_OPTION)
868     {
869       File file = chooser.getSelectedFile();
870
871       try
872       {
873         InputStreamReader in = new InputStreamReader(
874                 new FileInputStream(file), "UTF-8");
875
876         JalviewUserColours jucs = JalviewUserColours.unmarshal(in);
877
878         for (int i = jucs.getColourCount() - 1; i >= 0; i--)
879         {
880           String name;
881           jalview.schemabinding.version2.Colour newcol = jucs.getColour(i);
882           if (newcol.hasMax())
883           {
884             Color mincol = null, maxcol = null;
885             try
886             {
887               mincol = new Color(Integer.parseInt(newcol.getMinRGB(), 16));
888               maxcol = new Color(Integer.parseInt(newcol.getRGB(), 16));
889
890             } catch (Exception e)
891             {
892               Cache.log.warn("Couldn't parse out graduated feature color.",
893                       e);
894             }
895             FeatureColourI gcol = new FeatureColour(mincol, maxcol,
896                     newcol.getMin(), newcol.getMax());
897             if (newcol.hasAutoScale())
898             {
899               gcol.setAutoScaled(newcol.getAutoScale());
900             }
901             if (newcol.hasColourByLabel())
902             {
903               gcol.setColourByLabel(newcol.getColourByLabel());
904             }
905             if (newcol.hasThreshold())
906             {
907               gcol.setThreshold(newcol.getThreshold());
908             }
909             if (newcol.getThreshType().length() > 0)
910             {
911               String ttyp = newcol.getThreshType();
912               if (ttyp.equalsIgnoreCase("ABOVE"))
913               {
914                 gcol.setAboveThreshold(true);
915               }
916               if (ttyp.equalsIgnoreCase("BELOW"))
917               {
918                 gcol.setBelowThreshold(true);
919               }
920             }
921             fr.setColour(name = newcol.getName(), gcol);
922           }
923           else
924           {
925             Color color = new Color(
926                     Integer.parseInt(jucs.getColour(i).getRGB(), 16));
927             fr.setColour(name = jucs.getColour(i).getName(),
928                     new FeatureColour(color));
929           }
930           fr.setOrder(name, (i == 0) ? 0 : i / jucs.getColourCount());
931         }
932         if (table != null)
933         {
934           resetTable(null);
935           Object[][] data = ((FeatureTableModel) table.getModel())
936                   .getData();
937           ensureOrder(data);
938           updateFeatureRenderer(data, false);
939           table.repaint();
940         }
941       } catch (Exception ex)
942       {
943         System.out.println("Error loading User Colour File\n" + ex);
944       }
945     }
946   }
947
948   void save()
949   {
950     JalviewFileChooser chooser = new JalviewFileChooser("fc",
951             "Sequence Feature Colours");
952     chooser.setFileView(new JalviewFileView());
953     chooser.setDialogTitle(
954             MessageManager.getString("label.save_feature_colours"));
955     chooser.setToolTipText(MessageManager.getString("action.save"));
956
957     int value = chooser.showSaveDialog(this);
958
959     if (value == JalviewFileChooser.APPROVE_OPTION)
960     {
961       String choice = chooser.getSelectedFile().getPath();
962       jalview.schemabinding.version2.JalviewUserColours ucs = new jalview.schemabinding.version2.JalviewUserColours();
963       ucs.setSchemeName("Sequence Features");
964       try
965       {
966         PrintWriter out = new PrintWriter(new OutputStreamWriter(
967                 new FileOutputStream(choice), "UTF-8"));
968
969         Set<String> fr_colours = fr.getAllFeatureColours();
970         Iterator<String> e = fr_colours.iterator();
971         float[] sortOrder = new float[fr_colours.size()];
972         String[] sortTypes = new String[fr_colours.size()];
973         int i = 0;
974         while (e.hasNext())
975         {
976           sortTypes[i] = e.next();
977           sortOrder[i] = fr.getOrder(sortTypes[i]);
978           i++;
979         }
980         QuickSort.sort(sortOrder, sortTypes);
981         sortOrder = null;
982         for (i = 0; i < sortTypes.length; i++)
983         {
984           jalview.schemabinding.version2.Colour col = new jalview.schemabinding.version2.Colour();
985           col.setName(sortTypes[i]);
986           FeatureColourI fcol = fr.getFeatureStyle(sortTypes[i]);
987           if (fcol.isSimpleColour())
988           {
989             col.setRGB(Format.getHexString(fcol.getColour()));
990           }
991           else
992           {
993             col.setRGB(Format.getHexString(fcol.getMaxColour()));
994             col.setMin(fcol.getMin());
995             col.setMax(fcol.getMax());
996             col.setMinRGB(
997                     jalview.util.Format.getHexString(fcol.getMinColour()));
998             col.setAutoScale(fcol.isAutoScaled());
999             col.setThreshold(fcol.getThreshold());
1000             col.setColourByLabel(fcol.isColourByLabel());
1001             col.setThreshType(fcol.isAboveThreshold() ? "ABOVE"
1002                     : (fcol.isBelowThreshold() ? "BELOW" : "NONE"));
1003           }
1004           ucs.addColour(col);
1005         }
1006         ucs.marshal(out);
1007         out.close();
1008       } catch (Exception ex)
1009       {
1010         ex.printStackTrace();
1011       }
1012     }
1013   }
1014
1015   public void invertSelection()
1016   {
1017     for (int i = 0; i < table.getRowCount(); i++)
1018     {
1019       Boolean value = (Boolean) table.getValueAt(i, 2);
1020
1021       table.setValueAt(new Boolean(!value.booleanValue()), i, 2);
1022     }
1023   }
1024
1025   public void orderByAvWidth()
1026   {
1027     if (table == null || table.getModel() == null)
1028     {
1029       return;
1030     }
1031     Object[][] data = ((FeatureTableModel) table.getModel()).getData();
1032     float[] width = new float[data.length];
1033     float[] awidth;
1034     float max = 0;
1035     int num = 0;
1036     for (int i = 0; i < data.length; i++)
1037     {
1038       awidth = typeWidth.get(data[i][0]);
1039       if (awidth[0] > 0)
1040       {
1041         width[i] = awidth[1] / awidth[0];// *awidth[0]*awidth[2]; - better
1042         // weight - but have to make per
1043         // sequence, too (awidth[2])
1044         // if (width[i]==1) // hack to distinguish single width sequences.
1045         num++;
1046       }
1047       else
1048       {
1049         width[i] = 0;
1050       }
1051       if (max < width[i])
1052       {
1053         max = width[i];
1054       }
1055     }
1056     boolean sort = false;
1057     for (int i = 0; i < width.length; i++)
1058     {
1059       // awidth = (float[]) typeWidth.get(data[i][0]);
1060       if (width[i] == 0)
1061       {
1062         width[i] = fr.getOrder(data[i][0].toString());
1063         if (width[i] < 0)
1064         {
1065           width[i] = fr.setOrder(data[i][0].toString(), i / data.length);
1066         }
1067       }
1068       else
1069       {
1070         width[i] /= max; // normalize
1071         fr.setOrder(data[i][0].toString(), width[i]); // store for later
1072       }
1073       if (i > 0)
1074       {
1075         sort = sort || width[i - 1] > width[i];
1076       }
1077     }
1078     if (sort)
1079     {
1080       jalview.util.QuickSort.sort(width, data);
1081       // update global priority order
1082     }
1083
1084     updateFeatureRenderer(data, false);
1085     table.repaint();
1086   }
1087
1088   public void close()
1089   {
1090     try
1091     {
1092       frame.setClosed(true);
1093     } catch (Exception exe)
1094     {
1095     }
1096
1097   }
1098
1099   public void updateFeatureRenderer(Object[][] data)
1100   {
1101     updateFeatureRenderer(data, true);
1102   }
1103
1104   /**
1105    * Update the priority order of features; only repaint if this changed the
1106    * order of visible features
1107    * 
1108    * @param data
1109    * @param visibleNew
1110    */
1111   private void updateFeatureRenderer(Object[][] data, boolean visibleNew)
1112   {
1113     if (fr.setFeaturePriority(data, visibleNew))
1114     {
1115       af.alignPanel.paintAlignment(true, true);
1116     }
1117   }
1118
1119   private void jbInit() throws Exception
1120   {
1121     this.setLayout(new BorderLayout());
1122
1123     JPanel settingsPane = new JPanel();
1124     settingsPane.setLayout(new BorderLayout());
1125
1126     filtersPane = new JPanel();
1127
1128     dasSettingsPane.setLayout(new BorderLayout());
1129
1130     JPanel bigPanel = new JPanel();
1131     bigPanel.setLayout(new BorderLayout());
1132
1133     groupPanel = new JPanel();
1134     bigPanel.add(groupPanel, BorderLayout.NORTH);
1135
1136     JButton invert = new JButton(
1137             MessageManager.getString("label.invert_selection"));
1138     invert.setFont(JvSwingUtils.getLabelFont());
1139     invert.addActionListener(new ActionListener()
1140     {
1141       @Override
1142       public void actionPerformed(ActionEvent e)
1143       {
1144         invertSelection();
1145       }
1146     });
1147
1148     JButton optimizeOrder = new JButton(
1149             MessageManager.getString("label.optimise_order"));
1150     optimizeOrder.setFont(JvSwingUtils.getLabelFont());
1151     optimizeOrder.addActionListener(new ActionListener()
1152     {
1153       @Override
1154       public void actionPerformed(ActionEvent e)
1155       {
1156         orderByAvWidth();
1157       }
1158     });
1159
1160     JButton sortByScore = new JButton(
1161             MessageManager.getString("label.seq_sort_by_score"));
1162     sortByScore.setFont(JvSwingUtils.getLabelFont());
1163     sortByScore.addActionListener(new ActionListener()
1164     {
1165       @Override
1166       public void actionPerformed(ActionEvent e)
1167       {
1168         af.avc.sortAlignmentByFeatureScore(null);
1169       }
1170     });
1171     JButton sortByDens = new JButton(
1172             MessageManager.getString("label.sequence_sort_by_density"));
1173     sortByDens.setFont(JvSwingUtils.getLabelFont());
1174     sortByDens.addActionListener(new ActionListener()
1175     {
1176       @Override
1177       public void actionPerformed(ActionEvent e)
1178       {
1179         af.avc.sortAlignmentByFeatureDensity(null);
1180       }
1181     });
1182
1183     JButton help = new JButton(MessageManager.getString("action.help"));
1184     help.setFont(JvSwingUtils.getLabelFont());
1185     help.addActionListener(new ActionListener()
1186     {
1187       @Override
1188       public void actionPerformed(ActionEvent e)
1189       {
1190         try
1191         {
1192           Help.showHelpWindow(HelpId.SequenceFeatureSettings);
1193         } catch (HelpSetException e1)
1194         {
1195           e1.printStackTrace();
1196         }
1197       }
1198     });
1199     help.setFont(JvSwingUtils.getLabelFont());
1200     help.setText(MessageManager.getString("action.help"));
1201     help.addActionListener(new ActionListener()
1202     {
1203       @Override
1204       public void actionPerformed(ActionEvent e)
1205       {
1206         try
1207         {
1208           Help.showHelpWindow(HelpId.SequenceFeatureSettings);
1209         } catch (HelpSetException e1)
1210         {
1211           e1.printStackTrace();
1212         }
1213       }
1214     });
1215
1216     JButton cancel = new JButton(MessageManager.getString("action.cancel"));
1217     cancel.setFont(JvSwingUtils.getLabelFont());
1218     cancel.addActionListener(new ActionListener()
1219     {
1220       @Override
1221       public void actionPerformed(ActionEvent e)
1222       {
1223         fr.setTransparency(originalTransparency);
1224         fr.setFeatureFilters(originalFilters);
1225         updateFeatureRenderer(originalData);
1226         close();
1227       }
1228     });
1229
1230     JButton ok = new JButton(MessageManager.getString("action.ok"));
1231     ok.setFont(JvSwingUtils.getLabelFont());
1232     ok.addActionListener(new ActionListener()
1233     {
1234       @Override
1235       public void actionPerformed(ActionEvent e)
1236       {
1237         close();
1238       }
1239     });
1240
1241     JButton loadColours = new JButton(
1242             MessageManager.getString("label.load_colours"));
1243     loadColours.setFont(JvSwingUtils.getLabelFont());
1244     loadColours.addActionListener(new ActionListener()
1245     {
1246       @Override
1247       public void actionPerformed(ActionEvent e)
1248       {
1249         load();
1250       }
1251     });
1252
1253     JButton saveColours = new JButton(
1254             MessageManager.getString("label.save_colours"));
1255     saveColours.setFont(JvSwingUtils.getLabelFont());
1256     saveColours.addActionListener(new ActionListener()
1257     {
1258       @Override
1259       public void actionPerformed(ActionEvent e)
1260       {
1261         save();
1262       }
1263     });
1264     transparency.addChangeListener(new ChangeListener()
1265     {
1266       @Override
1267       public void stateChanged(ChangeEvent evt)
1268       {
1269         if (!inConstruction)
1270         {
1271           fr.setTransparency((100 - transparency.getValue()) / 100f);
1272           af.alignPanel.paintAlignment(true,true);
1273         }
1274       }
1275     });
1276
1277     transparency.setMaximum(70);
1278     transparency.setToolTipText(
1279             MessageManager.getString("label.transparency_tip"));
1280     fetchDAS.setText(MessageManager.getString("label.fetch_das_features"));
1281     fetchDAS.addActionListener(new ActionListener()
1282     {
1283       @Override
1284       public void actionPerformed(ActionEvent e)
1285       {
1286         fetchDAS_actionPerformed(e);
1287       }
1288     });
1289     saveDAS.setText(MessageManager.getString("action.save_as_default"));
1290     saveDAS.addActionListener(new ActionListener()
1291     {
1292       @Override
1293       public void actionPerformed(ActionEvent e)
1294       {
1295         saveDAS_actionPerformed(e);
1296       }
1297     });
1298
1299     JPanel dasButtonPanel = new JPanel();
1300     dasButtonPanel.setBorder(BorderFactory.createEtchedBorder());
1301     dasSettingsPane.setBorder(null);
1302     cancelDAS.setEnabled(false);
1303     cancelDAS.setText(MessageManager.getString("action.cancel_fetch"));
1304     cancelDAS.addActionListener(new ActionListener()
1305     {
1306       @Override
1307       public void actionPerformed(ActionEvent e)
1308       {
1309         cancelDAS_actionPerformed(e);
1310       }
1311     });
1312
1313     JTabbedPane tabbedPane = new JTabbedPane();
1314     this.add(tabbedPane, BorderLayout.CENTER);
1315     tabbedPane.addTab(MessageManager.getString("label.feature_settings"),
1316             settingsPane);
1317     tabbedPane.addTab(MessageManager.getString("label.filters"),
1318             filtersPane);
1319     // tabbedPane.addTab(MessageManager.getString("label.das_settings"),
1320     // dasSettingsPane);
1321
1322     JPanel transPanel = new JPanel(new GridLayout(1, 2));
1323     bigPanel.add(transPanel, BorderLayout.SOUTH);
1324
1325     JPanel transbuttons = new JPanel(new GridLayout(5, 1));
1326     transbuttons.add(optimizeOrder);
1327     transbuttons.add(invert);
1328     transbuttons.add(sortByScore);
1329     transbuttons.add(sortByDens);
1330     transbuttons.add(help);
1331     transPanel.add(transparency);
1332     transPanel.add(transbuttons);
1333
1334     JPanel buttonPanel = new JPanel();
1335     buttonPanel.add(ok);
1336     buttonPanel.add(cancel);
1337     buttonPanel.add(loadColours);
1338     buttonPanel.add(saveColours);
1339     bigPanel.add(scrollPane, BorderLayout.CENTER);
1340     dasSettingsPane.add(dasButtonPanel, BorderLayout.SOUTH);
1341     dasButtonPanel.add(fetchDAS);
1342     dasButtonPanel.add(cancelDAS);
1343     dasButtonPanel.add(saveDAS);
1344     settingsPane.add(bigPanel, BorderLayout.CENTER);
1345     settingsPane.add(buttonPanel, BorderLayout.SOUTH);
1346
1347     initFiltersTab();
1348   }
1349
1350   /**
1351    * Populates initial layout of the feature attribute filters panel
1352    */
1353   protected void initFiltersTab()
1354   {
1355     filters = new ArrayList<>();
1356
1357     /*
1358      * choose feature type
1359      */
1360     JPanel chooseTypePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
1361     chooseTypePanel.setBackground(Color.white);
1362     chooseTypePanel.setBorder(BorderFactory
1363             .createTitledBorder(MessageManager
1364                     .getString("label.feature_type")));
1365     filteredFeatureChoice = new JComboBox<>();
1366     filteredFeatureChoice.addItemListener(new ItemListener()
1367     {
1368       @Override
1369       public void itemStateChanged(ItemEvent e)
1370       {
1371         refreshFiltersDisplay();
1372       }
1373     });
1374     chooseTypePanel.add(new JLabel(MessageManager
1375             .getString("label.feature_to_filter")));
1376     chooseTypePanel.add(filteredFeatureChoice);
1377     populateFilterableFeatures();
1378
1379     /*
1380      * the panel with the filters for the selected feature type
1381      */
1382     JPanel filtersPanel = new JPanel(new GridLayout(0, 1));
1383     filtersPanel.setBackground(Color.white);
1384     filtersPanel.setBorder(BorderFactory
1385             .createTitledBorder(MessageManager.getString("label.filters")));
1386
1387     /*
1388      * add AND or OR radio buttons
1389      */
1390     JPanel andOrPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
1391     andOrPanel.setBackground(Color.white);
1392     andFilters = new JRadioButton("And");
1393     orFilters = new JRadioButton("Or");
1394     ActionListener actionListener = new ActionListener()
1395     {
1396       @Override
1397       public void actionPerformed(ActionEvent e)
1398       {
1399         filtersChanged();
1400       }
1401     };
1402     andFilters.addActionListener(actionListener);
1403     orFilters.addActionListener(actionListener);
1404     ButtonGroup andOr = new ButtonGroup();
1405     andOr.add(andFilters);
1406     andOr.add(orFilters);
1407     andFilters.setSelected(true);
1408     andOrPanel.add(new JLabel(MessageManager
1409             .getString("label.join_conditions")));
1410     andOrPanel.add(andFilters);
1411     andOrPanel.add(orFilters);
1412     filtersPanel.add(andOrPanel);
1413
1414     /*
1415      * panel with filters - populated by refreshFiltersDisplay
1416      */
1417     chooseFiltersPanel = new JPanel(new GridLayout(0, 1));
1418     filtersPanel.add(chooseFiltersPanel);
1419
1420     /*
1421      * a read-only text view of the current filters
1422      */
1423     JPanel showFiltersPanel = new JPanel(new BorderLayout(5, 5));
1424     showFiltersPanel.setBackground(Color.white);
1425     showFiltersPanel.setBorder(BorderFactory
1426             .createTitledBorder(MessageManager
1427                     .getString("label.match_condition")));
1428     filtersAsText = new JTextArea();
1429     filtersAsText.setLineWrap(true);
1430     filtersAsText.setWrapStyleWord(true);
1431     showFiltersPanel.add(filtersAsText);
1432
1433     filtersPane.setLayout(new BorderLayout());
1434     filtersPane.add(chooseTypePanel, BorderLayout.NORTH);
1435     filtersPane.add(filtersPanel, BorderLayout.CENTER);
1436     filtersPane.add(showFiltersPanel, BorderLayout.SOUTH);
1437
1438     /*
1439      * update display for initial feature type selection
1440      */
1441     refreshFiltersDisplay();
1442   }
1443
1444   /**
1445    * Adds entries to the 'choose feature to filter' drop-down choice. Only
1446    * feature types which have known attributes (so can be filtered) are
1447    * included, so recall this method to update the list (check for newly added
1448    * attributes).
1449    */
1450   protected void populateFilterableFeatures()
1451   {
1452     /*
1453      * suppress action handler while updating the list
1454      */
1455     ItemListener listener = filteredFeatureChoice.getItemListeners()[0];
1456     filteredFeatureChoice.removeItemListener(listener);
1457
1458     filteredFeatureChoice.removeAllItems();
1459     ReverseListIterator<String> types = new ReverseListIterator<>(
1460             fr.getRenderOrder());
1461
1462     boolean found = false;
1463     while (types.hasNext())
1464     {
1465       String type = types.next();
1466       if (FeatureAttributes.getInstance().hasAttributes(type))
1467       {
1468         filteredFeatureChoice.addItem(type);
1469         found = true;
1470       }
1471     }
1472     if (!found)
1473     {
1474       filteredFeatureChoice
1475               .addItem("No filterable feature attributes known");
1476     }
1477
1478     filteredFeatureChoice.addItemListener(listener);
1479
1480   }
1481
1482   /**
1483    * Refreshes the display to show any filters currently configured for the
1484    * selected feature type (editable, with 'remove' option), plus one extra row
1485    * for adding a condition. This should be called on change of selected feature
1486    * type, or after a filter has been removed, added or amended.
1487    */
1488   protected void refreshFiltersDisplay()
1489   {
1490     /*
1491      * clear the panel and list of filter conditions
1492      */
1493     chooseFiltersPanel.removeAll();
1494
1495     String selectedType = (String) filteredFeatureChoice.getSelectedItem();
1496
1497     filters.clear();
1498
1499     /*
1500      * look up attributes known for feature type
1501      */
1502     List<String> attNames = FeatureAttributes.getInstance().getAttributes(
1503             selectedType);
1504
1505     /*
1506      * if this feature type has filters set, load them first
1507      */
1508     KeyedMatcherSetI featureFilters = fr.getFeatureFilter(selectedType);
1509     filtersAsText.setText("");
1510     if (featureFilters != null)
1511     {
1512       filtersAsText.setText(featureFilters.toString());
1513       if (!featureFilters.isAnded())
1514       {
1515         orFilters.setSelected(true);
1516       }
1517       Iterator<KeyedMatcherI> matchers = featureFilters.getMatchers();
1518       while (matchers.hasNext())
1519       {
1520         filters.add(matchers.next());
1521       }
1522     }
1523
1524     /*
1525      * and an empty filter for the user to populate (add)
1526      */
1527     KeyedMatcherI noFilter = new KeyedMatcher("", Condition.values()[0], "");
1528     filters.add(noFilter);
1529
1530     /*
1531      * render the conditions in rows, each in its own JPanel
1532      */
1533     int i = 0;
1534     for (KeyedMatcherI filter : filters)
1535     {
1536       String key = filter.getKey();
1537       Condition condition = filter.getMatcher()
1538               .getCondition();
1539       String pattern = filter.getMatcher().getPattern();
1540       JPanel row = addFilter(key, attNames, condition, pattern, i);
1541       chooseFiltersPanel.add(row);
1542       i++;
1543     }
1544
1545     filtersPane.validate();
1546     filtersPane.repaint();
1547   }
1548
1549   /**
1550    * A helper method that constructs a panel with one filter condition:
1551    * <ul>
1552    * <li>a drop-down list of attribute names to choose from</li>
1553    * <li>a drop-down list of conditions to choose from</li>
1554    * <li>a text field for input of a match pattern</li>
1555    * <li>optionally, a 'remove' button</li>
1556    * </ul>
1557    * If attribute, condition or pattern are not null, they are set as defaults
1558    * for the input fields. The 'remove' button is added unless the pattern is
1559    * null or empty (incomplete filter condition).
1560    * 
1561    * @param attribute
1562    * @param attNames
1563    * @param cond
1564    * @param pattern
1565    * @param filterIndex
1566    * @return
1567    */
1568   protected JPanel addFilter(String attribute, List<String> attNames,
1569           Condition cond, String pattern, int filterIndex)
1570   {
1571     JPanel filterRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
1572     filterRow.setBackground(Color.white);
1573
1574     /*
1575      * inputs for attribute, condition, pattern
1576      */
1577     final JComboBox<String> attCombo = new JComboBox<>();
1578     JComboBox<Condition> condCombo = new JComboBox<>();
1579     JTextField patternField = new JTextField(8);
1580
1581     /*
1582      * action handlers that validate and (if valid) apply changes
1583      */
1584     ActionListener actionListener = new ActionListener()
1585     {
1586       @Override
1587       public void actionPerformed(ActionEvent e)
1588       {
1589         if (attCombo.getSelectedItem() != null)
1590         {
1591           if (validateFilter(patternField, condCombo))
1592           {
1593             updateFilter(attCombo, condCombo, patternField, filterIndex);
1594             filtersChanged();
1595           }
1596         }
1597       }
1598     };
1599     ItemListener itemListener = new ItemListener()
1600     {
1601       @Override
1602       public void itemStateChanged(ItemEvent e)
1603       {
1604         actionListener.actionPerformed(null);
1605       }
1606     };
1607
1608     /*
1609      * drop-down choice of attribute, with description as a tooltip 
1610      * if we can obtain it
1611      */
1612     String featureType = (String) filteredFeatureChoice.getSelectedItem();
1613     populateAttributesDropdown(attCombo, featureType, attNames);
1614     if ("".equals(attribute))
1615     {
1616       attCombo.setSelectedItem(null);
1617     }
1618     else
1619     {
1620       attCombo.setSelectedItem(attribute);
1621     }
1622     attCombo.addItemListener(itemListener);
1623
1624     filterRow.add(attCombo);
1625
1626     /*
1627      * drop-down choice of test condition
1628      */
1629     for (Condition c : Condition.values())
1630     {
1631       condCombo.addItem(c);
1632     }
1633     if (cond != null)
1634     {
1635       condCombo.setSelectedItem(cond);
1636     }
1637     condCombo.addItemListener(itemListener);
1638     filterRow.add(condCombo);
1639
1640     /*
1641      * pattern to match against
1642      */
1643     patternField.setText(pattern);
1644     patternField.addActionListener(actionListener);
1645     patternField.addFocusListener(new FocusAdapter()
1646     {
1647       @Override
1648       public void focusLost(FocusEvent e)
1649       {
1650         actionListener.actionPerformed(null);
1651       }
1652     });
1653     filterRow.add(patternField);
1654
1655     /*
1656      * add remove button if filter is populated (non-empty pattern)
1657      */
1658     if (pattern != null && pattern.trim().length() > 0)
1659     {
1660       // todo: gif for - button
1661       JButton removeCondition = new BasicArrowButton(SwingConstants.WEST);
1662       removeCondition.setToolTipText(MessageManager
1663               .getString("label.delete_row"));
1664       removeCondition.addActionListener(new ActionListener()
1665       {
1666         @Override
1667         public void actionPerformed(ActionEvent e)
1668         {
1669           filters.remove(filterIndex);
1670           filtersChanged();
1671         }
1672       });
1673       filterRow.add(removeCondition);
1674     }
1675
1676     return filterRow;
1677   }
1678
1679   /**
1680    * A helper method to build the drop-down choice of attributes for a feature.
1681    * Where metadata is available with a description for an attribute, that is
1682    * added as a tooltip.
1683    * 
1684    * @param attCombo
1685    * @param featureType
1686    * @param attNames
1687    */
1688   protected void populateAttributesDropdown(
1689           final JComboBox<String> attCombo, String featureType,
1690           List<String> attNames)
1691   {
1692     final ComboBoxTooltipRenderer renderer = new ComboBoxTooltipRenderer();
1693     attCombo.setRenderer(renderer);
1694     List<String> tips = new ArrayList<String>();
1695     if (attNames.isEmpty())
1696     {
1697       attCombo.addItem("---");
1698       attCombo.setToolTipText(MessageManager
1699               .getString("label.no_attributes_known"));
1700     }
1701     else
1702     {
1703       attCombo.setToolTipText("");
1704       FeatureAttributes fs = FeatureAttributes.getInstance();
1705       for (String attName : attNames)
1706       {
1707         attCombo.addItem(attName);
1708         String desc = fs.getDescription(featureType, attName);
1709         tips.add(desc == null ? "" : desc);
1710       }
1711     }
1712     renderer.setTooltips(tips);
1713     final MouseAdapter mouseListener = new MouseAdapter()
1714     {
1715       @Override
1716       public void mouseEntered(MouseEvent e)
1717       {
1718         int j = attCombo.getSelectedIndex();
1719         if (j > -1)
1720         {
1721           attCombo.setToolTipText(tips.get(j));
1722         }
1723       }
1724
1725       @Override
1726       public void mouseExited(MouseEvent e)
1727       {
1728         attCombo.setToolTipText(null);
1729       }
1730     };
1731     for (Component c : attCombo.getComponents())
1732     {
1733       c.addMouseListener(mouseListener);
1734     }
1735   }
1736
1737   /**
1738    * Action on any change to feature filtering, namely
1739    * <ul>
1740    * <li>change of selected attribute</li>
1741    * <li>change of selected condition</li>
1742    * <li>change of match pattern</li>
1743    * <li>removal of a condition</li>
1744    * </ul>
1745    * The action should be to
1746    * <ul>
1747    * <li>parse and validate the filters</li>
1748    * <li>if valid, update the filter text box</li>
1749    * <li>and apply the filters to the viewport</li>
1750    * </ul>
1751    */
1752   protected void filtersChanged()
1753   {
1754     /*
1755      * update the filter conditions for the feature type
1756      */
1757     String featureType = (String) filteredFeatureChoice.getSelectedItem();
1758     boolean anded = andFilters.isSelected();
1759     KeyedMatcherSetI combined = new KeyedMatcherSet();
1760
1761     for (KeyedMatcherI filter : filters)
1762     {
1763       String pattern = filter.getMatcher().getPattern();
1764       if (pattern.trim().length() > 0)
1765       {
1766         if (anded)
1767         {
1768           combined.and(filter);
1769         }
1770         else
1771         {
1772           combined.or(filter);
1773         }
1774       }
1775     }
1776
1777     /*
1778      * save the filter conditions in the FeatureRenderer
1779      * (note this might now be an empty filter with no conditions)
1780      */
1781     fr.setFeatureFilter(featureType, combined);
1782
1783     filtersAsText.setText(combined.toString());
1784
1785     refreshFiltersDisplay();
1786
1787     af.alignPanel.paintAlignment(true, true);
1788   }
1789
1790   /**
1791    * Constructs a filter condition from the given input fields, and replaces the
1792    * condition at filterIndex with the new one
1793    * 
1794    * @param attCombo
1795    * @param condCombo
1796    * @param valueField
1797    * @param filterIndex
1798    */
1799   protected void updateFilter(JComboBox<String> attCombo,
1800           JComboBox<Condition> condCombo, JTextField valueField,
1801           int filterIndex)
1802   {
1803     String attName = (String) attCombo.getSelectedItem();
1804     Condition cond = (Condition) condCombo.getSelectedItem();
1805     String pattern = valueField.getText();
1806     KeyedMatcherI km = new KeyedMatcher(attName, cond, pattern);
1807
1808     filters.set(filterIndex, km);
1809   }
1810
1811   public void fetchDAS_actionPerformed(ActionEvent e)
1812   {
1813     fetchDAS.setEnabled(false);
1814     cancelDAS.setEnabled(true);
1815     dassourceBrowser.setGuiEnabled(false);
1816     Vector<jalviewSourceI> selectedSources = dassourceBrowser
1817             .getSelectedSources();
1818     doDasFeatureFetch(selectedSources, true, true);
1819   }
1820
1821   /**
1822    * get the features from selectedSources for all or the current selection
1823    * 
1824    * @param selectedSources
1825    * @param checkDbRefs
1826    * @param promptFetchDbRefs
1827    */
1828   private void doDasFeatureFetch(List<jalviewSourceI> selectedSources,
1829           boolean checkDbRefs, boolean promptFetchDbRefs)
1830   {
1831     SequenceI[] dataset, seqs;
1832     int iSize;
1833     AlignmentViewport vp = af.getViewport();
1834     if (vp.getSelectionGroup() != null
1835             && vp.getSelectionGroup().getSize() > 0)
1836     {
1837       iSize = vp.getSelectionGroup().getSize();
1838       dataset = new SequenceI[iSize];
1839       seqs = vp.getSelectionGroup().getSequencesInOrder(vp.getAlignment());
1840     }
1841     else
1842     {
1843       iSize = vp.getAlignment().getHeight();
1844       seqs = vp.getAlignment().getSequencesArray();
1845     }
1846
1847     dataset = new SequenceI[iSize];
1848     for (int i = 0; i < iSize; i++)
1849     {
1850       dataset[i] = seqs[i].getDatasetSequence();
1851     }
1852
1853     cancelDAS.setEnabled(true);
1854     dasFeatureFetcher = new jalview.ws.DasSequenceFeatureFetcher(dataset,
1855             this, selectedSources, checkDbRefs, promptFetchDbRefs);
1856     af.getViewport().setShowSequenceFeatures(true);
1857     af.showSeqFeatures.setSelected(true);
1858   }
1859
1860   /**
1861    * blocking call to initialise the das source browser
1862    */
1863   public void initDasSources()
1864   {
1865     dassourceBrowser.initDasSources();
1866   }
1867
1868   /**
1869    * examine the current list of das sources and return any matching the given
1870    * nicknames in sources
1871    * 
1872    * @param sources
1873    *          Vector of Strings to resolve to DAS source nicknames.
1874    * @return sources that are present in source list.
1875    */
1876   public List<jalviewSourceI> resolveSourceNicknames(Vector<String> sources)
1877   {
1878     return dassourceBrowser.sourceRegistry.resolveSourceNicknames(sources);
1879   }
1880
1881   /**
1882    * get currently selected das sources. ensure you have called initDasSources
1883    * before calling this.
1884    * 
1885    * @return vector of selected das source nicknames
1886    */
1887   public Vector<jalviewSourceI> getSelectedSources()
1888   {
1889     return dassourceBrowser.getSelectedSources();
1890   }
1891
1892   /**
1893    * properly initialise DAS fetcher and then initiate a new thread to fetch
1894    * features from the named sources (rather than any turned on by default)
1895    * 
1896    * @param sources
1897    * @param block
1898    *          if true then runs in same thread, otherwise passes to the Swing
1899    *          executor
1900    */
1901   public void fetchDasFeatures(Vector<String> sources, boolean block)
1902   {
1903     initDasSources();
1904     List<jalviewSourceI> resolved = dassourceBrowser.sourceRegistry
1905             .resolveSourceNicknames(sources);
1906     if (resolved.size() == 0)
1907     {
1908       resolved = dassourceBrowser.getSelectedSources();
1909     }
1910     if (resolved.size() > 0)
1911     {
1912       final List<jalviewSourceI> dassources = resolved;
1913       fetchDAS.setEnabled(false);
1914       // cancelDAS.setEnabled(true); doDasFetch does this.
1915       Runnable fetcher = new Runnable()
1916       {
1917
1918         @Override
1919         public void run()
1920         {
1921           doDasFeatureFetch(dassources, true, false);
1922
1923         }
1924       };
1925       if (block)
1926       {
1927         fetcher.run();
1928       }
1929       else
1930       {
1931         SwingUtilities.invokeLater(fetcher);
1932       }
1933     }
1934   }
1935
1936   public void saveDAS_actionPerformed(ActionEvent e)
1937   {
1938     dassourceBrowser
1939             .saveProperties(jalview.bin.Cache.applicationProperties);
1940   }
1941
1942   public void complete()
1943   {
1944     fetchDAS.setEnabled(true);
1945     cancelDAS.setEnabled(false);
1946     dassourceBrowser.setGuiEnabled(true);
1947
1948   }
1949
1950   public void cancelDAS_actionPerformed(ActionEvent e)
1951   {
1952     if (dasFeatureFetcher != null)
1953     {
1954       dasFeatureFetcher.cancel();
1955     }
1956     complete();
1957   }
1958
1959   public void noDasSourceActive()
1960   {
1961     complete();
1962     JvOptionPane.showInternalConfirmDialog(Desktop.desktop,
1963             MessageManager.getString("label.no_das_sources_selected_warn"),
1964             MessageManager.getString("label.no_das_sources_selected_title"),
1965             JvOptionPane.DEFAULT_OPTION, JvOptionPane.INFORMATION_MESSAGE);
1966   }
1967
1968   /**
1969    * Answers true unless a numeric condition has been selected with a
1970    * non-numeric value. Sets the value field to RED with a tooltip if in error.
1971    * <p>
1972    * If the pattern entered is empty, this method returns false, but does not
1973    * mark the field as invalid. This supports selecting an attribute for a new
1974    * condition before a match pattern has been entered.
1975    * 
1976    * @param value
1977    * @param condCombo
1978    */
1979   protected boolean validateFilter(JTextField value,
1980           JComboBox<Condition> condCombo)
1981   {
1982     if (value == null || condCombo == null)
1983     {
1984       return true; // fields not populated
1985     }
1986   
1987     Condition cond = (Condition) condCombo.getSelectedItem();
1988     value.setBackground(Color.white);
1989     value.setToolTipText("");
1990     String v1 = value.getText().trim();
1991     if (v1.length() == 0)
1992     {
1993       return false;
1994     }
1995
1996     if (cond.isNumeric())
1997     {
1998       try
1999       {
2000         Float.valueOf(v1);
2001       } catch (NumberFormatException e)
2002       {
2003         value.setBackground(Color.red);
2004         value.setToolTipText(MessageManager
2005                 .getString("label.numeric_required"));
2006         return false;
2007       }
2008     }
2009   
2010     return true;
2011   }
2012
2013   // ///////////////////////////////////////////////////////////////////////
2014   // http://java.sun.com/docs/books/tutorial/uiswing/components/table.html
2015   // ///////////////////////////////////////////////////////////////////////
2016   class FeatureTableModel extends AbstractTableModel
2017   {
2018     FeatureTableModel(Object[][] data)
2019     {
2020       this.data = data;
2021     }
2022
2023     private String[] columnNames = {
2024         MessageManager.getString("label.feature_type"),
2025         MessageManager.getString("action.colour"),
2026         MessageManager.getString("label.display") };
2027
2028     private Object[][] data;
2029
2030     public Object[][] getData()
2031     {
2032       return data;
2033     }
2034
2035     public void setData(Object[][] data)
2036     {
2037       this.data = data;
2038     }
2039
2040     @Override
2041     public int getColumnCount()
2042     {
2043       return columnNames.length;
2044     }
2045
2046     public Object[] getRow(int row)
2047     {
2048       return data[row];
2049     }
2050
2051     @Override
2052     public int getRowCount()
2053     {
2054       return data.length;
2055     }
2056
2057     @Override
2058     public String getColumnName(int col)
2059     {
2060       return columnNames[col];
2061     }
2062
2063     @Override
2064     public Object getValueAt(int row, int col)
2065     {
2066       return data[row][col];
2067     }
2068
2069     @Override
2070     public Class getColumnClass(int c)
2071     {
2072       return getValueAt(0, c).getClass();
2073     }
2074
2075     @Override
2076     public boolean isCellEditable(int row, int col)
2077     {
2078       return col == 0 ? false : true;
2079     }
2080
2081     @Override
2082     public void setValueAt(Object value, int row, int col)
2083     {
2084       data[row][col] = value;
2085       fireTableCellUpdated(row, col);
2086       updateFeatureRenderer(data);
2087     }
2088
2089   }
2090
2091   class ColorRenderer extends JLabel implements TableCellRenderer
2092   {
2093     javax.swing.border.Border unselectedBorder = null;
2094
2095     javax.swing.border.Border selectedBorder = null;
2096
2097     final String baseTT = "Click to edit, right/apple click for menu.";
2098
2099     public ColorRenderer()
2100     {
2101       setOpaque(true); // MUST do this for background to show up.
2102       setHorizontalTextPosition(SwingConstants.CENTER);
2103       setVerticalTextPosition(SwingConstants.CENTER);
2104     }
2105
2106     @Override
2107     public Component getTableCellRendererComponent(JTable tbl, Object color,
2108             boolean isSelected, boolean hasFocus, int row, int column)
2109     {
2110       FeatureColourI cellColour = (FeatureColourI) color;
2111       // JLabel comp = new JLabel();
2112       // comp.
2113       setOpaque(true);
2114       // comp.
2115       // setBounds(getBounds());
2116       Color newColor;
2117       setToolTipText(baseTT);
2118       setBackground(tbl.getBackground());
2119       if (!cellColour.isSimpleColour())
2120       {
2121         Rectangle cr = tbl.getCellRect(row, column, false);
2122         FeatureSettings.renderGraduatedColor(this, cellColour,
2123                 (int) cr.getWidth(), (int) cr.getHeight());
2124
2125       }
2126       else
2127       {
2128         this.setText("");
2129         this.setIcon(null);
2130         newColor = cellColour.getColour();
2131         setBackground(newColor);
2132       }
2133       if (isSelected)
2134       {
2135         if (selectedBorder == null)
2136         {
2137           selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
2138                   tbl.getSelectionBackground());
2139         }
2140         setBorder(selectedBorder);
2141       }
2142       else
2143       {
2144         if (unselectedBorder == null)
2145         {
2146           unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
2147                   tbl.getBackground());
2148         }
2149         setBorder(unselectedBorder);
2150       }
2151
2152       return this;
2153     }
2154   }
2155
2156   /**
2157    * update comp using rendering settings from gcol
2158    * 
2159    * @param comp
2160    * @param gcol
2161    */
2162   public static void renderGraduatedColor(JLabel comp, FeatureColourI gcol)
2163   {
2164     int w = comp.getWidth(), h = comp.getHeight();
2165     if (w < 20)
2166     {
2167       w = (int) comp.getPreferredSize().getWidth();
2168       h = (int) comp.getPreferredSize().getHeight();
2169       if (w < 20)
2170       {
2171         w = 80;
2172         h = 12;
2173       }
2174     }
2175     renderGraduatedColor(comp, gcol, w, h);
2176   }
2177
2178   public static void renderGraduatedColor(JLabel comp, FeatureColourI gcol,
2179           int w, int h)
2180   {
2181     boolean thr = false;
2182     String tt = "";
2183     String tx = "";
2184     if (gcol.isAboveThreshold())
2185     {
2186       thr = true;
2187       tx += ">";
2188       tt += "Thresholded (Above " + gcol.getThreshold() + ") ";
2189     }
2190     if (gcol.isBelowThreshold())
2191     {
2192       thr = true;
2193       tx += "<";
2194       tt += "Thresholded (Below " + gcol.getThreshold() + ") ";
2195     }
2196     if (gcol.isColourByLabel())
2197     {
2198       tt = "Coloured by label text. " + tt;
2199       if (thr)
2200       {
2201         tx += " ";
2202       }
2203       tx += "Label";
2204       comp.setIcon(null);
2205     }
2206     else
2207     {
2208       Color newColor = gcol.getMaxColour();
2209       comp.setBackground(newColor);
2210       // System.err.println("Width is " + w / 2);
2211       Icon ficon = new FeatureIcon(gcol, comp.getBackground(), w, h, thr);
2212       comp.setIcon(ficon);
2213       // tt+="RGB value: Max (" + newColor.getRed() + ", "
2214       // + newColor.getGreen() + ", " + newColor.getBlue()
2215       // + ")\nMin (" + minCol.getRed() + ", " + minCol.getGreen()
2216       // + ", " + minCol.getBlue() + ")");
2217     }
2218     comp.setHorizontalAlignment(SwingConstants.CENTER);
2219     comp.setText(tx);
2220     if (tt.length() > 0)
2221     {
2222       if (comp.getToolTipText() == null)
2223       {
2224         comp.setToolTipText(tt);
2225       }
2226       else
2227       {
2228         comp.setToolTipText(tt + " " + comp.getToolTipText());
2229       }
2230     }
2231   }
2232 }
2233
2234 class FeatureIcon implements Icon
2235 {
2236   FeatureColourI gcol;
2237
2238   Color backg;
2239
2240   boolean midspace = false;
2241
2242   int width = 50, height = 20;
2243
2244   int s1, e1; // start and end of midpoint band for thresholded symbol
2245
2246   Color mpcolour = Color.white;
2247
2248   FeatureIcon(FeatureColourI gfc, Color bg, int w, int h, boolean mspace)
2249   {
2250     gcol = gfc;
2251     backg = bg;
2252     width = w;
2253     height = h;
2254     midspace = mspace;
2255     if (midspace)
2256     {
2257       s1 = width / 3;
2258       e1 = s1 * 2;
2259     }
2260     else
2261     {
2262       s1 = width / 2;
2263       e1 = s1;
2264     }
2265   }
2266
2267   @Override
2268   public int getIconWidth()
2269   {
2270     return width;
2271   }
2272
2273   @Override
2274   public int getIconHeight()
2275   {
2276     return height;
2277   }
2278
2279   @Override
2280   public void paintIcon(Component c, Graphics g, int x, int y)
2281   {
2282
2283     if (gcol.isColourByLabel())
2284     {
2285       g.setColor(backg);
2286       g.fillRect(0, 0, width, height);
2287       // need an icon here.
2288       g.setColor(gcol.getMaxColour());
2289
2290       g.setFont(new Font("Verdana", Font.PLAIN, 9));
2291
2292       // g.setFont(g.getFont().deriveFont(
2293       // AffineTransform.getScaleInstance(
2294       // width/g.getFontMetrics().stringWidth("Label"),
2295       // height/g.getFontMetrics().getHeight())));
2296
2297       g.drawString(MessageManager.getString("label.label"), 0, 0);
2298
2299     }
2300     else
2301     {
2302       Color minCol = gcol.getMinColour();
2303       g.setColor(minCol);
2304       g.fillRect(0, 0, s1, height);
2305       if (midspace)
2306       {
2307         g.setColor(Color.white);
2308         g.fillRect(s1, 0, e1 - s1, height);
2309       }
2310       g.setColor(gcol.getMaxColour());
2311       g.fillRect(0, e1, width - e1, height);
2312     }
2313   }
2314 }
2315
2316 class ColorEditor extends AbstractCellEditor
2317         implements TableCellEditor, ActionListener
2318 {
2319   FeatureSettings me;
2320
2321   FeatureColourI currentColor;
2322
2323   FeatureColourChooser chooser;
2324
2325   String type;
2326
2327   JButton button;
2328
2329   JColorChooser colorChooser;
2330
2331   JDialog dialog;
2332
2333   protected static final String EDIT = "edit";
2334
2335   int selectedRow = 0;
2336
2337   public ColorEditor(FeatureSettings me)
2338   {
2339     this.me = me;
2340     // Set up the editor (from the table's point of view),
2341     // which is a button.
2342     // This button brings up the color chooser dialog,
2343     // which is the editor from the user's point of view.
2344     button = new JButton();
2345     button.setActionCommand(EDIT);
2346     button.addActionListener(this);
2347     button.setBorderPainted(false);
2348     // Set up the dialog that the button brings up.
2349     colorChooser = new JColorChooser();
2350     dialog = JColorChooser.createDialog(button, "Select new Colour", true, // modal
2351             colorChooser, this, // OK button handler
2352             null); // no CANCEL button handler
2353   }
2354
2355   /**
2356    * Handles events from the editor button and from the dialog's OK button.
2357    */
2358   @Override
2359   public void actionPerformed(ActionEvent e)
2360   {
2361
2362     if (EDIT.equals(e.getActionCommand()))
2363     {
2364       // The user has clicked the cell, so
2365       // bring up the dialog.
2366       if (currentColor.isSimpleColour())
2367       {
2368         // bring up simple color chooser
2369         button.setBackground(currentColor.getColour());
2370         colorChooser.setColor(currentColor.getColour());
2371         dialog.setVisible(true);
2372       }
2373       else
2374       {
2375         // bring up graduated chooser.
2376         chooser = new FeatureColourChooser(me.fr, type);
2377         chooser.setRequestFocusEnabled(true);
2378         chooser.requestFocus();
2379         chooser.addActionListener(this);
2380       }
2381       // Make the renderer reappear.
2382       fireEditingStopped();
2383
2384     }
2385     else
2386     { // User pressed dialog's "OK" button.
2387       if (currentColor.isSimpleColour())
2388       {
2389         currentColor = new FeatureColour(colorChooser.getColor());
2390       }
2391       else
2392       {
2393         currentColor = chooser.getLastColour();
2394       }
2395       me.table.setValueAt(getCellEditorValue(), selectedRow, 1);
2396       fireEditingStopped();
2397       me.table.validate();
2398     }
2399   }
2400
2401   // Implement the one CellEditor method that AbstractCellEditor doesn't.
2402   @Override
2403   public Object getCellEditorValue()
2404   {
2405     return currentColor;
2406   }
2407
2408   // Implement the one method defined by TableCellEditor.
2409   @Override
2410   public Component getTableCellEditorComponent(JTable table, Object value,
2411           boolean isSelected, int row, int column)
2412   {
2413     currentColor = (FeatureColourI) value;
2414     this.selectedRow = row;
2415     type = me.table.getValueAt(row, 0).toString();
2416     button.setOpaque(true);
2417     button.setBackground(me.getBackground());
2418     if (!currentColor.isSimpleColour())
2419     {
2420       JLabel btn = new JLabel();
2421       btn.setSize(button.getSize());
2422       FeatureSettings.renderGraduatedColor(btn, currentColor);
2423       button.setBackground(btn.getBackground());
2424       button.setIcon(btn.getIcon());
2425       button.setText(btn.getText());
2426     }
2427     else
2428     {
2429       button.setText("");
2430       button.setIcon(null);
2431       button.setBackground(currentColor.getColour());
2432     }
2433     return button;
2434   }
2435 }