FeaturesSchemaEditor.java
001 /*
002  * Copyright (c) 1998-2005, The University of Sheffield.
003  
004  * This file is part of GATE (see http://gate.ac.uk/), and is free software,
005  * licenced under the GNU Library General Public License, Version 2, June 1991
006  * (in the distribution as file licence.html, and also available at
007  * http://gate.ac.uk/gate/licence.html).
008  
009  * FeaturesSchemaEditor.java
010  
011  * Valentin Tablan, May 18, 2004
012  
013  * $Id: FeaturesSchemaEditor.java 18884 2015-08-25 17:23:21Z markagreenwood $
014  */
015 package gate.gui;
016 
017 import gate.Factory;
018 import gate.FeatureMap;
019 import gate.Resource;
020 import gate.creole.AbstractResource;
021 import gate.creole.AnnotationSchema;
022 import gate.creole.FeatureSchema;
023 import gate.creole.ResourceInstantiationException;
024 import gate.creole.metadata.CreoleResource;
025 import gate.creole.metadata.GuiType;
026 import gate.event.FeatureMapListener;
027 import gate.swing.XJTable;
028 import gate.util.FeatureBearer;
029 import gate.util.GateRuntimeException;
030 import gate.util.ObjectComparator;
031 import gate.util.Strings;
032 
033 import java.awt.AWTKeyStroke;
034 import java.awt.Color;
035 import java.awt.Component;
036 import java.awt.Dimension;
037 import java.awt.Insets;
038 import java.awt.KeyboardFocusManager;
039 import java.awt.Rectangle;
040 import java.awt.event.ActionEvent;
041 import java.awt.event.ActionListener;
042 import java.beans.BeanInfo;
043 import java.beans.Introspector;
044 import java.util.ArrayList;
045 import java.util.Collections;
046 import java.util.HashSet;
047 import java.util.Iterator;
048 import java.util.List;
049 import java.util.Set;
050 
051 import javax.swing.DefaultCellEditor;
052 import javax.swing.DefaultComboBoxModel;
053 import javax.swing.JButton;
054 import javax.swing.JComboBox;
055 import javax.swing.JLabel;
056 import javax.swing.JTable;
057 import javax.swing.KeyStroke;
058 import javax.swing.SwingUtilities;
059 import javax.swing.table.AbstractTableModel;
060 import javax.swing.table.TableCellRenderer;
061 
062 /**
063  */
064 @SuppressWarnings({"serial","rawtypes","unchecked"})
065 @CreoleResource(name = "Resource Features", guiType = GuiType.SMALL,
066     resourceDisplayed = "gate.util.FeatureBearer")
067 public class FeaturesSchemaEditor extends XJTable
068         implements ResizableVisualResource, FeatureMapListener{
069   public FeaturesSchemaEditor(){
070 //    setBackground(UIManager.getDefaults().getColor("Table.background"));
071     instance = this;
072   }
073 
074   public void setTargetFeatures(FeatureMap features){
075     if(targetFeatures != nulltargetFeatures.removeFeatureMapListener(this);
076     this.targetFeatures = features;
077     populate();
078     if(targetFeatures != nulltargetFeatures.addFeatureMapListener(this);
079   }
080   
081   
082   @Override
083   public void cleanup() {
084     if(targetFeatures != null){
085       targetFeatures.removeFeatureMapListener(this);
086       targetFeatures = null;
087     }
088     target = null;
089     schema = null;
090   }
091 
092   /* (non-Javadoc)
093    * @see gate.VisualResource#setTarget(java.lang.Object)
094    */
095   @Override
096   public void setTarget(Object target){
097     this.target = (FeatureBearer)target;
098     setTargetFeatures(this.target.getFeatures());
099   }
100   
101   public void setSchema(AnnotationSchema schema){
102     this.schema = schema;
103     featuresModel.fireTableRowsUpdated(0, featureList.size() 1);
104   }
105     
106   /* (non-Javadoc)
107    * @see gate.event.FeatureMapListener#featureMapUpdated()
108    * Called each time targetFeatures is changed.
109    */
110   @Override
111   public void featureMapUpdated(){
112     SwingUtilities.invokeLater(new Runnable(){
113       @Override
114       public void run(){
115         populate();    
116       }
117     });
118   }
119   
120   
121   /** Initialise this resource, and return it. */
122   @Override
123   public Resource init() throws ResourceInstantiationException {
124     featureList = new ArrayList<Feature>();
125     emptyFeature = new Feature(""null);
126     featureList.add(emptyFeature);
127     initGUI();
128     return this;
129   }//init()
130   
131   protected void initGUI(){
132     featuresModel = new FeaturesTableModel();
133     setModel(featuresModel);
134     setTableHeader(null);
135     setSortable(false);
136     setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
137     setShowGrid(false);
138     setBackground(getBackground());
139     setIntercellSpacing(new Dimension(2,2));
140     setTabSkipUneditableCell(true);
141     setEditCellAsSoonAsFocus(true);
142     featureEditorRenderer = new FeatureEditorRenderer();
143     getColumnModel().getColumn(ICON_COL).
144         setCellRenderer(featureEditorRenderer);
145     getColumnModel().getColumn(NAME_COL).
146         setCellRenderer(featureEditorRenderer);
147     getColumnModel().getColumn(NAME_COL).
148         setCellEditor(featureEditorRenderer);
149     getColumnModel().getColumn(VALUE_COL).
150         setCellRenderer(featureEditorRenderer);
151     getColumnModel().getColumn(VALUE_COL).
152         setCellEditor(featureEditorRenderer);
153     getColumnModel().getColumn(DELETE_COL).
154         setCellRenderer(featureEditorRenderer);
155     getColumnModel().getColumn(DELETE_COL).
156       setCellEditor(featureEditorRenderer);
157     
158 //    //the background colour seems to change somewhere when using the GTK+ 
159 //    //look and feel on Linux, so we copy the value now and set it 
160 //    Color tableBG = getBackground();
161 //    //make a copy of the value (as the reference gets changed somewhere)
162 //    tableBG = new Color(tableBG.getRGB());
163 //    setBackground(tableBG);
164 
165     // allow Tab key to select the next cell in the table
166     setSurrendersFocusOnKeystroke(true);
167     setFocusCycleRoot(true);
168 
169     // remove (shift) control tab as traversal keys
170     Set<AWTKeyStroke> keySet = new HashSet<AWTKeyStroke>(
171       getFocusTraversalKeys(
172       KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS));
173     keySet.remove(KeyStroke.getKeyStroke("control TAB"));
174     setFocusTraversalKeys(
175       KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, keySet);
176     keySet = new HashSet<AWTKeyStroke>(
177       getFocusTraversalKeys(
178       KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS));
179     keySet.remove(KeyStroke.getKeyStroke("shift control TAB"));
180     setFocusTraversalKeys(
181       KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, keySet);
182 
183     // add (shift) control tab to go the container of this component
184     keySet.clear();
185     keySet.add(KeyStroke.getKeyStroke("control TAB"));
186     setFocusTraversalKeys(
187       KeyboardFocusManager.UP_CYCLE_TRAVERSAL_KEYS, keySet);
188     keySet.clear();
189     keySet.add(KeyStroke.getKeyStroke("shift control TAB"));
190     setFocusTraversalKeys(
191       KeyboardFocusManager.DOWN_CYCLE_TRAVERSAL_KEYS, keySet);
192   }
193   
194   /**
195    * Called internally whenever the data represented changes.
196    * Get feature names from targetFeatures and schema then sort them
197    * and add them to featureList.
198    * Fire a table data changed event for the feature table whith featureList
199    * used as data model.
200    */
201   protected void populate(){
202     featureList.clear();
203     //get all the existing features
204     Set fNames = new HashSet();
205     
206     if(targetFeatures != null){
207       //add all the schema features
208       fNames.addAll(targetFeatures.keySet());
209       if(schema != null && schema.getFeatureSchemaSet() != null){
210         for(FeatureSchema featureSchema : schema.getFeatureSchemaSet()) {
211           //        if(featureSchema.isRequired())
212           fNames.add(featureSchema.getFeatureName());
213         }
214       }
215       List featureNames = new ArrayList(fNames);
216       Collections.sort(featureNames);
217       for(Object featureName : featureNames) {
218         String name = (StringfeatureName;
219         Object value = targetFeatures.get(name);
220         featureList.add(new Feature(name, value));
221       }
222     }
223     if (!featureList.contains(emptyFeature)) {
224       featureList.add(emptyFeature);
225     }
226     featuresModel.fireTableDataChanged();
227 //    mainTable.setSize(mainTable.getPreferredScrollableViewportSize());
228   }
229 
230   FeatureMap targetFeatures;
231   FeatureBearer target;
232   Feature emptyFeature;
233   AnnotationSchema schema;
234   FeaturesTableModel featuresModel;
235   List<Feature> featureList;
236   FeatureEditorRenderer featureEditorRenderer;
237   FeaturesSchemaEditor instance;
238   
239   private static final int COLUMNS = 4;
240   private static final int ICON_COL = 0;
241   private static final int NAME_COL = 1;
242   private static final int VALUE_COL = 2;
243   private static final int DELETE_COL = 3;
244   
245   private static final Color REQUIRED_WRONG = Color.RED;
246   private static final Color OPTIONAL_WRONG = Color.ORANGE;
247 
248   protected class Feature{
249     String name;
250     Object value;
251 
252     public Feature(String name, Object value){
253       this.name = name;
254       this.value = value;
255     }
256     boolean isSchemaFeature(){
257       return schema != null && schema.getFeatureSchema(name!= null;
258     }
259     boolean isCorrect(){
260       if(schema == nullreturn true;
261       FeatureSchema fSchema = schema.getFeatureSchema(name);
262       return fSchema == null || fSchema.getPermittedValues() == null||
263              fSchema.getPermittedValues().contains(value);
264     }
265     boolean isRequired(){
266       if(schema == nullreturn false;
267       FeatureSchema fSchema = schema.getFeatureSchema(name);
268       return fSchema != null && fSchema.isRequired();
269     }
270     Object getDefaultValue(){
271       if(schema == nullreturn null;
272       FeatureSchema fSchema = schema.getFeatureSchema(name);
273       return fSchema == null null : fSchema.getFeatureValue();
274     }
275   }
276   
277   
278   protected class FeaturesTableModel extends AbstractTableModel{
279     @Override
280     public int getRowCount(){
281       return featureList.size();
282     }
283     
284     @Override
285     public int getColumnCount(){
286       return COLUMNS;
287     }
288     
289     @Override
290     public Object getValueAt(int row, int column){
291       Feature feature = featureList.get(row);
292       switch(column){
293         case NAME_COL:
294           return feature.name;
295         case VALUE_COL:
296           return feature.value;
297         default:
298           return null;
299       }
300     }
301     
302     @Override
303     public boolean isCellEditable(int rowIndex, int columnIndex){
304       return columnIndex == VALUE_COL || columnIndex == NAME_COL || 
305              columnIndex == DELETE_COL;
306     }
307     
308     @Override
309     public void setValueAt(Object aValue, int rowIndex,  int columnIndex){
310       Feature feature = featureList.get(rowIndex);
311       if (feature == null) { return}
312       if(targetFeatures == null){
313         targetFeatures = Factory.newFeatureMap();
314         target.setFeatures(targetFeatures);
315         setTargetFeatures(targetFeatures);
316       }
317       switch(columnIndex){
318         case VALUE_COL:
319           if (feature.value != null
320            && feature.value.equals(aValue)) { return}
321           feature.value = aValue;
322           if(feature.name != null && feature.name.length() 0){
323             targetFeatures.removeFeatureMapListener(instance);
324             targetFeatures.put(feature.name, aValue);
325             targetFeatures.addFeatureMapListener(instance);
326             SwingUtilities.invokeLater(new Runnable() {
327               @Override
328               public void run() {
329                 // edit the last row that is empty
330                 FeaturesSchemaEditor.this.editCellAt(FeaturesSchemaEditor.this.getRowCount() 1, NAME_COL);
331               }
332             });
333           }
334           break;
335         case NAME_COL:
336           if (feature.name.equals(aValue)) {
337             return;
338           }
339           targetFeatures.remove(feature.name);
340           feature.name = (String)aValue;
341           targetFeatures.put(feature.name, feature.value);
342           if(feature == emptyFeatureemptyFeature = new Feature(""null);
343           populate();
344           int newRow;
345           for (newRow = 0; newRow < FeaturesSchemaEditor.this.getRowCount(); newRow++) {
346             if (FeaturesSchemaEditor.this.getValueAt(newRow, NAME_COL).equals(feature.name)) {
347               break// find the previously selected row in the new table
348             }
349           }
350           final int newRowF = newRow;
351           SwingUtilities.invokeLater(new Runnable() {
352             @Override
353             public void run() {
354               // edit the cell containing the value associated with this name
355               FeaturesSchemaEditor.this.editCellAt(newRowF, VALUE_COL);
356             }
357           });
358           break;
359         case DELETE_COL:
360           //nothing
361           break;
362         default:
363           throw new GateRuntimeException("Non editable cell!");
364       }
365       
366     }
367     
368     @Override
369     public String getColumnName(int column){
370       switch(column){
371         case NAME_COL:
372           return "Name";
373         case VALUE_COL:
374           return "Value";
375         case DELETE_COL:
376           return "";
377         default:
378           return null;
379       }
380     }
381   }
382 
383 
384   protected class FeatureEditorRenderer extends DefaultCellEditor
385                                         implements TableCellRenderer {
386     
387     @Override
388     public boolean stopCellEditing() {
389       // this is a fix for a bug in Java 8 where tabbing out of the
390       // combo box doesn't store the value like it does in java 7
391       editorCombo.setSelectedItem(editorCombo.getEditor().getItem());
392       return super.stopCellEditing();
393     }
394     
395     public FeatureEditorRenderer(){
396       super(new JComboBox());
397       defaultComparator = new ObjectComparator();
398       editorCombo = (JComboBox)editorComponent;
399       editorCombo.setModel(new DefaultComboBoxModel());
400       editorCombo.setEditable(true);
401       editorCombo.addActionListener(new ActionListener(){
402         @Override
403         public void actionPerformed(ActionEvent evt){
404           stopCellEditing();
405         }
406       });
407       defaultBackground = editorCombo.getEditor().getEditorComponent()
408           .getBackground();
409       
410       rendererCombo = new JComboBox(){
411         @Override
412         public Dimension getMaximumSize() {
413           return getPreferredSize();
414         }
415         @Override
416         public Dimension getMinimumSize() {
417           return getPreferredSize();
418         }
419       };
420       rendererCombo.setModel(new DefaultComboBoxModel());
421       rendererCombo.setEditable(true);
422       rendererCombo.setOpaque(false);
423       
424       requiredIconLabel = new JLabel(){
425         @Override
426         public void repaint(long tm, int x, int y, int width, int height){}
427         @Override
428         public void repaint(Rectangle r){}
429         @Override
430         public void validate(){}
431         @Override
432         public void revalidate(){}
433         @Override
434         protected void firePropertyChange(String propertyName,
435                                           Object oldValue,
436                                           Object newValue){}
437         @Override
438         public Dimension getMaximumSize() {
439           return getPreferredSize();
440         }
441         @Override
442         public Dimension getMinimumSize() {
443           return getPreferredSize();
444         }        
445       };
446       requiredIconLabel.setIcon(MainFrame.getIcon("r"));
447       requiredIconLabel.setOpaque(false);
448       requiredIconLabel.setToolTipText("Required feature");
449       
450       optionalIconLabel = new JLabel(){
451         @Override
452         public void repaint(long tm, int x, int y, int width, int height){}
453         @Override
454         public void repaint(Rectangle r){}
455         @Override
456         public void validate(){}
457         @Override
458         public void revalidate(){}
459         @Override
460         protected void firePropertyChange(String propertyName,
461                                           Object oldValue,
462                                           Object newValue){}
463         @Override
464         public Dimension getMaximumSize() {
465           return getPreferredSize();
466         }
467         @Override
468         public Dimension getMinimumSize() {
469           return getPreferredSize();
470         }
471       };
472       optionalIconLabel.setIcon(MainFrame.getIcon("o"));
473       optionalIconLabel.setOpaque(false);
474       optionalIconLabel.setToolTipText("Optional feature");
475 
476       nonSchemaIconLabel = new JLabel(MainFrame.getIcon("c")){
477         @Override
478         public void repaint(long tm, int x, int y, int width, int height){}
479         @Override
480         public void repaint(Rectangle r){}
481         @Override
482         public void validate(){}
483         @Override
484         public void revalidate(){}
485         @Override
486         protected void firePropertyChange(String propertyName,
487                                           Object oldValue,
488                                           Object newValue){}
489         @Override
490         public Dimension getMaximumSize() {
491           return getPreferredSize();
492         }
493         @Override
494         public Dimension getMinimumSize() {
495           return getPreferredSize();
496         }
497       };
498       nonSchemaIconLabel.setToolTipText("Custom feature");
499       nonSchemaIconLabel.setOpaque(false);
500       
501       deleteButton = new JButton(MainFrame.getIcon("delete"));
502       deleteButton.setMargin(new Insets(0,0,0,0));
503       deleteButton.setToolTipText("Delete");
504       deleteButton.addActionListener(new ActionListener(){
505         @Override
506         public void actionPerformed(ActionEvent evt){
507           int row = FeaturesSchemaEditor.this.getEditingRow();
508           if(row < 0return;
509           Feature feature = featureList.get(row);
510           if(feature == emptyFeature){
511             feature.value = null;
512           }else{
513             featureList.remove(row);
514             targetFeatures.remove(feature.name);
515             featuresModel.fireTableRowsDeleted(row, row);
516 //            mainTable.setSize(mainTable.getPreferredScrollableViewportSize());
517           }
518         }
519       });
520     }    
521     
522     @Override
523     public Component getTableCellRendererComponent(JTable table, Object value,
524          boolean isSelected, boolean hasFocus, int row, int column){
525       Feature feature = featureList.get(row);
526       switch(column){
527         case ICON_COL: 
528           return feature.isSchemaFeature() 
529                  (feature.isRequired() 
530                          requiredIconLabel : 
531                          optionalIconLabel:
532                          nonSchemaIconLabel;  
533         case NAME_COL:
534           prepareCombo(rendererCombo, row, column);
535           rendererCombo.getPreferredSize();
536           return rendererCombo;
537         case VALUE_COL:
538           prepareCombo(rendererCombo, row, column);
539           return rendererCombo;
540         case DELETE_COL: return deleteButton;  
541         defaultreturn null;
542       }
543     }
544   
545     @Override
546     public Component getTableCellEditorComponent(JTable table,  Object value, 
547             boolean isSelected, int row, int column){
548       switch(column){
549         case NAME_COL:
550           prepareCombo(editorCombo, row, column);
551           return editorCombo;
552         case VALUE_COL:
553           prepareCombo(editorCombo, row, column);
554           return editorCombo;
555         case DELETE_COL: return deleteButton;  
556         defaultreturn null;
557       }
558 
559     }
560   
561     protected void prepareCombo(JComboBox combo, int row, int column){
562       Feature feature = featureList.get(row);
563       DefaultComboBoxModel comboModel = (DefaultComboBoxModel)combo.getModel()
564       comboModel.removeAllElements();
565       switch(column){
566         case NAME_COL:
567           List<String> fNames = new ArrayList<String>();
568           if(schema != null && schema.getFeatureSchemaSet() != null){
569             Iterator<FeatureSchema> fSchemaIter = schema.getFeatureSchemaSet().iterator();
570             while(fSchemaIter.hasNext())
571               fNames.add(fSchemaIter.next().getFeatureName());
572           }
573           if(!fNames.contains(feature.name))fNames.add(feature.name);
574           Collections.sort(fNames);
575           for(Iterator<String> nameIter = fNames.iterator()
576               nameIter.hasNext()
577               comboModel.addElement(nameIter.next()));
578           combo.getEditor().getEditorComponent().setBackground(defaultBackground);          
579           combo.setSelectedItem(feature.name);
580           break;
581         case VALUE_COL:
582           List<Object> fValues = new ArrayList<Object>();
583           if(feature.isSchemaFeature()){
584             Set<Object> permValues = schema.getFeatureSchema(feature.name).
585               getPermittedValues();
586             if(permValues != nullfValues.addAll(permValues);
587           }
588           if(!fValues.contains(feature.value)) fValues.add(feature.value);
589           Collections.sort(fValues, defaultComparator);
590           for(Iterator<Object> valIter = fValues.iterator()
591               valIter.hasNext()
592               comboModel.addElement(valIter.next()));
593           combo.getEditor().getEditorComponent().setBackground(feature.isCorrect() ?
594               defaultBackground :
595               (feature.isRequired() ? REQUIRED_WRONG : OPTIONAL_WRONG));
596           combo.setSelectedItem(feature.value);
597           break;
598         default: ;
599       }
600       
601     }
602 
603     JLabel requiredIconLabel;
604     JLabel optionalIconLabel;
605     JLabel nonSchemaIconLabel;
606     JComboBox editorCombo;
607     JComboBox rendererCombo;
608     JButton deleteButton;
609     ObjectComparator defaultComparator;
610     Color defaultBackground;
611   }
612   
613   /* 
614    * Resource implementation 
615    */
616   /** Accessor for features. */
617   @Override
618   public FeatureMap getFeatures(){
619     return features;
620   }//getFeatures()
621 
622   /** Mutator for features*/
623   @Override
624   public void setFeatures(FeatureMap features){
625     this.features = features;
626   }// setFeatures()
627 
628 
629   /**
630    * Used by the main GUI to tell this VR what handle created it. The VRs can
631    * use this information e.g. to add items to the popup for the resource.
632    */
633   @Override
634   public void setHandle(Handle handle){
635     this.handle = handle;
636   }
637 
638   //Parameters utility methods
639   /**
640    * Gets the value of a parameter of this resource.
641    @param paramaterName the name of the parameter
642    @return the current value of the parameter
643    */
644   @Override
645   public Object getParameterValue(String paramaterName)
646                 throws ResourceInstantiationException{
647     return AbstractResource.getParameterValue(this, paramaterName);
648   }
649 
650   /**
651    * Sets the value for a specified parameter.
652    *
653    @param paramaterName the name for the parameteer
654    @param parameterValue the value the parameter will receive
655    */
656   @Override
657   public void setParameterValue(String paramaterName, Object parameterValue)
658               throws ResourceInstantiationException{
659     // get the beaninfo for the resource bean, excluding data about Object
660     BeanInfo resBeanInf = null;
661     try {
662       resBeanInf = Introspector.getBeanInfo(this.getClass(), Object.class);
663     catch(Exception e) {
664       throw new ResourceInstantiationException(
665         "Couldn't get bean info for resource " this.getClass().getName()
666         + Strings.getNl() "Introspector exception was: " + e
667       );
668     }
669     AbstractResource.setParameterValue(this, resBeanInf, paramaterName, parameterValue);
670   }
671 
672   /**
673    * Sets the values for more parameters in one step.
674    *
675    @param parameters a feature map that has paramete names as keys and
676    * parameter values as values.
677    */
678   @Override
679   public void setParameterValues(FeatureMap parameters)
680               throws ResourceInstantiationException{
681     AbstractResource.setParameterValues(this, parameters);
682   }
683 
684   // Properties for the resource
685   protected FeatureMap features;
686   
687   /**
688    * The handle for this visual resource
689    */
690   protected Handle handle;
691   
692   
693 }