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