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