All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.editor.Annotations Maven / Gradle / Ivy

There is a newer version: RELEASE240
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.editor;

import java.util.LinkedList;
import java.util.HashMap;
import javax.swing.text.BadLocationException;
import java.util.Iterator;
import java.util.ArrayList;
import javax.swing.event.DocumentListener;
import javax.swing.event.DocumentEvent;
import java.util.EventListener;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.swing.JPopupMenu;
import javax.swing.Action;
import java.util.Map;
import java.util.TreeSet;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.event.EventListenerList;
import org.netbeans.editor.ext.ExtKit;
import org.netbeans.modules.editor.lib.drawing.ChainDrawMark;
import org.netbeans.modules.editor.lib.drawing.MarkChain;
import org.openide.awt.Actions;
import org.openide.awt.DynamicMenuContent;
import org.openide.util.ContextAwareAction;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.actions.Presenter;
import org.openide.util.lookup.Lookups;
import org.openide.util.lookup.ProxyLookup;

/** Annotations class act as data model containing all annotations attached
 * to one document. Class uses instances of private class LineAnnotations for 
 * grouping of added annotations by line. These objects (LineAnnotations) are
 * referenced from two collections. First one is Map where the key is Mark. 
 * It is used during the drawing in DrawLayerFactory.AnnotationLayer - when
 * the mark appears in mark change, the LineAnnotations instance is found for 
 * it and the active annotation on the line can be queried.
 * Second is List where the LineAnnotations are sorted by line number. This
 * list is used for drawing the annotations in the gutter when the sequential
 * order is important.
 * 
 * The class also listen on document. It need to know how many lines where
 * removed or added to refresh the LineAnnotations.line property.
 *
 * @author David Konecny
 * @since 07/2001
 */
public class Annotations implements DocumentListener {
    
    /** Map of [Mark, LineAnnotations] */
    private final Map lineAnnotationsByMark;
    
    /** List of [LineAnnotations] which is ordered by line number */
    private final List lineAnnotationsArray;

    private final MarkChain markChain;

    /** Reference to document */
    private final BaseDocument doc;

    /** List of listeners on AnnotationsListener*/
    private final EventListenerList listenerList;
    
    private final WeakEventListenerList weakListenerList;

    /** Property change listener on annotation type changes */
    private final PropertyChangeListener l;
    
    /** Whether the column with glyph icons is visible */
    private boolean glyphColumn = false;
    
    /** Whether the column with cycling button is visible*/
    private boolean glyphButtonColumn = false;
    
    /** Whether the gutter popup menu has been initialized */
    private boolean menuInitialized = false;

    /** Sorts the subMenu items */
    public static final Comparator MENU_COMPARATOR = new MenuComparator();

    private int lastGetLineAnnotationsIdx = -1;
    private int lastGetLineAnnotationsLine = -1;
    private LineAnnotations lastGetLineAnnotationsResult = null;

    public Annotations(BaseDocument doc) {
        lineAnnotationsByMark = new HashMap(30);
        lineAnnotationsArray = new ArrayList(20);
        listenerList =  new EventListenerList();
        weakListenerList = new WeakEventListenerList();
        
        this.doc = doc;
        this.markChain = new MarkChain(doc, null);

        // listener on document changes
        this.doc.addDocumentListener(this);
        
        l = new PropertyChangeListener() {
            public void propertyChange (PropertyChangeEvent evt) {
                if (evt.getPropertyName() == null || AnnotationDesc.PROP_ANNOTATION_TYPE.equals(evt.getPropertyName())) {
                    LineAnnotations lineAnnos;
                    synchronized (lineAnnotationsArray) {
                        AnnotationDesc anno = (AnnotationDesc)evt.getSource();
                        lineAnnos = (LineAnnotations)lineAnnotationsByMark.get(anno.getMark());
                        if (lineAnnos != null) {
                            lineAnnos.refreshAnnotations();
                        }
                    }
                    if (lineAnnos != null) {
                        refreshLine(lineAnnos.getLine());
                    }
                }
                if (evt.getPropertyName() == null || AnnotationDesc.PROP_MOVE_TO_FRONT.equals(evt.getPropertyName())) {
                    AnnotationDesc anno = (AnnotationDesc)evt.getSource();
                    frontAnnotation(anno);
                }
            }
        };

        AnnotationTypes.getTypes().addPropertyChangeListener( new PropertyChangeListener() {
            public void propertyChange (PropertyChangeEvent evt) {
                if (evt.getPropertyName() == null || AnnotationTypes.PROP_COMBINE_GLYPHS.equals(evt.getPropertyName())) {
                    synchronized (lineAnnotationsArray) {
                        for( Iterator it = lineAnnotationsArray.iterator(); it.hasNext(); ) {
                            LineAnnotations lineAnnos = it.next();
                            lineAnnos.refreshAnnotations();
                        }
                    }
                }
                if (evt.getPropertyName() == null || AnnotationTypes.PROP_ANNOTATION_TYPES.equals(evt.getPropertyName())) {
                    synchronized (lineAnnotationsArray) {
                        for( Iterator it = lineAnnotationsArray.iterator(); it.hasNext(); ) {
                            LineAnnotations lineAnnos = it.next();
                            for( Iterator it2 = lineAnnos.getAnnotations(); it2.hasNext(); ) {
                                AnnotationDesc anno = (AnnotationDesc)it2.next();
                                anno.updateAnnotationType();
                            }
                        }
                    }
                }
                fireChangedAll();
            }
        });
        
    }

