| /* |
| * 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)); |
| } |
| } |
| |
| } |