TextualDocumentView.java
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  *  Valentin Tablan, 22 March 2004
011  *
012  *  $Id: TextualDocumentView.java 12725 2010-06-04 09:13:50Z valyt $
013  */
014 package gate.gui.docview;
015 
016 import java.awt.*;
017 import java.awt.event.*;
018 import java.util.*;
019 import java.util.List;
020 
021 import javax.swing.*;
022 import javax.swing.Timer;
023 import javax.swing.text.*;
024 
025 
026 import gate.Annotation;
027 import gate.AnnotationSet;
028 import gate.Document;
029 import gate.corpora.DocumentContentImpl;
030 import gate.event.DocumentEvent;
031 import gate.event.DocumentListener;
032 import gate.gui.annedit.AnnotationData;
033 import gate.util.*;
034 
035 
036 /**
037  * This class provides a central view for a textual document.
038  */
039 
040 public class TextualDocumentView extends AbstractDocumentView {
041 
042   public TextualDocumentView(){
043     blinkingTagsForAnnotations = new HashMap<AnnotationData, HighlightData>();
044     //use linked lists as they grow and shrink in constant time and direct access
045     //is not required.
046     highlightsToAdd = new LinkedList<HighlightData>();
047     highlightsToRemove = new LinkedList<HighlightData>();
048     blinkingHighlightsToRemove = new HashSet<AnnotationData>();
049     blinkingHighlightsToAdd = new LinkedList<AnnotationData>();
050     gateDocListener = new GateDocumentListener();
051   }
052 
053   @Override
054   public void cleanup() {
055     super.cleanup();
056     highlightsMinder.stop();
057   }
058 
059   public Object addHighlight(AnnotationData aData, Color colour){
060     HighlightData hData = new HighlightData(aData, colour);
061     synchronized(TextualDocumentView.this) {
062       highlightsToAdd.add(hData);
063     }
064     highlightsMinder.restart();
065     return hData;
066   }
067 
068   /**
069    * Adds several highlights in one go.
070    * This method should be called from within the UI thread.
071    @param annotations the collection of annotations for which highlights
072    * are to be added.
073    @param colour the colour for the highlights.
074    @return the list of tags for the added highlights. The order of the
075    * elements corresponds to the order defined by the iterator of the
076    * collection of annotations provided.
077    */
078   public List addHighlights(Collection<AnnotationData> annotations, Color colour){
079     List<Object> tags = new ArrayList<Object>();
080     for(AnnotationData aData : annotationstags.add(addHighlight(aData, colour));
081     return tags;
082   }
083 
084   public void removeHighlight(Object tag){
085     synchronized(TextualDocumentView.this) {
086       highlightsToRemove.add((HighlightData)tag);
087     }
088     highlightsMinder.restart();
089   }
090 
091   /**
092    * Same as {@link #addHighlights(java.util.Collection, java.awt.Color)} but
093    * without the intermediate creation of HighlightData objects.
094    @param list list of HighlightData
095    */
096   public void addHighlights(List<HighlightData> list) {
097     for (HighlightData highlightData : list) {
098       synchronized(TextualDocumentView.this) {
099         highlightsToAdd.add(highlightData);
100       }
101     }
102     highlightsMinder.restart();
103   }
104 
105   /**
106    * Removes several highlights in one go.
107    @param tags the tags for the highlights to be removed
108    */
109   public void removeHighlights(Collection tags){
110     //this might get an optimised implementation at some point,
111     //for the time being this seems to work fine.
112     for(Object tag : tagsremoveHighlight(tag);
113   }
114 
115 
116 
117   public void scrollAnnotationToVisible(Annotation ann){
118     //if at least part of the blinking section is visible then we
119     //need to do no scrolling
120     //this is required for long annotations that span more than a
121     //screen
122     Rectangle visibleView = scroller.getViewport().getViewRect();
123     int viewStart = textView.viewToModel(visibleView.getLocation());
124     Point endPoint = new Point(visibleView.getLocation());
125     endPoint.translate(visibleView.width, visibleView.height);
126     int viewEnd = textView.viewToModel(endPoint);
127     int annStart = ann.getStartNode().getOffset().intValue();
128     int annEnd = ann.getEndNode().getOffset().intValue();
129     if(annEnd < viewStart || viewEnd < annStart){
130       try{
131         textView.scrollRectToVisible(textView.modelToView(annStart));
132       }catch(BadLocationException ble){
133         //this should never happen
134         throw new GateRuntimeException(ble);
135       }
136     }
137   }
138 
139 
140 
141   /**
142    * Gives access to the highliter's change highlight operation. Can be used to
143    * change the offset of an existing highlight.
144    @param tag the tag for the highlight
145    @param newStart new start offset.
146    @param newEnd new end offset.
147    @throws BadLocationException
148    */
149   public void moveHighlight(Object tag, int newStart, int newEnd)
150     throws BadLocationException{
151     if(tag instanceof HighlightData){
152       textView.getHighlighter().changeHighlight(((HighlightData)tag).tag, newStart, newEnd);
153     }
154   }
155 
156 
157 
158   /**
159    * Removes all blinking highlights and shows the new ones, corresponding to
160    * the new set of selected annotations
161    */
162   @Override
163   public void setSelectedAnnotations(List<AnnotationData> selectedAnnots) {
164     synchronized(blinkingTagsForAnnotations){
165       //clear the pending queue, if any
166       blinkingHighlightsToAdd.clear();
167       //request the removal of existing highlights
168       blinkingHighlightsToRemove.addAll(blinkingTagsForAnnotations.keySet());
169       //add all the new annotations to the "to add" queue
170       for(AnnotationData aData : selectedAnnots){
171         blinkingHighlightsToAdd.add(aData);
172       }
173       //restart the timer
174       highlightsMinder.restart();
175     }
176   }
177 
178 //  public void addBlinkingHighlight(Annotation ann){
179 //    synchronized(TextualDocumentView.this){
180 //      blinkingHighlightsToAdd.add(ann);
181 //
182 ////      blinkingTagsForAnnotations.put(ann.getId(),
183 ////              new HighlightData(ann, null, null));
184 //      highlightsMinder.restart();
185 //    }
186 //  }
187 
188 //  public void removeBlinkingHighlight(Annotation ann){
189 //    synchronized(TextualDocumentView.this) {
190 //      blinkingHighlightsToRemove.add(ann.getId());
191 //      highlightsMinder.restart();
192 //    }
193 //  }
194 
195 
196 //  public void removeAllBlinkingHighlights(){
197 //    synchronized(TextualDocumentView.this){
198 //      //clear the pending queue, if any
199 //      blinkingHighlightsToAdd.clear();
200 //      //request the removal of existing highlights
201 //      blinkingHighlightsToRemove.addAll(blinkingTagsForAnnotations.keySet());
202 ////      Iterator annIdIter = new ArrayList(blinkingTagsForAnnotations.keySet()).
203 ////        iterator();
204 ////      while(annIdIter.hasNext()){
205 ////        HighlightData annTag = blinkingTagsForAnnotations.remove(annIdIter.next());
206 ////        Object tag = annTag.tag;
207 ////        if(tag != null){
208 ////          Highlighter highlighter = textView.getHighlighter();
209 ////          highlighter.removeHighlight(tag);
210 ////        }
211 ////      }
212 //      highlightsMinder.restart();
213 //    }
214 //  }
215 
216 
217   public int getType() {
218     return CENTRAL;
219   }
220 
221   /**
222    * Stores the target (which should always be a {@link Document}) into the
223    {@link #document} field.
224    */
225   public void setTarget(Object target) {
226     if(document != null){
227       //remove the old listener
228       document.removeDocumentListener(gateDocListener);
229     }
230     super.setTarget(target);
231     //register the new listener
232     this.document.addDocumentListener(gateDocListener);
233   }
234 
235   public void setEditable(boolean editable) {
236     textView.setEditable(editable);
237   }
238 
239   /* (non-Javadoc)
240    * @see gate.gui.docview.AbstractDocumentView#initGUI()
241    */
242   protected void initGUI() {
243 //    textView = new JEditorPane();
244 //    textView.setContentType("text/plain");
245 //    textView.setEditorKit(new RawEditorKit());
246 
247     textView = new JTextArea();
248     textView.setAutoscrolls(false);
249     textView.setLineWrap(true);
250     textView.setWrapStyleWord(true);
251     // the selection is hidden when the focus is lost for some system
252     // like Linux, so we make sure it stays
253     // it is needed when doing a selection in the search textfield
254     textView.setCaret(new PermanentSelectionCaret());
255     scroller = new JScrollPane(textView);
256 
257     textView.setText(document.getContent().toString());
258     textView.getDocument().addDocumentListener(new SwingDocumentListener());
259     // display and put the caret at the beginning of the file
260     SwingUtilities.invokeLater(new Runnable() {
261       public void run() {
262         try {
263           if (textView.modelToView(0!= null) {
264             textView.scrollRectToVisible(textView.modelToView(0));
265           }
266           textView.select(00);
267           textView.requestFocus();
268         catch(BadLocationException e) {
269           e.printStackTrace();
270         }
271       }
272     });
273 //    contentPane = new JPanel(new BorderLayout());
274 //    contentPane.add(scroller, BorderLayout.CENTER);
275 
276 //    //get a pointer to the annotation list view used to display
277 //    //the highlighted annotations
278 //    Iterator horizViewsIter = owner.getHorizontalViews().iterator();
279 //    while(annotationListView == null && horizViewsIter.hasNext()){
280 //      DocumentView aView = (DocumentView)horizViewsIter.next();
281 //      if(aView instanceof AnnotationListView)
282 //        annotationListView = (AnnotationListView)aView;
283 //    }
284     highlightsMinder = new Timer(BLINK_DELAY, new UpdateHighlightsAction());
285     highlightsMinder.setInitialDelay(HIGHLIGHT_DELAY);
286     highlightsMinder.setDelay(BLINK_DELAY);
287     highlightsMinder.setRepeats(true);
288     highlightsMinder.setCoalesce(true);
289     highlightsMinder.start();
290 
291 //    blinker = new Timer(this.getClass().getCanonicalName() + "_blink_timer",
292 //            true);
293 //    final BlinkAction blinkAction = new BlinkAction();
294 //    blinker.scheduleAtFixedRate(new TimerTask(){
295 //      public void run() {
296 //        blinkAction.actionPerformed(null);
297 //      }
298 //    }, 0, BLINK_DELAY);
299     initListeners();
300   }
301 
302   public Component getGUI(){
303 //    return contentPane;
304     return scroller;
305   }
306 
307   protected void initListeners(){
308 //    textView.addComponentListener(new ComponentAdapter(){
309 //      public void componentResized(ComponentEvent e){
310 //        try{
311 //          scroller.getViewport().setViewPosition(
312 //                  textView.modelToView(0).getLocation());
313 //          scroller.paintImmediately(textView.getBounds());
314 //        }catch(BadLocationException ble){
315 //          //ignore
316 //        }
317 //      }
318 //    });
319 
320     // stop control+H from deleting text and transfers the key to the parent
321     textView.addKeyListener(new KeyAdapter() {
322       public void keyPressed(KeyEvent e) {
323         if (e.getKeyCode() == KeyEvent.VK_H
324          && e.isControlDown()) {
325           getGUI().dispatchEvent(e);
326           e.consume();
327         }
328       }
329     });
330   }
331 
332   protected void unregisterHooks(){}
333   protected void registerHooks(){}
334 
335   /**
336    * Blinks the blinking highlights if any.
337    */
338   protected class UpdateHighlightsAction extends AbstractAction{
339     public void actionPerformed(ActionEvent evt){
340       synchronized(blinkingTagsForAnnotations){
341         updateBlinkingHighlights();
342         updateNormalHighlights();
343       }
344     }
345 
346 
347     protected void updateBlinkingHighlights(){
348       //this needs to either add or remove the highlights
349 
350       //first remove the queued highlights
351       Highlighter highlighter = textView.getHighlighter();
352       for(AnnotationData aData : blinkingHighlightsToRemove){
353         HighlightData annTag = blinkingTagsForAnnotations.remove(aData);
354         if(annTag != null){
355           Object tag = annTag.tag;
356           if(tag != null){
357             //highlight was visible and will be removed
358             highlighter.removeHighlight(tag);
359             annTag.tag = null;
360           }
361         }
362       }
363       blinkingHighlightsToRemove.clear();
364       //then add the queued highlights
365       for(AnnotationData aData : blinkingHighlightsToAdd){
366         blinkingTagsForAnnotations.put(aData,
367                 new HighlightData(aData, null));
368       }
369       blinkingHighlightsToAdd.clear();
370 
371       //finally switch the state of the current blinking highlights
372       //get out as quickly as possible if nothing to do
373       if(blinkingTagsForAnnotations.isEmpty()) return;
374       Iterator annIdIter = new ArrayList(blinkingTagsForAnnotations.keySet()).
375         iterator();
376 
377       if(highlightsShown){
378         //hide current highlights
379         while(annIdIter.hasNext()){
380           HighlightData annTag =
381               blinkingTagsForAnnotations.get(annIdIter.next());
382 //          Annotation ann = annTag.annotation;
383           if (annTag != null) {
384             Object tag = annTag.tag;
385             if(tag != nullhighlighter.removeHighlight(tag);
386             annTag.tag = null;
387           }
388         }
389         highlightsShown = false;
390       }else{
391         //show highlights
392         while(annIdIter.hasNext()){
393           HighlightData annTag =
394               blinkingTagsForAnnotations.get(annIdIter.next());
395           if (annTag != null) {
396             Annotation ann = annTag.annotation;
397             try{
398               Object tag = highlighter.addHighlight(
399                       ann.getStartNode().getOffset().intValue(),
400                       ann.getEndNode().getOffset().intValue(),
401                       new DefaultHighlighter.DefaultHighlightPainter(
402                               textView.getSelectionColor()));
403               annTag.tag = tag;
404 //              scrollAnnotationToVisible(ann);
405             }catch(BadLocationException ble){
406               //this should never happen
407               throw new GateRuntimeException(ble);
408             }
409           }
410         }
411         highlightsShown = true;
412       }
413     }
414 
415     protected void updateNormalHighlights(){
416       synchronized(TextualDocumentView.this) {
417         if((highlightsToRemove.size() + highlightsToAdd.size()) 0){
418 //          Point viewPosition = scroller.getViewport().getViewPosition();
419           Highlighter highlighter = textView.getHighlighter();
420 //          textView.setVisible(false);
421 //          scroller.getViewport().setView(new JLabel("Updating"));
422           //add all new highlights
423           while(highlightsToAdd.size() 0){
424             HighlightData hData = highlightsToAdd.remove(0);
425             try{
426               hData.tag = highlighter.addHighlight(
427                       hData.annotation.getStartNode().getOffset().intValue(),
428                       hData.annotation.getEndNode().getOffset().intValue(),
429                       new DefaultHighlighter.DefaultHighlightPainter(hData.colour));
430             }catch(BadLocationException ble){
431               //the offsets should always be OK as they come from an annotation
432               ble.printStackTrace();
433             }
434 //            annotationListView.addAnnotation(hData, hData.annotation,
435 //                    hData.set);
436           }
437 
438           //remove all the highlights that need removing
439           while(highlightsToRemove.size() 0){
440             HighlightData hData = highlightsToRemove.remove(0);
441             if(hData.tag != null){
442               highlighter.removeHighlight(hData.tag);
443             }
444 //            annotationListView.removeAnnotation(hData);
445           }
446 
447 
448           //restore the updated view
449 //          scroller.getViewport().setView(textView);
450 //          textView.setVisible(true);
451 //          scroller.getViewport().setViewPosition(viewPosition);
452         }
453       }
454     }
455     protected boolean highlightsShown = false;
456   }
457 
458   private class HighlightData{
459     Annotation annotation;
460     AnnotationSet set;
461     Color colour;
462     Object tag;
463 
464     public HighlightData(AnnotationData aData, Color colour) {
465       this.annotation = aData.getAnnotation();
466       this.set = aData.getAnnotationSet();
467       this.colour = colour;
468     }
469   }
470 
471   protected class GateDocumentListener implements DocumentListener{
472 
473     public void annotationSetAdded(DocumentEvent e) {
474     }
475 
476     public void annotationSetRemoved(DocumentEvent e) {
477     }
478 
479     public void contentEdited(DocumentEvent e) {
480       if(active){
481         //reload the content.
482         textView.setText(document.getContent().toString());
483       }
484     }
485 
486     public void setActive(boolean active){
487       this.active = active;
488     }
489     private boolean active = true;
490   }
491 
492   protected class SwingDocumentListener implements javax.swing.event.DocumentListener{
493     public void insertUpdate(final javax.swing.event.DocumentEvent e) {
494       //propagate the edit to the document
495       try{
496         //deactivate our own listener so we don't get cycles
497         gateDocListener.setActive(false);
498         document.edit(new Long(e.getOffset())new Long(e.getOffset()),
499                       new DocumentContentImpl(
500                         e.getDocument().getText(e.getOffset(), e.getLength())));
501       }catch(BadLocationException ble){
502         ble.printStackTrace(Err.getPrintWriter());
503       }catch(InvalidOffsetException ioe){
504         ioe.printStackTrace(Err.getPrintWriter());
505       }finally{
506         //reactivate our listener
507         gateDocListener.setActive(true);
508       }
509 //      //update the offsets in the list
510 //      Component listView = annotationListView.getGUI();
511 //      if(listView != null) listView.repaint();
512     }
513 
514     public void removeUpdate(final javax.swing.event.DocumentEvent e) {
515       //propagate the edit to the document
516       try{
517         //deactivate our own listener so we don't get cycles
518         gateDocListener.setActive(false);
519         document.edit(new Long(e.getOffset()),
520                       new Long(e.getOffset() + e.getLength()),
521                       new DocumentContentImpl(""));
522       }catch(InvalidOffsetException ioe){
523         ioe.printStackTrace(Err.getPrintWriter());
524       }finally{
525         //reactivate our listener
526         gateDocListener.setActive(true);
527       }
528 //      //update the offsets in the list
529 //      Component listView = annotationListView.getGUI();
530 //      if(listView != null) listView.repaint();
531     }
532 
533     public void changedUpdate(javax.swing.event.DocumentEvent e) {
534       //some attributes changed: we don't care about that
535     }
536   }//class SwingDocumentListener implements javax.swing.event.DocumentListener
537 
538   // When the textPane loses the focus it doesn't really lose
539   // the selection, it just stops painting it so we need to force
540   // the painting
541   public class PermanentSelectionCaret extends DefaultCaret {
542     private boolean isFocused;
543     public void setSelectionVisible(boolean hasFocus) {
544       if (hasFocus != isFocused) {
545         isFocused = hasFocus;
546         super.setSelectionVisible(false);
547         super.setSelectionVisible(true);
548       }
549     }
550     public void focusGained(FocusEvent e) {
551       super.focusGained(e);
552       // force displaying the caret even if the document is not editable
553       super.setVisible(true);
554     }
555   }
556 
557   /**
558    * The scroll pane holding the text
559    */
560   protected JScrollPane scroller;
561 //  protected AnnotationListView annotationListView;
562 
563 //  /**
564 //   * The main panel containing the text scroll in the central location.
565 //   */
566 //  protected JPanel contentPane;
567 
568   protected GateDocumentListener gateDocListener;
569 
570   /**
571    * The annotations used for blinking highlights and their tags. A map from
572    {@link AnnotationData} to tag(i.e. {@link Object}).
573    */
574   protected Map<AnnotationData, HighlightData> blinkingTagsForAnnotations;
575 
576   /**
577    * This list stores the {@link HighlightData} values for annotations pending
578    * highlighting
579    */
580   protected List<HighlightData> highlightsToAdd;
581   
582   /**
583    * This list stores the {@link HighlightData} values for highlights that need
584    * to be removed
585    */
586   protected List<HighlightData> highlightsToRemove;
587   
588   /**
589    * Used internally to store the annotations for which blinking highlights 
590    * need to be removed.
591    */
592   protected Set<AnnotationData> blinkingHighlightsToRemove;  
593   
594   /**
595    * Used internally to store the annotations for which blinking highlights 
596    * need to be added.
597    */
598   protected List<AnnotationData> blinkingHighlightsToAdd;  
599   
600   protected Timer highlightsMinder;
601   
602   protected JTextArea textView;
603   
604   /**
605    * The delay used by the blinker.
606    */
607   protected final static int BLINK_DELAY = 400;
608   
609   /**
610    * The delay used by the highlights minder.
611    */
612   protected final static int HIGHLIGHT_DELAY = 100;
613 
614   /**
615    @return the textView
616    */
617   public JTextArea getTextView() {
618     return textView;
619   }
620 }