blob: af0205adf5a3681ebc07c0bd9565a0d30a0fff08 [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.module.graph;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collection;
import java.util.Collections;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.Document;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.StaticResource;
import org.netbeans.core.spi.multiview.CloseOperationState;
import org.netbeans.core.spi.multiview.MultiViewElement;
import org.netbeans.core.spi.multiview.MultiViewElementCallback;
import org.netbeans.modules.java.graph.DependencyGraphScene;
import org.netbeans.modules.java.graph.GraphEdge;
import org.openide.cookies.EditorCookie;
import org.openide.filesystems.FileChangeAdapter;
import org.openide.filesystems.FileChangeListener;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileRenameEvent;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.Parameters;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakListeners;
import org.openide.windows.TopComponent;
/**
*
* @author Tomas Zezula
*/
final class GraphTopComponent extends TopComponent implements MultiViewElement, Runnable {
@StaticResource
private static final String ZOOM_IN_ICON = "org/netbeans/modules/java/module/graph/resources/zoomin.gif"; //NOI18N
@StaticResource
private static final String ZOOM_OUT_ICON = "org/netbeans/modules/java/module/graph/resources/zoomout.gif";
@StaticResource
private static final String PUBLIC_ICON = "org/netbeans/modules/java/module/graph/resources/public.gif";
private static final RequestProcessor RP = new RequestProcessor(GraphTopComponent.class);
private final RequestProcessor.Task refreshTask = RP.create(this);
private final JScrollPane pane = new JScrollPane();
private final ChangeSupport changeSupport;
private boolean alreadyShown;
private boolean needsRefresh;
private MultiViewElementCallback callback;
private EditorToolbar toolbar;
private DependencyGraphScene<ModuleNode> scene;
private Collection<? extends DependencyEdge> edges;
private Timer timer = new Timer(500, new ActionListener() {
@Override public void actionPerformed(ActionEvent arg0) {
checkFindValue();
}
});
/**
* Creates new form GraphTopComponent
*/
GraphTopComponent(@NonNull final Lookup lkp) {
Parameters.notNull("lkp", lkp); //NOI18N
changeSupport = new ChangeSupport(
lkp.lookup(FileObject.class),
() -> needsRefresh = true);
associateLookup(lkp);
initComponents();
if( "Aqua".equals(UIManager.getLookAndFeel().getID()) ) { //NOI18N
setBackground(UIManager.getColor("NbExplorerView.background")); //NOI18N
}
txtFind.getDocument().addDocumentListener(new DocumentListener() {
@Override public void insertUpdate(DocumentEvent arg0) {
timer.restart();
}
@Override public void removeUpdate(DocumentEvent arg0) {
timer.restart();
}
@Override public void changedUpdate(DocumentEvent arg0) {
timer.restart();
}
});
jdkComboBox.setModel(new DefaultComboBoxModel(JDKVisibility.values()));
jdkComboBox.setRenderer(new JDKRenderer());
timer.setDelay(500);
timer.setRepeats(false);
}
@Override
public JComponent getVisualRepresentation() {
return this;
}
@Override
public JComponent getToolbarRepresentation() {
if (toolbar == null) {
toolbar = new EditorToolbar();
toolbar.setFloatable(false);
toolbar.setRollover(true);
toolbar.addSeparator();
Dimension space = new Dimension(3, 0);
toolbar.add(zoomIn);
toolbar.addSeparator(space);
toolbar.add(zoomOut);
toolbar.addSeparator(space);
toolbar.add(lblFind);
toolbar.add(txtFind);
toolbar.addSeparator(space);
toolbar.add(lblPath);
toolbar.add(maxPathSpinner);
toolbar.addSeparator();
toolbar.add(jdkLabel);
toolbar.add(jdkComboBox);
toolbar.addSeparator(space);
toolbar.add(transitiveCheckBox);
// toolbar.addSeparator(space);
// toolbar.add(lblScopes);
// toolbar.add(comScopes);
}
return toolbar;
}
@Override
public void componentOpened() {
super.componentOpened();
pane.setWheelScrollingEnabled(true);
add(pane, BorderLayout.CENTER);
alreadyShown = false;
needsRefresh = true;
}
@Override
public void componentActivated() {
super.componentActivated();
if (needsRefresh) {
refreshModel();
needsRefresh = false;
alreadyShown = true;
}
}
@Override
public void componentClosed() {
super.componentClosed();
}
@Override
public void componentDeactivated() {
super.componentDeactivated();
}
@Override
public void componentHidden() {
super.componentHidden();
}
@Override
public void componentShowing() {
super.componentShowing();
}
@Override
public void setMultiViewCallback(MultiViewElementCallback callback) {
this.callback = callback;
}
@Override
public CloseOperationState canCloseElement() {
return CloseOperationState.STATE_OK;
}
public void run() {
final FileObject moduleInfo = getLookup().lookup(FileObject.class);
assert moduleInfo != null;
final DependencyCalculator calc = new DependencyCalculator(moduleInfo);
final Collection<? extends ModuleNode> nodes = calc.getNodes();
final Collection<? extends DependencyEdge> edges = calc.getEdges();
SwingUtilities.invokeLater(()->displayScene(nodes, edges));
}
@NonNull
JScrollPane getScrollPane () {
return pane;
}
/**
* 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.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
jComboBox1 = new javax.swing.JComboBox<>();
jPanel1 = new javax.swing.JPanel();
jToolBar1 = new javax.swing.JToolBar();
zoomIn = new javax.swing.JButton();
zoomOut = new javax.swing.JButton();
lblFind = new javax.swing.JLabel();
txtFind = new javax.swing.JTextField();
lblPath = new javax.swing.JLabel();
maxPathSpinner = new javax.swing.JSpinner();
jdkLabel = new javax.swing.JLabel();
jdkComboBox = new javax.swing.JComboBox<>();
transitiveCheckBox = new javax.swing.JCheckBox();
jComboBox1.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" }));
setLayout(new java.awt.BorderLayout());
jPanel1.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT));
jToolBar1.setFloatable(false);
jToolBar1.setRollover(true);
zoomIn.setIcon(ImageUtilities.loadImageIcon(ZOOM_IN_ICON, true));
org.openide.awt.Mnemonics.setLocalizedText(zoomIn, org.openide.util.NbBundle.getMessage(GraphTopComponent.class, "GraphTopComponent.zoomIn.text")); // NOI18N
zoomIn.setFocusable(false);
zoomIn.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
zoomIn.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
zoomIn.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
zoomIn(evt);
}
});
jToolBar1.add(zoomIn);
zoomOut.setIcon(ImageUtilities.loadImageIcon(ZOOM_OUT_ICON, true));
org.openide.awt.Mnemonics.setLocalizedText(zoomOut, org.openide.util.NbBundle.getMessage(GraphTopComponent.class, "GraphTopComponent.zoomOut.text")); // NOI18N
zoomOut.setFocusable(false);
zoomOut.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
zoomOut.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
zoomOut.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
zoomOut(evt);
}
});
jToolBar1.add(zoomOut);
jPanel1.add(jToolBar1);
org.openide.awt.Mnemonics.setLocalizedText(lblFind, org.openide.util.NbBundle.getMessage(GraphTopComponent.class, "GraphTopComponent.lblFind.text")); // NOI18N
jPanel1.add(lblFind);
txtFind.setMaximumSize(new java.awt.Dimension(200, 22));
txtFind.setMinimumSize(new java.awt.Dimension(50, 19));
txtFind.setPreferredSize(new java.awt.Dimension(150, 22));
txtFind.setFont(new Font("Arial", java.awt.Font.PLAIN, 11));
jPanel1.add(txtFind);
org.openide.awt.Mnemonics.setLocalizedText(lblPath, org.openide.util.NbBundle.getMessage(GraphTopComponent.class, "GraphTopComponent.lblPath.text")); // NOI18N
lblPath.setToolTipText(org.openide.util.NbBundle.getMessage(GraphTopComponent.class, "GraphTopComponent.lblPath.toolTipText")); // NOI18N
jPanel1.add(lblPath);
maxPathSpinner.setModel(new javax.swing.SpinnerNumberModel(1, 1, 5, 1));
maxPathSpinner.setToolTipText(org.openide.util.NbBundle.getMessage(GraphTopComponent.class, "GraphTopComponent.maxPathSpinner.toolTipText")); // NOI18N
maxPathSpinner.setMaximumSize(new java.awt.Dimension(60, 32767));
maxPathSpinner.setRequestFocusEnabled(false);
maxPathSpinner.addChangeListener(new javax.swing.event.ChangeListener() {
public void stateChanged(javax.swing.event.ChangeEvent evt) {
maxPathSpinnerStateChanged(evt);
}
});
jPanel1.add(maxPathSpinner);
org.openide.awt.Mnemonics.setLocalizedText(jdkLabel, org.openide.util.NbBundle.getMessage(GraphTopComponent.class, "GraphTopComponent.jdkLabel.text")); // NOI18N
jdkLabel.setToolTipText(org.openide.util.NbBundle.getMessage(GraphTopComponent.class, "GraphTopComponent.jdkLabel.toolTipText")); // NOI18N
jPanel1.add(jdkLabel);
jdkComboBox.setMaximumSize(new java.awt.Dimension(300, 32767));
jdkComboBox.addItemListener(new java.awt.event.ItemListener() {
public void itemStateChanged(java.awt.event.ItemEvent evt) {
jdkComboBoxItemStateChanged(evt);
}
});
jPanel1.add(jdkComboBox);
org.openide.awt.Mnemonics.setLocalizedText(transitiveCheckBox, org.openide.util.NbBundle.getMessage(GraphTopComponent.class, "GraphTopComponent.transitiveCheckBox.text")); // NOI18N
transitiveCheckBox.setToolTipText(org.openide.util.NbBundle.getMessage(GraphTopComponent.class, "GraphTopComponent.transitiveCheckBox.toolTipText")); // NOI18N
transitiveCheckBox.setIcon(ImageUtilities.loadImageIcon(PUBLIC_ICON, true));
transitiveCheckBox.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
transitiveCheckBoxActionPerformed(evt);
}
});
jPanel1.add(transitiveCheckBox);
add(jPanel1, java.awt.BorderLayout.NORTH);
}// </editor-fold>//GEN-END:initComponents
private void zoomIn(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomIn
scene.setMyZoomFactor(scene.getZoomFactor() * 1.2);
scene.validate();
scene.repaint();
if (pane.getHorizontalScrollBar().isVisible() ||
pane.getVerticalScrollBar().isVisible()) {
revalidate();
repaint();
}
}//GEN-LAST:event_zoomIn
private void zoomOut(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomOut
scene.setMyZoomFactor(scene.getZoomFactor() * 0.8);
scene.validate();
scene.repaint();
if (!pane.getHorizontalScrollBar().isVisible() &&
!pane.getVerticalScrollBar().isVisible()) {
revalidate();
repaint();
}
}//GEN-LAST:event_zoomOut
private void maxPathSpinnerStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_maxPathSpinnerStateChanged
scene.highlightDepth(getSelectedDepth());
}//GEN-LAST:event_maxPathSpinnerStateChanged
private void jdkComboBoxItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_jdkComboBoxItemStateChanged
if (evt.getStateChange() == java.awt.event.ItemEvent.SELECTED && scene != null) {
scene.updateVisibility();
}
}//GEN-LAST:event_jdkComboBoxItemStateChanged
private void transitiveCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_transitiveCheckBoxActionPerformed
if (scene != null) {
scene.updateVisibility();
}
}//GEN-LAST:event_transitiveCheckBoxActionPerformed
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JComboBox<String> jComboBox1;
private javax.swing.JPanel jPanel1;
private javax.swing.JToolBar jToolBar1;
private javax.swing.JComboBox<String> jdkComboBox;
private javax.swing.JLabel jdkLabel;
private javax.swing.JLabel lblFind;
private javax.swing.JLabel lblPath;
private javax.swing.JSpinner maxPathSpinner;
private javax.swing.JCheckBox transitiveCheckBox;
private javax.swing.JTextField txtFind;
private javax.swing.JButton zoomIn;
private javax.swing.JButton zoomOut;
// End of variables declaration//GEN-END:variables
private void checkFindValue() {
String val = txtFind.getText().trim();
if ("".equals(val)) { //NOI18N
val = null;
}
scene.setSearchString(val);
}
@NbBundle.Messages({
"TXT_ComputingDependencies=Computing Dependencies...",
"TXT_RefreshingDependencies=Refreshing Dependencies..."
})
private void refreshModel() {
setPaneText(alreadyShown?
Bundle.TXT_RefreshingDependencies() :
Bundle.TXT_ComputingDependencies());
enableControls(false);
refreshTask.schedule(0);
}
private void enableControls(final boolean enable) {
zoomIn.setEnabled(enable);
zoomOut.setEnabled(enable);
}
private void setPaneText(@NonNull final String text) {
final JLabel lbl = new JLabel(text);
lbl.setHorizontalAlignment(JLabel.CENTER);
lbl.setVerticalAlignment(JLabel.CENTER);
pane.setViewportView(lbl);
}
private static final BasicStroke TRANSITIVE_STROKE = new BasicStroke (1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, BasicStroke.JOIN_MITER, new float[] {5, 5}, 0);
private void displayScene(
@NonNull final Collection<? extends ModuleNode> nodes,
@NonNull final Collection<? extends DependencyEdge> edges)
{
this.edges = edges;
DependencyGraphScene.PaintingProvider<ModuleNode> paintingProvider = new DependencyGraphScene.PaintingProvider<ModuleNode>() {
@Override
public boolean isVisible(ModuleNode node) {
switch((JDKVisibility)jdkComboBox.getSelectedItem()) {
case ALL:
return true;
case DIRECT:
if(!node.isJdk()) {
return true;
} else {
Collection<GraphEdge<ModuleNode>> edges = scene.findNodeEdges(scene.getGraphNodeRepresentant(node), true, true);
return edges.stream().anyMatch(e -> !e.getSource().isJdk() && !getEdge(e.getSource(), e.getTarget()).isTrasitive());
}
case NONE:
return !node.isJdk();
default:
assert false;
return true;
}
}
@Override
public boolean isVisible(ModuleNode source, ModuleNode target) {
DependencyEdge edge = getEdge(source, target);
assert edge != null;
if(edge.isTrasitive() && !transitiveCheckBox.isSelected()) {
return false;
}
switch((JDKVisibility)jdkComboBox.getSelectedItem()) {
case ALL:
return true;
case DIRECT:
case NONE:
return isVisible(source) && isVisible(target);
default:
assert false;
return true;
}
}
@Override
public Stroke getStroke(ModuleNode source, ModuleNode target) {
if(!transitiveCheckBox.isSelected()) {
return null;
}
DependencyEdge edge = getEdge(source, target);
assert edge != null;
Stroke s = null;
if(edge.isTrasitive()) {
s = TRANSITIVE_STROKE;
}
return s;
}
@Override
public Icon getIcon(ModuleNode node) {
return null;
}
@Override
public Color getColor(ModuleNode node) {
return null;
}
};
scene = new DependencyGraphScene<>(null, GraphTopComponent.this::getSelectedDepth, null, paintingProvider);
nodes.stream().forEach((n)-> {
scene.addGraphNodeImpl(n);
});
edges.stream().forEach((e)-> scene.addEdge(e.getSource(), e.getTarget()));
scene.calculatePrimaryPathsAndLevels();
JComponent sceneView = scene.getView();
if (sceneView == null) {
sceneView = scene.createView();
// vlv: print
sceneView.putClientProperty("print.printable", true); // NOI18N
}
pane.setViewportView(sceneView);
scene.setSurroundingScrollPane(pane);
scene.initialLayout();
scene.setSelectedObjects(Collections.singleton(scene.getRootGraphNode()));
if (scene.getMaxNodeDepth() > 1) {
lblPath.setVisible(true);
((SpinnerNumberModel)maxPathSpinner.getModel()).
setMaximum(Integer.valueOf(scene.getMaxNodeDepth()));
maxPathSpinner.setEnabled(true);
maxPathSpinner.setVisible(true);
}
scene.highlightDepth(getSelectedDepth());
enableControls(true);
}
private DependencyEdge getEdge(ModuleNode source, ModuleNode target) {
// XXX suboptimal, though still extremely cheap
for (DependencyEdge edge : edges) {
if(edge.getSource().equals(source) && edge.getTarget().equals(target)) {
return edge;
}
}
return null;
}
private static class EditorToolbar extends org.openide.awt.Toolbar {
public EditorToolbar() {
Border b = UIManager.getBorder("Nb.Editor.Toolbar.border"); //NOI18N
setBorder(b);
if( "Aqua".equals(UIManager.getLookAndFeel().getID()) ) { //NOI18N
setBackground(UIManager.getColor("NbExplorerView.background")); //NOI18N
}
}
@Override
public String getUIClassID() {
if( UIManager.get("Nb.Toolbar.ui") != null ) { //NOI18N
return "Nb.Toolbar.ui"; //NOI18N
}
return super.getUIClassID();
}
@Override
public String getName() {
return "editorToolbar"; //NOI18N
}
}
private int getSelectedDepth() {
return ((SpinnerNumberModel)maxPathSpinner.getModel()).getNumber().intValue();
}
private static class ChangeSupport extends FileChangeAdapter implements PropertyChangeListener, DocumentListener {
private final Runnable resetAction;
private final FileObject file;
private final EditorCookie.Observable ec;
private Document currentDoc;
ChangeSupport(
@NonNull final FileObject file,
@NonNull final Runnable resetAction) {
Parameters.notNull("file", file); //NOI18N
Parameters.notNull("resetAction", resetAction); //NOI18N
this.resetAction = resetAction;
this.file = file;
this.file.addFileChangeListener(WeakListeners.create(FileChangeListener.class, this, this.file));
EditorCookie.Observable cookie = null;
try {
final DataObject dobj = DataObject.find(file);
cookie = dobj.getLookup().lookup(EditorCookie.Observable.class);
} catch (DataObjectNotFoundException e) {
//pass
}
this.ec = cookie;
if (this.ec != null) {
this.ec.addPropertyChangeListener(WeakListeners.propertyChange(this, this.ec));
assignDocListener(this.ec);
}
}
@Override
public void fileDataCreated(FileEvent fe) {
reset();
}
@Override
public void fileChanged(FileEvent fe) {
reset();
}
@Override
public void fileDeleted(FileEvent fe) {
reset();
}
@Override
public void fileRenamed(FileRenameEvent fe) {
reset();
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (EditorCookie.Observable.PROP_DOCUMENT.equals(evt.getPropertyName())) {
assignDocListener(this.ec);
}
}
@Override
public void insertUpdate(DocumentEvent e) {
reset();
}
@Override
public void removeUpdate(DocumentEvent e) {
reset();
}
@Override
public void changedUpdate(DocumentEvent e) {
}
private void reset() {
resetAction.run();
}
private void assignDocListener(@NonNull final EditorCookie ec) {
if (currentDoc != null) {
currentDoc.removeDocumentListener(this);
}
currentDoc = ec.getDocument();
if (currentDoc != null) {
currentDoc.addDocumentListener(this);
}
}
}
@NbBundle.Messages({
"LBL_All=Show all JDK modules",
"LBL_Direct=Show only direct JDK dependencies",
"LBL_None=Hide all JDK modules"
})
private enum JDKVisibility {
ALL(Bundle.LBL_All()),
DIRECT(Bundle.LBL_Direct()),
NONE(Bundle.LBL_None());
final String displayName;
private JDKVisibility(String displayName) {
this.displayName = displayName;
}
}
private static class JDKRenderer extends DefaultListCellRenderer {
@Override
@SuppressWarnings({"AssignmentToMethodParameter"})
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
if(value instanceof JDKVisibility) {
value = ((JDKVisibility)value).displayName;
}
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
}
}
}