001 /*
002 * Copyright (c) 1998-2009, The University of Sheffield and Ontotext.
003 *
004 * This file is part of GATE (see http://gate.ac.uk/), and is free
005 * software, licenced under the GNU Library General Public License,
006 * Version 2, June 1991 (in the distribution as file licence.html,
007 * and also available at http://gate.ac.uk/gate/licence.html).
008 *
009 * Thomas Heitz - 7 July 2009
010 *
011 * $Id$
012 */
013
014 package gate.gui.docview;
015
016 import gate.event.AnnotationListener;
017 import gate.event.AnnotationEvent;
018 import gate.gui.annedit.AnnotationData;
019 import gate.gui.annedit.AnnotationDataImpl;
020 import gate.*;
021 import gate.util.InvalidOffsetException;
022 import gate.util.OffsetComparator;
023 import gate.gui.docview.AnnotationSetsView.*;
024 import gate.gui.docview.AnnotationStack.*;
025
026 import javax.swing.*;
027 import javax.swing.text.BadLocationException;
028 import javax.swing.event.*;
029 import java.awt.*;
030 import java.awt.event.*;
031 import java.util.*;
032 import java.util.Timer;
033 import java.util.List;
034 import java.text.Collator;
035
036 /**
037 * Show a stack view of highlighted annotations in the document
038 * centred on the document caret.
039 *
040 * When double clicked, an annotation is copied to another set in order
041 * to create a gold standard set from several annoatator sets.
042 *
043 * You can choose to display features with annotations by clicking
044 * the first column rectangles.
045 */
046 public class AnnotationStackView extends AbstractDocumentView
047 implements AnnotationListener {
048
049 public AnnotationStackView() {
050 typesFeatures = new HashMap<String,String>();
051 }
052
053 public void cleanup() {
054 super.cleanup();
055 textView = null;
056 }
057
058 protected void initGUI() {
059
060 //get a pointer to the text view used to display
061 //the selected annotations
062 Iterator centralViewsIter = owner.getCentralViews().iterator();
063 while(textView == null && centralViewsIter.hasNext()){
064 DocumentView aView = (DocumentView) centralViewsIter.next();
065 if(aView instanceof TextualDocumentView)
066 textView = (TextualDocumentView) aView;
067 }
068 // find the annotation set view associated with the document
069 Iterator verticalViewsIter = owner.getVerticalViews().iterator();
070 while(annotationSetsView == null && verticalViewsIter.hasNext()){
071 DocumentView aView = (DocumentView) verticalViewsIter.next();
072 if(aView instanceof AnnotationSetsView)
073 annotationSetsView = (AnnotationSetsView) aView;
074 }
075 //get a pointer to the list view
076 Iterator horizontalViewsIter = owner.getHorizontalViews().iterator();
077 while(annotationListView == null && horizontalViewsIter.hasNext()){
078 DocumentView aView = (DocumentView)horizontalViewsIter.next();
079 if(aView instanceof AnnotationListView)
080 annotationListView = (AnnotationListView)aView;
081 }
082 annotationListView.setOwner(owner);
083 document = textView.getDocument();
084
085 mainPanel = new JPanel();
086 mainPanel.setLayout(new BorderLayout());
087
088 // toolbar with previous and next annotation buttons
089 JToolBar toolBar = new JToolBar();
090 toolBar.setFloatable(false);
091 toolBar.addSeparator();
092 toolBar.add(previousAnnotationAction = new PreviousAnnotationAction());
093 toolBar.add(nextAnnotationAction = new NextAnnotationAction());
094 toolBar.addSeparator();
095 toolBar.add(targetSetLabel = new JLabel());
096 targetSetLabel.addMouseListener(new MouseAdapter() {
097 public void mouseClicked(MouseEvent e) {
098 askTargetSet();
099 }
100 });
101 targetSetLabel.setToolTipText(
102 "<html>Target set to copy annotation when double clicked.<br>" +
103 "Click to change it.</html>");
104 mainPanel.add(toolBar, BorderLayout.NORTH);
105
106 stackPanel = new AnnotationStack(100, 20);
107 scroller = new JScrollPane(stackPanel);
108 scroller.getViewport().setOpaque(true);
109 mainPanel.add(scroller, BorderLayout.CENTER);
110
111 initListeners();
112 }
113
114 public Component getGUI(){
115 return mainPanel;
116 }
117
118 protected void initListeners(){
119
120 stackPanel.addAncestorListener(new AncestorListener() {
121 public void ancestorAdded(AncestorEvent event) {
122 // when the view becomes visible
123 updateStackView();
124 }
125 public void ancestorMoved(AncestorEvent event) {
126 }
127 public void ancestorRemoved(AncestorEvent event) {
128 }
129 });
130
131 textView.getTextView().addCaretListener(new CaretListener() {
132 public void caretUpdate(CaretEvent e) {
133 updateStackView();
134 }
135 });
136 }
137
138 class PreviousAnnotationAction extends AbstractAction {
139 public PreviousAnnotationAction() {
140 super("Previous boundary");
141 putValue(SHORT_DESCRIPTION, "Centre the view on the closest previous " +
142 "annotation boundary among all displayed");
143 putValue(MNEMONIC_KEY, KeyEvent.VK_LEFT);
144 }
145 public void actionPerformed(ActionEvent e) {
146 nextAnnotationAction.setEnabled(true);
147 SortedSet<Annotation> set =
148 new TreeSet<Annotation>(new OffsetComparator());
149 for(SetHandler setHandler : annotationSetsView.setHandlers) {
150 for(TypeHandler typeHandler: setHandler.typeHandlers) {
151 if (typeHandler.isSelected()) {
152 set.addAll(setHandler.set.get(typeHandler.name).getContained(
153 0l, (long)textView.getTextView().getCaretPosition()-1));
154 }
155 }
156 }
157 if (set.size() > 0) {
158 textView.getTextView().setCaretPosition(
159 set.last().getEndNode().getOffset().intValue());
160 try {
161 textView.getTextView().scrollRectToVisible(
162 textView.getTextView().modelToView(
163 textView.getTextView().getCaretPosition()));
164 } catch (BadLocationException exception) {
165 exception.printStackTrace();
166 }
167 }
168 setEnabled(set.size() > 1);
169 textView.getTextView().requestFocusInWindow();
170 }
171 }
172
173 class NextAnnotationAction extends AbstractAction {
174 public NextAnnotationAction() {
175 super("Next boundary");
176 putValue(SHORT_DESCRIPTION, "Centre the view on the closest next " +
177 "annotation boundary among all displayed");
178 putValue(MNEMONIC_KEY, KeyEvent.VK_RIGHT);
179 }
180 public void actionPerformed(ActionEvent e) {
181 previousAnnotationAction.setEnabled(true);
182 SortedSet<Annotation> set = new TreeSet<Annotation>(
183 Collections.reverseOrder(new OffsetComparator()));
184 for(SetHandler setHandler : annotationSetsView.setHandlers) {
185 for(TypeHandler typeHandler: setHandler.typeHandlers) {
186 if (typeHandler.isSelected()) {
187 set.addAll(setHandler.set.get(typeHandler.name).getContained(
188 (long)textView.getTextView().getCaretPosition(),
189 document.getContent().size()-1));
190 }
191 }
192 }
193 if (set.size() > 0) {
194 textView.getTextView().setCaretPosition(
195 set.last().getEndNode().getOffset().intValue());
196 try {
197 textView.getTextView().scrollRectToVisible(
198 textView.getTextView().modelToView(
199 textView.getTextView().getCaretPosition()));
200 } catch (BadLocationException exception) {
201 exception.printStackTrace();
202 }
203 }
204 setEnabled(set.size() > 1);
205 textView.getTextView().requestFocusInWindow();
206 }
207 }
208
209 protected void registerHooks() { /* do nothing */ }
210
211 protected void unregisterHooks() { /* do nothing */ }
212
213 public int getType() {
214 return HORIZONTAL;
215 }
216
217 public void annotationUpdated(AnnotationEvent e) {
218 updateStackView();
219 }
220
221 public void updateStackView() {
222 if (textView == null) { return; }
223
224 int caretPosition = textView.getTextView().getCaretPosition();
225
226 // get the context around the annotation
227 int context = 40;
228 String text = "";
229 try {
230 text = document.getContent().getContent(
231 Math.max(0l, caretPosition - context),
232 Math.min(document.getContent().size(),
233 caretPosition + 1 + context)).toString();
234 } catch (InvalidOffsetException e) {
235 e.printStackTrace();
236 }
237
238 // initialise the annotation stack
239 stackPanel.setText(text);
240 stackPanel.setExpressionStartOffset(caretPosition);
241 stackPanel.setExpressionEndOffset(caretPosition + 1);
242 stackPanel.setContextBeforeSize(Math.min(caretPosition, context));
243 stackPanel.setContextAfterSize(Math.min(
244 document.getContent().size().intValue()-1-caretPosition, context));
245 stackPanel.setAnnotationMouseListener(new AnnotationMouseListener());
246 stackPanel.setHeaderMouseListener(new HeaderMouseListener());
247 stackPanel.clearAllRows();
248
249 // add stack rows and annotations for each selected annotation set
250 // in the annotation sets view
251 for(SetHandler setHandler : annotationSetsView.setHandlers) {
252 for(TypeHandler typeHandler: setHandler.typeHandlers) {
253 if (typeHandler.isSelected()) {
254 stackPanel.addRow(setHandler.set.getName(), typeHandler.name,
255 typesFeatures.get(typeHandler.name),
256 null, null, AnnotationStack.CROP_MIDDLE);
257 Set<Annotation> annotations = setHandler.set.get(typeHandler.name)
258 .get(Math.max(0l, caretPosition - context), Math.min(
259 document.getContent().size(), caretPosition + 1 + context));
260 for (Annotation annotation : annotations) {
261 stackPanel.addAnnotation(annotation);
262 }
263 }
264 }
265 }
266
267 stackPanel.drawStack();
268 }
269
270 /** @return true if the user input a valid annotation set */
271 boolean askTargetSet() {
272 Object[] messageObjects;
273 final JTextField setsTextField = new JTextField("consensus", 15);
274 if (document.getAnnotationSetNames() != null
275 && !document.getAnnotationSetNames().isEmpty()) {
276 Collator collator = Collator.getInstance(Locale.ENGLISH);
277 collator.setStrength(Collator.TERTIARY);
278 SortedSet<String> setNames = new TreeSet<String>(collator);
279 setNames.addAll(document.getAnnotationSetNames());
280 JList list = new JList(setNames.toArray());
281 list.setVisibleRowCount(Math.min(10, setNames.size()));
282 list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
283 list.setSelectedValue(targetSetName, true);
284 JScrollPane scroll = new JScrollPane(list);
285 JPanel vspace = new JPanel();
286 vspace.setSize(0, 5);
287 list.addListSelectionListener(new ListSelectionListener() {
288 public void valueChanged(ListSelectionEvent e) {
289 JList list = (JList) e.getSource();
290 if (list.getSelectedValue() != null) {
291 setsTextField.setText((String) list.getSelectedValue());
292 }
293 }
294 });
295 messageObjects = new Object[] { "Existing annotation sets:",
296 scroll, vspace, "Target set:", setsTextField };
297 } else {
298 messageObjects = new Object[] { "Target set:", setsTextField };
299 }
300 String options[] = { "Use this target set", "Cancel" };
301 JOptionPane optionPane = new JOptionPane(
302 messageObjects, JOptionPane.QUESTION_MESSAGE,
303 JOptionPane.YES_NO_OPTION, null, options, "Cancel");
304 JDialog optionDialog = optionPane.createDialog(
305 owner, "Copy annotation to another set");
306 setsTextField.requestFocus();
307 optionDialog.setVisible(true);
308 Object selectedValue = optionPane.getValue();
309 if (selectedValue == null
310 || selectedValue.equals("Cancel")
311 || setsTextField.getText().trim().length() == 0) {
312 textView.getTextView().requestFocusInWindow();
313 return false;
314 }
315 targetSetName = setsTextField.getText();
316 targetSetLabel.setText("Target set: " + targetSetName);
317 textView.getTextView().requestFocusInWindow();
318 return true;
319 }
320
321 class AnnotationMouseListener extends StackMouseListener {
322
323 public AnnotationMouseListener() {
324 }
325
326 public AnnotationMouseListener(String setName, String annotationId) {
327 AnnotationSet set = document.getAnnotations(setName);
328 annotation = set.get(Integer.valueOf(annotationId));
329 if (annotation != null) {
330 // the annotation has been found by its id
331 annotationData = new AnnotationDataImpl(set, annotation);
332 }
333 }
334
335 public MouseInputAdapter createListener(String... parameters) {
336 switch(parameters.length) {
337 case 3:
338 return new AnnotationMouseListener(parameters[0], parameters[2]);
339 case 5:
340 return new AnnotationMouseListener(parameters[0], parameters[4]);
341 default:
342 return null;
343 }
344 }
345
346 public void mousePressed(MouseEvent me) {
347 processMouseEvent(me);
348 }
349 public void mouseReleased(MouseEvent me) {
350 processMouseEvent(me);
351 }
352 public void mouseClicked(MouseEvent me) {
353 processMouseEvent(me);
354 }
355 public void processMouseEvent(MouseEvent me) {
356 if(me.isPopupTrigger()) {
357 // add annotation editors context menu
358 JPopupMenu popup = new JPopupMenu();
359 List<Action> specificEditorActions =
360 annotationListView.getSpecificEditorActions(
361 annotationData.getAnnotationSet(), annotationData.getAnnotation());
362 for (Action action : specificEditorActions) {
363 popup.add(action);
364 }
365 for (Action action : annotationListView.getGenericEditorActions(
366 annotationData.getAnnotationSet(), annotationData.getAnnotation())) {
367 if (specificEditorActions.contains(action)) { continue; }
368 popup.add(action);
369 }
370 popup.show(me.getComponent(), me.getX(), me.getY());
371
372 } else if (me.getID() == MouseEvent.MOUSE_CLICKED
373 && me.getButton() == MouseEvent.BUTTON1
374 && me.getClickCount() == 2) {
375 if (targetSetName == null) {
376 if (!askTargetSet()) { return; }
377 }
378 // copy the annotation to the target annotation set
379 try {
380 document.getAnnotations(targetSetName).add(
381 annotation.getStartNode().getOffset(),
382 annotation.getEndNode().getOffset(),
383 annotation.getType(),
384 annotation.getFeatures());
385 } catch (InvalidOffsetException e) {
386 e.printStackTrace();
387 }
388
389 // wait some time
390 Date timeToRun = new Date(System.currentTimeMillis() + 1000);
391 Timer timer = new Timer("Annotation stack view select type", true);
392 timer.schedule(new TimerTask() {
393 public void run() {
394 SwingUtilities.invokeLater(new Runnable() { public void run() {
395 // select the new annotation and update the stack view
396 annotationSetsView.selectAnnotation(annotation,
397 document.getAnnotations(targetSetName));
398 }});
399 }
400 }, timeToRun);
401 }
402 textView.getTextView().requestFocusInWindow();
403 }
404
405 public void mouseEntered(MouseEvent e) {
406 dismissDelay = toolTipManager.getDismissDelay();
407 initialDelay = toolTipManager.getInitialDelay();
408 reshowDelay = toolTipManager.getReshowDelay();
409 enabled = toolTipManager.isEnabled();
410 Component component = e.getComponent();
411 if (!isTooltipSet && component instanceof JLabel) {
412 isTooltipSet = true;
413 JLabel label = (JLabel) component;
414 String toolTip = (label.getToolTipText() == null) ?
415 "" : label.getToolTipText();
416 toolTip = toolTip.replaceAll("</?html>", "");
417 toolTip = "<html>" + (toolTip.length() == 0 ? "" : toolTip + "<br>")
418 + "Double click to copy this annotation.<br>"
419 + "Right click to edit</html>";
420 label.setToolTipText(toolTip);
421 }
422 // make the tooltip indefinitely shown when the mouse is over
423 toolTipManager.setDismissDelay(Integer.MAX_VALUE);
424 toolTipManager.setInitialDelay(0);
425 toolTipManager.setReshowDelay(0);
426 toolTipManager.setEnabled(true);
427 }
428
429 public void mouseExited(MouseEvent e) {
430 toolTipManager.setDismissDelay(dismissDelay);
431 toolTipManager.setInitialDelay(initialDelay);
432 toolTipManager.setReshowDelay(reshowDelay);
433 toolTipManager.setEnabled(enabled);
434 }
435
436 ToolTipManager toolTipManager = ToolTipManager.sharedInstance();
437 int dismissDelay, initialDelay, reshowDelay;
438 boolean enabled;
439 Annotation annotation;
440 AnnotationData annotationData;
441 boolean isTooltipSet = false;
442 }
443
444 protected class HeaderMouseListener extends StackMouseListener {
445
446 public HeaderMouseListener() {
447 }
448
449 public HeaderMouseListener(String type, String feature) {
450 this.type = type;
451 this.feature = feature;
452 init();
453 }
454
455 public HeaderMouseListener(String type) {
456 this.type = type;
457 init();
458 }
459
460 void init() {
461 mainPanel.addAncestorListener(new AncestorListener() {
462 public void ancestorMoved(AncestorEvent event) {}
463 public void ancestorAdded(AncestorEvent event) {}
464 public void ancestorRemoved(AncestorEvent event) {
465 // no parent so need to be disposed explicitly
466 if (popupWindow != null) { popupWindow.dispose(); }
467 }
468 });
469 }
470
471 public MouseInputAdapter createListener(String... parameters) {
472 switch(parameters.length) {
473 case 1:
474 return new HeaderMouseListener(parameters[0]);
475 case 2:
476 return new HeaderMouseListener(parameters[0], parameters[1]);
477 default:
478 return null;
479 }
480 }
481
482 // when double clicked shows a list of features for this annotation type
483 public void mouseClicked(MouseEvent e) {
484 if (popupWindow != null && popupWindow.isVisible()) {
485 popupWindow.dispose();
486 return;
487 }
488 if (e.getButton() != MouseEvent.BUTTON1
489 || e.getClickCount() != 2) {
490 return;
491 }
492 // get a list of features for the current annotation type
493 TreeSet<String> features = new TreeSet<String>();
494 Set<String> setNames = new HashSet<String>();
495 if (document.getAnnotationSetNames() != null) {
496 setNames.addAll(document.getAnnotationSetNames());
497 }
498 setNames.add(null); // default set name
499 for (String setName : setNames) {
500 int count = 0;
501 for (Annotation annotation :
502 document.getAnnotations(setName).get(type)) {
503 for (Object feature : annotation.getFeatures().keySet()) {
504 features.add((String) feature);
505 }
506 count++; // checks only the 50 first annotations per set
507 if (count == 50) { break; } // to avoid slowing down
508 }
509 }
510 features.add(" ");
511 // create the list component
512 final JList list = new JList(features.toArray());
513 list.setVisibleRowCount(Math.min(8, features.size()));
514 list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
515 list.setBackground(Color.WHITE);
516 list.addMouseListener(new MouseAdapter() {
517 public void mouseClicked(MouseEvent e) {
518 if (e.getClickCount() == 1) {
519 String feature = (String) list.getSelectedValue();
520 if (feature.equals(" ")) {
521 typesFeatures.remove(type);
522 } else {
523 typesFeatures.put(type, feature);
524 }
525 popupWindow.setVisible(false);
526 popupWindow.dispose();
527 updateStackView();
528 textView.getTextView().requestFocusInWindow();
529 }
530 }
531 });
532 // create the window that will contain the list
533 popupWindow = new JWindow();
534 popupWindow.addKeyListener(new KeyAdapter() {
535 public void keyPressed(KeyEvent e) {
536 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
537 popupWindow.setVisible(false);
538 popupWindow.dispose();
539 }
540 }
541 });
542 popupWindow.add(new JScrollPane(list));
543 Component component = e.getComponent();
544 popupWindow.setBounds(
545 component.getLocationOnScreen().x,
546 component.getLocationOnScreen().y + component.getHeight(),
547 component.getWidth(),
548 Math.min(8*component.getHeight(),
549 features.size()*component.getHeight()));
550 popupWindow.pack();
551 popupWindow.setVisible(true);
552 SwingUtilities.invokeLater(new Runnable() {
553 public void run() {
554 String newFeature = typesFeatures.get(type);
555 if (newFeature == null) { newFeature = " "; }
556 list.setSelectedValue(newFeature, true);
557 popupWindow.requestFocusInWindow();
558 }});
559 }
560
561 public void mouseEntered(MouseEvent e) {
562 Component component = e.getComponent();
563 if (component instanceof JLabel
564 && ((JLabel)component).getToolTipText() == null) {
565 ((JLabel)component).setToolTipText("Double click to choose a feature.");
566 }
567 }
568
569 String type;
570 String feature;
571 JWindow popupWindow;
572 }
573
574 JLabel targetSetLabel;
575 AnnotationStack stackPanel;
576 JScrollPane scroller;
577 JPanel mainPanel;
578 TextualDocumentView textView;
579 AnnotationSetsView annotationSetsView;
580 AnnotationListView annotationListView;
581 String targetSetName;
582 Document document;
583 PreviousAnnotationAction previousAnnotationAction;
584 NextAnnotationAction nextAnnotationAction;
585 /** optionally map a type to a feature when the feature value
586 * must be displayed in the rectangle annotation */
587 Map<String,String> typesFeatures;
588 }
|