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