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