1   /*
2    *  Copyright (c) 1998-2004, 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.21 2004/11/23 15:20:37 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   }
272   
273   public boolean isShowing(){
274     return bottomWindow.isShowing();
275   }
276   
277   /**
278    * Shows the UI(s) involved in annotation editing.
279    *
280    */
281   public void show(boolean autohide){
282     placeWindows();
283     bottomWindow.setVisible(true);
284     if(autohide) hideTimer.restart();
285   }
286   
287   protected void placeWindows(){
288     //calculate position
289     try{
290       Rectangle startRect = textPane.modelToView(ann.getStartNode().
291         getOffset().intValue());
292       Rectangle endRect = textPane.modelToView(ann.getEndNode().
293             getOffset().intValue());
294       Point topLeft = textPane.getLocationOnScreen();
295       int x = topLeft.x + startRect.x;
296       int y = topLeft.y + endRect.y + endRect.height;
297 
298       //make sure the window doesn't start lower 
299       //than the end of the visible rectangle
300       Rectangle visRect = textPane.getVisibleRect();
301       int maxY = topLeft.y + visRect.y + visRect.height;      
302       
303       //make sure window doesn't get off-screen
304       bottomWindow.pack();
305       Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
306       boolean revalidate = false;
307       if(bottomWindow.getSize().width > screenSize.width){
308         bottomWindow.setSize(screenSize.width, bottomWindow.getSize().height);
309         revalidate = true;
310       }
311       if(bottomWindow.getSize().height > screenSize.height){
312         bottomWindow.setSize(bottomWindow.getSize().width, screenSize.height);
313         revalidate = true;
314       }
315       
316       if(revalidate) bottomWindow.validate();
317       //calculate max X
318       int maxX = screenSize.width - bottomWindow.getSize().width;
319       //calculate max Y
320       if(maxY + bottomWindow.getSize().height > screenSize.height){
321         maxY = screenSize.height - bottomWindow.getSize().height;
322       }
323       
324       //correct position
325       if(y > maxY) y = maxY;
326       if(x > maxX) x = maxX;
327       bottomWindow.setLocation(x, y);
328       
329     }catch(BadLocationException ble){
330       //this should never occur
331       throw new GateRuntimeException(ble);
332     }
333   }
334   
335   /**
336    * Changes the span of an existing annotation by creating a new annotation 
337    * with the same ID, type and features but with the new start and end offsets.
338    * @param set the annotation set 
339    * @param oldAnnotation the annotation to be moved
340    * @param newStartOffset the new start offset
341    * @param newEndOffset the new end offset
342    */
343   protected void moveAnnotation(AnnotationSet set, Annotation oldAnnotation, 
344           Long newStartOffset, Long newEndOffset) throws InvalidOffsetException{
345     //Moving is done by deleting the old annotation and creating a new one.
346     //If this was the last one of one type it would mess up the gui which 
347     //"forgets" about this type and then it recreates it (with a different 
348     //colour and not visible
349     //We need to store the metadata about this type so we can recreate it if 
350     //needed
351     AnnotationSetsView.TypeHandler oldHandler = setsView.getTypeHandler(
352             set.getName(), oldAnnotation.getType());
353     
354     Integer oldID = oldAnnotation.getId();
355     set.remove(oldAnnotation);
356     set.add(oldID, newStartOffset, newEndOffset,
357             oldAnnotation.getType(), oldAnnotation.getFeatures());
358     setAnnotation(set.get(oldID), set);
359     AnnotationSetsView.TypeHandler newHandler = setsView.getTypeHandler(
360             set.getName(), oldAnnotation.getType());
361     
362     if(newHandler != oldHandler){
363       //hide all highlights (if any) so we can show them in the right colour
364       newHandler.setSelected(false);
365       newHandler.colour = oldHandler.colour;
366       newHandler.setSelected(oldHandler.isSelected());
367     }
368   }
369   
370   public void hide(){
371 //    topWindow.setVisible(false);
372     bottomWindow.setVisible(false);
373   }
374   
375   /**
376    * Base class for actions on annotations.
377    */
378   protected abstract class AnnotationAction extends AbstractAction{
379     public AnnotationAction(String name, Icon icon){
380       super("", icon);
381       putValue(SHORT_DESCRIPTION, name);
382       
383     }
384   }
385 
386   protected class StartOffsetLeftAction extends AnnotationAction{
387     public StartOffsetLeftAction(){
388       super("<html><b>Extend</b><br><small>SHIFT = 5 characters, CTRL-SHIFT = 10 characters</small></html>", 
389               MainFrame.getIcon("extend-left.gif"));
390     }
391     
392     public void actionPerformed(ActionEvent evt){
393       Annotation oldAnn = ann;
394       int increment = 1;
395       if((evt.getModifiers() & ActionEvent.SHIFT_MASK) > 0){
396         //CTRL pressed -> use tokens for advancing
397         increment = SHIFT_INCREMENT;
398         if((evt.getModifiers() & ActionEvent.CTRL_MASK) > 0){
399           increment = CTRL_SHIFT_INCREMENT;
400         }
401       }
402       long newValue = ann.getStartNode().getOffset().longValue() - increment;
403       if(newValue < 0) newValue = 0;
404       try{
405         moveAnnotation(set, ann, new Long(newValue), 
406                 ann.getEndNode().getOffset());
407       }catch(InvalidOffsetException ioe){
408         throw new GateRuntimeException(ioe);
409       }
410     }
411   }
412   
413   protected class StartOffsetRightAction extends AnnotationAction{
414     public StartOffsetRightAction(){
415       super("<html><b>Shrink</b><br><small>SHIFT = 5 characters, " +
416             "CTRL-SHIFT = 10 characters</small></html>", 
417             MainFrame.getIcon("extend-right.gif"));
418     }
419     
420     public void actionPerformed(ActionEvent evt){
421       long endOffset = ann.getEndNode().getOffset().longValue(); 
422       int increment = 1;
423       if((evt.getModifiers() & ActionEvent.SHIFT_MASK) > 0){
424         //CTRL pressed -> use tokens for advancing
425         increment = SHIFT_INCREMENT;
426         if((evt.getModifiers() & ActionEvent.CTRL_MASK) > 0){
427           increment = CTRL_SHIFT_INCREMENT;
428         }
429       }
430       
431       long newValue = ann.getStartNode().getOffset().longValue()  + increment;
432       if(newValue > endOffset) newValue = endOffset;
433       try{
434         moveAnnotation(set, ann, new Long(newValue), 
435                 ann.getEndNode().getOffset());
436       }catch(InvalidOffsetException ioe){
437         throw new GateRuntimeException(ioe);
438       }
439     }
440   }
441 
442   protected class EndOffsetLeftAction extends AnnotationAction{
443     public EndOffsetLeftAction(){
444       super("<html><b>Shrink</b><br><small>SHIFT = 5 characters, " +
445             "CTRL-SHIFT = 10 characters</small></html>",
446             MainFrame.getIcon("extend-left.gif"));
447     }
448     
449     public void actionPerformed(ActionEvent evt){
450       long startOffset = ann.getStartNode().getOffset().longValue(); 
451       int increment = 1;
452       if((evt.getModifiers() & ActionEvent.SHIFT_MASK) > 0){
453         //CTRL pressed -> use tokens for advancing
454         increment = SHIFT_INCREMENT;
455         if((evt.getModifiers() & ActionEvent.CTRL_MASK) > 0){
456           increment =CTRL_SHIFT_INCREMENT;
457         }
458       }
459       
460       long newValue = ann.getEndNode().getOffset().longValue()  - increment;
461       if(newValue < startOffset) newValue = startOffset;
462       try{
463         moveAnnotation(set, ann, ann.getStartNode().getOffset(), 
464                 new Long(newValue));
465       }catch(InvalidOffsetException ioe){
466         throw new GateRuntimeException(ioe);
467       }
468     }
469   }
470   
471   protected class EndOffsetRightAction extends AnnotationAction{
472     public EndOffsetRightAction(){
473       super("<html><b>Extend</b><br><small>SHIFT = 5 characters, " +
474             "CTRL-SHIFT = 10 characters</small></html>", 
475             MainFrame.getIcon("extend-right.gif"));
476     }
477     
478     public void actionPerformed(ActionEvent evt){
479       long maxOffset = textView.getDocument().
480           getContent().size().longValue() -1; 
481 //      Long newEndOffset = ann.getEndNode().getOffset();
482       int increment = 1;
483       if((evt.getModifiers() & ActionEvent.SHIFT_MASK) > 0){
484         //CTRL pressed -> use tokens for advancing
485         increment = SHIFT_INCREMENT;
486         if((evt.getModifiers() & ActionEvent.CTRL_MASK) > 0){
487           increment = CTRL_SHIFT_INCREMENT;
488         }
489       }
490       long newValue = ann.getEndNode().getOffset().longValue() + increment;
491       if(newValue > maxOffset) newValue = maxOffset;
492       try{
493         moveAnnotation(set, ann, ann.getStartNode().getOffset(),
494                 new Long(newValue));
495       }catch(InvalidOffsetException ioe){
496         throw new GateRuntimeException(ioe);
497       }
498     }
499   }
500   
501   
502   protected class DeleteAnnotationAction extends AnnotationAction{
503     public DeleteAnnotationAction(){
504       super("Delete", MainFrame.getIcon("delete.gif"));
505     }
506     
507     public void actionPerformed(ActionEvent evt){
508       set.remove(ann);
509       hide();
510     }
511   }
512   
513   protected class DismissAction extends AbstractAction{
514     public DismissAction(){
515       super("");
516       Icon icon = UIManager.getIcon("InternalFrame.closeIcon");
517       if(icon == null) icon = MainFrame.getIcon("exit.gif");
518       putValue(SMALL_ICON, icon);
519       putValue(SHORT_DESCRIPTION, "Dismiss");
520     }
521     
522     public void actionPerformed(ActionEvent evt){
523       hide();
524     }
525   }
526   
527   protected class ApplyAction extends AbstractAction{
528     public ApplyAction(){
529       super("Apply");
530 //      putValue(SHORT_DESCRIPTION, "Apply");
531     }
532     
533     public void actionPerformed(ActionEvent evt){
534       hide();
535     }
536   }
537   
538   protected JWindow bottomWindow;
539 
540   protected JComboBox typeCombo;
541   protected FeaturesSchemaEditor featuresEditor;
542   protected JScrollPane scroller;
543   
544   protected StartOffsetLeftAction solAction;
545   protected StartOffsetRightAction sorAction;
546   protected EndOffsetLeftAction eolAction;
547   protected EndOffsetRightAction eorAction;
548   protected DismissAction dismissAction;
549   
550   protected DeleteAnnotationAction delAction;
551   protected Timer hideTimer;
552   protected static final int HIDE_DELAY = 1500;
553   protected static final int SHIFT_INCREMENT = 5;
554   protected static final int CTRL_SHIFT_INCREMENT = 10;
555     
556   protected Object highlight;
557   
558   /**
559    * Stores the Annotation schema objects available in the system.
560    * The annotation types are used as keys for the map.
561    */
562   protected Map schemasByType;
563   
564   
565   protected TextualDocumentView textView;
566   protected AnnotationSetsView setsView;
567   protected JEditorPane textPane;
568   protected Annotation ann;
569   protected AnnotationSet set;
570 }
571