blob: 6b588755d78231a6898d79bc03bb4a8b146068f7 [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.php.editor.nav.hierarchy;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Image;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import org.netbeans.api.annotations.common.StaticResource;
import org.netbeans.modules.csl.api.UiUtils;
import org.netbeans.modules.php.editor.api.ElementQuery.Index;
import org.netbeans.modules.php.editor.api.QualifiedName;
import org.netbeans.modules.php.editor.api.elements.ClassElement;
import org.netbeans.modules.php.editor.api.elements.TreeElement;
import org.netbeans.modules.php.editor.api.elements.TypeElement;
import org.netbeans.modules.php.editor.model.FileScope;
import org.netbeans.modules.php.editor.model.Model;
import org.netbeans.modules.php.editor.model.ModelUtils;
import org.openide.filesystems.FileObject;
import org.openide.util.HelpCtx;
import org.openide.util.ImageUtilities;
import org.openide.util.NbBundle;
import org.openide.util.NbBundle.Messages;
/**
* @author Radek Matous
*/
@org.netbeans.api.annotations.common.SuppressWarnings({"SE_BAD_FIELD_STORE"})
public class ClassHierarchyPanel extends JPanel implements HelpCtx.Provider {
private static final int MAX_STACK_DEPTH = 250;
private final JTree tree;
private final DefaultTreeModel treeModel;
private final MouseAdapter mouseListener = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
TreePath selPath = tree.getSelectionPath();
if (selPath != null) {
if (e.getClickCount() == 2) {
Object lastPathComponent = selPath.getLastPathComponent();
if (lastPathComponent instanceof TypeNode) {
final TypeNode typeNode = (TypeNode) lastPathComponent;
final FileObject fileObject = typeNode.getFileObject();
if (fileObject != null && fileObject.isValid()) {
UiUtils.open(fileObject, typeNode.getOffset());
}
}
}
}
}
};
public ClassHierarchyPanel(boolean isView) {
initComponents();
if (!isView) {
toolBar.remove(0);
toolBar.remove(0);
subtypeButton.setFocusable(true);
supertypeButton.setFocusable(true);
}
setName(NbBundle.getMessage(getClass(), "CTL_ClassHierarchyTopComponent")); // NOI18N
setToolTipText(NbBundle.getMessage(getClass(), "HINT_ClassHierarchyTopComponent")); // NOI18N
tree = new JTree();
treeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
tree.setModel(treeModel);
tree.setToggleClickCount(0);
tree.setCellRenderer(new TreeRenderer());
tree.putClientProperty("JTree.lineStyle", "Angled"); //NOI18N
tree.expandRow(0);
tree.setShowsRootHandles(true);
tree.setSelectionRow(0);
tree.setRootVisible(false);
hierarchyPane.add(tree);
hierarchyPane.setViewportView(tree);
tree.addMouseListener(mouseListener);
}
void setModel(Model model) {
treeModel.setRoot(createRoot(model, subtypeButton.isSelected()));
expandAll(tree);
}
private static void expandAll(JTree tree) {
int row = 0;
while (row < tree.getRowCount()) {
tree.expandRow(row);
row++;
}
}
/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
java.awt.GridBagConstraints gridBagConstraints;
directionGroup = new javax.swing.ButtonGroup();
toolBar = new javax.swing.JToolBar();
refreshButton = new javax.swing.JButton();
jSeparator1 = new javax.swing.JToolBar.Separator();
supertypeButton = new javax.swing.JToggleButton();
subtypeButton = new javax.swing.JToggleButton();
jPanel2 = new javax.swing.JPanel();
hierarchyPane = new javax.swing.JScrollPane();
setLayout(new java.awt.GridBagLayout());
toolBar.setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 1, 1, 1));
toolBar.setFloatable(false);
toolBar.setBorderPainted(false);
toolBar.setMaximumSize(new java.awt.Dimension(74, 26));
toolBar.setMinimumSize(new java.awt.Dimension(74, 26));
toolBar.setOpaque(false);
refreshButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/netbeans/modules/php/editor/resources/refresh.png"))); // NOI18N
refreshButton.setToolTipText(org.openide.util.NbBundle.getMessage(ClassHierarchyPanel.class, "ClassHierarchyPanel.refreshButton.toolTipText")); // NOI18N
refreshButton.setFocusable(false);
refreshButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
refreshButton.setMaximumSize(new java.awt.Dimension(24, 24));
refreshButton.setMinimumSize(new java.awt.Dimension(24, 24));
refreshButton.setPreferredSize(new java.awt.Dimension(24, 24));
refreshButton.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
refreshButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
refreshButtonActionPerformed(evt);
}
});
toolBar.add(refreshButton);
toolBar.add(jSeparator1);
directionGroup.add(supertypeButton);
supertypeButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/netbeans/modules/php/editor/resources/supertypehierarchy.gif"))); // NOI18N
supertypeButton.setSelected(true);
supertypeButton.setToolTipText(org.openide.util.NbBundle.getMessage(ClassHierarchyPanel.class, "ClassHierarchyPanel.supertypeButton.toolTipText")); // NOI18N
supertypeButton.setFocusable(false);
supertypeButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
supertypeButton.setMaximumSize(new java.awt.Dimension(24, 24));
supertypeButton.setMinimumSize(new java.awt.Dimension(24, 24));
supertypeButton.setPreferredSize(new java.awt.Dimension(24, 24));
supertypeButton.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
supertypeButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
supertypeButtonActionPerformed(evt);
}
});
toolBar.add(supertypeButton);
directionGroup.add(subtypeButton);
subtypeButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/netbeans/modules/php/editor/resources/subtypehierarchy.gif"))); // NOI18N
subtypeButton.setSelected(false);
subtypeButton.setToolTipText(org.openide.util.NbBundle.getMessage(ClassHierarchyPanel.class, "ClassHierarchyPanel.subtypeButton.toolTipText")); // NOI18N
subtypeButton.setFocusable(false);
subtypeButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
subtypeButton.setMaximumSize(new java.awt.Dimension(24, 24));
subtypeButton.setMinimumSize(new java.awt.Dimension(24, 24));
subtypeButton.setPreferredSize(new java.awt.Dimension(24, 24));
subtypeButton.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
subtypeButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
subtypeButtonActionPerformed(evt);
}
});
toolBar.add(subtypeButton);
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 0;
gridBagConstraints.gridy = 2;
gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 0);
add(toolBar, gridBagConstraints);
jPanel2.setFocusable(false);
jPanel2.setMinimumSize(new java.awt.Dimension(1, 1));
jPanel2.setPreferredSize(new java.awt.Dimension(1, 1));
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 0;
gridBagConstraints.gridy = 1;
gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
add(jPanel2, gridBagConstraints);
hierarchyPane.setFocusable(false);
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 0;
gridBagConstraints.gridy = 0;
gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
gridBagConstraints.weightx = 1.0;
gridBagConstraints.weighty = 1.0;
add(hierarchyPane, gridBagConstraints);
}// </editor-fold>//GEN-END:initComponents
private void refresh() {
PhpHierarchyTopComponent view = PhpHierarchyTopComponent.findInstance();
if (view != null) {
view.refresh();
}
}
private void refreshButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_refreshButtonActionPerformed
refresh();
}//GEN-LAST:event_refreshButtonActionPerformed
private void subtypeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_subtypeButtonActionPerformed
refresh();
}//GEN-LAST:event_subtypeButtonActionPerformed
private void supertypeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_supertypeButtonActionPerformed
refresh();
}//GEN-LAST:event_supertypeButtonActionPerformed
@Override
public boolean requestFocusInWindow() {
super.requestFocusInWindow();
return hierarchyPane.requestFocusInWindow();
}
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.ButtonGroup directionGroup;
private javax.swing.JScrollPane hierarchyPane;
private javax.swing.JPanel jPanel2;
private javax.swing.JToolBar.Separator jSeparator1;
private javax.swing.JButton refreshButton;
private javax.swing.JToggleButton subtypeButton;
private javax.swing.JToggleButton supertypeButton;
private javax.swing.JToolBar toolBar;
// End of variables declaration//GEN-END:variables
@Override
public HelpCtx getHelpCtx() {
return new HelpCtx("PhpTypeView"); // NOI18N
}
@org.netbeans.api.annotations.common.SuppressWarnings({"EI_EXPOSE_REP2"})
protected abstract static class AbstractTypeNode implements TreeNode {
private AbstractTypeNode[] childern;
protected AbstractTypeNode() {
childern = new AbstractTypeNode[0];
}
@Override
public Enumeration children() {
return new Enumeration() {
int idx = 0;
@Override
public boolean hasMoreElements() {
return idx < getChildCount();
}
@Override
public Object nextElement() {
return getChildAt(idx++);
}
};
}
@Override
public boolean getAllowsChildren() {
return !isLeaf();
}
@Override
public TreeNode getChildAt(int childIndex) {
return this.childern[childIndex];
}
@Override
public int getChildCount() {
return this.childern.length;
}
@Override
public int getIndex(TreeNode node) {
for (int i = 0; i < childern.length; i++) {
AbstractTypeNode classNode = childern[i];
if (classNode == node) {
return i;
}
}
return -1;
}
@Override
public boolean isLeaf() {
return getChildCount() == 0;
}
public void setChildern(AbstractTypeNode[] childern) {
this.childern = childern;
}
//these methods are used rendering
public abstract Image getIcon();
public abstract String toStringAsHtml();
}
private static TypeNode[] sortTypes(final TypeNode[] types) {
Arrays.<TypeNode>sort(types, new Comparator<TypeNode>() {
@Override
public int compare(TypeNode o1, TypeNode o2) {
int compareTo = Boolean.valueOf(o1.isClass).compareTo(o2.isClass);
return compareTo == 0 ? o1.toString().compareToIgnoreCase(o2.toString()) : compareTo;
}
});
return types;
}
protected TreeNode createRoot(final Model model, final boolean subDirection) {
final FileRootNode retval = new FileRootNode(model);
FileScope fileScope = model.getFileScope();
Set<TypeElement> types = new HashSet<>();
Set<TypeElement> recursionDetection = new HashSet<>();
types.addAll(ModelUtils.getDeclaredClasses(fileScope));
types.addAll(ModelUtils.getDeclaredInterfaces(fileScope));
types.addAll(ModelUtils.getDeclaredTraits(fileScope));
TypeNode[] childernNodes = new TypeNode[types.size()];
if (types.size() > 0) {
Index index = fileScope.getIndexScope().getIndex();
Iterator<TypeElement> iterator = types.iterator();
for (int i = 0; iterator.hasNext(); i++) {
recursionDetection.clear();
TypeElement type = iterator.next();
TreeElement<TypeElement> treeType = subDirection
? index.getInheritedByTypesAsTree(type, types)
: index.getInheritedTypesAsTree(type, types);
recursionDetection.add(type);
childernNodes[i] = createTypeNode(retval, treeType, recursionDetection, Integer.valueOf(0));
}
}
retval.setChildern(sortTypes(childernNodes));
return retval;
}
@org.netbeans.api.annotations.common.SuppressWarnings({"DLS_DEAD_LOCAL_STORE"})
private static TypeNode createTypeNode(
final TreeNode parent, final TreeElement<TypeElement> classElement, Set<TypeElement> recursionDetection, Integer stackDepth) {
stackDepth++;
final TypeNode retval = new TypeNode(parent, classElement);
final Set<TreeElement<TypeElement>> children = classElement.children();
ArrayList<TypeNode> childernList = new ArrayList<>();
if (stackDepth <= MAX_STACK_DEPTH) {
for (TreeElement<TypeElement> child : children) {
if (recursionDetection.add(child.getElement())) {
childernList.add(createTypeNode(retval, child, recursionDetection, stackDepth));
}
}
} else {
childernList.add(new ErrTypeNode(parent, classElement));
}
stackDepth--;
retval.setChildern(sortTypes(childernList.toArray(new TypeNode[childernList.size()])));
return retval;
}
private static final class FileRootNode extends AbstractTypeNode {
private final String filename;
private FileRootNode(final Model model) {
FileObject fileObject = model.getFileScope().getFileObject();
filename = fileObject == null ? "?" : fileObject.getNameExt(); //NOI18N
}
@Override
public TreeNode getParent() {
return null;
}
@Override
public String toString() {
return filename;
}
//not supposed to be visible
@Override
public Image getIcon() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public String toStringAsHtml() {
throw new UnsupportedOperationException("Not supported yet.");
}
}
private static class TypeNode extends AbstractTypeNode {
@StaticResource
private static final String CLASS_ICON = "org/netbeans/modules/php/editor/resources/class.png"; //NOI18N
@StaticResource
private static final String IFACE_ICON = "org/netbeans/modules/php/editor/resources/interface.png"; //NOI18N
@StaticResource
private static final String TRAIT_ICON = "org/netbeans/modules/php/editor/resources/trait.png"; //NOI18N
private static final String FONT_GRAY_COLOR = "<font color=\"#999999\">"; //NOI18N
private static final String CLOSE_FONT = "</font>"; //NOI18N
private final TreeNode parent;
private final String name;
private final List<String> superTypes;
private final FileObject fileObject;
private final int offset;
private final boolean isClass;
private final boolean isInterface;
private final boolean isTrait;
public TypeNode(final TreeNode parent, final TreeElement<TypeElement> classElement) {
this.parent = parent;
TypeElement type = classElement.getElement();
this.name = type.getName();
this.superTypes = new ArrayList<>();
this.fileObject = type.getFileObject();
this.offset = type.getOffset();
this.isClass = type.isClass();
this.isInterface = type.isInterface();
this.isTrait = type.isTrait();
if (type instanceof ClassElement) {
ClassElement clz = (ClassElement) type;
QualifiedName superClassName = clz.getSuperClassName();
if (superClassName != null) {
this.superTypes.add(superClassName.toString());
}
}
Set<QualifiedName> superInterfaces = type.getSuperInterfaces();
for (QualifiedName supeIfaceName : superInterfaces) {
if (supeIfaceName != null) {
this.superTypes.add(supeIfaceName.toString());
}
}
}
@Override
public TreeNode getParent() {
return parent;
}
@Override
public String toString() {
return String.format("%s%s", name, superTypes); //NOI18N
}
@Override
public Image getIcon() {
String img;
if (isClass) {
img = CLASS_ICON;
} else if (isInterface) {
img = IFACE_ICON;
} else if (isTrait) {
img = TRAIT_ICON;
} else {
assert false : "Unknown type";
img = CLASS_ICON;
}
return ImageUtilities.loadImage(img);
}
@Override
public String toStringAsHtml() {
StringBuilder superTypeString = new StringBuilder();
for (String superTypeName : superTypes) {
if (superTypeString.length() != 0) {
superTypeString.append(", "); //NOI18N
} else {
superTypeString.append("::"); //NOI18N
}
superTypeString.append(superTypeName);
}
return String.format("<html>%s%s%s%s </html>", name, //NOI18N
TypeNode.FONT_GRAY_COLOR, superTypeString.toString(),
TypeNode.CLOSE_FONT);
}
/**
* @return the fileObject
*/
public FileObject getFileObject() {
return fileObject;
}
/**
* @return the offset
*/
public int getOffset() {
return offset;
}
}
@Messages({
"# {0} - max number of childs",
"TooManyChilds=Too many childs detected (max {0})."
})
private static class ErrTypeNode extends TypeNode {
@StaticResource
private static final String ICON = "org/netbeans/modules/php/editor/resources/error-glyph.gif"; //NOI18N
ErrTypeNode(TreeNode parent, TreeElement<TypeElement> classElement) {
super(parent, classElement);
}
@Override
public String toString() {
return Bundle.TooManyChilds(MAX_STACK_DEPTH);
}
@Override
public String toStringAsHtml() {
return "<html><span style='color: red; font-size: 0.9em;'>" + Bundle.TooManyChilds(MAX_STACK_DEPTH) + "</html>"; //NOI18N
}
@Override
public Image getIcon() {
return ImageUtilities.loadImage(ICON);
}
}
public static class TreeRenderer extends JPanel implements TreeCellRenderer {
private static final JList LIST_FOR_COLORS = new JList();
protected JLabel label;
public TreeRenderer() {
setLayout(new BorderLayout());
setOpaque(true);
this.label = new JLabel();
add(label, BorderLayout.CENTER);
label.setOpaque(false);
}
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean isSelected, boolean expanded,
boolean leaf, int row, boolean hasFocus) {
String stringValue = tree.convertValueToText(value, isSelected,
expanded, leaf, row, hasFocus);
setEnabled(tree.isEnabled());
if (value instanceof TypeNode) {
TypeNode n = (TypeNode) value;
stringValue = n.toStringAsHtml();
label.setIcon(new ImageIcon(n.getIcon()));
}
if (isSelected) {
label.setForeground(LIST_FOR_COLORS.getSelectionForeground());
setOpaque(true);
setBackground(LIST_FOR_COLORS.getSelectionBackground());
} else {
label.setForeground(tree.getForeground());
setOpaque(false);
}
label.setText(stringValue);
return this;
}
}
}