AnnotationStackView.java
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.MainFrame;
019 import gate.gui.annedit.AnnotationData;
020 import gate.gui.annedit.AnnotationDataImpl;
021 import gate.*;
022 import gate.util.InvalidOffsetException;
023 import gate.util.OffsetComparator;
024 import gate.gui.docview.AnnotationSetsView.*;
025 import gate.gui.docview.AnnotationStack.*;
026 
027 import javax.swing.*;
028 import javax.swing.text.BadLocationException;
029 import javax.swing.event.*;
030 import java.awt.*;
031 import java.awt.event.*;
032 import java.util.*;
033 import java.util.Timer;
034 import java.util.List;
035 import java.text.Collator;
036 import java.util.regex.Matcher;
037 import java.util.regex.Pattern;
038 
039 /**
040  * Show a stack view of highlighted annotations in the document
041  * centred on the document caret.
042  *
043  * When double clicked, an annotation is copied to another set in order
044  * to create a gold standard set from several annotator sets.
045  *
046  * You can choose to display a feature value by double clicking
047  * the first column rectangles.
048  */
049 @SuppressWarnings("serial")
050 public class AnnotationStackView  extends AbstractDocumentView
051   implements AnnotationListener {
052 
053   public AnnotationStackView() {
054     typesFeatures = new HashMap<String,String>();
055     annotationMouseListener = new AnnotationMouseListener();
056     headerMouseListener = new HeaderMouseListener();
057   }
058 
059   @Override
060   public void cleanup() {
061     super.cleanup();
062     textView = null;
063   }
064 
065   @Override
066   protected void initGUI() {
067 
068     //get a pointer to the text view used to display
069     //the selected annotations
070     Iterator<DocumentView> centralViewsIter = owner.getCentralViews().iterator();
071     while(textView == null && centralViewsIter.hasNext()){
072       DocumentView aView = centralViewsIter.next();
073       if(aView instanceof TextualDocumentView)
074         textView = (TextualDocumentViewaView;
075     }
076     // find the annotation set view associated with the document
077     Iterator<DocumentView> verticalViewsIter = owner.getVerticalViews().iterator();
078     while(annotationSetsView == null && verticalViewsIter.hasNext()){
079       DocumentView aView = verticalViewsIter.next();
080       if(aView instanceof AnnotationSetsView)
081         annotationSetsView = (AnnotationSetsViewaView;
082     }
083     //get a pointer to the list view
084     Iterator<DocumentView> horizontalViewsIter = owner.getHorizontalViews().iterator();
085     while(annotationListView == null && horizontalViewsIter.hasNext()){
086       DocumentView aView = horizontalViewsIter.next();
087       if(aView instanceof AnnotationListView)
088         annotationListView = (AnnotationListView)aView;
089     }
090     annotationListView.setOwner(owner);
091     document = textView.getDocument();
092 
093     mainPanel = new JPanel();
094     mainPanel.setLayout(new BorderLayout());
095 
096     // toolbar with previous and next annotation buttons
097     JToolBar toolBar = new JToolBar();
098     toolBar.setFloatable(false);
099     toolBar.addSeparator();
100     toolBar.add(previousAnnotationAction = new PreviousAnnotationAction());
101     toolBar.add(nextAnnotationAction = new NextAnnotationAction());
102     overlappingCheckBox = new JCheckBox("Overlapping");
103     overlappingCheckBox.setToolTipText("Skip non overlapping annotations.");
104     toolBar.add(overlappingCheckBox);
105     toolBar.addSeparator();
106     toolBar.add(targetSetLabel = new JLabel("Target set: Undefined"));
107     targetSetLabel.addMouseListener(new MouseAdapter() {
108       @Override
109       public void mouseClicked(MouseEvent e) {
110         askTargetSet();
111       }
112     });
113     targetSetLabel.setToolTipText(
114       "<html>Target set to copy annotation when double clicked.<br>" +
115       "Click to change it.</html>");
116     mainPanel.add(toolBar, BorderLayout.NORTH);
117 
118     stackPanel = new AnnotationStack(10030);
119     scroller = new JScrollPane(stackPanel);
120     scroller.getViewport().setOpaque(true);
121     mainPanel.add(scroller, BorderLayout.CENTER);
122 
123     initListeners();
124   }
125 
126   @Override
127   public Component getGUI(){
128     return mainPanel;
129   }
130 
131   protected void initListeners(){
132 
133     stackPanel.addAncestorListener(new AncestorListener() {
134       @Override
135       public void ancestorAdded(AncestorEvent event) {
136         // when the view becomes visible
137           updateStackView();
138       }
139       @Override
140       public void ancestorMoved(AncestorEvent event) {
141       }
142       @Override
143       public void ancestorRemoved(AncestorEvent event) {
144       }
145     });
146 
147     textView.getTextView().addCaretListener(new CaretListener() {
148       @Override
149       public void caretUpdate(CaretEvent e) {
150         updateStackView();
151       }
152     });
153   }
154 
155   class PreviousAnnotationAction extends AbstractAction {
156     public PreviousAnnotationAction() {
157       super("Previous boundary");
158       putValue(SHORT_DESCRIPTION,
159         "<html>Move to the previous annotation boundary" +
160         "&nbsp;&nbsp;<font color=#667799><small>Alt-Left" +
161         "&nbsp;&nbsp;</small></font></html>");
162       putValue(MNEMONIC_KEY, KeyEvent.VK_LEFT);
163     }
164     @Override
165     public void actionPerformed(ActionEvent e) {
166       nextAnnotationAction.setEnabled(true);
167       List<Annotation> list = new ArrayList<Annotation>();
168       for(SetHandler setHandler : annotationSetsView.setHandlers) {
169         for(TypeHandler typeHandler: setHandler.typeHandlers) {
170           if (typeHandler.isSelected()) {
171             // adds all the annotations from the selected type contained
172             // between the beginning of the document and the caret position - 1
173             list.addAll(setHandler.set.get(typeHandler.name).getContained(
174               0l(long)textView.getTextView().getCaretPosition()-1));
175           }
176         }
177       }
178       boolean enabled = false;
179       if (list.size() 0) {
180         if (overlappingCheckBox.isSelected()) {
181           Collections.sort(list, new OffsetComparator());
182           boolean found = false;
183           for (int i = list.size()-1; i > 0; i--) {
184             if (list.get(i).overlaps(list.get(i-1))) {
185               if (found) { enabled = truebreak}
186               // set the caret on the previous overlapping annotation found
187               textView.getTextView().setCaretPosition(
188                 list.get(i).getEndNode().getOffset().intValue());
189               found = true;
190             }
191           }
192         else {
193           SortedSet<Annotation> set =
194             new TreeSet<Annotation>(new OffsetComparator());
195           set.addAll(list)// remove same offsets annotations
196           list = new ArrayList<Annotation>(set);
197           // set the caret on the previous annotation found
198           textView.getTextView().setCaretPosition(
199             list.get(list.size()-1).getEndNode().getOffset().intValue());
200           enabled = (list.size() 1);
201         }
202         try {
203           textView.getTextView().scrollRectToVisible(
204           textView.getTextView().modelToView(
205             textView.getTextView().getCaretPosition()));
206         catch (BadLocationException exception) {
207           exception.printStackTrace();
208         }
209       }
210       setEnabled(enabled);
211       textView.getTextView().requestFocusInWindow();
212     }
213   }
214 
215   class NextAnnotationAction extends AbstractAction {
216     public NextAnnotationAction() {
217       super("Next boundary");
218       putValue(SHORT_DESCRIPTION,
219         "<html>Move to the next annotation boundary" +
220         "&nbsp;&nbsp;<font color=#667799><small>Alt-Right" +
221         "&nbsp;&nbsp;</small></font></html>");
222       putValue(MNEMONIC_KEY, KeyEvent.VK_RIGHT);
223     }
224     @Override
225     public void actionPerformed(ActionEvent e) {
226       previousAnnotationAction.setEnabled(true);
227       List<Annotation> list = new ArrayList<Annotation>();
228       for(SetHandler setHandler : annotationSetsView.setHandlers) {
229         for(TypeHandler typeHandler: setHandler.typeHandlers) {
230           if (typeHandler.isSelected()) {
231             // adds all the annotations from the selected type contained
232             // between the caret position and the end of the document
233             list.addAll(setHandler.set.get(typeHandler.name).getContained(
234               (long)textView.getTextView().getCaretPosition(),
235               document.getContent().size()-1));
236           }
237         }
238       }
239       boolean enabled = false;
240       if (list.size() 0) {
241         if (overlappingCheckBox.isSelected()) {
242           Collections.sort(list, new OffsetComparator());
243           boolean found = false;
244           for (int i = 0; i < list.size()-1; i++) {
245             if (list.get(i).overlaps(list.get(i+1))) {
246               if (found) { enabled = truebreak}
247               // set the caret on the next overlapping annotation found
248               textView.getTextView().setCaretPosition(
249                 list.get(i).getEndNode().getOffset().intValue());
250               found = true;
251             }
252           }
253         else {
254           SortedSet<Annotation> set =
255             new TreeSet<Annotation>(new OffsetComparator());
256           set.addAll(list)// remove same offsets annotations
257           list = new ArrayList<Annotation>(set);
258           // set the caret on the next annotation found
259           textView.getTextView().setCaretPosition(
260             list.get(0).getEndNode().getOffset().intValue());
261           enabled = (list.size() 1);
262         }
263         try {
264           textView.getTextView().scrollRectToVisible(
265           textView.getTextView().modelToView(
266             textView.getTextView().getCaretPosition()));
267         catch (BadLocationException exception) {
268           exception.printStackTrace();
269         }
270       }
271       setEnabled(enabled);
272       textView.getTextView().requestFocusInWindow();
273     }
274   }
275 
276   @Override
277   protected void registerHooks() { /* do nothing */ }
278 
279   @Override
280   protected void unregisterHooks() { /* do nothing */ }
281 
282   @Override
283   public int getType() {
284     return HORIZONTAL;
285   }
286 
287   @Override
288   public void annotationUpdated(AnnotationEvent e) {
289     updateStackView();
290   }
291 
292   public void updateStackView() {
293     if (textView == null) { return}
294 
295     int caretPosition = textView.getTextView().getCaretPosition();
296 
297     // get the context around the annotation
298     int context = 40;
299     String text = "";
300     try {
301       text = document.getContent().getContent(
302         Math.max(0l, caretPosition - context),
303         Math.min(document.getContent().size(),
304                  caretPosition + + context)).toString();
305     catch (InvalidOffsetException e) {
306       e.printStackTrace();
307     }
308 
309     // initialise the annotation stack
310     stackPanel.setText(text);
311     stackPanel.setExpressionStartOffset(caretPosition);
312     stackPanel.setExpressionEndOffset(caretPosition + 1);
313     stackPanel.setContextBeforeSize(Math.min(caretPosition, context));
314     stackPanel.setContextAfterSize(Math.min(
315       document.getContent().size().intValue()-1-caretPosition, context));
316     stackPanel.setAnnotationMouseListener(annotationMouseListener);
317     stackPanel.setHeaderMouseListener(headerMouseListener);
318     stackPanel.clearAllRows();
319 
320     // add stack rows and annotations for each selected annotation set
321     // in the annotation sets view
322     for(SetHandler setHandler : annotationSetsView.setHandlers) {
323       for(TypeHandler typeHandler: setHandler.typeHandlers) {
324         if (typeHandler.isSelected()) {
325           stackPanel.addRow(setHandler.set.getName(), typeHandler.name,
326             typesFeatures.get(typeHandler.name),
327             null, null, AnnotationStack.CROP_MIDDLE);
328           Set<Annotation> annotations = setHandler.set.get(typeHandler.name)
329             .get(Math.max(0l, caretPosition - context), Math.min(
330               document.getContent().size(), caretPosition + + context));
331           for (Annotation annotation : annotations) {
332             stackPanel.addAnnotation(annotation);
333           }
334         }
335       }
336     }
337 
338     stackPanel.drawStack();
339   }
340 
341   /** @return true if the user input a valid annotation set */
342   boolean askTargetSet() {
343     Object[] messageObjects;
344     final JTextField setsTextField = new JTextField("consensus"15);
345     if (document.getAnnotationSetNames() != null
346      && !document.getAnnotationSetNames().isEmpty()) {
347       Collator collator = Collator.getInstance(Locale.ENGLISH);
348       collator.setStrength(Collator.TERTIARY);
349       SortedSet<String> setNames = new TreeSet<String>(collator);
350       setNames.addAll(document.getAnnotationSetNames());
351       JList<String> list = new JList<String>(setNames.toArray(new String[setNames.size()]));
352       list.setVisibleRowCount(Math.min(10, setNames.size()));
353       list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
354       list.setSelectedValue(targetSetName, true);
355       JScrollPane scroll = new JScrollPane(list);
356       JPanel vspace = new JPanel();
357       vspace.setSize(05);
358       list.addListSelectionListener(new ListSelectionListener() {
359         @Override
360         public void valueChanged(ListSelectionEvent e) {
361           @SuppressWarnings("unchecked")
362           JList<String> list = (JList<String>e.getSource();
363           if (list.getSelectedValue() != null) {
364             setsTextField.setText(list.getSelectedValue());
365           }
366         }
367       });
368       messageObjects = new Object[] { "Existing annotation sets:",
369         scroll, vspace, "Target set:", setsTextField };
370     else {
371       messageObjects = new Object[] { "Target set:", setsTextField };
372     }
373     String options[] "Use this target set""Cancel" };
374     JOptionPane optionPane = new JOptionPane(
375       messageObjects, JOptionPane.QUESTION_MESSAGE,
376       JOptionPane.YES_NO_OPTION, null, options, "Cancel");
377     JDialog optionDialog = optionPane.createDialog(
378       owner, "Copy annotation to another set");
379     setsTextField.requestFocus();
380     optionDialog.setVisible(true);
381     Object selectedValue = optionPane.getValue();
382     if (selectedValue == null
383      || selectedValue.equals("Cancel")
384      || setsTextField.getText().trim().length() == 0) {
385       textView.getTextView().requestFocusInWindow();
386       return false;
387     }
388     targetSetName = setsTextField.getText();
389     targetSetLabel.setText("Target set: " + targetSetName);
390     textView.getTextView().requestFocusInWindow();
391     return true;
392   }
393 
394   class AnnotationMouseListener extends StackMouseListener {
395 
396     public AnnotationMouseListener() {
397     }
398 
399     public AnnotationMouseListener(String setName, String annotationId) {
400       AnnotationSet set = document.getAnnotations(setName);
401       annotation = set.get(Integer.valueOf(annotationId));
402       if (annotation != null) {
403         // the annotation has been found by its id
404         annotationData = new AnnotationDataImpl(set, annotation);
405       }
406     }
407 
408     @Override
409     public MouseInputAdapter createListener(String... parameters) {
410       switch(parameters.length) {
411         case 3:
412           return new AnnotationMouseListener(parameters[0], parameters[2]);
413         case 5:
414           return new AnnotationMouseListener(parameters[0], parameters[4]);
415         default:
416           return null;
417       }
418     }
419 
420     @Override
421     public void mousePressed(MouseEvent me) {
422       processMouseEvent(me);
423     }
424     @Override
425     public void mouseReleased(MouseEvent me) {
426       processMouseEvent(me);
427     }
428     @Override
429     public void mouseClicked(MouseEvent me) {
430       processMouseEvent(me);
431     }
432     public void processMouseEvent(MouseEvent me) {
433 
434       if(me.isPopupTrigger()) { // context menu
435         // add annotation editors to the context menu
436         JPopupMenu popup = new JPopupMenu();
437         List<Action> specificEditorActions =
438           annotationListView.getSpecificEditorActions(
439           annotationData.getAnnotationSet(), annotationData.getAnnotation());
440         for (Action action : specificEditorActions) {
441           popup.add(action);
442         }
443         List<Action> genericEditorActions =
444           annotationListView.getGenericEditorActions(
445             annotationData.getAnnotationSet(), annotationData.getAnnotation());
446         for (Action action : genericEditorActions) {
447           if (specificEditorActions.contains(action)) { continue}
448           popup.add(action);
449         }
450         if (specificEditorActions.size() + genericEditorActions.size() 1) {
451           popup.show(me.getComponent(), me.getX(), me.getY());
452         else // only one choice so use it directly
453           if (specificEditorActions.size() == 1) {
454             specificEditorActions.get(0).actionPerformed(null);
455           else {
456             genericEditorActions.get(0).actionPerformed(null);
457           }
458         }
459 
460       else if (me.getID() == MouseEvent.MOUSE_CLICKED
461               && me.getButton() == MouseEvent.BUTTON1
462               && me.getClickCount() == 1
463               && (me.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK!= 0
464               && (me.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK!= 0) {
465               // control + shift + click -> delete the annotation
466         annotationData.getAnnotationSet().remove(annotation);
467 
468       else if (me.getID() == MouseEvent.MOUSE_CLICKED
469               && me.getButton() == MouseEvent.BUTTON1
470               && me.getClickCount() == 1
471               && (me.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK!= 0) {
472                // control + click
473         String featureDisplayed = typesFeatures.get(annotation.getType());
474         Object[] features;
475         if (featureDisplayed != null) {
476           features = new Object[]{featureDisplayed};
477         else {
478           features = annotation.getFeatures().keySet().toArray();
479         }
480         for (Object feature : features) {
481           String value = (Stringannotation.getFeatures().get(feature);
482           if (value == null) { continue}
483           Pattern pattern = Pattern.compile("(https?://[^\\s,;]+)");
484           Matcher matcher = pattern.matcher(value);
485           if (matcher.find()) {
486             // if the feature value contains an url display it in a browser
487             MainFrame.getInstance().showHelpFrame(
488               matcher.group()"Annotation Stack View");
489           }
490         }
491 
492       else if (me.getID() == MouseEvent.MOUSE_CLICKED
493               && me.getButton() == MouseEvent.BUTTON1
494               && me.getClickCount() == 2) { // double click
495         if (targetSetName == null) {
496           if (!askTargetSet()) { return}
497         }
498         // copy the annotation to the target annotation set
499         try {
500           FeatureMap params = Factory.newFeatureMap();
501           params.putAll(annotation.getFeatures());
502           document.getAnnotations(targetSetName).add(
503             annotation.getStartNode().getOffset(),
504             annotation.getEndNode().getOffset(),
505             annotation.getType(),
506             params);
507         catch (InvalidOffsetException e) {
508           e.printStackTrace();
509         }
510         // wait some time
511         Date timeToRun = new Date(System.currentTimeMillis() 500);
512         Timer timer = new Timer("Annotation stack view select type"true);
513         timer.schedule(new TimerTask() {
514           @Override
515           public void run() {
516             SwingUtilities.invokeLater(new Runnable() { @Override
517             public void run() {
518               // select the new annotation and update the stack view
519               annotationSetsView.setTypeSelected(targetSetName,
520                 annotation.getType()true);
521             }});
522           }
523         }, timeToRun);
524       }
525       textView.getTextView().requestFocusInWindow();
526     }
527 
528     @Override
529     public void mouseEntered(MouseEvent e) {
530       dismissDelay = toolTipManager.getDismissDelay();
531       initialDelay = toolTipManager.getInitialDelay();
532       reshowDelay = toolTipManager.getReshowDelay();
533       enabled = toolTipManager.isEnabled();
534       Component component = e.getComponent();
535       if (component instanceof JLabel
536       && !((JLabel)component).getToolTipText().contains("Ctr-Sh-click")) {
537         JLabel label = (JLabelcomponent;
538         String toolTip = (label.getToolTipText() == null?
539           "" : label.getToolTipText();
540         toolTip = toolTip.replaceAll("</?html>""");
541         toolTip = "<html>"
542           (toolTip.length() == "" ("<b>" + toolTip + "</b><br>"))
543           "Double-click to copy. Right-click to edit.<br>"
544           "Ctr-click to show URL. Ctr-Sh-click to delete.</html>";
545         label.setToolTipText(toolTip);
546       }
547       // make the tooltip indefinitely shown when the mouse is over
548       toolTipManager.setDismissDelay(Integer.MAX_VALUE);
549       toolTipManager.setInitialDelay(0);
550       toolTipManager.setReshowDelay(0);
551       toolTipManager.setEnabled(true);
552     }
553 
554     @Override
555     public void mouseExited(MouseEvent e) {
556       toolTipManager.setDismissDelay(dismissDelay);
557       toolTipManager.setInitialDelay(initialDelay);
558       toolTipManager.setReshowDelay(reshowDelay);
559       toolTipManager.setEnabled(enabled);
560     }
561 
562     ToolTipManager toolTipManager = ToolTipManager.sharedInstance();
563     int dismissDelay, initialDelay, reshowDelay;
564     boolean enabled;
565     Annotation annotation;
566     AnnotationData annotationData;
567   }
568 
569   protected class HeaderMouseListener extends StackMouseListener {
570 
571     public HeaderMouseListener() {
572     }
573 
574     public HeaderMouseListener(String type, String feature) {
575       this.type = type;
576       this.feature = feature;
577       init();
578     }
579 
580     public HeaderMouseListener(String type) {
581       this.type = type;
582       init();
583     }
584 
585     void init() {
586       mainPanel.addAncestorListener(new AncestorListener() {
587         @Override
588         public void ancestorMoved(AncestorEvent event) {}
589         @Override
590         public void ancestorAdded(AncestorEvent event) {}
591         @Override
592         public void ancestorRemoved(AncestorEvent event) {
593           // no parent so need to be disposed explicitly
594           if (popupWindow != null) { popupWindow.dispose()}
595         }
596       });
597     }
598 
599     @Override
600     public MouseInputAdapter createListener(String... parameters) {
601       switch(parameters.length) {
602         case 1:
603           return new HeaderMouseListener(parameters[0]);
604         case 2:
605           return new HeaderMouseListener(parameters[0], parameters[1]);
606         default:
607           return null;
608       }
609     }
610 
611     // when double clicked shows a list of features for this annotation type
612     @Override
613     public void mouseClicked(MouseEvent e) {
614       if (popupWindow != null && popupWindow.isVisible()) {
615         popupWindow.dispose();
616         return;
617       }
618       if (e.getButton() != MouseEvent.BUTTON1
619        || e.getClickCount() != 2) {
620         return;
621       }
622       // get a list of features for the current annotation type
623       TreeSet<String> features = new TreeSet<String>();
624       Set<String> setNames = new HashSet<String>();
625       if (document.getAnnotationSetNames() != null) {
626         setNames.addAll(document.getAnnotationSetNames());
627       }
628       setNames.add(null)// default set name
629       for (String setName : setNames) {
630         int count = 0;
631         for (Annotation annotation :
632           document.getAnnotations(setName).get(type)) {
633           for (Object feature : annotation.getFeatures().keySet()) {
634             features.add((Stringfeature);
635           }
636           count++; // checks only the 50 first annotations per set
637           if (count == 50) { break// to avoid slowing down
638         }
639       }
640       features.add("          ");
641       // create the list component
642       final JList<String> list = new JList<String>(features.toArray(new String[features.size()]));
643       list.setVisibleRowCount(Math.min(8, features.size()));
644       list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
645       list.setBackground(Color.WHITE);
646       list.addMouseListener(new MouseAdapter() {
647         @Override
648         public void mouseClicked(MouseEvent e) {
649           if (e.getClickCount() == 1) {
650             String feature = list.getSelectedValue();
651             if (feature.equals("          ")) {
652               typesFeatures.remove(type);
653             else {
654               typesFeatures.put(type, feature);
655             }
656             popupWindow.setVisible(false);
657             popupWindow.dispose();
658             updateStackView();
659             textView.getTextView().requestFocusInWindow();
660           }
661         }
662       });
663       // create the window that will contain the list
664       popupWindow = new JWindow();
665       popupWindow.addKeyListener(new KeyAdapter() {
666         @Override
667         public void keyPressed(KeyEvent e) {
668           if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
669             popupWindow.setVisible(false);
670             popupWindow.dispose();
671           }
672         }
673       });
674       popupWindow.add(new JScrollPane(list));
675       Component component = e.getComponent();
676       popupWindow.setBounds(
677         component.getLocationOnScreen().x,
678         component.getLocationOnScreen().y + component.getHeight(),
679         component.getWidth(),
680         Math.min(8*component.getHeight(),
681           features.size()*component.getHeight()));
682       popupWindow.pack();
683       popupWindow.setVisible(true);
684       SwingUtilities.invokeLater(new Runnable() {
685       @Override
686       public void run() {
687         String newFeature = typesFeatures.get(type);
688         if (newFeature == null) { newFeature = "          "}
689         list.setSelectedValue(newFeature, true);
690         popupWindow.requestFocusInWindow();
691       }});
692     }
693 
694     @Override
695     public void mouseEntered(MouseEvent e) {
696       Component component = e.getComponent();
697       if (component instanceof JLabel
698       && ((JLabel)component).getToolTipText() == null) {
699         ((JLabel)component).setToolTipText("Double click to choose a feature.");
700       }
701     }
702 
703     String type;
704     String feature;
705     JWindow popupWindow;
706   }
707 
708   // external objects
709   TextualDocumentView textView;
710   AnnotationSetsView annotationSetsView;
711   AnnotationListView annotationListView;
712 
713   // user interface elements
714   JLabel targetSetLabel;
715   String targetSetName;
716   JCheckBox overlappingCheckBox;
717   AnnotationStack stackPanel;
718   JScrollPane scroller;
719   JPanel mainPanel;
720 
721   // actions
722   PreviousAnnotationAction previousAnnotationAction;
723   NextAnnotationAction nextAnnotationAction;
724 
725   // local objects
726   /** optionally map a type to a feature when the feature value
727    *  must be displayed in the rectangle annotation */
728   Map<String,String> typesFeatures;
729   AnnotationMouseListener annotationMouseListener;
730   HeaderMouseListener headerMouseListener;
731 }