blob: 8a83d5bd4e98d859109ed159ba244cce7b1e0411 [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.graph;
import java.awt.Color;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.Timer;
import javax.swing.UIManager;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.StaticResource;
import org.netbeans.api.visual.action.ActionFactory;
import org.netbeans.api.visual.action.SelectProvider;
import org.netbeans.api.visual.action.WidgetAction;
import org.netbeans.api.visual.border.BorderFactory;
import org.netbeans.api.visual.layout.LayoutFactory;
import org.netbeans.api.visual.model.ObjectState;
import org.netbeans.api.visual.widget.ImageWidget;
import org.netbeans.api.visual.widget.LabelWidget;
import org.netbeans.api.visual.widget.LevelOfDetailsWidget;
import org.netbeans.api.visual.widget.Scene;
import org.netbeans.api.visual.widget.Widget;
import static org.netbeans.modules.java.graph.Bundle.ACT_FixVersionConflict;
import static org.netbeans.modules.java.graph.Bundle.TIP_MultipleConflict;
import static org.netbeans.modules.java.graph.Bundle.TIP_MultipleWarning;
import static org.netbeans.modules.java.graph.Bundle.TIP_SingleConflict;
import static org.netbeans.modules.java.graph.Bundle.TIP_SingleWarning;
import static org.netbeans.modules.java.graph.DependencyGraphScene.VersionProvider.VERSION_CONFLICT;
import static org.netbeans.modules.java.graph.DependencyGraphScene.VersionProvider.VERSION_NO_CONFLICT;
import org.netbeans.api.visual.widget.general.IconNodeWidget;
import org.netbeans.api.visual.widget.general.IconNodeWidget.TextOrientation;
import org.openide.util.ImageUtilities;
import org.openide.util.NbBundle.Messages;
import org.openide.util.Parameters;
/**
*
* @author mkleint
*/
class NodeWidget<I extends GraphNodeImplementation> extends Widget implements ActionListener {
static final Color ROOT = new Color(178, 228, 255);
static final Color DIRECTS = new Color(178, 228, 255);
static final Color DIRECTS_CONFLICT = new Color(235, 88, 194);
static final Color DISABLE_HIGHTLIGHT = new Color(255, 255, 194);
static final Color HIGHTLIGHT = new Color(255, 255, 129);
static final Color DISABLE_CONFLICT = new Color(219, 155, 153);
static final Color CONFLICT = new Color(219, 11, 5);
static final Color MANAGED = new Color(30, 255, 150);
static final Color WARNING = new Color(255, 150, 20);
static final Color DISABLE_WARNING = EdgeWidget.deriveColor(WARNING, 0.7f);
private static final int LEFT_TOP = 1;
private static final int LEFT_BOTTOM = 2;
private static final int RIGHT_TOP = 3;
private static final int RIGHT_BOTTOM = 4;
private static final @StaticResource String LOCK_ICON = "org/netbeans/modules/java/graph/resources/lock.png";
private static final @StaticResource String LOCK_BROKEN_ICON = "org/netbeans/modules/java/graph/resources/lock-broken.png";
private static final @StaticResource String BULB_ICON = "org/netbeans/modules/java/graph/resources/bulb.gif";
private static final @StaticResource String BULB_HIGHLIGHT_ICON = "org/netbeans/modules/java/graph/resources/bulb-highlight.gif";
private GraphNode<I> node;
private boolean readable = false;
private boolean enlargedFromHover = false;
private Timer hoverTimer;
private Color hoverBorderC;
private IconNodeWidget nodeW;
private LabelWidget versionW;
private Widget contentW;
private ImageWidget lockW, fixHintW;
private int paintState = EdgeWidget.REGULAR;
private Font origFont;
private Color origForeground;
private String tooltipText;
private final WidgetAction fixConflictAction;
private final WidgetAction sceneHoverActionAction;
// for use from FruchtermanReingoldLayout
public double locX, locY, dispX, dispY;
private boolean fixed;
NodeWidget(@NonNull DependencyGraphScene scene, GraphNode<I> node, final Action fixConflictAction, final WidgetAction sceneHoverActionAction) {
super(scene);
Parameters.notNull("node", node);
if(fixConflictAction != null) {
Parameters.notNull("sceneHoverActionAction", sceneHoverActionAction);
}
this.node = node;
this.fixConflictAction = fixConflictAction != null ? ActionFactory.createSelectAction(new SelectProvider() {
@Override public boolean isAimingAllowed(Widget widget, Point localLocation, boolean invertSelection) {
return false;
}
@Override public boolean isSelectionAllowed(Widget widget, Point localLocation, boolean invertSelection) {
return true;
}
@Override
public void select(Widget widget, Point localLocation, boolean invertSelection) {
fixConflictAction.actionPerformed(null);
}
}) : null;
this.sceneHoverActionAction = sceneHoverActionAction;
setLayout(LayoutFactory.createVerticalFlowLayout());
updateTooltip();
initContent(scene, node.getImpl(), scene.getIcon(node));
hoverTimer = new Timer(500, this);
hoverTimer.setRepeats(false);
hoverBorderC = UIManager.getColor("TextPane.selectionBackground");
if (hoverBorderC == null) {
hoverBorderC = Color.GRAY;
}
}
@Messages({"TIP_Text=<html>{0}<br>{1}</html>",
"TIP_SingleConflict=Conflict with <b>{0}</b> version required by <b>{1}</b>",
"TIP_SingleWarning=Warning, older version <b>{0}</b> requested by <b>{1}</b>",
"TIP_MultipleConflict=Conflicts with:<table><thead><tr><th>Version</th><th>Artifact</th></tr></thead><tbody>",
"TIP_MultipleWarning=Warning, older versions requested:<table><thead><tr><th>Version</th><th>Artifact</th></tr></thead><tbody>"})
private void updateTooltip () {
DependencyGraphScene scene = getDependencyGraphScene();
tooltipText = Bundle.TIP_Text(node.getTooltipText(), scene.supportsVersions() ? getConflictTooltip(node) : "");
setToolTipText(tooltipText);
}
public String getConflictTooltip(GraphNode<I> node) {
DependencyGraphScene<I> scene = getDependencyGraphScene();
StringBuilder tooltip = new StringBuilder();
int conflictCount = 0;
I firstConflict = null;
int conflictType = node.getConflictType(scene::isConflict, scene::compareVersions);
if (conflictType != VERSION_NO_CONFLICT) {
for (I nd : node.getDuplicatesOrConflicts()) {
if (scene.isConflict(nd)) {
conflictCount++;
if (firstConflict == null) {
firstConflict = nd;
}
}
}
}
if (conflictCount == 1) {
I parent = firstConflict.getParent();
String version = scene.getVersion(firstConflict);
String requester = parent != null ? parent.getName() : "???";
tooltip.append(conflictType == VERSION_CONFLICT ? TIP_SingleConflict(version, requester) : TIP_SingleWarning(version, requester));
} else if (conflictCount > 1) {
tooltip.append(conflictType == VERSION_CONFLICT ? TIP_MultipleConflict() : TIP_MultipleWarning());
for (I nd : node.getDuplicatesOrConflicts()) {
if (scene.isConflict(nd)) {
tooltip.append("<tr><td>");
tooltip.append(scene.getVersion(nd));
tooltip.append("</td>");
tooltip.append("<td>");
GraphNodeImplementation parent = nd.getParent();
if (parent != null) {
// Artifact artifact = parent.getArtifact();
// assert artifact != null;
tooltip.append(parent.getName());
}
tooltip.append("</td></tr>");
}
}
tooltip.append("</tbody></table>");
}
return tooltip.toString();
}
private DependencyGraphScene getDependencyGraphScene() {
return (DependencyGraphScene)getScene();
}
public void highlightText(String searchTerm) {
if (searchTerm != null && node.getName().contains(searchTerm)) {
nodeW.setBackground(HIGHTLIGHT);
nodeW.setOpaque(true);
setPaintState(EdgeWidget.REGULAR);
setReadable(true);
} else {
//reset
nodeW.setBackground(Color.WHITE);
nodeW.setOpaque(false);
setPaintState(EdgeWidget.GRAYED);
setReadable(false);
}
}
public void setPaintState (int state) {
if (this.paintState == state) {
return;
}
this.paintState = state;
updatePaintContent();
}
public int getPaintState () {
return paintState;
}
void setFixed(boolean fixed) {
this.fixed = fixed;
}
boolean isFixed() {
return fixed;
}
void updatePaintContent() {
if (origForeground == null) {
origForeground = getForeground();
}
boolean isDisabled = paintState == EdgeWidget.DISABLED;
Color foreC = origForeground;
if (paintState == EdgeWidget.GRAYED || isDisabled) {
foreC = UIManager.getColor("textInactiveText");
if (foreC == null) {
foreC = Color.LIGHT_GRAY;
}
if (isDisabled) {
foreC = new Color ((int)(foreC.getAlpha() / 1.3f), foreC.getRed(),
foreC.getGreen(), foreC.getBlue());
}
}
contentW.setBorder(BorderFactory.createLineBorder(10, foreC));
nodeW.setForeground(foreC);
if(versionW != null) {
versionW.setForeground(foreC);
}
if (lockW != null) {
lockW.setPaintAsDisabled(paintState == EdgeWidget.GRAYED);
lockW.setVisible(!isDisabled);
}
setToolTipText(paintState != EdgeWidget.DISABLED ? tooltipText : null);
contentW.repaint();
setVisible(((DependencyGraphScene)getScene()).isVisible(node));
}
@Messages("ACT_FixVersionConflict=Fix Version Conflict...")
private void initContent (DependencyGraphScene scene, GraphNodeImplementation impl, Icon icon) {
contentW = new LevelOfDetailsWidget(scene, 0.05, 0.1, Double.MAX_VALUE, Double.MAX_VALUE);
contentW.setBorder(BorderFactory.createLineBorder(10));
contentW.setLayout(LayoutFactory.createVerticalFlowLayout(LayoutFactory.SerialAlignment.JUSTIFY, 1));
//Artifact name (with optional project icon on the left)
nodeW = new IconNodeWidget(scene, TextOrientation.RIGHT_CENTER);
nodeW.setLabel(node.getImpl().getQualifiedName() + " ");
if (null != icon) {
nodeW.setImage(ImageUtilities.icon2Image(icon));
}
nodeW.getLabelWidget().setUseGlyphVector(true);
if (node.isRoot()) {
Font defF = scene.getDefaultFont();
nodeW.getLabelWidget().setFont(defF.deriveFont(Font.BOLD, defF.getSize() + 3f));
}
contentW.addChild(nodeW);
if(getDependencyGraphScene().supportsVersions()) {
Widget versionDetW = new LevelOfDetailsWidget(scene, 0.5, 0.7, Double.MAX_VALUE, Double.MAX_VALUE);
versionDetW.setLayout(LayoutFactory.createHorizontalFlowLayout(LayoutFactory.SerialAlignment.CENTER, 2));
contentW.addChild(versionDetW);
versionW = new LabelWidget(scene);
versionW.setLabel(scene.getVersion(node.getImpl()));
versionW.setUseGlyphVector(true);
int mngState = node.getManagedState();
if (mngState != GraphNode.UNMANAGED) {
lockW = new ImageWidget(scene,
mngState == GraphNode.MANAGED ? ImageUtilities.loadImage(LOCK_ICON) : ImageUtilities.loadImage(LOCK_BROKEN_ICON));
}
versionDetW.addChild(versionW);
if (lockW != null) {
versionDetW.addChild(lockW);
}
}
// fix hint
if (fixConflictAction != null) {
Widget rootW = new Widget(scene);
rootW.setLayout(LayoutFactory.createOverlayLayout());
fixHintW = new ImageWidget(scene, ImageUtilities.loadImage(BULB_ICON));
fixHintW.setVisible(false);
fixHintW.setToolTipText(ACT_FixVersionConflict());
fixHintW.getActions().addAction(sceneHoverActionAction);
fixHintW.getActions().addAction(fixConflictAction);
Widget panelW = new Widget(scene);
panelW.setLayout(LayoutFactory.createVerticalFlowLayout(LayoutFactory.SerialAlignment.LEFT_TOP, 0));
panelW.setBorder(BorderFactory.createEmptyBorder(0, 3));
panelW.addChild(fixHintW);
rootW.addChild(panelW);
rootW.addChild(contentW);
addChild(rootW);
} else {
addChild(contentW);
}
}
public void modelChanged () {
DependencyGraphScene scene = getDependencyGraphScene();
if(scene.supportsVersions()) {
versionW.setLabel(scene.getVersion(node.getImpl()));
if (fixConflictAction == null && fixHintW != null) {
fixHintW.setVisible(false);
fixHintW = null;
}
}
updateTooltip();
repaint();
}
@Override
protected void paintBackground() {
super.paintBackground();
if (paintState == EdgeWidget.DISABLED) {
return;
}
DependencyGraphScene scene = getDependencyGraphScene();
Graphics2D g = scene.getGraphics();
Rectangle bounds = getClientArea();
if (node.isRoot()) {
paintBottom(g, bounds, ROOT, Color.WHITE, bounds.height / 2);
} else {
Color scopeC = scene.getColor(node);
if(scopeC != null) {
paintCorner(RIGHT_BOTTOM, g, bounds, scopeC, Color.WHITE, bounds.width / 2, bounds.height / 2);
}
int conflictType = scene.supportsVersions() ? node.getConflictType(scene::isConflict, scene::compareVersions) : VERSION_NO_CONFLICT;
Color leftTopC = null;
if (conflictType != VERSION_NO_CONFLICT) {
leftTopC = conflictType == VERSION_CONFLICT
? (paintState == EdgeWidget.GRAYED ? DISABLE_CONFLICT : CONFLICT)
: (paintState == EdgeWidget.GRAYED ? DISABLE_WARNING : WARNING);
} else {
int state = node.getManagedState();
if (GraphNode.OVERRIDES_MANAGED == state) {
leftTopC = WARNING;
}
}
if (leftTopC != null) {
paintCorner(LEFT_TOP, g, bounds, leftTopC, Color.WHITE, bounds.width, bounds.height / 2);
}
if (node.getPrimaryLevel() == 1) {
paintBottom(g, bounds, DIRECTS, Color.WHITE, bounds.height / 6);
}
}
if (getState().isHovered() || getState().isSelected()) {
paintHover(g, bounds, hoverBorderC, getState().isSelected());
}
}
private static void paintCorner (int corner, Graphics2D g, Rectangle bounds,
Color c1, Color c2, int x, int y) {
double h = y*y + x*x;
int gradX = (int)(y*y*x / h);
int gradY = (int)(y*x*x / h);
Point startPoint = new Point();
Point direction = new Point();
switch (corner) {
case LEFT_TOP:
startPoint.x = bounds.x;
startPoint.y = bounds.y;
direction.x = 1;
direction.y = 1;
break;
case LEFT_BOTTOM:
startPoint.x = bounds.x;
startPoint.y = bounds.y + bounds.height;
direction.x = 1;
direction.y = -1;
break;
case RIGHT_TOP:
startPoint.x = bounds.x + bounds.width;
startPoint.y = bounds.y;
direction.x = -1;
direction.y = 1;
break;
case RIGHT_BOTTOM:
startPoint.x = bounds.x + bounds.width;
startPoint.y = bounds.y + bounds.height;
direction.x = -1;
direction.y = -1;
break;
default:
throw new IllegalArgumentException("Corner id not valid"); //NOI18N
}
g.setPaint(new GradientPaint(startPoint.x, startPoint.y, c1,
startPoint.x + direction.x * gradX,
startPoint.y + direction.y * gradY, c2));
g.fillRect(
Math.min(startPoint.x, startPoint.x + direction.x * x),
Math.min(startPoint.y, startPoint.y + direction.y * y),
x, y);
}
private static void paintBottom (Graphics2D g, Rectangle bounds, Color c1, Color c2, int thickness) {
g.setPaint(new GradientPaint(bounds.x, bounds.y + bounds.height, c1,
bounds.x, bounds.y + bounds.height - thickness, c2));
g.fillRect(bounds.x, bounds.y + bounds.height - thickness, bounds.width, thickness);
}
private static void paintHover (Graphics2D g, Rectangle bounds, Color c, boolean selected) {
g.setColor(c);
g.drawRect(bounds.x + 1, bounds.y + 1, bounds.width - 2, bounds.height - 2);
if (!selected) {
g.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), 150));
}
g.drawRect(bounds.x + 2, bounds.y + 2, bounds.width - 4, bounds.height - 4);
if (selected) {
g.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), 150));
} else {
g.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), 75));
}
g.drawRect(bounds.x + 3, bounds.y + 3, bounds.width - 6, bounds.height - 6);
}
@Override
protected void notifyStateChanged(ObjectState previousState, ObjectState state) {
super.notifyStateChanged(previousState, state);
boolean repaintNeeded = false;
boolean updateNeeded = false;
if (paintState != EdgeWidget.DISABLED) {
if (!previousState.isHovered() && state.isHovered()) {
hoverTimer.restart();
repaintNeeded = true;
}
if (previousState.isHovered() && !state.isHovered()) {
hoverTimer.stop();
repaintNeeded = true;
updateNeeded = enlargedFromHover;
enlargedFromHover = false;
}
}
if (previousState.isSelected() != state.isSelected()) {
updateNeeded = true;
}
if (updateNeeded) {
updateContent();
} else if (repaintNeeded) {
repaint();
}
}
@Override public void actionPerformed(ActionEvent e) {
enlargedFromHover = true;
updateContent();
}
public void setReadable (boolean readable) {
if (this.readable == readable) {
return;
}
this.readable = readable;
updateContent();
}
public boolean isReadable () {
return readable;
}
public GraphNode getNode () {
return node;
}
/**
* readable widgets are calculated based on scene zoom factor when zoom factor changes, the readable scope should too
*/
public void updateReadableZoom() {
if (isReadable()) {
updateContent();
}
}
private void updateContent () {
boolean makeReadable = getState().isSelected() || readable;
Font origF = getOrigFont();
Font newF = origF;
if (makeReadable) {
bringToFront();
// enlarge fonts so that content is readable
newF = getReadable(getScene(), origF);
}
nodeW.getLabelWidget().setFont(newF);
if (versionW != null) {
versionW.setFont(newF);
}
if (fixHintW != null) {
fixHintW.setVisible(makeReadable);
}
}
private Font getOrigFont () {
if (origFont == null) {
origFont = nodeW.getFont();
if (origFont == null) {
origFont = getScene().getDefaultFont();
}
}
return origFont;
}
public static Font getReadable (Scene scene, Font original) {
float fSizeRatio = scene.getDefaultFont().getSize() / (float)original.getSize();
float ratio = (float) Math.max (1, fSizeRatio / Math.max(0.0001f, scene.getZoomFactor()));
if (ratio != 1.0f) {
return original.deriveFont(original.getSize() * ratio);
}
return original;
}
public void bulbHovered () {
if (fixHintW != null) {
fixHintW.setImage(ImageUtilities.loadImage(BULB_HIGHLIGHT_ICON));
}
}
public void bulbUnhovered () {
if (fixHintW != null) {
fixHintW.setImage(ImageUtilities.loadImage(BULB_ICON));
}
}
}