1   /*
2    *  Copyright (c) 1998-2005, The University of Sheffield.
3    *
4    *  This file is part of GATE (see http://gate.ac.uk/), and is free
5    *  software, licenced under the GNU Library General Public License,
6    *  Version 2, June 1991 (in the distribution as file licence.html,
7    *  and also available at http://gate.ac.uk/gate/licence.html).
8    *
9    *  AnnotationEditor.java
10   *
11   *  Valentin Tablan, Apr 5, 2004
12   *
13   *  $Id: AnnotationEditor.java,v 1.23 2006/02/22 14:04:42 valyt Exp $
14   */
15  
16  package gate.gui.docview;
17  
18  import java.awt.*;
19  import java.awt.event.*;
20  import java.util.*;
21  
22  import javax.swing.*;
23  import javax.swing.Timer;
24  import javax.swing.border.LineBorder;
25  import javax.swing.text.BadLocationException;
26  
27  import gate.*;
28  import gate.creole.AnnotationSchema;
29  import gate.creole.ResourceInstantiationException;
30  import gate.event.CreoleEvent;
31  import gate.event.CreoleListener;
32  import gate.gui.FeaturesSchemaEditor;
33  import gate.gui.MainFrame;
34  import gate.util.*;
35  import gate.util.GateException;
36  import gate.util.GateRuntimeException;
37  
38  
39  /**
40   * @author Valentin Tablan
41   *
42   */
43  public class AnnotationEditor{
44    /**
45     * 
46     */
47    public AnnotationEditor(TextualDocumentView textView,
48                            AnnotationSetsView setsView){
49      this.textView = textView;
50      textPane = (JEditorPane)((JScrollPane)textView.getGUI())
51            .getViewport().getView();
52      this.setsView = setsView;
53      initGUI();
54    }
55    
56    protected void initData(){
57      schemasByType = new HashMap();
58      try{
59        java.util.List schemas = Gate.getCreoleRegister().
60          getAllInstances("gate.creole.AnnotationSchema");
61        for(Iterator schIter = schemas.iterator(); 
62            schIter.hasNext();){
63          AnnotationSchema aSchema = (AnnotationSchema)schIter.next();
64          schemasByType.put(aSchema.getAnnotationName(), aSchema);
65        }
66      }catch(GateException ge){
67        throw new GateRuntimeException(ge);
68      }
69      
70      CreoleListener creoleListener = new CreoleListener(){
71        public void resourceLoaded(CreoleEvent e){
72          Resource newResource =  e.getResource();
73          if(newResource instanceof AnnotationSchema){
74            AnnotationSchema aSchema = (AnnotationSchema)newResource;
75            schemasByType.put(aSchema.getAnnotationName(), aSchema);
76          }
77        }
78        
79        public void resourceUnloaded(CreoleEvent e){
80          Resource newResource =  e.getResource();
81          if(newResource instanceof AnnotationSchema){
82            AnnotationSchema aSchema = (AnnotationSchema)newResource;
83            if(schemasByType.containsValue(aSchema)){
84              schemasByType.remove(aSchema.getAnnotationName());
85            }
86          }
87        }
88        
89        public void datastoreOpened(CreoleEvent e){
90          
91        }
92        public void datastoreCreated(CreoleEvent e){
93          
94        }
95        public void datastoreClosed(CreoleEvent e){
96          
97        }
98        public void resourceRenamed(Resource resource,
99                                String oldName,
100                               String newName){
101       }  
102     };
103     Gate.getCreoleRegister().addCreoleListener(creoleListener); 
104   }
105   
106   protected void initBottomWindow(Window parent){
107     bottomWindow = new JWindow(parent);
108     JPanel pane = new JPanel();
109     pane.setBorder(BorderFactory.createLineBorder(Color.BLACK, 1));
110     pane.setLayout(new GridBagLayout());
111     pane.setBackground(UIManager.getLookAndFeelDefaults().
112             getColor("ToolTip.background"));
113     bottomWindow.setContentPane(pane);
114 
115     Insets insets0 = new Insets(0, 0, 0, 0);
116     GridBagConstraints constraints = new GridBagConstraints();
117     constraints.fill = GridBagConstraints.NONE;
118     constraints.anchor = GridBagConstraints.CENTER;
119     constraints.gridwidth = 1;
120     constraints.gridy = 0;
121     constraints.gridx = GridBagConstraints.RELATIVE;
122     constraints.weightx = 0;
123     constraints.weighty= 0;
124     constraints.insets = insets0;
125 
126     JButton btn = new JButton(solAction);
127     btn.setContentAreaFilled(false);
128     btn.setBorderPainted(false);
129     btn.setMargin(insets0);
130     pane.add(btn, constraints);
131     
132     btn = new JButton(sorAction);
133     btn.setContentAreaFilled(false);
134     btn.setBorderPainted(false);
135     btn.setMargin(insets0);
136     pane.add(btn, constraints);
137     
138     btn = new JButton(delAction);
139     btn.setContentAreaFilled(false);
140     btn.setBorderPainted(false);
141     btn.setMargin(insets0);
142     constraints.insets = new Insets(0, 20, 0, 20);
143     pane.add(btn, constraints);
144     constraints.insets = insets0;
145     
146     btn = new JButton(eolAction);
147     btn.setContentAreaFilled(false);
148     btn.setBorderPainted(false);
149     btn.setMargin(insets0);
150     pane.add(btn, constraints);
151     
152     btn = new JButton(eorAction);
153     btn.setContentAreaFilled(false);
154     btn.setBorderPainted(false);
155     btn.setMargin(insets0);
156     pane.add(btn, constraints);
157     
158     dismissAction = new DismissAction(); 
159     btn = new JButton(dismissAction);
160     constraints.insets = new Insets(0, 10, 0, 0);
161     constraints.anchor = GridBagConstraints.NORTHEAST;
162     constraints.weightx = 1;
163     btn.setBorder(null);
164     pane.add(btn, constraints);
165     constraints.anchor = GridBagConstraints.CENTER;
166     constraints.insets = insets0;
167 
168     
169     typeCombo = new JComboBox();
170     typeCombo.setEditable(true);
171     typeCombo.setBackground(UIManager.getLookAndFeelDefaults().
172             getColor("ToolTip.background"));
173     constraints.fill = GridBagConstraints.HORIZONTAL;
174     constraints.gridy = 1;
175     constraints.gridwidth = 6;
176     constraints.weightx = 1;
177     constraints.insets = new Insets(3, 2, 2, 2);
178     pane.add(typeCombo, constraints);
179     
180     featuresEditor = new FeaturesSchemaEditor();
181     featuresEditor.setBackground(UIManager.getLookAndFeelDefaults().
182             getColor("ToolTip.background"));
183     try{
184       featuresEditor.init();
185     }catch(ResourceInstantiationException rie){
186       throw new GateRuntimeException(rie);
187     }
188     scroller = new JScrollPane(featuresEditor.getTable());
189     
190     constraints.gridy = 2;
191     constraints.weighty = 1;
192     constraints.fill = GridBagConstraints.BOTH;
193     pane.add(scroller, constraints);
194   }
195   
196 
197   protected void initListeners(){
198     MouseListener windowMouseListener = new MouseAdapter(){
199       public void mouseEntered(MouseEvent evt){
200         hideTimer.stop();
201       }
202     };
203 
204     bottomWindow.getRootPane().addMouseListener(windowMouseListener);
205 //    featuresEditor.addMouseListener(windowMouseListener);
206     
207     ((JComponent)bottomWindow.getContentPane()).
208         getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
209         put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "dismiss");
210     ((JComponent)bottomWindow.getContentPane()).
211         getActionMap().put("dismiss", dismissAction);
212     
213     typeCombo.addActionListener(new ActionListener(){
214       public void actionPerformed(ActionEvent evt){
215         String newType = typeCombo.getSelectedItem().toString();
216         if(ann != null && ann.getType().equals(newType)) return;
217         //annotation editing
218         Integer oldId = ann.getId();
219         Annotation oldAnn = ann;
220         set.remove(ann);
221         try{
222           set.add(oldId, oldAnn.getStartNode().getOffset(), 
223                   oldAnn.getEndNode().getOffset(), 
224                   newType, oldAnn.getFeatures());
225           setAnnotation(set.get(oldId), set);
226           
227           setsView.setTypeSelected(set.getName(), newType, true);
228           setsView.setLastAnnotationType(newType);
229         }catch(InvalidOffsetException ioe){
230           throw new GateRuntimeException(ioe);
231         }
232       }
233     });
234   }
235   
236   protected void initGUI(){
237     solAction = new StartOffsetLeftAction();
238     sorAction = new StartOffsetRightAction();
239     eolAction = new EndOffsetLeftAction();
240     eorAction = new EndOffsetRightAction();
241     delAction = new DeleteAnnotationAction();
242     
243     initData();
244     initBottomWindow(SwingUtilities.getWindowAncestor(textView.getGUI()));
245     initListeners();
246     
247     hideTimer = new Timer(HIDE_DELAY, new ActionListener(){
248       public void actionPerformed(ActionEvent evt){
249         hide();
250       }
251     });
252     hideTimer.setRepeats(false);
253     
254   }
255   
256   public void setAnnotation(Annotation ann, AnnotationSet set){
257    this.ann = ann;
258    this.set = set;
259    //repopulate the types combo
260    String annType = ann.getType();
261    Set types = new HashSet(schemasByType.keySet());
262    types.add(annType);
263    types.addAll(set.getAllTypes());
264    java.util.List typeList = new ArrayList(types);
265    Collections.sort(typeList);
266    typeCombo.setModel(new DefaultComboBoxModel(typeList.toArray()));
267    typeCombo.setSelectedItem(annType);
268    
269    featuresEditor.setSchema((AnnotationSchema)schemasByType.get(annType));
270    featuresEditor.setTargetFeatures(ann.getFeatures());
271    bottomWindow.doLayout();
272   }
273   
274   public boolean isShowing(){
275     return bottomWindow.isShowing();
276   }
277   
278   /**
279    * Shows the UI(s) involved in annotation editing.
280    *
281    */
282   public void show(boolean autohide){
283     placeWindows();
284     bottomWindow.setVisible(true);
285     if(autohide) hideTimer.restart();
286   }
287   
288   protected void placeWindows(){
289     //calculate position
290     try{
291       Rectangle startRect = textPane.modelToView(ann.getStartNode().
292         getOffset().intValue());
293       Rectangle endRect = textPane.modelToView(ann.getEndNode().
294             getOffset().intValue());
295       Point topLeft = textPane.getLocationOnScreen();
296       int x = topLeft.x + startRect.x;
297       int y = topLeft.y + endRect.y + endRect.height;
298 
299       //make sure the window doesn't start lower 
300       //than the end of the visible rectangle
301       Rectangle visRect = textPane.getVisibleRect();
302       int maxY = topLeft.y + visRect.y + visRect.height;      
303       
304       //make sure window doesn't get off-screen
305       bottomWindow.pack();
306       Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
307       boolean revalidate = false;
308       if(bottomWindow.getSize().width > screenSize.width){
309         bottomWindow.setSize(screenSize.width, bottomWindow.getSize().height);
310         revalidate = true;
311       }
312       if(bottomWindow.getSize().height > screenSize.height){
313         bottomWindow.setSize(bottomWindow.getSize().width, screenSize.height);
314         revalidate = true;
315       }
316       
317       if(revalidate) bottomWindow.validate();
318       //calculate max X
319       int maxX = screenSize.width - bottomWindow.getSize().width;
320       //calculate max Y
321       if(maxY + bottomWindow.getSize().height > screenSize.height){
322         maxY = screenSize.height - bottomWindow.getSize().height;
323       }
324       
325       //correct position
326       if(y > maxY) y = maxY;
327       if(x > maxX) x = maxX;
328       bottomWindow.setLocation(x, y);
329       
330     }catch(BadLocationException ble){
331       //this should never occur
332       throw new GateRuntimeException(ble);
333     }
334   }
335   
336   /**
337    * Changes the span of an existing annotation by creating a new annotation 
338    * with the same ID, type and features but with the new start and end offsets.
339    * @param set the annotation set 
340    * @param oldAnnotation the annotation to be moved
341    * @param newStartOffset the new start offset
342    * @param newEndOffset the new end offset
343    */
344   protected void moveAnnotation(AnnotationSet set, Annotation oldAnnotation, 
345           Long newStartOffset, Long newEndOffset) throws InvalidOffsetException{
346     //Moving is done by deleting the old annotation and creating a new one.
347     //If this was the last one of one type it would mess up the gui which 
348     //"forgets" about this type and then it recreates it (with a different 
349     //colour and not visible
350     //We need to store the metadata about this type so we can recreate it if 
351     //needed
352     AnnotationSetsView.TypeHandler oldHandler = setsView.getTypeHandler(
353             set.getName(), oldAnnotation.getType());
354     
355     Integer oldID = oldAnnotation.getId();
356     set.remove(oldAnnotation);
357     set.add(oldID, newStartOffset, newEndOffset,
358             oldAnnotation.getType(), oldAnnotation.getFeatures());
359     setAnnotation(set.get(oldID), set);
360     AnnotationSetsView.TypeHandler newHandler = setsView.getTypeHandler(
361             set.getName(), oldAnnotation.getType());
362     
363     if(newHandler != oldHandler){
364       //hide all highlights (if any) so we can show them in the right colour
365       newHandler.setSelected(false);
366       newHandler.colour = oldHandler.colour;
367       newHandler.setSelected(oldHandler.isSelected());
368     }
369   }
370   
371   public void hide(){
372 //    topWindow.setVisible(false);
373     bottomWindow.setVisible(false);
374   }
375   
376   /**
377    * Base class for actions on annotations.
378    */
379   protected abstract class AnnotationAction extends AbstractAction{
380     public AnnotationAction(String name, Icon icon){
381       super("", icon);
382       putValue(SHORT_DESCRIPTION, name);
383       
384     }
385   }
386 
387   protected class StartOffsetLeftAction extends AnnotationAction{
388     public StartOffsetLeftAction(){
389       super("<html><b>Extend</b><br><small>SHIFT = 5 characters, CTRL-SHIFT = 10 characters</small></html>", 
390               MainFrame.getIcon("extend-left.gif"));
391     }
392     
393     public void actionPerformed(ActionEvent evt){
394       Annotation oldAnn = ann;
395       int increment = 1;
396       if((evt.getModifiers() & ActionEvent.SHIFT_MASK) > 0){
397         //CTRL pressed -> use tokens for advancing
398         increment = SHIFT_INCREMENT;
399         if((evt.getModifiers() & ActionEvent.CTRL_MASK) > 0){
400           increment = CTRL_SHIFT_INCREMENT;
401         }
402       }
403       long newValue = ann.getStartNode().getOffset().longValue() - increment;
404       if(newValue < 0) newValue = 0;
405       try{
406         moveAnnotation(set, ann, new Long(newValue), 
407                 ann.getEndNode().getOffset());
408       }catch(InvalidOffsetException ioe){
409         throw new GateRuntimeException(ioe);
410       }
411     }
412   }
413   
414   protected class StartOffsetRightAction extends AnnotationAction{
415     public StartOffsetRightAction(){
416       super("<html><b>Shrink</b><br><small>SHIFT = 5 characters, " +
417             "CTRL-SHIFT = 10 characters</small></html>", 
418             MainFrame.getIcon("extend-right.gif"));
419     }
420     
421     public void actionPerformed(ActionEvent evt){
422       long endOffset = ann.getEndNode().getOffset().longValue(); 
423       int increment = 1;
424       if((evt.getModifiers() & ActionEvent.SHIFT_MASK) > 0){
425         //CTRL pressed -> use tokens for advancing
426         increment = SHIFT_INCREMENT;
427         if((evt.getModifiers() & ActionEvent.CTRL_MASK) > 0){
428           increment = CTRL_SHIFT_INCREMENT;
429         }
430       }
431       
432       long newValue = ann.getStartNode().getOffset().longValue()  + increment;
433       if(newValue > endOffset) newValue = endOffset;
434       try{
435         moveAnnotation(set, ann, new Long(newValue), 
436                 ann.getEndNode().getOffset());
437       }catch(InvalidOffsetException ioe){
438         throw new GateRuntimeException(ioe);
439       }
440     }
441   }
442 
443   protected class EndOffsetLeftAction extends AnnotationAction{
444     public EndOffsetLeftAction(){
445       super("<html><b>Shrink</b><br><small>SHIFT = 5 characters, " +
446             "CTRL-SHIFT = 10 characters</small></html>",
447             MainFrame.getIcon("extend-left.gif"));
448     }
449     
450     public void actionPerformed(ActionEvent evt){
451       long startOffset = ann.getStartNode().getOffset().longValue(); 
452       int increment = 1;
453       if((evt.getModifiers() & ActionEvent.SHIFT_MASK) > 0){
454         //CTRL pressed -> use tokens for advancing
455         increment = SHIFT_INCREMENT;
456         if((evt.getModifiers() & ActionEvent.CTRL_MASK) > 0){
457           increment =CTRL_SHIFT_INCREMENT;
458         }
459       }
460       
461       long newValue = ann.getEndNode().getOffset().longValue()  - increment;
462       if(newValue < startOffset) newValue = startOffset;
463       try{
464         moveAnnotation(set, ann, ann.getStartNode().getOffset(), 
465                 new Long(newValue));
466       }catch(InvalidOffsetException ioe){
467         throw new GateRuntimeException(ioe);
468       }
469     }
470   }
471   
472   protected class EndOffsetRightAction extends AnnotationAction{
473     public EndOffsetRightAction(){
474       super("<html><b>Extend</b><br><small>SHIFT = 5 characters, " +
475             "CTRL-SHIFT = 10 characters</small></html>", 
476             MainFrame.getIcon("extend-right.gif"));
477     }
478     
479     public void actionPerformed(ActionEvent evt){
480       long maxOffset = textView.getDocument().
481           getContent().size().longValue() -1; 
482 //      Long newEndOffset = ann.getEndNode().getOffset();
483       int increment = 1;
484       if((evt.getModifiers() & ActionEvent.SHIFT_MASK) > 0){
485         //CTRL pressed -> use tokens for advancing
486         increment = SHIFT_INCREMENT;
487         if((evt.getModifiers() & ActionEvent.CTRL_MASK) > 0){
488           increment = CTRL_SHIFT_INCREMENT;
489         }
490       }
491       long newValue = ann.getEndNode().getOffset().longValue() + increment;
492       if(newValue > maxOffset) newValue = maxOffset;
493       try{
494         moveAnnotation(set, ann, ann.getStartNode().getOffset(),
495                 new Long(newValue));
496       }catch(InvalidOffsetException ioe){
497         throw new GateRuntimeException(ioe);
498       }
499     }
500   }
501   
502   
503   protected class DeleteAnnotationAction extends AnnotationAction{
504     public DeleteAnnotationAction(){
505       super("Delete", MainFrame.getIcon("delete.gif"));
506     }
507     
508     public void actionPerformed(ActionEvent evt){
509       set.remove(ann);
510       hide();
511     }
512   }
513   
514   protected class DismissAction extends AbstractAction{
515     public DismissAction(){
516       super("");
517       Icon icon = UIManager.getIcon("InternalFrame.closeIcon");
518       if(icon == null) icon = MainFrame.getIcon("exit.gif");
519       putValue(SMALL_ICON, icon);
520       putValue(SHORT_DESCRIPTION, "Dismiss");
521     }
522     
523     public void actionPerformed(ActionEvent evt){
524       hide();
525     }
526   }
527   
528   protected class ApplyAction extends AbstractAction{
529     public ApplyAction(){
530       super("Apply");
531 //      putValue(SHORT_DESCRIPTION, "Apply");
532     }
533     
534     public void actionPerformed(ActionEvent evt){
535       hide();
536     }
537   }
538   
539   protected JWindow bottomWindow;
540 
541   protected JComboBox typeCombo;
542   protected FeaturesSchemaEditor featuresEditor;
543   protected JScrollPane scroller;
544   
545   protected StartOffsetLeftAction solAction;
546   protected StartOffsetRightAction sorAction;
547   protected EndOffsetLeftAction eolAction;
548   protected EndOffsetRightAction eorAction;
549   protected DismissAction dismissAction;
550   
551   protected DeleteAnnotationAction delAction;
552   protected Timer hideTimer;
553   protected static final int HIDE_DELAY = 1500;
554   protected static final int SHIFT_INCREMENT = 5;
555   protected static final int CTRL_SHIFT_INCREMENT = 10;
556     
557   protected Object highlight;
558   
559   /**
560    * Stores the Annotation schema objects available in the system.
561    * The annotation types are used as keys for the map.
562    */
563   protected Map schemasByType;
564   
565   
566   protected TextualDocumentView textView;
567   protected AnnotationSetsView setsView;
568   protected JEditorPane textPane;
569   protected Annotation ann;
570   protected AnnotationSet set;
571 }
572