001 /*
002 * Copyright (c) 1995-2010, The University of Sheffield. See the file
003 * COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
004 *
005 * This file is part of GATE (see http://gate.ac.uk/), and is free
006 * software, licenced under the GNU Library General Public License,
007 * Version 2, June 1991 (in the distribution as file licence.html,
008 * and also available at http://gate.ac.uk/gate/licence.html).
009 *
010 * AbstractDocumentView.java
011 *
012 * Valentin Tablan, Sep 11, 2007
013 *
014 * $Id: SchemaFeaturesEditor.java 13677 2011-04-15 13:04:29Z valyt $
015 */
016 package gate.gui.annedit;
017
018 import gate.Factory;
019 import gate.FeatureMap;
020 import gate.creole.AnnotationSchema;
021 import gate.creole.FeatureSchema;
022 import gate.creole.ResourceInstantiationException;
023 import gate.swing.JChoice;
024
025 import java.awt.BorderLayout;
026 import java.awt.Color;
027 import java.awt.Component;
028 import java.awt.GridBagConstraints;
029 import java.awt.GridBagLayout;
030 import java.awt.HeadlessException;
031 import java.awt.Insets;
032 import java.awt.event.ActionEvent;
033 import java.awt.event.ActionListener;
034 import java.io.File;
035 import java.net.MalformedURLException;
036 import java.util.Arrays;
037 import java.util.HashSet;
038 import java.util.LinkedHashMap;
039 import java.util.Map;
040 import java.util.Set;
041
042 import javax.swing.AbstractAction;
043 import javax.swing.BorderFactory;
044 import javax.swing.Box;
045 import javax.swing.BoxLayout;
046 import javax.swing.JCheckBox;
047 import javax.swing.JComponent;
048 import javax.swing.JFrame;
049 import javax.swing.JLabel;
050 import javax.swing.JPanel;
051 import javax.swing.JTextField;
052 import javax.swing.JToolBar;
053 import javax.swing.border.Border;
054 import javax.swing.event.DocumentEvent;
055 import javax.swing.event.DocumentListener;
056
057 /**
058 * A GUI component for editing a feature map based on a feature schema object.
059 */
060 public class SchemaFeaturesEditor extends JPanel{
061
062 protected static enum FeatureType{
063 /**
064 * Type for features that have a range of possible values
065 */
066 nominal,
067
068 /**
069 * Type for boolean features.
070 */
071 bool,
072
073 /**
074 * Type for free text features.
075 */
076 text};
077
078 protected class FeatureEditor{
079
080 /**
081 * Constructor for nominal features
082 * @param featureName
083 * @param values
084 * @param defaultValue
085 */
086 public FeatureEditor(String featureName, String[] values,
087 String defaultValue){
088 this.featureName = featureName;
089 this.type = FeatureType.nominal;
090 this.values = values;
091 this.defaultValue = defaultValue;
092 buildGui();
093 }
094
095 /**
096 * Constructor for boolean features
097 * @param featureName
098 * @param defaultValue
099 */
100 public FeatureEditor(String featureName, Boolean defaultValue){
101 this.featureName = featureName;
102 this.type = FeatureType.bool;
103 if (defaultValue != null )
104 this.defaultValue = defaultValue.booleanValue() ? BOOLEAN_TRUE : BOOLEAN_FALSE;
105 else
106 this.defaultValue = null;
107 this.values = new String[]{BOOLEAN_FALSE, BOOLEAN_TRUE};
108 buildGui();
109 }
110
111 /**
112 * Constructor for plain text features
113 * @param featureName
114 * @param defaultValue
115 */
116 public FeatureEditor(String featureName, String defaultValue){
117 this.featureName = featureName;
118 this.type = FeatureType.text;
119 this.defaultValue = defaultValue;
120 this.values = null;
121 buildGui();
122 }
123
124 /**
125 * Builds the GUI according to the internally stored values.
126 */
127 protected void buildGui(){
128 //prepare the action listener
129 sharedActionListener = new ActionListener(){
130 public void actionPerformed(ActionEvent e) {
131 Object newValue = null;
132 if(e.getSource() == checkbox){
133 newValue = new Boolean(checkbox.isSelected());
134 }else if(e.getSource() == textField){
135 newValue = textField.getText();
136 }else if(e.getSource() == jchoice){
137 newValue = jchoice.getSelectedItem();
138 if(newValue != null && type == FeatureType.bool){
139 //convert eh new value to Boolean
140 newValue = new Boolean(BOOLEAN_TRUE == newValue);
141 }
142 }else if(e.getSource() == SchemaFeaturesEditor.this){
143 //synthetic event
144 newValue = getValue();
145 }
146
147 if(featureMap != null && e.getSource() != SchemaFeaturesEditor.this){
148 if(newValue != null){
149 if(newValue != featureMap.get(featureName)){
150 featureMap.put(featureName, newValue);
151 }
152 }else{
153 featureMap.remove(featureName);
154 }
155 }
156
157
158 //if the change makes this feature map non schema-compliant,
159 //highlight this feature editor
160 if(required && newValue == null){
161 if(getGui().getBorder() != highlightBorder){
162 getGui().setBorder(highlightBorder);
163 }
164 }else{
165 if(getGui().getBorder() != defaultBorder){
166 getGui().setBorder(defaultBorder);
167 }
168 }
169 }
170 };
171
172 //build the empty shell
173 gui = new JPanel();
174 gui.setAlignmentX(Component.LEFT_ALIGNMENT);
175 gui.setLayout(new BoxLayout(gui, BoxLayout.Y_AXIS));
176 switch(type) {
177 case nominal:
178 //use JChoice
179 jchoice = new JChoice(values);
180 jchoice.setDefaultButtonMargin(new Insets(0, 2, 0, 2));
181 jchoice.setMaximumFastChoices(20);
182 jchoice.setMaximumWidth(300);
183 jchoice.setSelectedItem(value);
184 jchoice.addActionListener(sharedActionListener);
185 gui.add(jchoice);
186 break;
187 case bool:
188 //new implementation -> use JChoice instead of JCheckBox in order
189 //to allow "unset" value (i.e. null)
190 jchoice = new JChoice(values);
191 jchoice.setDefaultButtonMargin(new Insets(0, 2, 0, 2));
192 jchoice.setMaximumFastChoices(20);
193 jchoice.setMaximumWidth(300);
194 if (BOOLEAN_TRUE.equals(value))
195 jchoice.setSelectedItem(BOOLEAN_TRUE);
196 else if (BOOLEAN_FALSE.equals(value))
197 jchoice.setSelectedItem(BOOLEAN_FALSE);
198 else
199 jchoice.setSelectedItem(null);
200 jchoice.addActionListener(sharedActionListener);
201 gui.add(jchoice);
202 break;
203
204 // case bool:
205 // gui.setLayout(new BoxLayout(gui, BoxLayout.LINE_AXIS));
206 // checkbox = new JCheckBox();
207 // checkbox.addActionListener(sharedActionListener);
208 // if(defaultValue != null){
209 // checkbox.setSelected(Boolean.parseBoolean(defaultValue));
210 // }
211 // gui.add(checkbox);
212 // break;
213 case text:
214 gui.setLayout(new BoxLayout(gui, BoxLayout.LINE_AXIS));
215 textField = new JNullableTextField();
216 textField.setColumns(20);
217 if(value != null){
218 textField.setText(value);
219 }else if(defaultValue != null){
220 textField.setText(defaultValue);
221 }
222 textField.addDocumentListener(new DocumentListener(){
223 public void changedUpdate(DocumentEvent e) {
224 sharedActionListener.actionPerformed(
225 new ActionEvent(textField, ActionEvent.ACTION_PERFORMED,
226 null));
227 }
228 public void insertUpdate(DocumentEvent e) {
229 sharedActionListener.actionPerformed(
230 new ActionEvent(textField, ActionEvent.ACTION_PERFORMED,
231 null));
232 }
233 public void removeUpdate(DocumentEvent e) {
234 sharedActionListener.actionPerformed(
235 new ActionEvent(textField, ActionEvent.ACTION_PERFORMED,
236 null));
237 }
238 });
239 gui.add(textField);
240 break;
241 }
242
243 defaultBorder = BorderFactory.createEmptyBorder(2, 2, 2, 2);
244 highlightBorder = BorderFactory.createLineBorder(Color.RED, 2);
245 gui.setBorder(defaultBorder);
246 }
247
248 protected JNullableTextField textField;
249 protected JCheckBox checkbox;
250 protected JChoice jchoice;
251
252 protected Border defaultBorder;
253
254 protected Border highlightBorder;
255
256
257 /**
258 * The type of the feature.
259 */
260 protected FeatureType type;
261
262 /**
263 * The name of the feature
264 */
265 protected String featureName;
266
267 /**
268 *
269 * The GUI used for editing the feature.
270 */
271 protected JComponent gui;
272
273 /**
274 * Permitted values for nominal features.
275 */
276 protected String[] values;
277
278 /**
279 * Is this feature required
280 */
281 protected boolean required;
282
283 /**
284 * The action listener that acts upon UI actions on nay of the widgets.
285 */
286 protected ActionListener sharedActionListener;
287
288 /**
289 * Default value as string.
290 */
291 protected String defaultValue;
292
293 /**
294 * The value of the feature
295 */
296 protected String value;
297
298 /**
299 * @return the type
300 */
301 public FeatureType getType() {
302 return type;
303 }
304 /**
305 * @param type the type to set
306 */
307 public void setType(FeatureType type) {
308 this.type = type;
309 }
310 /**
311 * @return the values
312 */
313 public String[] getValues() {
314 return values;
315 }
316 /**
317 * @param values the values to set
318 */
319 public void setValues(String[] values) {
320 this.values = values;
321 }
322 /**
323 * @return the defaultValue
324 */
325 public String getDefaultValue() {
326 return defaultValue;
327 }
328
329 /**
330 * @param defaultValue the defaultValue to set
331 */
332 public void setDefaultValue(String defaultValue) {
333 this.defaultValue = defaultValue;
334 }
335
336 /**
337 * Sets the value for this feature
338 * @param value
339 */
340 /**
341 * @param value
342 */
343 public void setValue(String value) {
344 // cache the actually provided value: if the value is null, we need to
345 // know, as the text editor would return "" when asked rather than null
346 this.value = value;
347 switch(type){
348 case nominal:
349 jchoice.setSelectedItem(value);
350 break;
351 case bool:
352 if (BOOLEAN_TRUE.equals(value))
353 jchoice.setSelectedItem(BOOLEAN_TRUE);
354 else if (BOOLEAN_FALSE.equals(value))
355 jchoice.setSelectedItem(BOOLEAN_FALSE);
356 else
357 jchoice.setSelectedItem(null);
358 break;
359 // case bool:
360 // checkbox.setSelected(value != null && Boolean.parseBoolean(value));
361 // break;
362 case text:
363 textField.setText(value);
364 break;
365 }
366 //call the action listener to update the border
367 sharedActionListener.actionPerformed(
368 new ActionEvent(SchemaFeaturesEditor.this,
369 ActionEvent.ACTION_PERFORMED, ""));
370 }
371
372 public Object getValue(){
373 switch(type){
374 case nominal:
375 return jchoice.getSelectedItem();
376 case bool:
377 Object choiceValue = jchoice.getSelectedItem();
378 return choiceValue == null ? null :
379 new Boolean(choiceValue == BOOLEAN_TRUE);
380 // case bool:
381 // return new Boolean(checkbox.isSelected());
382 case text:
383 return textField.getText();
384 default:
385 return null;
386 }
387 }
388 /**
389 * @return the featureName
390 */
391 public String getFeatureName() {
392 return featureName;
393 }
394 /**
395 * @param featureName the featureName to set
396 */
397 public void setFeatureName(String featureName) {
398 this.featureName = featureName;
399 }
400
401 /**
402 * @return the gui
403 */
404 public JComponent getGui() {
405 if(gui == null) buildGui();
406 return gui;
407 }
408 /**
409 * The maximum number of values to be represented as a buttons flow (as
410 * opposed to a combo-box).
411 */
412 private static final int MAX_BUTTONS_FLOW = 10;
413
414 /**
415 * @return the required
416 */
417 public boolean isRequired() {
418 return required;
419 }
420
421 /**
422 * @param required the required to set
423 */
424 public void setRequired(boolean required) {
425 this.required = required;
426 }
427
428 }
429
430 public SchemaFeaturesEditor(AnnotationSchema schema){
431 this.schema = schema;
432 featureSchemas = new LinkedHashMap<String, FeatureSchema>();
433 if(schema != null && schema.getFeatureSchemaSet() != null){
434 for(FeatureSchema aFSchema : schema.getFeatureSchemaSet()){
435 featureSchemas.put(aFSchema.getFeatureName(), aFSchema);
436 }
437 }
438 initGui();
439 }
440
441 public static void main(String[] args){
442 try {
443 JFrame aFrame = new JFrame("New Annotation Editor");
444
445 AnnotationSchema aSchema = new AnnotationSchema();
446 aSchema.setXmlFileUrl(new File("/home/valyt/tmp/bug/schema.xml").toURI().toURL());
447 aSchema.init();
448
449 final SchemaFeaturesEditor fsEditor = new SchemaFeaturesEditor(aSchema);
450
451 aFrame.getContentPane().add(fsEditor, BorderLayout.CENTER);
452 aFrame.pack();
453 aFrame.setVisible(true);
454
455 JToolBar tBar = new JToolBar();
456 tBar.add(new AbstractAction("New Values!"){
457 /* (non-Javadoc)
458 * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
459 */
460 public void actionPerformed(ActionEvent e) {
461 FeatureMap fMap = Factory.newFeatureMap();
462
463 fMap.put("boolean-false", new Boolean(true));
464 fMap.put("boolean-true", new Boolean(false));
465 fMap.put("nominal-long", "val10");
466 fMap.put("nominal-short", "val6");
467 fMap.put("free-text", "New text!");
468 fsEditor.editFeatureMap(fMap);
469
470 }
471 });
472
473 tBar.add(new AbstractAction("Null Values!"){
474 /* (non-Javadoc)
475 * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
476 */
477 public void actionPerformed(ActionEvent e) {
478 fsEditor.editFeatureMap(null);
479
480 }
481 });
482 aFrame.getContentPane().add(tBar, BorderLayout.NORTH);
483
484
485 }
486 catch(HeadlessException e) {
487 e.printStackTrace();
488 }
489 catch(MalformedURLException e) {
490 e.printStackTrace();
491 }
492 catch(ResourceInstantiationException e) {
493 e.printStackTrace();
494 }
495
496
497 }
498
499 protected void initGui(){
500 setLayout(new GridBagLayout());
501 GridBagConstraints constraints = new GridBagConstraints();
502 constraints.anchor = GridBagConstraints.WEST;
503 constraints.fill = GridBagConstraints.BOTH;
504 constraints.insets = new Insets(2,2,2,2);
505 constraints.weightx = 0;
506 constraints.weighty = 0;
507 int gridy = 0;
508 constraints.gridx = GridBagConstraints.RELATIVE;
509
510
511 //build the feature editors
512 featureEditors = new LinkedHashMap<String, FeatureEditor>();
513 Set<FeatureSchema> fsSet = schema.getFeatureSchemaSet();
514 if(fsSet != null){
515 for(FeatureSchema aFeatureSchema : fsSet){
516 String aFeatureName = aFeatureSchema.getFeatureName();
517 String defaultValue = aFeatureSchema.getFeatureValue();
518 if(defaultValue != null && defaultValue.length() == 0)
519 defaultValue = null;
520 String[] valuesArray = null;
521 Set values = aFeatureSchema.getPermittedValues();
522 if(values != null && values.size() > 0){
523 valuesArray = new String[values.size()];
524 int i = 0;
525 for(Object aValue : values){
526 valuesArray[i++] = aValue.toString();
527 }
528 Arrays.sort(valuesArray);
529 }
530 //build the right editor for the current feature
531 FeatureEditor anEditor;
532 if(valuesArray != null && valuesArray.length > 0){
533 //we have a set of allowed values -> nominal feature
534 anEditor = new FeatureEditor(aFeatureName, valuesArray,
535 defaultValue);
536 }else{
537 //we don't have any permitted set of values specified
538 if(aFeatureSchema.getFeatureValueClass().equals(Boolean.class)){
539 //boolean value
540 Boolean tValue = null;
541 if (BOOLEAN_FALSE.equals(defaultValue))
542 tValue = false;
543 else if (BOOLEAN_TRUE.equals(defaultValue))
544 tValue = true;
545
546 anEditor = new FeatureEditor(aFeatureName, tValue);
547 }else{
548 //plain text value
549 anEditor = new FeatureEditor(aFeatureName, defaultValue);
550 }
551 }
552 anEditor.setRequired(aFeatureSchema.isRequired());
553 featureEditors.put(aFeatureName, anEditor);
554 }
555 }
556 //add the feature editors in the alphabetical order
557 for(String featureName : featureEditors.keySet()){
558 FeatureEditor featureEditor = featureEditors.get(featureName);
559 constraints.gridy = gridy++;
560 JLabel nameLabel = new JLabel(
561 "<html>" + featureName +
562 (featureEditor.isRequired() ? "<b><font color='red'>*</font></b>: " : ": ") +
563 "</html>");
564 add(nameLabel, constraints);
565 constraints.weightx = 1;
566 add(featureEditor.getGui(), constraints);
567 constraints.weightx = 0;
568 // //add a horizontal spacer
569 // constraints.weightx = 1;
570 // add(Box.createHorizontalGlue(), constraints);
571 // constraints.weightx = 0;
572 }
573 //add a vertical spacer
574 constraints.weighty = 1;
575 constraints.gridy = gridy++;
576 constraints.gridx = GridBagConstraints.LINE_START;
577 add(Box.createVerticalGlue(), constraints);
578 }
579
580 /**
581 * Method called to initiate editing of a new feature map.
582 * @param featureMap
583 */
584 public void editFeatureMap(FeatureMap featureMap){
585 this.featureMap = featureMap;
586 featureMapUpdated();
587 }
588
589 /* (non-Javadoc)
590 * @see gate.event.FeatureMapListener#featureMapUpdated()
591 */
592 public void featureMapUpdated() {
593 //the underlying F-map was changed
594 // 1) validate that known features are schema-compliant
595 if(featureMap != null){
596 for(Object aFeatureName : new HashSet<Object>(featureMap.keySet())){
597 //first check if the feature is allowed
598 if(featureSchemas.keySet().contains(aFeatureName)){
599 FeatureSchema fSchema = featureSchemas.get(aFeatureName);
600 Object aFeatureValue = featureMap.get(aFeatureName);
601 //check if the value is permitted
602 Class<?> featureValueClass = fSchema.getFeatureValueClass();
603 if(featureValueClass.equals(Boolean.class) ||
604 featureValueClass.equals(Integer.class) ||
605 featureValueClass.equals(Short.class) ||
606 featureValueClass.equals(Byte.class) ||
607 featureValueClass.equals(Float.class) ||
608 featureValueClass.equals(Double.class)){
609 //just check the right type
610 if(!featureValueClass.isAssignableFrom(aFeatureValue.getClass())){
611 //invalid value type
612 featureMap.remove(aFeatureName);
613 }
614 }else if(featureValueClass.equals(String.class)){
615 if(fSchema.getPermittedValues() != null &&
616 !fSchema.getPermittedValues().contains(aFeatureValue)){
617 //invalid value
618 featureMap.remove(aFeatureName);
619 }
620 }
621 }else{
622 //feature not permitted -> ignore
623 // featureMap.remove(aFeatureName);
624 }
625 }
626 }
627 // 2) then update all the displays
628 for(String featureName : featureEditors.keySet()){
629 // FeatureSchema fSchema = featureSchemas.get(featureName);
630 FeatureEditor aFeatureEditor = featureEditors.get(featureName);
631 Object featureValue = featureMap == null ?
632 null : featureMap.get(featureName);
633 if(featureValue == null){
634 //we don't have a value from the featureMap
635 //use the default
636 featureValue = aFeatureEditor.getDefaultValue();
637 //if we still don't have a value, use the last used value
638 // if(featureValue == null ||
639 // ( featureValue instanceof String &&
640 // ((String)featureValue).length() == 0
641 // ) ){
642 // featureValue = aFeatureEditor.getValue();
643 // }
644 if(featureValue != null && featureMap != null){
645 //we managed to find a relevant value -> save it in the feature map
646 featureMap.put(featureName, featureValue);
647 }
648 }else{
649
650 //Some values need converting to String
651 FeatureSchema fSchema = featureSchemas.get(featureName);
652 Class<?> featureValueClass = fSchema.getFeatureValueClass();
653 if(featureValueClass.equals(Boolean.class)){
654 featureValue = ((Boolean)featureValue).booleanValue() ?
655 BOOLEAN_TRUE : BOOLEAN_FALSE;
656 }else if(featureValueClass.equals(String.class)){
657 //already a String - nothing to do
658 }else{
659 //some other type
660 featureValue = featureValue.toString();
661 }
662 }
663 aFeatureEditor.setValue((String)featureValue);
664 }
665 }
666
667
668 /**
669 * Label for the <tt>true</tt> boolean value.
670 */
671 private static final String BOOLEAN_TRUE = "True";
672
673 /**
674 * Label for the <tt>false</tt> boolean value.
675 */
676 private static final String BOOLEAN_FALSE = "False";
677
678
679 /**
680 * The feature schema for this editor
681 */
682 protected AnnotationSchema schema;
683
684 /**
685 * Stored the individual feature schemas, indexed by name.
686 */
687 protected Map<String, FeatureSchema> featureSchemas;
688
689 /**
690 * The feature map currently being edited.
691 */
692 protected FeatureMap featureMap;
693
694
695 /**
696 * A Map storing the editor for each feature.
697 */
698 protected Map<String, FeatureEditor> featureEditors;
699 }
|