    /** Add annotation */
    public void addAnnotation(AnnotationDesc anno) {
        LineAnnotations lineAnnos;
        synchronized (lineAnnotationsArray) {
            // create mark for this annotation. One mark can be shared by more annotations
            MarkChain chain = markChain;
            try {
                /* Always adds a fresh mark. It helps to behave well
                 * in the following scenario:
                 * 1. add annotaion at line e.g. 3.
                 * 2. add annotation at line 4.
                 * 3. goto begining of line 2.
                 * 4. select lines 2,3,4,5 by pressing down arrow.
                 * 5. remove selected lines.
                 * 6. Undo by ctrl-z.
                 * If reusing marks this will result into line selection
                 * being at line 2 but gutter marking of annotations
                 * being at line 4.
                 * It happens because although DocumentLine.updatePositionRef()
                 * removes and adds the annotations on modified lines, it does that
                 * sequentially for each line so it removes
                 * first of the two DocumentLine's annotations
                 * and tries to readd it to begining of removed block
                 * but it finds there an existing mark
                 * from second DocumentLine's annotation.
                 * It reuses the mark but as the mark was inside
                 * the removed block originally the undo restores its position
                 * inside the block.
                 * Creation of fresh marks fixes that problem.
                 */
                chain.addMark(anno.getOffset(), true);

            } catch (BadLocationException e) {
                /*
                 * This is problematic point.
                 * Once this place is reached the annotation desc
                 * will have null mark and will
                 * not in fact be actively present
                 * in the document.
                 * Such annotation will then throw NPE
                 * in removeAnnotation().
                 * Hopefully causes of getting to this place
                 * were eliminated.
                 */
                throw new IllegalStateException("offset=" + anno.getOffset() // NOI18N
                    + ", docLen=" + doc.getLength()); // NOI18N
            }
            // attach created mark to annotation
            ChainDrawMark annoMark = chain.getAddedMark();
            if (annoMark == null) {
                throw new NullPointerException();
            }
            anno.setMark(annoMark);

            // #33165 - different strategy - first trying to search
            // by the mark in the map [mark, lineAnnos]. That should
            // eliminate problems when a mark is tried to be removed
            // from the mark chain a second time.
            // Hopefully this will not cause any other sorts of problems.

            lineAnnos = (LineAnnotations)lineAnnotationsByMark.get(annoMark);
            if (lineAnnos == null) {
                // fine LineAnnotations instance corresponding to the line of this annotation
                // or create new LineAnnotations if this is first annotation on this line
                lineAnnos = getLineAnnotations(anno.getLine());
                if (lineAnnos == null) {
                    lineAnnos = new LineAnnotations();
                    lineAnnos.addAnnotation(anno);
                    if (lineAnnotationsByMark.put(anno.getMark(), lineAnnos) != null) {
                        throw new IllegalStateException("Mark already in the map."); // NOI18N
                    }

                    // insert newly created LineAnnotations into sorted array
                    int pos = Collections.binarySearch(lineAnnotationsArray, lineAnnos, new LineAnnotationsComparator());

                    lineAnnotationsArray.add(-pos - 1, lineAnnos);
                    lastGetLineAnnotationsIdx = -1;
                    lastGetLineAnnotationsLine = -1;
                    lastGetLineAnnotationsResult = null;
                }
                else {
                    lineAnnos.addAnnotation(anno);
                    // check whether this mark is in lineAnnotationsByMark Map
                    // it is possible that Line.Part annotations will have more marks
                    // for one line
                    if (lineAnnotationsByMark.get(anno.getMark()) == null)
                        lineAnnotationsByMark.put(anno.getMark(), lineAnnos);
                }

            } else { // mark was already in lineAnnotationsByMark map
                lineAnnos.addAnnotation(anno);
            }

            // add listener on changes of annotation type
            anno.addPropertyChangeListener(l);

            // ignore annotation types with default icon
            if (anno.isVisible() && (!anno.isDefaultGlyph() || (anno.isDefaultGlyph() && lineAnnos.getCount() > 1))) {
                glyphColumn = true;
            }

            if (lineAnnos.getCount() > 1)
                glyphButtonColumn = true;
        }

        // notify view that it must be redrawn
        refreshLine(lineAnnos.getLine());

//        System.out.println("AFTER ADD:\n" + dumpLineAnnotationsArray());
    }

    /** Remove annotation */
    public void removeAnnotation(AnnotationDesc anno) {
        int line;
        synchronized (lineAnnotationsArray) {
            // find LineAnnotations for the mark
            ChainDrawMark annoMark = (ChainDrawMark)anno.getMark();
            if (annoMark == null) {
                // Already removed? See #116955
                return;
            }

            LineAnnotations lineAnnos = (LineAnnotations)lineAnnotationsByMark.get(annoMark);

            line = lineAnnos.getLine();
            // remove annotation from the line
            lineAnnos.removeAnnotation(anno);

            // check if this mark is referenced or not. If not, remove it
            if (!lineAnnos.isMarkStillReferenced(annoMark)) {
                if (lineAnnotationsByMark.remove(annoMark) != lineAnnos) {
                    throw new IllegalStateException();
                }

                MarkChain chain = markChain;
                // Partial fix of #33165 - rather remove mark explicitly than through its offset
                //chain.removeMark(anno.getOffset());
                if (!chain.removeMark(annoMark)) {
                    throw new IllegalStateException("Mark not removed"); // NOI18N
                }
            }

            // if there is no more annotations on the line, remove LineAnnotations
            if (lineAnnos.getCount() == 0) {
                int index = lineAnnotationsArray.indexOf(lineAnnos);
                // #90220 - Sometimes there seems to be an inconsistency that the lineAnnos
                // is not present in the array.
                if (index != -1) {
                    lastGetLineAnnotationsIdx = -1;
                    lastGetLineAnnotationsLine = -1;
                    lastGetLineAnnotationsResult = null;
                    lineAnnotationsArray.remove(index);
                }
            }

            // clear the mark from annotation
            anno.setMark(null);

            // remove listener on changes of annotation type
            anno.removePropertyChangeListener(l);
        }
        
        // notify view that must be redrawn
        refreshLine(line);
//        System.out.println("AFTER REMOVE:\n" + dumpLineAnnotationsArray());
    }
    
