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    *  Valentin Tablan, 22 March 2004
10   *
11   *  $Id: TextualDocumentView.java,v 1.23 2006/02/01 18:23:21 valyt Exp $
12   */
13  package gate.gui.docview;
14  
15  import java.awt.*;
16  import java.awt.event.*;
17  import java.io.IOException;
18  import java.io.Reader;
19  import java.util.*;
20  import java.util.List;
21  
22  import javax.swing.*;
23  import javax.swing.Timer;
24  import javax.swing.text.*;
25  
26  
27  import gate.Annotation;
28  import gate.AnnotationSet;
29  import gate.Document;
30  import gate.corpora.DocumentContentImpl;
31  import gate.event.DocumentEvent;
32  import gate.event.DocumentListener;
33  import gate.util.*;
34  
35  
36  /**
37   * This class provides a central view for a textual document.
38   */
39  
40  public class TextualDocumentView extends AbstractDocumentView {
41  
42    public TextualDocumentView(){
43      blinkingTagsForAnnotations = new HashMap();
44      gateDocListener = new GateDocumentListener();
45    }
46    
47    public Object addHighlight(Annotation ann, AnnotationSet set, Color colour){
48      Highlighter highlighter = textView.getHighlighter();
49      try{
50        Object tag = highlighter.addHighlight(
51                ann.getStartNode().getOffset().intValue(),
52                ann.getEndNode().getOffset().intValue(),
53                new DefaultHighlighter.DefaultHighlightPainter(colour));
54        annotationListView.addAnnotation(tag, ann, set);
55        return tag;
56      }catch(BadLocationException ble){
57        //the offsets should always be OK as they come from an annotation
58        throw new GateRuntimeException(ble.toString());
59      }
60    }
61  
62    
63    public void scrollAnnotationToVisible(Annotation ann){
64      //if at least part of the blinking section is visible then we
65      //need to do no scrolling
66      //this is required for long annotations that span more than a 
67      //screen
68      Rectangle visibleView = scroller.getViewport().getViewRect();
69      int viewStart = textView.viewToModel(visibleView.getLocation());
70      Point endPoint = new Point(visibleView.getLocation());
71      endPoint.translate(visibleView.width, visibleView.height);
72      int viewEnd = textView.viewToModel(endPoint);
73      int annStart = ann.getStartNode().getOffset().intValue();
74      int annEnd = ann.getEndNode().getOffset().intValue();
75      if(annEnd < viewStart || viewEnd < annStart){
76        try{
77          textView.scrollRectToVisible(textView.modelToView(annStart));
78        }catch(BadLocationException ble){
79          //this should never happen
80          throw new GateRuntimeException(ble);
81        }
82      }
83    }
84    
85    public void removeHighlight(Object tag){
86      Highlighter highlighter = textView.getHighlighter();
87      highlighter.removeHighlight(tag);
88      annotationListView.removeAnnotation(tag);
89    }
90  
91    /**
92     * Gives access to the highliter's change highlight operation. Can be used to 
93     * change the offset of an existing highlight.
94     * @param tag the tag for the highlight
95     * @param newStart new start offset.
96     * @param newEnd new end offset.
97     * @throws BadLocationException
98     */
99    public void moveHighlight(Object tag, int newStart, int newEnd) 
100     throws BadLocationException{
101     textView.getHighlighter().changeHighlight(tag, newStart, newEnd);
102   }
103   
104   /**
105    * Ads several highlights in one go. 
106    * This method should <b>not</b> be called from within the UI thread.
107    * @param annotations the collection of annotations for which highlights 
108    * are to be added.
109    * @param set the annotation set all the annotations belong to.
110    * @param colour the colour for the highlights.
111    * @return the list of tags for the added highlights. The order of the 
112    * elements corresponds to the order defined by the iterator of the 
113    * collection of annotations provided. 
114    */
115   public List addHighlights(Collection annotations, 
116           AnnotationSet set, Color colour){
117     //hide the text pane to speed up rendering.
118     SwingUtilities.invokeLater(new Runnable(){
119       public void run(){
120         textView.setVisible(false);
121         scroller.getViewport().setView(new JLabel("Updating"));
122       }
123     });
124     //wait for the textual view to be hidden
125     while(textView.isVisible()) 
126       try{
127         Thread.sleep(30);
128       }catch(InterruptedException ie){
129         //ignore
130       }
131     
132     Highlighter highlighter = textView.getHighlighter();
133     
134     Iterator annIter = annotations.iterator();
135     List tagsList = new ArrayList(annotations.size());
136     while(annIter.hasNext()){
137       Annotation ann = (Annotation)annIter.next();
138       try{
139         Object tag = highlighter.addHighlight(
140                 ann.getStartNode().getOffset().intValue(),
141                 ann.getEndNode().getOffset().intValue(),
142                 new DefaultHighlighter.DefaultHighlightPainter(colour));
143         tagsList.add(tag);
144       }catch(BadLocationException ble){
145         //the offsets should always be OK as they come from an annotation
146         throw new GateRuntimeException(ble.toString());
147       }
148     }
149     annotationListView.addAnnotations(tagsList, annotations, set);
150     SwingUtilities.invokeLater(new Runnable(){
151       public void run(){
152         scroller.getViewport().setView(textView);
153         textView.setVisible(true);
154       }
155     });
156     return tagsList;
157   }
158   
159   /**
160    * Removes several highlights in one go. 
161    * This method should <b>not</b> be called from within the UI thread.
162    * @param tags the tags for the highlights to be removed
163    */
164   public void removeHighlights(Collection tags){
165     //hide the text pane to speed up rendering.
166     SwingUtilities.invokeLater(new Runnable(){
167       public void run(){
168         textView.setVisible(false);
169         scroller.getViewport().setView(new JLabel("Updating"));
170       }
171     });
172     //wait for the textual view to be hidden.
173     while(textView.isVisible()) 
174       try{
175         Thread.sleep(30);
176       }catch(InterruptedException ie){
177         //ignore
178       }
179     
180     Highlighter highlighter = textView.getHighlighter();
181     
182     Iterator tagIter = tags.iterator();
183     while(tagIter.hasNext()){
184       highlighter.removeHighlight(tagIter.next());
185     }
186     annotationListView.removeAnnotations(tags);
187     SwingUtilities.invokeLater(new Runnable(){
188       public void run(){
189         scroller.getViewport().setView(textView);
190         textView.setVisible(true);
191       }
192     });
193   }
194 
195   
196   public void addBlinkingHighlight(Annotation ann){
197     synchronized(blinkingTagsForAnnotations){
198       blinkingTagsForAnnotations.put(ann.getId(), new AnnotationTag(ann, null));
199     }
200   }
201   
202   public void removeBlinkingHighlight(Annotation ann){
203     synchronized(blinkingTagsForAnnotations){
204       AnnotationTag annTag = (AnnotationTag)blinkingTagsForAnnotations.
205           remove(ann.getId()); 
206       if(annTag != null && annTag.getTag() != null)
207           textView.getHighlighter().removeHighlight(annTag.getTag());
208     }
209   }
210   
211   
212   public void removeAllBlinkingHighlights(){
213     synchronized(blinkingTagsForAnnotations){
214       Iterator annIdIter = new ArrayList(blinkingTagsForAnnotations.keySet()).
215         iterator();
216       while(annIdIter.hasNext()){
217         AnnotationTag annTag = (AnnotationTag)
218             blinkingTagsForAnnotations.remove(annIdIter.next());
219         Annotation ann = annTag.getAnnotation();
220         Object tag = annTag.getTag();
221         if(tag != null){
222           Highlighter highlighter = textView.getHighlighter();
223           highlighter.removeHighlight(tag);
224         }
225       }
226     }
227   }
228   
229   
230   public int getType() {
231     return CENTRAL;
232   }
233 
234   /**
235    * Stores the target (which should always be a {@link Document}) into the 
236    * {@link #document} field.
237    */
238   public void setTarget(Object target) {
239     if(document != null){
240       //remove the old listener
241       document.removeDocumentListener(gateDocListener);
242     }
243     super.setTarget(target);
244     //register the new listener
245     this.document.addDocumentListener(gateDocListener);
246   }
247 
248   
249   /* (non-Javadoc)
250    * @see gate.gui.docview.AbstractDocumentView#initGUI()
251    */
252   protected void initGUI() {
253     textView = new JEditorPane();
254     textView.setContentType("text/plain");
255     textView.setEditorKit(new RawEditorKit());
256     textView.setAutoscrolls(false);
257     scroller = new JScrollPane(textView);
258 
259     textView.setText(document.getContent().toString());
260     textView.getDocument().addDocumentListener(new SwingDocumentListener());
261     scroller.getViewport().setViewPosition(new Point(0, 0));
262     
263     //get a pointer to the annotation list view used to display
264     //the highlighted annotations 
265     Iterator horizViewsIter = owner.getHorizontalViews().iterator();
266     while(annotationListView == null && horizViewsIter.hasNext()){
267       DocumentView aView = (DocumentView)horizViewsIter.next();
268       if(aView instanceof AnnotationListView) 
269         annotationListView = (AnnotationListView)aView;
270     }
271     blinker = new Timer(BLINK_DELAY, new BlinkAction());
272     blinker.setRepeats(true);
273     blinker.start();
274     initListeners();
275   }
276   
277   public Component getGUI(){
278     return scroller;
279   }
280   
281   protected void initListeners(){
282     textView.addComponentListener(new ComponentAdapter(){
283       public void componentResized(ComponentEvent e){
284         try{
285           scroller.getViewport().setViewPosition(
286                   textView.modelToView(0).getLocation());
287           scroller.paintImmediately(textView.getBounds());
288         }catch(BadLocationException ble){
289           //ignore
290         }
291       }
292     });
293   }
294   protected void unregisterHooks(){}
295   protected void registerHooks(){}
296   
297   
298   /**
299    * Blinks the blinking highlights if any.
300    */
301   protected class BlinkAction extends AbstractAction{
302     public void actionPerformed(ActionEvent evt){
303       //this needs to either add or remove the highlights
304       synchronized(blinkingTagsForAnnotations){
305         //get out as quickly as possible if nothing to do
306         if(blinkingTagsForAnnotations.isEmpty()) return;
307         Iterator annIdIter = new ArrayList(blinkingTagsForAnnotations.keySet()).
308           iterator();
309         Highlighter highlighter = textView.getHighlighter();
310         if(highlightsShown){
311           //hide current highlights
312           while(annIdIter.hasNext()){
313             AnnotationTag annTag = (AnnotationTag)
314               blinkingTagsForAnnotations.get(annIdIter.next());
315             Annotation ann = annTag.getAnnotation();
316             Object tag = annTag.getTag();
317             if(tag != null) highlighter.removeHighlight(tag);
318             annTag.setTag(null);
319           }
320           highlightsShown = false;
321         }else{
322           //show highlights
323           while(annIdIter.hasNext()){
324             AnnotationTag annTag = (AnnotationTag)
325               blinkingTagsForAnnotations.get(annIdIter.next());
326             Annotation ann = annTag.getAnnotation();
327             try{
328               Object tag = highlighter.addHighlight(
329                       ann.getStartNode().getOffset().intValue(),
330                       ann.getEndNode().getOffset().intValue(),
331                       new DefaultHighlighter.DefaultHighlightPainter(
332                               textView.getSelectionColor()));
333               annTag.setTag(tag);
334 //              scrollAnnotationToVisible(ann);
335             }catch(BadLocationException ble){
336               //this should never happen
337               throw new GateRuntimeException(ble);
338             }
339           }
340           highlightsShown = true;
341         }
342       }
343     }
344     protected boolean highlightsShown = false;
345   }
346     
347   class GateDocumentListener implements DocumentListener{
348 
349     public void annotationSetAdded(DocumentEvent e) {
350     }
351 
352     public void annotationSetRemoved(DocumentEvent e) {
353     }
354 
355     public void contentEdited(DocumentEvent e) {
356       if(active){
357         //reload the content.
358         textView.setText(document.getContent().toString());
359       }
360     }
361     
362     public void setActive(boolean active){
363       this.active = active;
364     }
365     private boolean active = true;
366   }
367   
368   class SwingDocumentListener implements javax.swing.event.DocumentListener{
369     public void insertUpdate(final javax.swing.event.DocumentEvent e) {
370       //propagate the edit to the document
371       try{
372         //deactivate our own listener so we don't get cycles
373         gateDocListener.setActive(false);
374         document.edit(new Long(e.getOffset()), new Long(e.getOffset()),
375                       new DocumentContentImpl(
376                         e.getDocument().getText(e.getOffset(), e.getLength())));
377       }catch(BadLocationException ble){
378         ble.printStackTrace(Err.getPrintWriter());
379       }catch(InvalidOffsetException ioe){
380         ioe.printStackTrace(Err.getPrintWriter());
381       }finally{
382         //reactivate our listener
383         gateDocListener.setActive(true);
384       }
385       //update the offsets in the list
386       Component listView = annotationListView.getGUI();
387       if(listView != null) listView.repaint();
388     }
389 
390     public void removeUpdate(final javax.swing.event.DocumentEvent e) {
391       //propagate the edit to the document
392       try{
393         //deactivate our own listener so we don't get cycles
394         gateDocListener.setActive(false);        
395         document.edit(new Long(e.getOffset()),
396                       new Long(e.getOffset() + e.getLength()),
397                       new DocumentContentImpl(""));
398       }catch(InvalidOffsetException ioe){
399         ioe.printStackTrace(Err.getPrintWriter());
400       }finally{
401         //reactivate our listener
402         gateDocListener.setActive(true);
403       }
404       //update the offsets in the list
405       Component listView = annotationListView.getGUI();
406       if(listView != null) listView.repaint();
407     }
408 
409     public void changedUpdate(javax.swing.event.DocumentEvent e) {
410       //some attributes changed: we don't care about that
411     }
412   }//class SwingDocumentListener implements javax.swing.event.DocumentListener
413 
414   /**
415    * A structure that holds an annotation and its tag.
416    */
417   class AnnotationTag{
418     
419     /**
420      * @param tag The tag to set.
421      */
422     public void setTag(Object tag){
423       this.tag = tag;
424     }
425     public AnnotationTag(Annotation annotation, Object tag){
426       this.annotation = annotation;
427       this.tag = tag;
428     }
429     
430     /**
431      * @return Returns the annotation.
432      */
433     public Annotation getAnnotation(){
434       return annotation;
435     }
436     /**
437      * @return Returns the tag.
438      */
439     public Object getTag(){
440       return tag;
441     }
442     Annotation annotation;
443     Object tag;
444   }
445   
446   protected JScrollPane scroller;
447   protected AnnotationListView annotationListView;
448   
449   protected GateDocumentListener gateDocListener;
450 
451   /**
452    * The annotations used for blinking highlights and their tags. A map from 
453    * {@link Annotation} ID to tag(i.e. {@link Object}).
454    */
455   protected Map blinkingTagsForAnnotations;
456   
457   protected Timer blinker;
458   
459   protected JEditorPane textView;
460   
461   /**
462    * The delay used by the blinker.
463    */
464   protected final static int BLINK_DELAY = 400;
465 }
466