blob: 1defa19f57c24afa63d8c69955338525b104f6a1 [file] [log] [blame]
/*
* 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.modules.java.navigation;
import java.awt.Image;
import java.awt.datatransfer.Transferable;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Spliterators;
import java.util.function.Supplier;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.ModuleElement;
import javax.swing.Action;
import javax.swing.Icon;
import org.netbeans.api.actions.Openable;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.java.source.ClasspathInfo;
import org.netbeans.api.java.source.ElementHandle;
import org.netbeans.api.java.source.SourceUtils;
import org.netbeans.api.java.source.TreePathHandle;
import org.netbeans.api.java.source.ui.ElementIcons;
import org.netbeans.modules.java.navigation.ElementNode.Description;
import org.netbeans.modules.java.navigation.actions.OpenAction;
import org.netbeans.modules.refactoring.api.ui.RefactoringActionsFactory;
import org.openide.filesystems.FileObject;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.Parameters;
import org.openide.util.Union2;
import org.openide.util.Utilities;
import org.openide.util.datatransfer.PasteType;
import org.openide.util.lookup.AbstractLookup;
import org.openide.util.lookup.InstanceContent;
import org.openide.util.lookup.InstanceContent.Convertor;
/** Node representing an Element
*
* @author Petr Hrebejk
*/
public class ElementNode extends AbstractNode implements Iterable<ElementNode> {
private static final String ACTION_FOLDER = "Navigator/Actions/Members/text/x-java"; //NOI18N
private static Node WAIT_NODE;
private OpenAction openAction;
private Description description;
/** Creates a new instance of TreeNode */
public ElementNode( Description description ) {
super(
description.subs == null ? Children.LEAF: new ElementChilren(description.subs, description.ui.getFilters()),
prepareLookup(description));
this.description = description;
setDisplayName( description.name );
}
@Override
public Image getIcon(int type) {
final Icon icon = description.icon.get();
return icon == null ? super.getIcon(type) : ImageUtilities.icon2Image(icon);
}
@Override
public Image getOpenedIcon(int type) {
return getIcon(type);
}
@Override
public java.lang.String getDisplayName() {
if (description.name != null) {
return description.name;
}
if (description.fileObject != null) {
return description.fileObject.getNameExt();
}
return null;
}
@Override
public String getHtmlDisplayName() {
return description.htmlHeader;
}
@Override
public Action[] getActions( boolean context ) {
if ( context || description.name == null ) {
return description.ui.getActions();
} else {
final Action panelActions[] = description.ui.getActions();
final List<? extends Action> standardActions;
final List<? extends Action> additionalActions;
if (description.kind == ElementKind.OTHER) {
standardActions = Collections.singletonList(getOpenAction());
additionalActions = Collections.<Action>emptyList();
} else {
standardActions = Arrays.asList(new Action[] {
getOpenAction(),
RefactoringActionsFactory.whereUsedAction(),
RefactoringActionsFactory.popupSubmenuAction()
});
additionalActions = Utilities.actionsForPath(ACTION_FOLDER);
}
final int standardActionsSize = standardActions.isEmpty() ? 0 : standardActions.size() + 1;
final int additionalActionSize = additionalActions.isEmpty() ? 0 : additionalActions.size() + 1;
final List<Action> actions = new ArrayList<>(standardActionsSize + additionalActionSize + panelActions.length);
if (standardActionsSize > 0) {
actions.addAll(standardActions);
actions.add(null);
}
if (additionalActionSize > 0) {
actions.addAll(additionalActions);
actions.add(null);
}
actions.addAll(Arrays.asList(panelActions));
return actions.toArray(new Action[actions.size()]);
}
}
@Override
public Action getPreferredAction() {
return getOpenAction();
}
@Override
public boolean canCopy() {
return false;
}
@Override
public boolean canCut() {
return false;
}
@Override
public boolean canDestroy() {
return false;
}
@Override
public boolean canRename() {
return false;
}
@Override
public PasteType getDropType(Transferable t, int action, int index) {
return null;
}
@Override
public Transferable drag() throws IOException {
return null;
}
@Override
protected void createPasteTypes(Transferable t, List<PasteType> s) {
// Do nothing
}
private synchronized Action getOpenAction() {
if (openAction == null) {
openAction = OpenAction.create(description.openable);
}
return openAction;
}
static synchronized Node getWaitNode() {
if ( WAIT_NODE == null ) {
WAIT_NODE = new WaitNode();
}
return WAIT_NODE;
}
public void refreshRecursively() {
Children ch = getChildren();
if ( ch instanceof ElementChilren ) {
boolean scrollOnExpand = description.ui.getScrollOnExpand();
description.ui.setScrollOnExpand( false );
((ElementChilren)ch).resetKeys(description.subs, description.ui.getFilters());
for( Node sub : ch.getNodes() ) {
description.ui.expandNode(sub);
((ElementNode)sub).refreshRecursively();
}
description.ui.setScrollOnExpand( scrollOnExpand );
}
}
@NonNull
public Stream<ElementNode> stream() {
return StreamSupport.stream(
Spliterators.spliteratorUnknownSize(iterator(), 0),
false);
}
@Override
public Iterator<ElementNode> iterator() {
return new Iterator<ElementNode>() {
private final Deque<ElementNode> todo = new ArrayDeque<>();
{
todo.push(ElementNode.this);
}
@Override
public boolean hasNext() {
return !todo.isEmpty();
}
@Override
public ElementNode next() {
if (todo.isEmpty()) {
throw new NoSuchElementException();
}
final ElementNode n = todo.pop();
final Node[] clds = n.getChildren().getNodes();
for (int i=clds.length-1; i>=0; i--) {
if (clds[i] instanceof ElementNode) {
todo.push((ElementNode)clds[i]);
}
}
return n;
}
};
}
public void updateRecursively( Description newDescription ) {
Children ch = getChildren();
if ( ch instanceof ElementChilren ) {
HashSet<Description> oldSubs = new HashSet<Description>( description.subs );
// Create a hashtable which maps Description to node.
// We will then identify the nodes by the description. The trick is
// that the new and old description are equal and have the same hashcode
Node[] nodes = ch.getNodes( true );
HashMap<Description,ElementNode> oldD2node = new HashMap<Description,ElementNode>();
for (Node node : nodes) {
oldD2node.put(((ElementNode)node).description, (ElementNode)node);
}
// Now refresh keys
((ElementChilren)ch).resetKeys(newDescription.subs, newDescription.ui.getFilters());
// Reread nodes
nodes = ch.getNodes( true );
for( Description newSub : newDescription.subs ) {
ElementNode node = oldD2node.get(newSub);
if ( node != null ) { // filtered out
if ( !oldSubs.contains(newSub) && node.getChildren() != Children.LEAF) {
description.ui.expandNode(node); // Make sure new nodes get expanded
}
node.updateRecursively( newSub ); // update the node recursively
}
}
}
Description oldDescription = description; // Remember old description
description = newDescription; // set new descrioption to the new node
if ( oldDescription.htmlHeader != null && !oldDescription.htmlHeader.equals(description.htmlHeader)) {
// Different headers => we need to fire displayname change
fireDisplayNameChange(oldDescription.htmlHeader, description.htmlHeader);
}
if( oldDescription.modifiers != null && !oldDescription.modifiers.equals(newDescription.modifiers)) {
fireIconChange();
fireOpenedIconChange();
}
}
public Description getDescritption() {
return description;
}
@NonNull
private static Lookup prepareLookup(@NonNull final Description d) {
final InstanceContent ic = new InstanceContent();
ic.add(d, ConvertDescription2FileObject);
ic.add(d, ConvertDescription2DataObject);
if (d.handle != null) {
ic.add(d, ConvertDescription2TreePathHandle);
}
return new AbstractLookup(ic);
}
private static final Convertor<Description, TreePathHandle> ConvertDescription2TreePathHandle = new InstanceContent.Convertor<Description, TreePathHandle>() {
@Override public TreePathHandle convert(Description obj) {
return obj.getTreePathHandle();
}
@Override public Class<? extends TreePathHandle> type(Description obj) {
return TreePathHandle.class;
}
@Override public String id(Description obj) {
return "IL[" + obj.toString();
}
@Override public String displayName(Description obj) {
return id(obj);
}
};
private static final Convertor<Description, FileObject> ConvertDescription2FileObject = new InstanceContent.Convertor<Description, FileObject>() {
public FileObject convert(Description d) {
return d.getFileObject();
}
public Class<? extends FileObject> type(Description obj) {
return FileObject.class;
}
public String id(Description obj) {
return "IL[" + obj.toString();
}
public String displayName(Description obj) {
return id(obj);
}
};
private static final Convertor<Description, DataObject> ConvertDescription2DataObject = new InstanceContent.Convertor<Description, DataObject>(){
public DataObject convert(Description d) {
try {
final FileObject fo = d.getFileObject();
return fo == null ? null : DataObject.find(fo);
} catch (DataObjectNotFoundException ex) {
return null;
}
}
public Class<? extends DataObject> type(Description obj) {
return DataObject.class;
}
public String id(Description obj) {
return "IL[" + obj.toString();
}
public String displayName(Description obj) {
return id(obj);
}
};
private static final class ElementChilren extends Children.Keys<Description> {
public ElementChilren(Collection<Description> descriptions, ClassMemberFilters filters ) {
resetKeys( descriptions, filters );
}
protected Node[] createNodes(Description key) {
return new Node[] {new ElementNode(key)};
}
void resetKeys( Collection<Description> descriptions, ClassMemberFilters filters ) {
setKeys( filters.filter(descriptions) );
}
}
/** Stores all interesting data about given element.
*/
static final class Description {
public static final Comparator<Description> ALPHA_COMPARATOR =
new DescriptionComparator(true);
public static final Comparator<Description> POSITION_COMPARATOR =
new DescriptionComparator(false);
private final Union2<ElementHandle<?>,TreePathHandle> handle;
final ClassMemberPanelUI ui;
final String name;
final ElementKind kind;
final int posInKind;
final Supplier<Icon> icon;
final Openable openable;
final Set<Modifier> modifiers;
final long pos;
final ClasspathInfo cpInfo;
final boolean isInherited;
final boolean isTopLevel;
FileObject fileObject; // For the root description
Collection<Description> subs;
String htmlHeader;
private Description(ClassMemberPanelUI ui) {
this.ui = ui;
this.name = null;
this.handle = null;
this.kind = null;
this.posInKind = 0;
this.isInherited = false;
this.isTopLevel = false;
this.icon = () -> null;
this.openable = () -> {};
this.cpInfo = null;
this.pos = -1;
this.modifiers = Collections.emptySet();
}
private Description(
@NonNull final ClassMemberPanelUI ui,
@NonNull final String name,
@NullAllowed final Union2<ElementHandle<?>,TreePathHandle> handle,
@NonNull final ElementKind kind,
final int posInKind,
@NonNull final ClasspathInfo cpInfo,
@NonNull final Set<Modifier> modifiers,
final long pos,
final boolean inherited,
final boolean topLevel,
@NonNull Supplier<Icon> icon,
@NonNull Openable openable) {
Parameters.notNull("ui", ui); //NOI18N
Parameters.notNull("name", name); //NOI18N
Parameters.notNull("kind", kind); //NOI18N
Parameters.notNull("icon", icon); //NOI18N
Parameters.notNull("openable", openable); //NOI18N
this.ui = ui;
this.name = name;
this.handle = handle;
this.kind = kind;
this.posInKind = posInKind;
this.cpInfo = cpInfo;
this.modifiers = modifiers;
this.pos = pos;
this.icon = icon;
this.isInherited = inherited;
this.isTopLevel = topLevel;
this.openable = openable;
}
private Description(@NonNull ClassMemberPanelUI ui,
@NonNull final String name,
@NonNull final ElementHandle<? extends Element> elementHandle,
final int posInKind,
@NonNull final ClasspathInfo cpInfo,
@NonNull final Set<Modifier> modifiers,
final long pos,
final boolean inherited,
final boolean topLevel) {
Parameters.notNull("ui", ui); //NOI18N
Parameters.notNull("name", name); //NOI18N
Parameters.notNull("elementHandle", elementHandle); //NOI18N
this.ui = ui;
this.name = name;
this.handle = Union2.<ElementHandle<?>,TreePathHandle>createFirst(elementHandle);
this.kind = elementHandle.getKind();
this.posInKind = posInKind;
this.cpInfo = cpInfo;
this.modifiers = modifiers;
this.pos = pos;
this.isInherited = inherited;
this.isTopLevel = topLevel;
this.icon = () -> ElementIcons.getElementIcon(this.kind, this.modifiers);
this.openable = () -> {OpenAction.openable(elementHandle, getFileObject(), name).open();};
}
@CheckForNull
ElementHandle<?> getElementHandle() {
if (handle == null) {
return null;
}
return handle.hasFirst() ?
handle.first() :
handle.second().getElementHandle();
}
@CheckForNull
TreePathHandle getTreePathHandle() {
if (handle == null) {
return null;
}
return handle.hasSecond() ?
handle.second() :
TreePathHandle.from(handle.first(), cpInfo);
}
public FileObject getFileObject() {
if (isInherited) {
assert getElementHandle() != null : handle;
return SourceUtils.getFile(getElementHandle(), cpInfo);
} else {
return ui.getFileObject();
}
}
@Override
public boolean equals(Object o) {
if ( o == null ) {
//System.out.println("- f nul");
return false;
}
if ( !(o instanceof Description)) {
// System.out.println("- not a desc");
return false;
}
Description d = (Description)o;
if ( kind != d.kind ) {
// System.out.println("- kind");
return false;
}
if (this.name != d.name && (this.name == null || !this.name.equals(d.name))) {
// System.out.println("- name");
return false;
}
if (this.handle != d.handle && (this.handle == null || !this.handle.equals(d.handle))) {
return false;
}
return true;
}
public int hashCode() {
int hash = 7;
hash = 29 * hash + (this.name != null ? this.name.hashCode() : 0);
hash = 29 * hash + (this.kind != null ? this.kind.hashCode() : 0);
// hash = 29 * hash + (this.modifiers != null ? this.modifiers.hashCode() : 0);
return hash;
}
@NonNull
static Description root(ClassMemberPanelUI ui) {
return new Description(ui);
}
@NonNull
static Description element(
@NonNull final ClassMemberPanelUI ui,
@NonNull final String name,
@NonNull final ElementHandle<? extends Element> elementHandle,
@NonNull final ClasspathInfo cpInfo,
@NonNull final Set<Modifier> modifiers,
final long pos,
boolean inherited,
boolean topLevel) {
return new Description(ui, name, elementHandle, 0, cpInfo, modifiers, pos, inherited, topLevel);
}
@NonNull
static Description directive(
@NonNull final ClassMemberPanelUI ui,
@NonNull final String name,
@NonNull final TreePathHandle treePathHandle,
@NonNull final ModuleElement.DirectiveKind kind,
@NonNull final ClasspathInfo cpInfo,
final long pos,
@NonNull final Openable openable) {
return new Description(
ui,
name,
Union2.<ElementHandle<?>,TreePathHandle>createSecond(treePathHandle),
ElementKind.OTHER,
directivePosInKind(kind),
cpInfo,
EnumSet.of(Modifier.PUBLIC),
pos,
false,
false,
()->ElementIcons.getModuleDirectiveIcon(kind),
openable);
}
@NonNull
static Description directive(
@NonNull final ClassMemberPanelUI ui,
@NonNull final String name,
@NonNull final ModuleElement.DirectiveKind kind,
@NonNull final ClasspathInfo cpInfo,
@NonNull final Openable openable) {
return new Description(
ui,
name,
null,
ElementKind.OTHER,
directivePosInKind(kind),
cpInfo,
EnumSet.of(Modifier.PUBLIC),
-1,
false,
false,
()->ElementIcons.getModuleDirectiveIcon(kind),
openable);
}
private static int directivePosInKind(ModuleElement.DirectiveKind kind) {
switch (kind) {
case EXPORTS:
return 0;
case OPENS:
return 1;
case REQUIRES:
return 2;
case USES:
return 3;
case PROVIDES:
return 4;
default:
return 100;
}
}
private static class DescriptionComparator implements Comparator<Description> {
boolean alpha;
DescriptionComparator( boolean alpha ) {
this.alpha = alpha;
}
public int compare(Description d1, Description d2) {
if ( alpha ) {
return alphaCompare( d1, d2 );
}
else {
if( d1.isInherited && !d2.isInherited )
return 1;
if( !d1.isInherited && d2.isInherited )
return -1;
if( d1.isInherited && d2.isInherited ) {
return alphaCompare( d1, d2 );
}
return d1.pos == d2.pos ? 0 : d1.pos < d2.pos ? -1 : 1;
}
}
int alphaCompare( Description d1, Description d2 ) {
if ( k2i(d1.kind) != k2i(d2.kind) ) {
return k2i(d1.kind) - k2i(d2.kind);
}
if (d1.posInKind != d2.posInKind) {
return d1.posInKind - d2.posInKind;
}
return d1.name.compareTo(d2.name);
}
int k2i( ElementKind kind ) {
switch( kind ) {
case CONSTRUCTOR:
return 1;
case METHOD:
return 2;
case FIELD:
return 3;
case CLASS:
case INTERFACE:
case ENUM:
case ANNOTATION_TYPE:
case MODULE:
return 4;
default:
return 100;
}
}
}
}
private static class WaitNode extends AbstractNode {
private Image waitIcon = ImageUtilities.loadImage("org/netbeans/modules/java/navigation/resources/wait.gif"); // NOI18N
WaitNode( ) {
super( Children.LEAF );
}
@Override
public Image getIcon(int type) {
return waitIcon;
}
@Override
public Image getOpenedIcon(int type) {
return getIcon(type);
}
@java.lang.Override
public java.lang.String getDisplayName() {
return NbBundle.getMessage(ElementNode.class, "LBL_WaitNode"); // NOI18N
}
}
}