    /** Finds active annotation for the Mark. It is called from DrawLayer 
     * when it found the DrawMark */
    public AnnotationDesc getActiveAnnotation(Mark mark) {
        synchronized (lineAnnotationsArray) {
            LineAnnotations annos;
            annos = (LineAnnotations)lineAnnotationsByMark.get(mark);
            if (annos == null) {
                return null;
            }
            AnnotationDesc anno = annos.getActive();
            // it is possible that some other mark on the line (means
            // some other annotations) is active
            if (anno==null || anno.getMark() != mark) {
                return null;
            }
            return anno;
        }
    }

    /** Returns the active annotation for the line given by Mark. */
    AnnotationDesc getLineActiveAnnotation(Mark mark) {
        synchronized (lineAnnotationsArray) {
            LineAnnotations annos;
            annos = (LineAnnotations)lineAnnotationsByMark.get(mark);
            if (annos == null) {
                return null;
            }
            AnnotationDesc anno = annos.getActive();
            return anno;
        }
    }
    
    /** Finds LineAnnotations for the given line number */
    protected LineAnnotations getLineAnnotations(int line) {
        synchronized (lineAnnotationsArray) {
            LineAnnotations annos;

            if (lastGetLineAnnotationsIdx != -1 && lastGetLineAnnotationsLine != -1 && lastGetLineAnnotationsResult != null) {
                if (lastGetLineAnnotationsLine == line) {
                    return lastGetLineAnnotationsResult;
                } else if (   lastGetLineAnnotationsLine + 1 == line && lastGetLineAnnotationsIdx + 1 < lineAnnotationsArray.size()
                           && (annos = (LineAnnotations)lineAnnotationsArray.get(lastGetLineAnnotationsIdx + 1)).getLine() == line) {
                    lastGetLineAnnotationsIdx++;
                    lastGetLineAnnotationsLine = annos.getLine();
                    lastGetLineAnnotationsResult = annos;
                    return annos;
                }
            }

            // since fix of #33165 it is possible to use binary search here
            int low = 0;
            int high = lineAnnotationsArray.size() - 1;
            while (low <= high) {
                int mid = (low + high) / 2;
                annos = (LineAnnotations)lineAnnotationsArray.get(mid);
                int annosLine = annos.getLine();
                if (line < annosLine) {
                    high = mid - 1;
                } else if (line > annosLine) {
                    low = mid + 1;
                } else {
                    lastGetLineAnnotationsIdx = mid;
                    lastGetLineAnnotationsLine = annosLine;
                    lastGetLineAnnotationsResult = annos;
                    return annos;
                }
            }

            lastGetLineAnnotationsIdx = -1;
            lastGetLineAnnotationsLine = -1;
            lastGetLineAnnotationsResult = null;
            return null;
        }
    }

    /** Returns the active annotation for the given line number.
     * It is called from the glyph gutter*/
    public AnnotationDesc getActiveAnnotation(int line) {
        LineAnnotations annos = getLineAnnotations(line);
        if (annos == null)
            return null;
        return annos.getActive();
    }

    /** Move annotation in front of others. The activated annotation
     * is moved in front of other annotations on the same line */
    public void frontAnnotation(AnnotationDesc anno) {
        int line = anno.getLine();
        LineAnnotations annos = getLineAnnotations(line);
        if (annos == null)
            return;
        annos.activate(anno);
        refreshLine(line);
    }

    /** Activate next annotation on the line. Used for cycling 
     * through the annotations */
    public AnnotationDesc activateNextAnnotation(int line) {
        LineAnnotations annos = getLineAnnotations(line);
        if (annos == null)
            return null;
        AnnotationDesc aa = annos.activateNext();
        refreshLine(line);
        return aa;
    }

