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 : annotations) tags.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 : tags) removeHighlight(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(0, 0);
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 != null) highlighter.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 }
|