/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed 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 com.android.tools.idea.editors.navigation;
import com.android.SdkConstants;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.rendering.api.ViewInfo;
import com.android.ide.common.resources.ResourceResolver;
import com.android.navigation.*;
import com.android.resources.ResourceType;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.editors.navigation.macros.Analyser;
import com.android.tools.idea.rendering.*;
import com.android.tools.idea.wizard.NewAndroidActivityWizard;
import com.intellij.ide.dnd.DnDEvent;
import com.intellij.ide.dnd.DnDManager;
import com.intellij.ide.dnd.DnDTarget;
import com.intellij.ide.dnd.TransferableWrapper;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.JBMenuItem;
import com.intellij.openapi.ui.JBPopupMenu;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.xml.XmlFileImpl;
import com.intellij.psi.xml.XmlTag;
import com.intellij.ui.Gray;
import com.intellij.ui.JBColor;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.LineBorder;
import java.awt.*;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.*;
import java.util.List;
import static com.android.tools.idea.editors.navigation.Utilities.*;
@SuppressWarnings("UseOfSystemOutOrSystemErr")
public class NavigationView extends JComponent {
//private static final Logger LOG = Logger.getInstance("#" + NavigationView.class.getName());
public static final com.android.navigation.Dimension GAP = new com.android.navigation.Dimension(500, 100);
private static final Color BACKGROUND_COLOR = new JBColor(Gray.get(192), Gray.get(70));
private static final Color TRIGGER_BACKGROUND_COLOR = new JBColor(Gray.get(200), Gray.get(60));
private static final Color SNAP_GRID_LINE_COLOR_MINOR = new JBColor(Gray.get(180), Gray.get(60));
private static final Color SNAP_GRID_LINE_COLOR_MIDDLE = new JBColor(Gray.get(170), Gray.get(50));
private static final Color SNAP_GRID_LINE_COLOR_MAJOR = new JBColor(Gray.get(160), Gray.get(40));
private static final float ZOOM_FACTOR = 1.1f;
// Snap grid
private static final int MINOR_SNAP = 32;
private static final int MIDDLE_COUNT = 5;
private static final int MAJOR_COUNT = 10;
public static final Dimension MINOR_SNAP_GRID = new Dimension(MINOR_SNAP, MINOR_SNAP);
public static final Dimension MIDDLE_SNAP_GRID = scale(MINOR_SNAP_GRID, MIDDLE_COUNT);
public static final Dimension MAJOR_SNAP_GRID = scale(MINOR_SNAP_GRID, MAJOR_COUNT);
public static final int MIN_GRID_LINE_SEPARATION = 8;
public static final int LINE_WIDTH = 12;
private static final Point MULTIPLE_DROP_STRIDE = point(MAJOR_SNAP_GRID);
private static final String ID_PREFIX = "@+id/";
private static final Color TRANSITION_LINE_COLOR = new JBColor(new Color(80, 80, 255), new Color(40, 40, 255));
private static final Condition<Component> SCREENS = instanceOf(AndroidRootComponent.class);
private static final Condition<Component> EDITORS = not(SCREENS);
private static final boolean DRAW_DESTINATION_RECTANGLES = false;
private static final boolean DEBUG = false;
// See http://www.google.com/design/spec/patterns/gestures.html#gestures-gestures
private static final Color GESTURE_ICON_COLOR = new JBColor(new Color(0xE64BA7), new Color(0xE64BA7));
private final RenderingParameters myRenderingParams;
private final NavigationModel myNavigationModel;
private final SelectionModel mySelectionModel;
private final Assoc<State, AndroidRootComponent> myStateComponentAssociation = new Assoc<State, AndroidRootComponent>();
private final Assoc<Transition, Component> myTransitionEditorAssociation = new Assoc<Transition, Component>();
private boolean myStateCacheIsValid;
private boolean myTransitionEditorCacheIsValid;
private Map<State, Map<String, RenderedView>> myLocationToRenderedView = new IdentityHashMap<State, Map<String, RenderedView>>();
private Image myBackgroundImage;
private Point myMouseLocation;
private Transform myTransform = new Transform(1 / 4f);
// Configuration
private boolean showRollover = false;
private boolean mDrawGrid = false;
/*
void foo(String layoutFileBaseName) {
System.out.println("layoutFileBaseName = " + layoutFileBaseName);
Module module = myMyRenderingParams.myFacet.getModule();
ConfigurationManager manager = ConfigurationManager.create(module);
LocalResourceRepository resources = AppResourceRepository.getAppResources(module, true);
for (Device device : manager.getDevices()) {
com.android.sdklib.devices.State portrait = device.getDefaultState().deepCopy();
com.android.sdklib.devices.State landscape = device.getDefaultState().deepCopy();
portrait.setOrientation(ScreenOrientation.PORTRAIT);
landscape.setOrientation(ScreenOrientation.LANDSCAPE);
System.out.println("file = " + getMatchingFile(layoutFileBaseName, resources, DeviceConfigHelper.getFolderConfig(portrait)));
System.out.println("file = " + getMatchingFile(layoutFileBaseName, resources, DeviceConfigHelper.getFolderConfig(landscape)));
}
}
*/
/* In projects with one module with an AndroidFacet, return that AndroidFacet. */
@Nullable
private static AndroidFacet getAndroidFacet(@NotNull Project project, @NotNull NavigationEditor.ErrorHandler handler) {
AndroidFacet result = null;
for (Module module : ModuleManager.getInstance(project).getModules()) {
AndroidFacet facet = AndroidFacet.getInstance(module);
if (facet == null) {
continue;
}
if (result == null) {
result = facet;
}
else {
handler.handleError("", "Sorry, Navigation Editor does not yet support multiple module projects. ");
return null;
}
}
return result;
}
@Nullable
public static RenderingParameters getRenderingParams(@NotNull Project project,
@NotNull VirtualFile file,
@NotNull NavigationEditor.ErrorHandler handler) {
AndroidFacet facet = getAndroidFacet(project, handler);
if (facet == null) {
return null;
}
Configuration configuration = facet.getConfigurationManager().getConfiguration(file);
return new RenderingParameters(project, configuration, facet);
}
public NavigationView(RenderingParameters renderingParams, NavigationModel model, SelectionModel selectionModel) {
myRenderingParams = renderingParams;
myNavigationModel = model;
mySelectionModel = selectionModel;
setFocusable(true);
setLayout(null);
// Mouse listener
{
MouseAdapter mouseListener = new MyMouseListener();
addMouseListener(mouseListener);
addMouseMotionListener(mouseListener);
}
// Focus listener
{
addFocusListener(new FocusListener() {
@Override
public void focusGained(FocusEvent focusEvent) {
repaint();
}
@Override
public void focusLost(FocusEvent focusEvent) {
repaint();
}
});
}
// Drag and Drop listener
{
final DnDManager dndManager = DnDManager.getInstance();
dndManager.registerTarget(new MyDnDTarget(), this);
}
// Key listeners
{
Action remove = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
mySelectionModel.getSelection().remove();
setSelection(Selections.NULL);
}
};
registerKeyBinding(KeyEvent.VK_DELETE, "delete", remove);
registerKeyBinding(KeyEvent.VK_BACK_SPACE, "backspace", remove);
}
// Model listener
{
myNavigationModel.getListeners().add(new Listener<NavigationModel.Event>() {
@Override
public void notify(@NotNull NavigationModel.Event event) {
if (DEBUG) System.out.println("NavigationView:: <listener> " + myStateCacheIsValid + " " + myTransitionEditorCacheIsValid);
if (event.operandType.isAssignableFrom(State.class)) {
myStateCacheIsValid = false;
}
if (event.operandType.isAssignableFrom(Transition.class)) {
myTransitionEditorCacheIsValid = false;
}
revalidate();
repaint();
}
});
}
}
@Nullable
private static RenderedView getRenderedView(AndroidRootComponent c, Point location) {
return c.getRenderedView(diff(location, c.getLocation()));
}
@Nullable
Transition getTransition(AndroidRootComponent sourceComponent, @Nullable RenderedView namedSourceLeaf, Point mouseUpLocation) {
Component destComponent = getComponentAt(mouseUpLocation);
if (sourceComponent != destComponent) {
if (destComponent instanceof AndroidRootComponent) {
AndroidRootComponent destinationRoot = (AndroidRootComponent)destComponent;
RenderedView endLeaf = getRenderedView(destinationRoot, mouseUpLocation);
RenderedView namedEndLeaf = getNamedParent(endLeaf);
Map<AndroidRootComponent, State> rootComponentToState = getStateComponentAssociation().valueToKey;
Locator sourceLocator = Locator.of(rootComponentToState.get(sourceComponent), getViewId(namedSourceLeaf));
Locator destinationLocator = Locator.of(rootComponentToState.get(destComponent), getViewId(namedEndLeaf));
return new Transition("", sourceLocator, destinationLocator);
}
}
return null;
}
static Rectangle getBounds(AndroidRootComponent c, @Nullable RenderedView leaf) {
if (leaf == null) {
return c.getBounds();
}
Rectangle r = c.transform.getBounds(leaf);
return new Rectangle(c.getX() + r.x, c.getY() + r.y, r.width, r.height);
}
Rectangle getNamedLeafBoundsAt(Component sourceComponent, Point location) {
Component destComponent = getComponentAt(location);
if (sourceComponent != destComponent) {
if (destComponent instanceof AndroidRootComponent) {
AndroidRootComponent destinationRoot = (AndroidRootComponent)destComponent;
RenderedView endLeaf = getRenderedView(destinationRoot, location);
RenderedView namedEndLeaf = getNamedParent(endLeaf);
return getBounds(destinationRoot, namedEndLeaf);
}
}
return new Rectangle(location);
}
public float getScale() {
return myTransform.myScale;
}
public void setScale(float scale) {
myTransform = new Transform(scale);
myBackgroundImage = null;
for (AndroidRootComponent root : getStateComponentAssociation().keyToValue.values()) {
root.setScale(scale);
}
setPreferredSize();
revalidate();
repaint();
}
public void zoom(boolean in) {
setScale(myTransform.myScale * (in ? ZOOM_FACTOR : 1 / ZOOM_FACTOR));
}
private Assoc<State, AndroidRootComponent> getStateComponentAssociation() {
if (!myStateCacheIsValid) {
syncStateCache(myStateComponentAssociation);
myStateCacheIsValid = true;
}
return myStateComponentAssociation;
}
private Assoc<Transition, Component> getTransitionEditorAssociation() {
if (!myTransitionEditorCacheIsValid) {
syncTransitionCache(myTransitionEditorAssociation);
myTransitionEditorCacheIsValid = true;
}
return myTransitionEditorAssociation;
}
@Nullable
static String getViewId(@Nullable RenderedView leaf) {
if (leaf != null) {
XmlTag tag = leaf.tag;
if (tag != null) {
String attributeValue = tag.getAttributeValue("android:id");
if (attributeValue != null && attributeValue.startsWith(ID_PREFIX)) {
return attributeValue.substring(ID_PREFIX.length());
}
}
}
return null;
}
@Nullable
static String getViewId(@Nullable ViewInfo leaf) {
if (leaf != null) {
Object cookie = leaf.getCookie();
if (cookie instanceof XmlTag) {
XmlTag tag = (XmlTag)cookie;
String attributeValue = tag.getAttributeValue("android:id");
if (attributeValue != null && attributeValue.startsWith(ID_PREFIX)) {
return attributeValue.substring(ID_PREFIX.length());
}
}
}
return null;
}
@Nullable
static RenderedView getNamedParent(@Nullable RenderedView view) {
while (view != null && getViewId(view) == null) {
view = view.getParent();
}
return view;
}
private Map<String, RenderedView> getNameToRenderedView(State state) {
Map<String, RenderedView> result = myLocationToRenderedView.get(state);
if (result == null) {
AndroidRootComponent androidRootComponent = getStateComponentAssociation().keyToValue.get(state);
if (androidRootComponent == null) {
return Collections.emptyMap();
}
RenderResult renderResult = androidRootComponent.getRenderResult();
if (renderResult == null) {
return Collections.emptyMap(); // rendering library hasn't loaded, temporarily return an empty map
}
RenderedViewHierarchy hierarchy = renderResult.getHierarchy();
if (hierarchy == null) {
return Collections.emptyMap();
}
List<RenderedView> roots = hierarchy.getRoots();
Map<String, RenderedView> renderedViews = new HashMap<String, RenderedView>();
for (RenderedView root : roots) {
renderedViews.putAll(createViewNameToRenderedView(root));
}
myLocationToRenderedView.put(state, result = renderedViews);
}
return result;
}
private static Map<String, RenderedView> createViewNameToRenderedView(@NotNull RenderedView root) {
final Map<String, RenderedView> result = new HashMap<String, RenderedView>();
new Object() {
void walk(RenderedView parent) {
for (RenderedView child : parent.getChildren()) {
String id = getViewId(child);
if (id != null) {
result.put(id, child);
}
walk(child);
}
}
}.walk(root);
return result;
}
private static Map<String, ViewInfo> createViewNameToViewInfo(@NotNull ViewInfo root) {
final Map<String, ViewInfo> result = new HashMap<String, ViewInfo>();
new Object() {
void walk(ViewInfo parent) {
for (ViewInfo child : parent.getChildren()) {
String id = getViewId(child);
if (id != null) {
result.put(id, child);
}
walk(child);
}
}
}.walk(root);
return result;
}
static void paintLeaf(Graphics g, @Nullable RenderedView leaf, Color color, AndroidRootComponent component) {
if (leaf != null) {
Color oldColor = g.getColor();
g.setColor(color);
drawRectangle(g, getBounds(component, leaf));
g.setColor(oldColor);
}
}
private void registerKeyBinding(int keyCode, String name, Action action) {
InputMap inputMap = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
inputMap.put(KeyStroke.getKeyStroke(keyCode, 0), name);
getActionMap().put(name, action);
}
private void setSelection(@NotNull Selections.Selection selection) {
mySelectionModel.setSelection(selection);
// the re-validate() call shouldn't be necessary but removing it causes orphaned
// combo-boxes to remain visible (and click-able) after a 'remove' operation
revalidate();
repaint();
}
private void moveSelection(Point location) {
mySelectionModel.getSelection().moveTo(location);
revalidate();
repaint();
}
private void setMouseLocation(Point mouseLocation) {
myMouseLocation = mouseLocation;
if (showRollover) {
repaint();
}
}
private void finaliseSelectionLocation(Point location) {
mySelectionModel.setSelection(mySelectionModel.getSelection().finaliseSelectionLocation(location));
revalidate();
repaint();
}
/*
private List<State> findDestinationsFor(State state, Set<State> exclude) {
List<State> result = new ArrayList<State>();
for (Transition transition : myNavigationModel) {
State source = transition.getSource();
if (source.equals(state)) {
State destination = transition.getDestination();
if (!exclude.contains(destination)) {
result.add(destination);
}
}
}
return result;
}
*/
private void drawGrid(Graphics g, Color c, Dimension modelSize, int width, int height) {
g.setColor(c);
Dimension viewSize = myTransform.modelToView(com.android.navigation.Dimension.create(modelSize));
if (viewSize.width < MIN_GRID_LINE_SEPARATION || viewSize.height < MIN_GRID_LINE_SEPARATION) {
return;
}
for (int x = 0; x < myTransform.viewToModelW(width); x += modelSize.width) {
int vx = myTransform.modelToViewX(x);
g.drawLine(vx, 0, vx, getHeight());
}
for (int y = 0; y < myTransform.viewToModelH(height); y += modelSize.height) {
int vy = myTransform.modelToViewY(y);
g.drawLine(0, vy, getWidth(), vy);
}
}
private void drawBackground(Graphics g, int width, int height) {
g.setColor(BACKGROUND_COLOR);
g.fillRect(0, 0, width, height);
drawGrid(g, SNAP_GRID_LINE_COLOR_MINOR, MINOR_SNAP_GRID, width, height);
drawGrid(g, SNAP_GRID_LINE_COLOR_MIDDLE, MIDDLE_SNAP_GRID, width, height);
drawGrid(g, SNAP_GRID_LINE_COLOR_MAJOR, MAJOR_SNAP_GRID, width, height);
}
private Image getBackGroundImage() {
if (myBackgroundImage == null ||
myBackgroundImage.getWidth(null) != getWidth() ||
myBackgroundImage.getHeight(null) != getHeight()) {
myBackgroundImage = UIUtil.createImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
drawBackground(myBackgroundImage.getGraphics(), getWidth(), getHeight());
}
return myBackgroundImage;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
// draw background
if (mDrawGrid) {
g.drawImage(getBackGroundImage(), 0, 0, null);
}
else {
Color tmp = getBackground();
g.setColor(BACKGROUND_COLOR);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(tmp);
}
// draw component shadows
for (Component c : getStateComponentAssociation().keyToValue.values()) {
Rectangle r = c.getBounds();
ShadowPainter.drawRectangleShadow(g, r.x, r.y, r.width, r.height);
}
}
public static Graphics2D createLineGraphics(Graphics g, int lineWidth) {
Graphics2D g2D = (Graphics2D)g.create();
g2D.setColor(TRANSITION_LINE_COLOR);
g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2D.setStroke(new BasicStroke(lineWidth));
return g2D;
}
private static Rectangle getCorner(Point a, int cornerDiameter) {
int cornerRadius = cornerDiameter / 2;
return new Rectangle(a.x - cornerRadius, a.y - cornerRadius, cornerDiameter, cornerDiameter);
}
private static void drawLine(Graphics g, Point a, Point b) {
g.drawLine(a.x, a.y, b.x, b.y);
}
private static void drawArrow(Graphics g, Point a, Point b, int lineWidth) {
Utilities.drawArrow(g, a.x, a.y, b.x, b.y, lineWidth);
}
private static void drawRectangle(Graphics g, Rectangle r) {
g.drawRect(r.x, r.y, r.width, r.height);
}
private static int x1(Rectangle src) {
return src.x;
}
private static int x2(Rectangle dst) {
return dst.x + dst.width;
}
private static int y1(Rectangle src) {
return src.y;
}
private static int y2(Rectangle dst) {
return dst.y + dst.height;
}
static class Line {
public final Point a;
public final Point b;
Line(Point a, Point b) {
this.a = a;
this.b = b;
}
Point project(Point p) {
boolean horizontal = a.x == b.x;
boolean vertical = a.y == b.y;
if (!horizontal && !vertical) {
throw new UnsupportedOperationException();
}
return horizontal ? new Point(a.x, p.y) : new Point(p.x, a.y);
}
}
static Line getMidLine(Rectangle src, Rectangle dst) {
Point midSrc = centre(src);
Point midDst = centre(dst);
int dx = Math.abs(midSrc.x - midDst.x);
int dy = Math.abs(midSrc.y - midDst.y);
boolean horizontal = dx >= dy;
int middle;
if (horizontal) {
middle = x1(src) - x2(dst) > 0 ? (x2(dst) + x1(src)) / 2 : (x2(src) + x1(dst)) / 2;
}
else {
middle = y1(src) - y2(dst) > 0 ? (y2(dst) + y1(src)) / 2 : (y2(src) + y1(dst)) / 2;
}
Point a = horizontal ? new Point(middle, midSrc.y) : new Point(midSrc.x, middle);
Point b = horizontal ? new Point(middle, midDst.y) : new Point(midDst.x, middle);
return new Line(a, b);
}
private Line getMidLine(Transition t) {
Map<State, AndroidRootComponent> m = getStateComponentAssociation().keyToValue;
State src = t.getSource().getState();
State dst = t.getDestination().getState();
return getMidLine(m.get(src).getBounds(), m.get(dst).getBounds());
}
static Point[] getControlPoints(Rectangle src, Rectangle dst, Line midLine) {
Point a = midLine.project(centre(src));
Point b = midLine.project(centre(dst));
return new Point[]{project(a, src), a, b, project(b, dst)};
}
private Point[] getControlPoints(Transition t) {
return getControlPoints(getBounds(t.getSource()), getBounds(t.getDestination()), getMidLine(t));
}
private static int getTurnLength(Point[] points, float scale) {
int N = points.length;
int cornerDiameter = (int)(Math.min(MAJOR_SNAP_GRID.width, MAJOR_SNAP_GRID.height) * scale);
for (int i = 0; i < N - 1; i++) {
Point a = points[i];
Point b = points[i + 1];
int length = (int)length(diff(b, a));
if (i != 0 && i != N - 2) {
length /= 2;
}
cornerDiameter = Math.min(cornerDiameter, length);
}
return cornerDiameter;
}
private static void drawCurve(Graphics g, Point[] points, float scale) {
final int N = points.length;
final int cornerDiameter = getTurnLength(points, scale);
boolean horizontal = points[0].x != points[1].x;
Point previous = points[0];
for (int i = 1; i < N - 1; i++) {
Rectangle turn = getCorner(points[i], cornerDiameter);
Point startTurn = project(previous, turn);
drawLine(g, previous, startTurn);
Point endTurn = project(points[i + 1], turn);
drawCorner(g, startTurn, endTurn, horizontal);
previous = endTurn;
horizontal = !horizontal;
}
Point endPoint = points[N - 1];
if (length(diff(previous, endPoint)) > 1) { //
drawArrow(g, previous, endPoint, (int)(LINE_WIDTH * scale));
}
}
public void drawTransition(Graphics g, Rectangle src, Rectangle dst, Point[] controlPoints) {
// draw source rect
drawRectangle(g, src);
// draw curved 'Manhattan route' from source to destination
drawCurve(g, controlPoints, myTransform.myScale);
// draw destination rect
if (DRAW_DESTINATION_RECTANGLES) {
Color oldColor = g.getColor();
g.setColor(JBColor.CYAN);
drawRectangle(g, dst);
g.setColor(oldColor);
}
}
private void drawTransition(Graphics g, Transition t) {
drawTransition(g, getBounds(t.getSource()), getBounds(t.getDestination()), getControlPoints(t));
}
public void paintTransitions(Graphics g) {
for (Transition transition : myNavigationModel.getTransitions()) {
drawTransition(g, transition);
}
}
private static int angle(Point p) {
//if ((p.x == 0) == (p.y == 0)) {
// throw new IllegalArgumentException();
//}
return p.x > 0 ? 0 : p.y < 0 ? 90 : p.x < 0 ? 180 : 270;
}
private static void drawCorner(Graphics g, Point a, Point b, boolean horizontal) {
int radiusX = Math.abs(a.x - b.x);
int radiusY = Math.abs(a.y - b.y);
Point centre = horizontal ? new Point(a.x, b.y) : new Point(b.x, a.y);
int startAngle = angle(diff(a, centre));
int endAngle = angle(diff(b, centre));
int dangle = endAngle - startAngle;
int angle = dangle - (Math.abs(dangle) <= 180 ? 0 : 360 * sign(dangle));
g.drawArc(centre.x - radiusX, centre.y - radiusY, radiusX * 2, radiusY * 2, startAngle, angle);
}
private RenderedView getRenderedView(Locator locator) {
return getNameToRenderedView(locator.getState()).get(locator.getViewName());
}
private void paintRollover(Graphics2D lineGraphics) {
if (myMouseLocation == null || !showRollover) {
return;
}
Component component = getComponentAt(myMouseLocation);
if (component instanceof AndroidRootComponent) {
Stroke oldStroke = lineGraphics.getStroke();
lineGraphics.setStroke(new BasicStroke(1));
AndroidRootComponent androidRootComponent = (AndroidRootComponent)component;
RenderedView leaf = getRenderedView(androidRootComponent, myMouseLocation);
RenderedView namedLeaf = getNamedParent(leaf);
paintLeaf(lineGraphics, leaf, JBColor.RED, androidRootComponent);
paintLeaf(lineGraphics, namedLeaf, JBColor.BLUE, androidRootComponent);
lineGraphics.setStroke(oldStroke);
}
}
private void paintSelection(Graphics g) {
mySelectionModel.getSelection().paint(g, hasFocus());
mySelectionModel.getSelection().paintOver(g);
}
private void paintChildren(Graphics g, Condition<Component> condition) {
Rectangle bounds = new Rectangle();
for (int i = getComponentCount() - 1; i >= 0; i--) {
Component child = getComponent(i);
if (condition.value(child)) {
child.getBounds(bounds);
Graphics cg = g.create(bounds.x, bounds.y, bounds.width, bounds.height);
child.paint(cg);
}
}
}
@Override
protected void paintChildren(Graphics g) {
paintChildren(g, SCREENS);
Graphics2D lineGraphics = createLineGraphics(g, myTransform.modelToViewW(LINE_WIDTH));
paintTransitions(lineGraphics);
paintRollover(lineGraphics);
paintSelection(g);
paintChildren(g, EDITORS);
}
private Rectangle getBounds(Locator source) {
Map<State, AndroidRootComponent> stateToComponent = getStateComponentAssociation().keyToValue;
AndroidRootComponent component = stateToComponent.get(source.getState());
return getBounds(component, getRenderedView(source));
}
@Override
public void doLayout() {
Map<Transition, Component> transitionToEditor = getTransitionEditorAssociation().keyToValue;
Map<State, AndroidRootComponent> stateToComponent = getStateComponentAssociation().keyToValue;
for (State state : stateToComponent.keySet()) {
AndroidRootComponent root = stateToComponent.get(state);
root.setLocation(myTransform.modelToView(myNavigationModel.getStateToLocation().get(state)));
root.setSize(root.getPreferredSize());
}
for (Transition transition : myNavigationModel.getTransitions()) {
String gesture = transition.getType();
if (gesture != null) {
Component editor = transitionToEditor.get(transition);
if (editor == null) { // if model is changed on another thread we may see null here (with new notification system)
continue;
}
if (editor.getParent() == null) { // unclear why this happens
add(editor);
}
Dimension preferredSize = editor.getPreferredSize();
Point[] points = getControlPoints(transition);
Point location = diff(midPoint(points[1], points[2]), midPoint(preferredSize));
editor.setLocation(location);
editor.setSize(preferredSize);
}
}
}
private <K, V extends Component> void removeLeftovers(Assoc<K, V> assoc, Collection<K> a) {
for (Map.Entry<K, V> e : new ArrayList<Map.Entry<K, V>>(assoc.keyToValue.entrySet())) {
K k = e.getKey();
V v = e.getValue();
if (!a.contains(k)) {
assoc.remove(k, v);
remove(v);
repaint();
}
}
}
private static JComponent getPressGestureIcon() {
return new JComponent() {
private Dimension SIZE = new Dimension(24, 24);
@Override
public Dimension getPreferredSize() {
return SIZE;
}
@Override
public void paintComponent(Graphics g) {
RenderingHints rh = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
((Graphics2D)g).setRenderingHints(rh);
g.setColor(GESTURE_ICON_COLOR);
g.fillOval(0, 0, SIZE.width - 1, SIZE.height - 1);
}
};
}
private static JLabel getSwipeGestureIcon() {
JLabel result = new JLabel("<->");
result.setFont(result.getFont().deriveFont(20f));
result.setForeground(TRANSITION_LINE_COLOR);
result.setBackground(TRIGGER_BACKGROUND_COLOR);
result.setBorder(new LineBorder(TRANSITION_LINE_COLOR, 1));
result.setOpaque(true);
return result;
}
private static Component createEditorFor(final Transition transition) {
String gesture = transition.getType();
return gesture.equals(Transition.PRESS) ? getPressGestureIcon() : getSwipeGestureIcon();
}
private void syncTransitionCache(Assoc<Transition, Component> assoc) {
if (DEBUG) System.out.println("NavigationView: syncTransitionCache");
// add anything that is in the model but not in our cache
for (Transition transition : myNavigationModel.getTransitions()) {
if (!assoc.keyToValue.containsKey(transition)) {
Component editor = createEditorFor(transition);
add(editor);
assoc.add(transition, editor);
}
}
// remove anything that is in our cache but not in the model
removeLeftovers(assoc, myNavigationModel.getTransitions());
}
@Nullable
public static PsiFile getLayoutXmlFile(boolean menu, @Nullable String resourceName, Configuration configuration, Project project) {
ResourceType resourceType = menu ? ResourceType.MENU : ResourceType.LAYOUT;
PsiManager psiManager = PsiManager.getInstance(project);
ResourceResolver resourceResolver = configuration.getResourceResolver();
if (resourceResolver == null) {
return null;
}
ResourceValue projectResource = resourceResolver.getProjectResource(resourceType, resourceName);
if (projectResource == null) { /// seems to happen when we create a new resource
return null;
}
VirtualFile file = virtualFile(new File(projectResource.getValue()));
return file == null ? null : psiManager.findFile(file);
}
private AndroidRootComponent createRootComponentFor(State state) {
boolean isMenu = state instanceof MenuState;
Module module = myRenderingParams.myFacet.getModule();
String resourceName = isMenu ? state.getXmlResourceName() : Analyser.getXMLFileName(module, state.getClassName(), true);
PsiFile psiFile = getLayoutXmlFile(isMenu, resourceName, myRenderingParams.myConfiguration, myRenderingParams.myProject);
AndroidRootComponent result = new AndroidRootComponent(myRenderingParams, psiFile, isMenu);
result.setScale(myTransform.myScale);
return result;
}
private void syncStateCache(Assoc<State, AndroidRootComponent> assoc) {
if (DEBUG) System.out.println("NavigationView: syncStateCache");
assoc.clear();
removeAll();
//repaint();
// add anything that is in the model but not in our cache
for (State state : myNavigationModel.getStates()) {
if (!assoc.keyToValue.containsKey(state)) {
AndroidRootComponent root = createRootComponentFor(state);
assoc.add(state, root);
add(root);
}
}
setPreferredSize();
}
private static com.android.navigation.Point getMaxLoc(Collection<com.android.navigation.Point> locations) {
int maxX = 0;
int maxY = 0;
for (com.android.navigation.Point location : locations) {
maxX = Math.max(maxX, location.x);
maxY = Math.max(maxY, location.y);
}
return new com.android.navigation.Point(maxX, maxY);
}
private void setPreferredSize() {
com.android.navigation.Dimension size = myRenderingParams.getDeviceScreenSize();
com.android.navigation.Dimension gridSize = new com.android.navigation.Dimension(size.width + GAP.width, size.height + GAP.height);
com.android.navigation.Point maxLoc = getMaxLoc(myNavigationModel.getStateToLocation().values());
Dimension max = myTransform.modelToView(new com.android.navigation.Dimension(maxLoc.x + gridSize.width, maxLoc.y + gridSize.height));
setPreferredSize(max);
}
private Selections.Selection createSelection(Point mouseDownLocation, boolean shiftDown) {
Component component = getComponentAt(mouseDownLocation);
if (component instanceof NavigationView) {
return Selections.NULL;
}
Transition transition = getTransitionEditorAssociation().valueToKey.get(component);
if (component instanceof AndroidRootComponent) {
AndroidRootComponent androidRootComponent = (AndroidRootComponent)component;
if (!shiftDown) {
State state = getStateComponentAssociation().valueToKey.get(androidRootComponent);
if (state == null) {
return Selections.NULL;
}
setComponentZOrder(androidRootComponent, 0);
return new Selections.AndroidRootComponentSelection(myNavigationModel, androidRootComponent, transition, myRenderingParams,
mouseDownLocation, state, myTransform);
}
else {
RenderedView leaf = getRenderedView(androidRootComponent, mouseDownLocation);
return new Selections.RelationSelection(myNavigationModel, androidRootComponent, mouseDownLocation, getNamedParent(leaf), this);
}
}
else {
return new Selections.ComponentSelection<Component>(myRenderingParams, myNavigationModel, component, transition);
}
}
private class MyMouseListener extends MouseAdapter {
private void showPopup(MouseEvent e) {
JPopupMenu menu = new JBPopupMenu();
JMenuItem anItem = new JBMenuItem("New Activity...");
anItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent actionEvent) {
Module module = myRenderingParams.myFacet.getModule();
NewAndroidActivityWizard dialog = new NewAndroidActivityWizard(module, null, null);
dialog.init();
dialog.setOpenCreatedFiles(false);
dialog.show();
}
});
menu.add(anItem);
menu.show(e.getComponent(), e.getX(), e.getY());
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger()) {
showPopup(e);
return;
}
if (e.getButton() != MouseEvent.BUTTON1) {
return;
}
Point location = e.getPoint();
boolean modified = (e.isShiftDown() || e.isControlDown() || e.isMetaDown());
setSelection(createSelection(location, modified));
requestFocus();
}
@Override
public void mouseMoved(MouseEvent e) {
if (e.getButton() != MouseEvent.BUTTON1) {
return;
}
setMouseLocation(e.getPoint());
}
@Override
public void mouseDragged(MouseEvent e) {
if (e.getButton() != MouseEvent.BUTTON1) {
return;
}
moveSelection(e.getPoint());
}
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
Component child = getComponentAt(e.getPoint());
if (child instanceof AndroidRootComponent) {
AndroidRootComponent androidRootComponent = (AndroidRootComponent)child;
androidRootComponent.launchLayoutEditor();
}
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) {
showPopup(e);
return;
}
if (e.getButton() != MouseEvent.BUTTON1) {
return;
}
finaliseSelectionLocation(e.getPoint());
}
}
private class MyDnDTarget implements DnDTarget {
private int applicableDropCount = 0;
private void execute(State state, boolean execute) {
if (!getStateComponentAssociation().keyToValue.containsKey(state)) {
if (execute) {
myNavigationModel.addState(state);
}
else {
applicableDropCount++;
}
}
}
private void dropOrPrepareToDrop(DnDEvent anEvent, boolean execute) {
Object attachedObject = anEvent.getAttachedObject();
if (attachedObject instanceof TransferableWrapper) {
TransferableWrapper wrapper = (TransferableWrapper)attachedObject;
PsiElement[] psiElements = wrapper.getPsiElements();
Point dropLoc = anEvent.getPointOn(NavigationView.this);
if (psiElements != null) {
for (PsiElement element : psiElements) {
if (element instanceof XmlFileImpl) {
PsiFile containingFile = element.getContainingFile();
PsiDirectory dir = containingFile.getParent();
if (dir != null && dir.getName().equals(SdkConstants.FD_RES_MENU)) {
String resourceName = ResourceHelper.getResourceName(containingFile);
State state = new MenuState(resourceName);
execute(state, execute);
}
}
if (element instanceof PsiQualifiedNamedElement) {
PsiQualifiedNamedElement namedElement = (PsiQualifiedNamedElement)element;
String qualifiedName = namedElement.getQualifiedName();
if (qualifiedName != null) {
State state = new ActivityState(qualifiedName);
Dimension size = myRenderingParams.getDeviceScreenSizeFor(myTransform);
Point dropLocation = diff(dropLoc, midPoint(size));
myNavigationModel.getStateToLocation().put(state, myTransform.viewToModel(snap(dropLocation, MIDDLE_SNAP_GRID)));
execute(state, execute);
dropLoc = Utilities.add(dropLocation, MULTIPLE_DROP_STRIDE);
}
}
}
}
}
if (execute) {
revalidate();
repaint();
}
}
@Override
public boolean update(DnDEvent anEvent) {
applicableDropCount = 0;
dropOrPrepareToDrop(anEvent, false);
anEvent.setDropPossible(applicableDropCount > 0);
return false;
}
@Override
public void drop(DnDEvent anEvent) {
dropOrPrepareToDrop(anEvent, true);
}
@Override
public void cleanUpOnLeave() {
}
@Override
public void updateDraggedImage(Image image, Point dropPoint, Point imageOffset) {
}
}
}