    /** Get next line number with some annotation*/
    public int getNextLineWithAnnotation(int line) {
        synchronized (lineAnnotationsArray) {
            LineAnnotations annos;

            if (lastGetLineAnnotationsIdx != -1 && lastGetLineAnnotationsLine != -1 && lastGetLineAnnotationsResult != null) {
                if (lastGetLineAnnotationsLine == line) {
                    return lastGetLineAnnotationsLine;
                } else if (lastGetLineAnnotationsLine + 1 == line && lastGetLineAnnotationsIdx + 1 < lineAnnotationsArray.size()) {
                    annos = (LineAnnotations)lineAnnotationsArray.get(lastGetLineAnnotationsIdx + 1);
                    lastGetLineAnnotationsIdx++;
                    lastGetLineAnnotationsLine = annos.getLine();
                    lastGetLineAnnotationsResult = annos;
                    return lastGetLineAnnotationsLine;
                }
            }


            // since fix of #33165 it is possible to use binary search here
            int low = 0;
            int high = lineAnnotationsArray.size() - 1;
            while (low <= high) {
                int mid = (low + high) / 2;
                annos = (LineAnnotations)lineAnnotationsArray.get(mid);
                int annosLine = annos.getLine();
                if (line < annosLine) {
                    high = mid - 1;
                } else if (line > annosLine) {
                    low = mid + 1;
                } else {
                    lastGetLineAnnotationsIdx = mid;
                    lastGetLineAnnotationsLine = annosLine;
                    lastGetLineAnnotationsResult = annos;
                    return annosLine;
                }
            }

            // not found annotation on exact line, follow the search with simple searching
            for (int i=low; i= line){
                    lastGetLineAnnotationsIdx = i;
                    lastGetLineAnnotationsLine = annosLine;
                    lastGetLineAnnotationsResult = annos;
                    return annosLine;
                }
            }

            lastGetLineAnnotationsIdx = -1;
            lastGetLineAnnotationsLine = -1;
            lastGetLineAnnotationsResult = null;
            return -1;
        }
    }

    /**
     * Searches line annotations for given type
     * @param line source line
     * @param type annotation type name
     * @return {@link AnnotationDesc} of given type on provided line
     */
    public AnnotationDesc getAnnotation(int line, String type) {
        LineAnnotations lineAnnotations = getLineAnnotations(line);
        if (lineAnnotations != null)
            return lineAnnotations.getType(type);
        else
            return null;
    }
        
    /** 
     * Return list of passive annotations which should be drawn on the background.
     * @deprecated The method name contains a typo. Use {@link getPassiveAnnotationsForLine} 
     */
    public AnnotationDesc[] getPasiveAnnotations(int line) {
        return getPassiveAnnotationsForLine(line);
    }
    
    /**
     * @param line
     * @return list of passive annotations which should be drawn on the background.
     */
    public AnnotationDesc[] getPassiveAnnotationsForLine(int line) {
        LineAnnotations annos = getLineAnnotations(line);
        if (annos == null)
            return null;
        return annos.getPassive();
    }
    
    /** Return list of passive annotations attached on the line of given offset */
    public AnnotationDesc[]  getPassiveAnnotations(int offset){
        int lineIndex = doc.getDefaultRootElement().getElementIndex(offset);
        return (lineIndex>=0) ? getPassiveAnnotationsForLine(lineIndex) : null;
    }

    /** Returns number of visible annotations on the line*/
    public int getNumberOfAnnotations(int line) {
        LineAnnotations annos = getLineAnnotations(line);
        if (annos == null)
            return 0;
        return annos.getCount();
    }
    

    /** Notify view that it is necessary to redraw the line of the document  */
    protected void refreshLine(int line) {
        fireChangedLine(line);
    }
    
    /** Checks the number of removed lines and recalculate
     * LineAnnotations.line property */
    public void removeUpdate(DocumentEvent e) {
        BaseDocumentEvent be = (BaseDocumentEvent)e;
        int countOfDeletedLines = be.getLFCount();
        if (countOfDeletedLines == 0)
            return;
        
/* #33165 - line in line-annotations handled in a different way
        int changedLine = be.getLine();

        LineAnnotations annos;
        for (int i=0; i changedLine && annos.getLine() < changedLine+countOfDeletedLines)
                annos.setLine(changedLine);
            if (annos.getLine() > changedLine)
                annos.setLine(annos.getLine()-countOfDeletedLines);
        }
 */
        // fire event to AnnotationsListeners that everything should be redraw
        fireChangedAll();
    }
    
    /** Checks the number of inserted lines and recalculate
     * LineAnnotations.line property */
    public void insertUpdate(DocumentEvent e) {
        BaseDocumentEvent be = (BaseDocumentEvent)e;
        int countOfInsertedLines = be.getLFCount();
        if (countOfInsertedLines == 0)
            return;
        
/* #33165 - line in line-annotations handled in a different way
        int changedLine = be.getLine();

        LineAnnotations annos;
        LineAnnotations current = null;
        for (int i=0; i e.getOffset())
                current = annos;
            if (annos.getLine() > changedLine)
                annos.setLine(annos.getLine()+countOfInsertedLines);
        }
        if (current != null)
            current.setLine(current.getLine()+countOfInsertedLines);
 */
        
        // fire event to AnnotationsListeners that everything should be redraw
        fireChangedAll();
    }
    
    /**Gives notification that an attribute or set of attributes changed.*/
    public void changedUpdate(DocumentEvent e) {
    }
    
    /** Add AnnotationsListener listener */
    public void addAnnotationsListener(AnnotationsListener listener) {
        if (listener instanceof GlyphGutter) {
            weakListenerList.add(AnnotationsListener.class, listener);
        } else {
            listenerList.add(AnnotationsListener.class, listener);
        }
    }

    /** Remove AnnotationsListener listener */
    public void removeAnnotationsListener(AnnotationsListener listener) {
        if (listener instanceof GlyphGutter) {
            weakListenerList.remove(AnnotationsListener.class, listener);
        } else {
            listenerList.remove(AnnotationsListener.class, listener);
        }
    }

    /** Fire AnnotationsListener.ChangedLine change*/
    protected void fireChangedLine(int line) {
	// Guaranteed to return a non-null array
	Object[][] listeners2 = new Object[][]{listenerList.getListenerList(), weakListenerList.getListenerList()};
        
	// Process the listeners last to first, notifying
	// those that are interested in this event
        for (int j = 0; j < listeners2.length; j++) {
            Object[] listeners = listeners2[j];
        
	    for (int i = listeners.length - 2; i >= 0; i -= 2) {
                if (listeners[i] == AnnotationsListener.class) {
		// Lazily create the event:
                    // if (e == null)
                    // e = new ListSelectionEvent(this, firstIndex, lastIndex);
                    ((AnnotationsListener) listeners[i + 1]).changedLine(line);
                }
            }
        }
    }
   
    /** Fire AnnotationsListener.ChangedAll change*/
    protected void fireChangedAll() {
	// Guaranteed to return a non-null array
	Object[][] listeners2 = new Object[][]{listenerList.getListenerList(), weakListenerList.getListenerList()};
	// Process the listeners last to first, notifying
	// those that are interested in this event
        for (int j = 0; j < listeners2.length; j++) {
            Object[] listeners = listeners2[j];
            for (int i = listeners.length - 2; i >= 0; i -= 2) {
                if (listeners[i] == AnnotationsListener.class) {
		// Lazily create the event:
                    // if (e == null)
                    // e = new ListSelectionEvent(this, firstIndex, lastIndex);
                    ((AnnotationsListener) listeners[i + 1]).changedAll();
                }
            }
        }
    }

    /** Return whether this document has or had any glyph icon attached.
     * This method is called from glyph gutter to check whether the glyph column
     * should be drawn or not. */
    public boolean isGlyphColumn() {
        return glyphColumn;
    }
    
    /** Return whether this document has or had more annotations on one line.
     * This method is called from glyph gutter to check whether the glyph cycling 
     * column should be drawn or not. */
    public boolean isGlyphButtonColumn() {
        return glyphButtonColumn;
    }

    
    private void addAcceleretors(Action a, JMenuItem item, BaseKit kit){
        // Try to get the accelerator
        javax.swing.text.JTextComponent target = Utilities.getFocusedComponent();
        if (target == null) return;
        javax.swing.text.Keymap km = target.getKeymap();
        if (km != null) {
            javax.swing.KeyStroke[] keys = km.getKeyStrokesForAction(a);
            if (keys != null && keys.length > 0) {
                item.setAccelerator(keys[0]);
            }else{
                // retrieve via actionName
                String actionName = (String)a.getValue(Action.NAME);
                if (actionName == null) return;
                BaseAction action = (BaseAction)kit.getActionByName(actionName);
                if (action == null) return;
                keys = km.getKeyStrokesForAction(action);
                if (keys != null && keys.length > 0) {
                    item.setAccelerator(keys[0]);
                }                        
            }
        }
    }
    
    private Action getAction(AnnotationDesc anno, Action action) {
        if (action instanceof ContextAwareAction && anno instanceof Lookup.Provider) {
            Lookup lookup = ((Lookup.Provider) anno).getLookup();
            action = ((ContextAwareAction) action).createContextAwareInstance(lookup);
        }
        return action;
    }
    
    /** Creates menu item for the given action. It must handle the BaseActions, which
     * have localized name stored not in Action.NAME property. */
    private JMenuItem createMenuItem(Action action, BaseKit kit) {
        JMenuItem item;
        if (action instanceof BaseAction) {
            item = new JMenuItem( ((BaseAction)action).getPopupMenuText(null) );
            item.addActionListener(action);
            addAcceleretors(action, item, kit);
        } else if (action instanceof Presenter.Popup) {
            item = ((Presenter.Popup) action).getPopupPresenter();        
        } else {
            item = new JMenuItem( (String)action.getValue(Action.NAME) );
            Actions.connect(item, action, true);
            addAcceleretors(action, item, kit);
        }
        return item;
    }

    /** Creates popup menu with all actions for the given line. */
    public JPopupMenu createPopupMenu(BaseKit kit, int line) {
        return createMenu(kit, line, false).getPopupMenu();
    }

    private JMenu createSubMenu(AnnotationDesc anno, BaseKit kit) {
        JMenu subMenu = null;
        Action[] actions = anno.getActions();
        if (actions != null) {
            subMenu = new JMenu(anno.getAnnotationTypeInstance().getDescription());
            for (int j=0; j createSubMenus(AnnotationDesc anno, BaseKit kit) {
        JMenu subMenu = createSubMenu(anno, kit);
        if (subMenu != null) {
            return Collections.singletonList(subMenu);
        } else if (anno instanceof AnnotationCombination) {
            // if the annotation does not have defined actions,
            // add actions of all sub-annotation
            List subAnnotations = ((AnnotationCombination) anno).getCombinedAnnotations();
            List subMenus = new ArrayList(subAnnotations.size());
            for (AnnotationDesc ad : subAnnotations) {
                subMenu = createSubMenu(ad, kit);
                if (subMenu != null) {
                    subMenus.add(subMenu);
                }
            }
            return subMenus;
        } else {
            return Collections.emptyList();
        }
    }

    private void initMenu(JMenu pm, BaseKit kit, int line){
        LineAnnotations annos = getLineAnnotations(line);
        Map types = new HashMap(AnnotationTypes.getTypes().getVisibleAnnotationTypeNamesCount() * 4/3);

        TreeSet orderedSubMenus = new TreeSet(MENU_COMPARATOR);

        if (annos != null) {
            
            // first, add actions for active annotation
            AnnotationDesc anno = annos.getActive();
            if (anno != null) {
                List subMenus = createSubMenus(anno, kit);
                orderedSubMenus.addAll(subMenus);
                types.put(anno.getAnnotationType(), anno.getAnnotationType());
            }

            // second, add submenus for all pasive annotations
            AnnotationDesc[] pasiveAnnos = annos.getPassive();
            if (pasiveAnnos != null) {
                for (int i=0; i < pasiveAnnos.length; i++) {
                    List subMenus = createSubMenus(pasiveAnnos[i], kit);
                    orderedSubMenus.addAll(subMenus);
                    types.put(pasiveAnnos[i].getAnnotationType(), pasiveAnnos[i].getAnnotationType());
                }
            }
        }

        // third, add all remaining possible actions to the end of the list
        AnnotationType type;
        for (Iterator i = AnnotationTypes.getTypes().getAnnotationTypeNames(); i.hasNext(); ) {
            type = AnnotationTypes.getTypes().getType(i.next());
            if (type == null || !type.isVisible())
                continue;
            if (types.get(type.getName()) != null)
                continue;
            Action[] actions = type.getActions();
            if (actions != null) {
                JMenu subMenu = new JMenu(type.getDescription());
                for (int j=0; j 0){
                    //pm.add(subMenu);
                    orderedSubMenus.add(subMenu);
                }
            }
        }
        
        if (!orderedSubMenus.isEmpty()){
            Iterator iter = orderedSubMenus.iterator();
            while(iter.hasNext()){
                JMenu subMenu = iter.next();
                pm.add(subMenu);
            }
            pm.addSeparator();
        }
        
        
        // add checkbox for enabling/disabling of line numbers
        Action action = kit.getActionByName(BaseKit.toggleLineNumbersAction);
        JMenuItem popupItem = getPopupMenuItem(action);
        if (popupItem != null)
            pm.add(popupItem);
        
        action = kit.getActionByName(ExtKit.toggleToolbarAction);
        if (action != null) {
            popupItem = getPopupMenuItem(action);
            if (popupItem != null)
                pm.add(popupItem);
        }
        menuInitialized = true;
    }

    private static JMenuItem getPopupMenuItem(Action action) {
        JMenuItem popupItem = null;
        if (action instanceof BaseAction) {
            popupItem = ((BaseAction) action).getPopupMenuItem(null);
        }
        if (popupItem == null) {
            if (action instanceof Presenter.Popup) {
                popupItem = ((Presenter.Popup) action).getPopupPresenter();
            }
        }
        return popupItem;
    }

    private static final class DelayedMenu extends JMenu{
        
        RequestProcessor.Task task;
        
        public DelayedMenu(String s){
            super(s);
        }
        
        public @Override JPopupMenu getPopupMenu() {
            RequestProcessor.Task t = task;
            if (t!=null && !t.isFinished()){
                t.waitFinished();
            }
            return super.getPopupMenu();
        }
        
        void setTask(RequestProcessor.Task task){
            this.task = task;
        }
        
        void clearTask() {
            this.task = null; // clear the finished task to avoid leaking
        }
    } // End of DelayedMenu class
    
    private JMenu createMenu(BaseKit kit, int line, boolean backgroundInit){
        final DelayedMenu pm = new DelayedMenu(NbBundle.getBundle(BaseKit.class).getString("generate-gutter-popup"));
        final BaseKit fKit = kit;
        final int fLine = line;

        if (backgroundInit){
            RequestProcessor rp = RequestProcessor.getDefault();
            RequestProcessor.Task task = rp.create(new Runnable(){
                public void run(){
                    initMenu(pm, fKit, fLine);
                    pm.clearTask(); // clear the finished task reference to avoid leaking
                }
            });
            pm.setTask(task); // set before task execution so that always cleaned properly
            task.schedule(0);
        }else{
            initMenu(pm, fKit, fLine);
        }

        return pm;
    }
    
    /** Creates popup menu with all actions for the given line. */
    public JMenu createMenu(BaseKit kit, int line) {
        boolean bkgInit = menuInitialized;
        menuInitialized = true;
        return createMenu(kit, line, !bkgInit);
    }
    
    private String dumpAnnotaionDesc(AnnotationDesc ad) {
        return "offset=" + ad.getOffset() // NOI18N
            + "(ls=" + doc.getParagraphElement(ad.getOffset()).getStartOffset() // NOI18N
            + "), line=" + ad.getLine() // NOI18N
            + ", type=" + ad.getAnnotationType(); // NOI18N
    }

    private String dumpLineAnnotationsArray() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < lineAnnotationsArray.size(); i++) {
            LineAnnotations la = lineAnnotationsArray.get(i);
            LinkedList annos = la.annos;
            sb.append("[" + i + "]: line=" + la.getLine() // NOI18N
                + ", anos:"); // NOI18N
            for (AnnotationDesc ad : annos) {
                sb.append("\n    " + dumpAnnotaionDesc(ad)); // NOI18N
            }
            sb.append('\n');
        }
        return sb.toString();
    }

    @Override
    public String toString() {
        return dumpLineAnnotationsArray() + ", lastGetLineAnnotationsIdx = " + lastGetLineAnnotationsIdx + ", lastGetLineAnnotationsLine = " + lastGetLineAnnotationsLine;
    }
            
    /** Manager of all annotations attached to one line. Class stores
     * the references to all annotations from one line in List and also
     * stores which annotation is active, count of visible annotations 
     * and line number. */
    public static class LineAnnotations extends Object {

        /** List with all annotations in this LineAnnotations */
        private final LinkedList annos;

        /** List with all visible annotations in this LineAnnotations */
        private LinkedList annosVisible;
        
        /** Active annotation. Used only in case there is more than one
         * annotation on the line */
        private AnnotationDesc active;
        
        protected LineAnnotations() {
            annos = new LinkedList();
            annosVisible = new LinkedList();
        }

        /** Add annotation to this line and activate it. */
        public void addAnnotation(AnnotationDesc anno) {
            synchronized (annos) {
                annos.add(anno);
                refreshAnnotations();
            }
        }
        
        /** Remove annotation from this line. Refresh the active one
         * and count of visible. */
        public void removeAnnotation(AnnotationDesc anno) {
            synchronized (annos) {
                annos.remove(anno);
                refreshAnnotations();
            }
        }

        /** Return the active line annotation. */
        public AnnotationDesc getActive() {
            synchronized (annos) {
                return active;
            }
        }

        /**
         * @param type searched annotation type name
         * @return {@link AnnotationDesc} of given type from currently visible
         * annotations or null if there is none for given type
         */
        AnnotationDesc getType(String type) {
            synchronized (annos) {
                for (AnnotationDesc ad : annosVisible) {
                    if (ad.getAnnotationType().equals(type)) {
                        return ad;
                    }
                }
                return null;
            }
        }

        /** Getter for the line number property */
        public int getLine() {
            // #33165 - delegating of getting of the line number to first anno
            synchronized (annos) {
                return (annos.size() > 0)
                    ? annos.peek().getLine()
                    : 0;
            }
        }

        /** Setter for the line number property */
        public void setLine(int line) {
            throw new IllegalStateException("Setting of line number not allowed"); // NOI18N
        }

        /** 
         * Gets the array of all passive and visible annotations 
         * @deprecated The method name contains typo. Use {@link getPassive()} instead.
         */
        public AnnotationDesc[] getPasive() {
            return getPassive();
        }

        /** Gets the array of all passive and visible annotations */
        public AnnotationDesc[] getPassive() {
            synchronized (annos) {
                if (getCount() <= 1)
                    return null;
                AnnotationDesc[] pasives = new AnnotationDesc[getCount()-1];
                int startIndex = annosVisible.indexOf(getActive());
                int index = startIndex;
                int i=0;
                while (true) {
                    index++;
                    if (index >= annosVisible.size())
                        index = 0;
                    if (index == startIndex)
                        break;

                    pasives[i] = annosVisible.get(index);
                    i++;
                }
                return pasives;
            }
        }

        /** Make the given annotation active. */
        public boolean activate(AnnotationDesc anno) {
            synchronized (annos) {
                int i,j;
                i = annosVisible.indexOf(anno);

                if (i == -1) {
                    // was anno combined by some type ??
                    for(j=0; j < annosVisible.size(); j++) {
                        if (annosVisible.get(j) instanceof AnnotationCombination) {
                            if (((AnnotationCombination)annosVisible.get(j)).isAnnotationCombined(anno)) {
                                i = j;
                                anno = (AnnotationCombination)annosVisible.get(j);
                                break;
                            }
                        }
                    }
                }

                if (i == -1)
                    return false;

                if (annosVisible.get(i) == null)
                    return false;

                if (anno == active || !anno.isVisible())
                    return false;

                active = anno;

                return true;
            }
        }

        /** Get count of visible annotations on the line */
        public int getCount() {
            synchronized (annos) {
                return annosVisible.size();
            }
        }

        /** Activate next annoation on the line. Used during the cycling. */
        public AnnotationDesc activateNext() {
            synchronized (annos) {
                if (getCount() <= 1)
                    return active;

                int current = annosVisible.indexOf(active);
                current++;
                if (current >= getCount())
                    current = 0;
                active = annosVisible.get(current);
                return active;
            }
        }
        
        /** Searches all combination annotation type and sort them
         * by getCombinationOrder into combTypes array
         * which is passed as paramter. */
        private static void fillInCombinationsAndOrderThem(LinkedList combTypes) {
            AnnotationType type;
            AnnotationType.CombinationMember[] combs;
            
            AnnotationTypes types = AnnotationTypes.getTypes();
            for (Iterator it = types.getAnnotationTypeNames(); it.hasNext(); ) {
                String typeName = it.next();
                type = types.getType(typeName);
                if (type == null)
                    continue;
                combs = type.getCombinations();
                if (combs != null && type.isWholeLine() &&
                    (combs.length >= 2 || (combs.length == 1 && combs[0].isAbsorbAll())) ) {
                    if (type.getCombinationOrder() == 0) {
                        combTypes.add(type);
                    } else {
                        boolean inserted = false;
                        for (int i=0; i < combTypes.size(); i++) {
                            if ( (combTypes.get(i)).getCombinationOrder() > type.getCombinationOrder()) {
                                combTypes.add(i, type);
                                inserted = true;
                                break;
                            }
                        }
                        if (!inserted)
                            combTypes.add(type);
                    }
                }
            }
        }

        /** For the given combination annotation type and list of annotations
         * it finds all annotations which are combined by this combination
         * and inserts into list of annotations new combined annotation which
         * wraps combined annotations. The result list of annotations can
         * contain null values for annotations which were combined. */
        private boolean combineType(AnnotationType combType, LinkedList annosDupl) {

            int i, j, k;
            boolean matchedType;
            int countOfAnnos = 0;
            int valid_optional_count = 0;
            
            LinkedList combinedAnnos = new LinkedList();

            AnnotationType.CombinationMember[] combs = combType.getCombinations();
            
            // check that there is match between line annos & all types specified in combination
            boolean matchedComb = true;
            AnnotationType.CombinationMember comb;
            AnnotationDesc anno;
            for (i=0; i < combs.length; i++) {

                comb = combs[i];
                matchedType = false;
                
                // check that for one specified combination type there exist some annotation
                for (j=0; j < annosDupl.size(); j++) {
                    
                    anno = annosDupl.get(j);
                    
                    if (anno == null)
                        continue;

                    // check whether this annotation matches the specified combination type
                    if (comb.getName().equals( anno.getAnnotationType() )) {
                        countOfAnnos++;

                        // now check if the combination has specified some minimum count of annos
                        if (comb.getMinimumCount() == 0) {
                            matchedType = true;
                            countOfAnnos++;
                            combinedAnnos.add(anno);
                            if (!comb.isAbsorbAll())
                                break;
                        } else {
                            int requiredCount = comb.getMinimumCount() - 1;
                            for (k=j+1; (k < annosDupl.size()) && (requiredCount > 0); k++) {
                                if (annosDupl.get(k) == null)
                                    continue;
                                if (comb.getName().equals( (annosDupl.get(k)).getAnnotationType() )) {
                                    requiredCount--;
                                }
                            }
                            if (requiredCount == 0) {
                                matchedType = true;
                                
                                combinedAnnos.add(anno);
                                for (k=j+1; k < annosDupl.size(); k++) {
                                    if (annosDupl.get(k) == null)
                                        continue;
                                    if (comb.getName().equals( ((AnnotationDesc)annosDupl.get(k)).getAnnotationType() )) {
                                        countOfAnnos++;
                                        combinedAnnos.add(annosDupl.get(k));
                                    }
                                }
                            }
                            break;
                        }
                        
                    }
                    
                }
                
                if (matchedType) {
                    if (comb.isOptional())
                        valid_optional_count++;
                } else {
                    if (!comb.isOptional()) {
                        matchedComb = false;
                        break;
                    }
                }
                
            }
            if (combType.getMinimumOptionals() > valid_optional_count)
                matchedComb = false;

            AnnotationCombination annoComb = null;
            if (matchedComb) {

                boolean activateComb = false;
                
                for (i=0; i();
                    for (AnnotationDesc ad : annos) {
                        if (ad.isVisible()) {
                            annosVisible.add(ad);
                        }
                    }
                } else {

                    // combination are enabled
                    LinkedList annosDupl = new LinkedList(annos);

                    // List of all annotation types
                    LinkedList combTypes = new LinkedList();

                    // first, fill in the array with combination types sorted by the order
                    fillInCombinationsAndOrderThem(combTypes);

                    for (int ct=0; ct < combTypes.size(); ct++) {
                        combineType(combTypes.get(ct), annosDupl);
                    }

                    annosVisible = new LinkedList();

                    // add remaining not combined annotations into the line annotations array
                    for (AnnotationDesc ad : annosDupl) {
                        if (ad.isVisible()) {
                            annosVisible.add(ad);
                        }
                    }
                }

                //Order by priority
                Collections.sort(annosVisible);

                // update the active annotation
                if (annosVisible.size() > 0) {
                        active = annosVisible.get(0);
                }
            }
        }

        /** Is this given mark still referenced by some annotation or it
         * can be removed from the draw mark chain */
        public boolean isMarkStillReferenced(Mark mark) {
            synchronized (annos) {
                for(AnnotationDesc ad : annos) {
                    if (ad.getMark() == mark)
                       return true;
                }
                return false;
            }
        }
        
        public Iterator getAnnotations() {
            synchronized (annos) {
                return annos.iterator();
            }
        }
        
    } // End of LineAnnotations class
    

    /** Listener for listening on changes in Annotations object.*/
    public interface AnnotationsListener extends EventListener {

        /** This method is fired when annotations on the line are changed - 
         * annotation was added, removed, changed, etc. */
        public void changedLine(int Line);

        /** It is not possible to trace what have changed and so the listeners
         * are only informed that something has changed and that the change
         * must be reflected somehow (most probably by complete redraw). */
        public void changedAll();

    } // End of AnnotationsListener interface

    /** Annotation which is used for representation of combined annotations. 
     * Some basic operations like getLine etc. are delegated to one of the
     * annotations which are representd by this combined annotation. The only
     * added functionality is for tooltip text and annotation type.
     */
    static final class AnnotationCombination extends AnnotationDesc implements Lookup.Provider {
        
        /** Delegate annotaiton */
        private AnnotationDesc delegate;

        /** Annotation type */
        private String type;
        
        /** List of annotations which are combined */
        private LinkedList list;
        
        public AnnotationCombination(String type, AnnotationDesc delegate) {
            super(delegate.getOffset(), delegate.getLength());
            this.delegate = delegate;
            this.type = type;
            updateAnnotationType();
            list = new LinkedList();
            list.add(delegate);
        }
        
        /** Getter for offset of this annotation  */
        public int getOffset() {
            return delegate.getOffset();
        }
        
        /** Getter for line number of this annotation  */
        public int getLine() {
            return delegate.getLine();
        }
        
        /** Getter for localized tooltip text for this annotation  */
        public String getShortDescription() {
            return getAnnotationTypeInstance().getDescription();
        }
        
        /** Getter for annotation type name  */
        public String getAnnotationType() {
            return type;
        }

        /** Add the annotation to this combination */
        public void addCombinedAnnotation(AnnotationDesc anno) {
            list.add(anno);
        }

        /** Is the given annotation part of this combination */
        public boolean isAnnotationCombined(AnnotationDesc anno) {
            if (list.indexOf(anno) == -1)
                return false;
            else
                return true;
        }

        List getCombinedAnnotations() {
            return list;
        }

        /** Get Mark which represent this annotation in document */
        /* package */ @Override Mark getMark() {
            return delegate.getMark();
        }

        public Lookup getLookup() {
            Lookup l = null;
            for (AnnotationDesc ad : list) {
                if (ad instanceof Lookup.Provider) {
                    Lookup adl = ((Lookup.Provider) ad).getLookup();
                    if (l == null) {
                        l = adl;
                    } else {
                        l = new ProxyLookup(l, adl);
                    }
                }
            }
            if (l == null) {
                l = Lookups.fixed(new Object[0]);
            }
            return l;
        }
        
    } // End of AnnotationCombination class

    public static final class MenuComparator implements Comparator {

        public MenuComparator() {
        }
        
        public int compare(JMenu menuOne, JMenu menuTwo) {
            if (menuTwo == null || menuOne == null) return 0;
            String menuOneText = menuOne.getText();
            String menuTwoText = menuTwo.getText();
            if (menuTwoText == null || menuOneText == null) return 0;
            return menuOneText.compareTo(menuTwoText);
        }
    } // End of MenuComparator class

    private static class LineAnnotationsComparator implements Comparator {
        @Override
        public int compare(LineAnnotations o1, LineAnnotations o2) {
            return o1.getLine() - o2.getLine();
        }
    }
    
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy