com.android.tools.idea.editors.navigation.NavigationView.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.editors.navigation.NavigationView.java

Source

/*
 * 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.resources.ResourceResolver;
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.editors.navigation.macros.CodeGenerator;
import com.android.tools.idea.editors.navigation.macros.FragmentEntry;
import com.android.tools.idea.editors.navigation.model.*;
import com.android.tools.idea.model.ManifestInfo;
import com.android.tools.idea.rendering.*;
import com.android.tools.idea.wizard.NewAndroidActivityWizard;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
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.diagnostic.Logger;
import com.intellij.openapi.module.Module;
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.util.Conditions;
import com.intellij.openapi.vfs.VfsUtil;
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.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import javax.swing.border.LineBorder;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.*;

import static com.android.tools.idea.editors.navigation.NavigationEditorUtils.*;

public class NavigationView extends JComponent {
    private static final Logger LOG = Logger.getInstance(NavigationView.class.getName());
    public static final ModelDimension GAP = new ModelDimension(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));

    public 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 Condition<Component> SCREENS = instanceOf(AndroidRootComponent.class);
    private static final Condition<Component> EDITORS = Conditions.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 static final String DEVICE_DEFAULT_THEME_NAME = SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX
            + "Theme.DeviceDefault";
    public static final int RESOURCE_SUFFIX_LENGTH = ".xml".length();
    public static final String LIST_VIEW_ID = "list";
    public static final int FAKE_OVERFLOW_MENU_WIDTH = 10;
    public static final boolean SHOW_FAKE_OVERFLOW_MENUS = true;

    private final RenderingParameters myRenderingParams;
    private final NavigationModel myNavigationModel;
    private final SelectionModel mySelectionModel;
    private final CodeGenerator myCodeGenerator;

    private final BiMap<State, AndroidRootComponent> myStateComponentAssociation = HashBiMap.create();
    private final BiMap<Transition, Component> myTransitionEditorAssociation = HashBiMap.create();

    private boolean myStateCacheIsValid;
    private boolean myTransitionEditorCacheIsValid;
    private Map<State, Map<String, RenderedView>> myNameToRenderedView = new IdentityHashMap<State, Map<String, RenderedView>>();
    private Image myBackgroundImage;
    private Point myMouseLocation;
    private Transform myTransform = new Transform(1 / 4f);

    // Configuration

    private boolean myShowRollover = false;
    @SuppressWarnings("FieldCanBeLocal")
    private boolean myDrawGrid = false;

    public NavigationView(RenderingParameters renderingParams, NavigationModel model, SelectionModel selectionModel,
            CodeGenerator codeGenerator) {
        myRenderingParams = renderingParams;
        myNavigationModel = model;
        mySelectionModel = selectionModel;
        myCodeGenerator = codeGenerator;

        setFocusable(true);
        setLayout(null);

        // Mouse listener
        MouseAdapter mouseListener = new MyMouseListener();
        addMouseListener(mouseListener);
        addMouseMotionListener(mouseListener);

        // Popup menu
        final JPopupMenu menu = new JBPopupMenu();
        final JMenuItem anItem = new JBMenuItem("New Activity...");
        anItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent actionEvent) {
                Module module = myRenderingParams.facet.getModule();
                NewAndroidActivityWizard dialog = new NewAndroidActivityWizard(module, null, null);
                dialog.init();
                dialog.setOpenCreatedFiles(false);
                dialog.show();
            }
        });
        menu.add(anItem);
        setComponentPopupMenu(menu);

        // 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) {
                setSelection(Selections.NULL);
            }
        };
        registerKeyBinding(KeyEvent.VK_DELETE, "delete", remove);
        registerKeyBinding(KeyEvent.VK_BACK_SPACE, "backspace", remove);

        // Model listener
        myNavigationModel.getListeners().add(new Listener<Event>() {
            @Override
            public void notify(@NotNull Event event) {
                if (DEBUG)
                    LOG.info("NavigationView:: <listener> " + myStateCacheIsValid + " "
                            + myTransitionEditorCacheIsValid);
                if (event.operandType.isAssignableFrom(State.class)) {
                    myStateCacheIsValid = false;
                }
                if (event.operandType.isAssignableFrom(Transition.class)) {
                    myTransitionEditorCacheIsValid = false;
                }
                if (event == NavigationEditor.PROJECT_READ) {
                    setSelection(Selections.NULL);
                }
                revalidate();
                repaint();
            }
        });
    }

    @Nullable
    private static RenderedView getRenderedView(AndroidRootComponent c, Point location) {
        return c.getRenderedView(diff(location, c.getLocation()));
    }

    @Nullable
    private String getFragmentClassName(State sourceState, @Nullable RenderedView namedSourceLeaf) {
        if (namedSourceLeaf == null) {
            return null;
        }
        if (sourceState instanceof ActivityState) {
            ActivityState sourceActivityState = (ActivityState) sourceState;
            XmlTag tag = namedSourceLeaf.tag;
            if (tag == null) {
                return null;
            }
            PsiFile fragmentFile = tag.getContainingFile();
            String resourceFileName = fragmentFile.getName();
            String resourceName = resourceFileName.substring(0, resourceFileName.length() - RESOURCE_SUFFIX_LENGTH);
            for (FragmentEntry fragment : sourceActivityState.getFragments()) {
                Module module = myRenderingParams.facet.getModule();
                String fragmentClassName = fragment.className;
                String resource = Analyser.getXMLFileName(module, fragmentClassName, false);
                if (resource == null) {
                    PsiClass listClass = NavigationEditorUtils.getPsiClass(module, "android.app.ListFragment");
                    if (listClass == null) {
                        LOG.warn("Can't find: android.app.ListFragment");
                        continue;
                    }
                    PsiClass psiClass = NavigationEditorUtils.getPsiClass(module, fragmentClassName);
                    if (psiClass != null && (psiClass.isInheritor(listClass, true))) {
                        if (tag.getName().equals("ListView")) {
                            return fragmentClassName;
                        }
                    }
                }
                if (resourceName.equals(resource)) {
                    return fragmentClassName;
                }
            }
        }
        return null;
    }

    void createTransition(AndroidRootComponent sourceComponent, @Nullable RenderedView namedSourceLeaf,
            Point mouseUpLocation) {
        Component destComponent = getComponentAt(mouseUpLocation);
        if (sourceComponent != destComponent) {
            if (destComponent instanceof AndroidRootComponent) {
                AndroidRootComponent destinationRoot = (AndroidRootComponent) destComponent;
                if (destinationRoot.isMenu()) {
                    return;
                }
                RenderedView endLeaf = getRenderedView(destinationRoot, mouseUpLocation);
                RenderedView namedEndLeaf = HierarchyUtils.getNamedParent(endLeaf);

                Map<AndroidRootComponent, State> rootComponentToState = getStateComponentAssociation().inverse();
                State sourceState = rootComponentToState.get(sourceComponent);
                String fragmentClassName = getFragmentClassName(sourceState, namedSourceLeaf);
                Locator sourceLocator = Locator.of(sourceState, fragmentClassName,
                        HierarchyUtils.getViewId(namedSourceLeaf));
                Locator destinationLocator = Locator.of(rootComponentToState.get(destComponent),
                        HierarchyUtils.getViewId(namedEndLeaf));
                myCodeGenerator
                        .implementTransition(new Transition(Transition.PRESS, sourceLocator, destinationLocator));
            }
        }
    }

    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 + AndroidRootComponent.PADDING,
                c.getY() + r.y + AndroidRootComponent.getTopShift(), r.width, r.height);
    }

    Rectangle getNamedLeafBoundsAt(Component sourceComponent, Point location, boolean penetrate) {
        Component destComponent = getComponentAt(location);
        if (sourceComponent != destComponent) {
            if (destComponent instanceof AndroidRootComponent) {
                AndroidRootComponent destinationRoot = (AndroidRootComponent) destComponent;
                if (!destinationRoot.isMenu()) {
                    if (!penetrate) {
                        return destinationRoot.getBounds();
                    }
                    RenderedView endLeaf = getRenderedView(destinationRoot, location);
                    RenderedView namedEndLeaf = HierarchyUtils.getNamedParent(endLeaf);
                    return getBounds(destinationRoot, namedEndLeaf);
                }
            }
        }
        return new Rectangle(location);
    }

    public void setScale(float scale) {
        myTransform = new Transform(scale);
        myBackgroundImage = null;
        for (AndroidRootComponent root : getStateComponentAssociation().values()) {
            root.setScale(scale);
        }
        setPreferredSize();

        revalidate();
        repaint();
    }

    public void zoom(int n) {
        setScale(myTransform.myScale * (float) Math.pow(ZOOM_FACTOR, n));
    }

    private BiMap<State, AndroidRootComponent> getStateComponentAssociation() {
        if (!myStateCacheIsValid) {
            syncStateCache(myStateComponentAssociation);
            myStateCacheIsValid = true;
        }
        return myStateComponentAssociation;
    }

    private BiMap<Transition, Component> getTransitionEditorAssociation() {
        if (!myTransitionEditorCacheIsValid) {
            syncTransitionCache(myTransitionEditorAssociation);
            myTransitionEditorCacheIsValid = true;
        }
        return myTransitionEditorAssociation;
    }

    private static Map<String, RenderedView> computeNameToRenderedView(RenderedViewHierarchy hierarchy) {
        Map<String, RenderedView> result = new HashMap<String, RenderedView>();
        for (RenderedView root : hierarchy.getRoots()) {
            result.putAll(createViewNameToRenderedView(root));
        }
        return result;
    }

    private Map<String, RenderedView> getNameToRenderedView(State state) {
        Map<String, RenderedView> result = myNameToRenderedView.get(state);
        if (result == null) {
            AndroidRootComponent androidRootComponent = getStateComponentAssociation().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();
            }

            myNameToRenderedView.put(state, result = computeNameToRenderedView(hierarchy));
        }
        return result;
    }

    private static void fillViewByIdMap(RenderedView parent, Map<String, RenderedView> map) {
        for (RenderedView child : parent.getChildren()) {
            String id = HierarchyUtils.getViewId(child);
            if (id != null) {
                map.put(id, child);
            }
            // The view of a ListActivity or ListFragment may not have an id.
            // To make th views of these special classes locatable, add an entry for all elements where the tag name is "ListView".
            // todo deal with multiple listViews in a single layout
            XmlTag tag = child.tag;
            if (tag != null) {
                if (tag.getName().equals("ListView")) {
                    map.put(LIST_VIEW_ID, child);
                }
            }
            fillViewByIdMap(child, map);
        }
    }

    private static Map<String, RenderedView> createViewNameToRenderedView(@NotNull RenderedView root) {
        final Map<String, RenderedView> result = new HashMap<String, RenderedView>();
        // Add fake rendered view for overflow menus so that sources of a menu transitions are shown upper right
        if (SHOW_FAKE_OVERFLOW_MENUS) {
            int w = FAKE_OVERFLOW_MENU_WIDTH;
            result.put(Analyser.FAKE_OVERFLOW_MENU_ID,
                    new RenderedView(root, null, null, root.x + root.w - w, 0, w, w));
        }
        fillViewByIdMap(root, result);
        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 (myShowRollover) {
            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(ModelDimension.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 (myDrawGrid) {
            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().values()) {
            Rectangle r = c.getBounds();
            ShadowPainter.drawRectangleShadow(g, r.x, r.y, r.width - AndroidRootComponent.PADDING,
                    r.height - AndroidRootComponent.PADDING);
        }
    }

    static Point[] getControlPoints(Rectangle src, Rectangle dst, Line midLine) {
        Point a = midLine.a;
        Point b = midLine.b;
        return new Point[] { project(a, src), a, b, project(b, dst) };
    }

    private Point[] getControlPoints(Transition t) {
        Rectangle srcBounds = getBounds(t.getSource());
        Rectangle dstBounds = getBounds(t.getDestination());
        return getControlPoints(srcBounds, dstBounds, NavigationEditorUtils.getMidLine(srcBounds, dstBounds));
    }

    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.getViewId());
    }

    private void paintRollover(Graphics2D lineGraphics) {
        if (myMouseLocation == null || !myShowRollover) {
            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 = HierarchyUtils.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();
        AndroidRootComponent component = stateToComponent.get(source.getState());
        return getBounds(component, getRenderedView(source));
    }

    @Override
    public void doLayout() {
        Map<Transition, Component> transitionToEditor = getTransitionEditorAssociation();

        Map<State, AndroidRootComponent> stateToComponent = getStateComponentAssociation();
        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(BiMap<K, V> assoc, Collection<K> a) {
        for (Map.Entry<K, V> e : new ArrayList<Map.Entry<K, V>>(assoc.entrySet())) {
            K k = e.getKey();
            V v = e.getValue();
            if (!a.contains(k)) {
                assoc.remove(k);
                remove(v);
                repaint();
            }
        }
    }

    private JComponent getPressGestureIcon() {
        return new JComponent() {
            private ModelDimension SIZE = new ModelDimension(100, 100);

            @Override
            public Dimension getPreferredSize() {
                return myTransform.modelToView(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, getWidth() - 1, getHeight() - 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 Component createEditorFor(final Transition transition) {
        String gesture = transition.getType();
        return gesture.equals(Transition.PRESS) ? getPressGestureIcon() : getSwipeGestureIcon();
    }

    private void syncTransitionCache(BiMap<Transition, Component> assoc) {
        if (DEBUG)
            LOG.info("NavigationView: syncTransitionCache");
        // add anything that is in the model but not in our cache
        for (Transition transition : myNavigationModel.getTransitions()) {
            if (!assoc.containsKey(transition)) {
                Component editor = createEditorFor(transition);
                add(editor);
                assoc.put(transition, editor);
            }
        }
        // remove anything that is in our cache but not in the model
        removeLeftovers(assoc, myNavigationModel.getTransitions());
    }

    @Nullable
    private static VirtualFile getLayoutXmlVirtualFile(boolean menu, @Nullable String resourceName,
            Configuration configuration) {
        ResourceType resourceType = menu ? ResourceType.MENU : ResourceType.LAYOUT;
        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;
        }
        return VfsUtil.findFileByIoFile(new File(projectResource.getValue()), false);
    }

    @Nullable
    public static PsiFile getLayoutXmlFile(boolean menu, @Nullable String resourceName, Configuration configuration,
            Project project) {
        VirtualFile file = getLayoutXmlVirtualFile(menu, resourceName, configuration);
        return file == null ? null : PsiManager.getInstance(project).findFile(file);
    }

    private RenderingParameters getActivityRenderingParameters(Module module, String className) {
        ManifestInfo manifestInfo = ManifestInfo.get(module, false);
        Configuration newConfiguration = myRenderingParams.configuration.clone();
        String theme = manifestInfo.getManifestTheme();
        ManifestInfo.ActivityAttributes activityAttributes = manifestInfo.getActivityAttributes(className);
        if (activityAttributes != null) {
            String activityTheme = activityAttributes.getTheme();
            theme = activityTheme != null ? activityTheme : theme;
        }
        newConfiguration.setTheme(theme);
        return myRenderingParams.withConfiguration(newConfiguration);
    }

    private AndroidRootComponent createUnscaledRootComponentFor(State state) {
        boolean isMenu = state instanceof MenuState;
        Module module = myRenderingParams.facet.getModule();
        String resourceName = Analyser.getXMLFileName(module, state.getClassName(), true);
        String menuName = isMenu ? ((MenuState) state).getXmlResourceName() : null;
        VirtualFile virtualFile = getLayoutXmlVirtualFile(false, resourceName, myRenderingParams.configuration);
        if (virtualFile == null) {
            return new AndroidRootComponent(state.getClassName(), myRenderingParams, null, menuName);
        } else {
            PsiFile psiFile = PsiManager.getInstance(myRenderingParams.project).findFile(virtualFile);
            RenderingParameters params = getActivityRenderingParameters(module, state.getClassName());
            return new AndroidRootComponent(state.getClassName(), params, psiFile, menuName);
        }
    }

    private AndroidRootComponent createRootComponentFor(State state) {
        AndroidRootComponent result = createUnscaledRootComponentFor(state);
        result.setScale(myTransform.myScale);
        return result;
    }

    private void syncStateCache(BiMap<State, AndroidRootComponent> assoc) {
        if (DEBUG)
            LOG.info("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.containsKey(state)) {
                AndroidRootComponent root = createRootComponentFor(state);
                assoc.put(state, root);
                add(root);
            }
        }

        setPreferredSize();
    }

    private static ModelPoint getMaxLoc(Collection<ModelPoint> locations) {
        int maxX = 0;
        int maxY = 0;
        for (ModelPoint location : locations) {
            maxX = Math.max(maxX, location.x);
            maxY = Math.max(maxY, location.y);
        }
        return new ModelPoint(maxX, maxY);
    }

    private void setPreferredSize() {
        ModelDimension size = myRenderingParams.getDeviceScreenSize();
        ModelDimension gridSize = new ModelDimension(size.width + GAP.width, size.height + GAP.height);
        ModelPoint maxLoc = getMaxLoc(myNavigationModel.getStateToLocation().values());
        Dimension max = myTransform
                .modelToView(new ModelDimension(maxLoc.x + gridSize.width, maxLoc.y + gridSize.height));
        setPreferredSize(max);
    }

    private void bringToFront(@Nullable State state) {
        if (state != null) {
            AndroidRootComponent menuComponent = getStateComponentAssociation().get(state);
            if (menuComponent != null) {
                setComponentZOrder(menuComponent, 0);
            }
        }
    }

    private static void debug(@Nullable String s) {
        //if (DEBUG) System.out.println(s);
        //noinspection ConstantConditions
        LOG.debug(s);
    }

    private static void debug(String name, @Nullable RenderedView view) {
        if (DEBUG)
            debug(name + ": \n" + HierarchyUtils.toString(view));
    }

    private Selections.Selection createSelection(Point mouseDownLocation, boolean shiftDown) {
        Component component = getComponentAt(mouseDownLocation);
        if (component instanceof NavigationView) {
            return Selections.NULL;
        }
        Transition transition = getTransitionEditorAssociation().inverse().get(component);
        if (component instanceof AndroidRootComponent) {
            Point location = AndroidRootComponent.relativePoint(mouseDownLocation);
            // Select a top-level 'screen'
            AndroidRootComponent androidRootComponent = (AndroidRootComponent) component;
            State state = getStateComponentAssociation().inverse().get(androidRootComponent);
            if (!shiftDown) {
                if (state == null) {
                    return Selections.NULL;
                }
                bringToFront(state);
                if (state instanceof ActivityState) {
                    bringToFront(myNavigationModel.findAssociatedMenuState((ActivityState) state));
                }
                return new Selections.AndroidRootComponentSelection(myNavigationModel, androidRootComponent,
                        transition, myRenderingParams, location, state, myTransform);
            } else {
                // Select a specific view
                RenderedView leaf = getRenderedView(androidRootComponent, location);
                if (leaf == null) {
                    return Selections.NULL;
                }
                debug("root", HierarchyUtils.getRoot(leaf));
                debug("leaf", leaf);
                RenderedView namedParent = HierarchyUtils.getNamedParent(leaf);
                if (namedParent == null) {
                    return Selections.NULL;
                }
                debug("namedParent", namedParent);

                if (myNavigationModel.findTransitionWithSource(
                        Locator.of(state, HierarchyUtils.getViewId(namedParent))) != null) {
                    return Selections.NULL;
                }
                return new Selections.ViewSelection(androidRootComponent, location, namedParent, this);
            }
        } else {
            // Select the transition/gesture component
            return new Selections.ComponentSelection<Component>(myRenderingParams, myNavigationModel, component,
                    transition);
        }
    }

    private class MyMouseListener extends MouseAdapter {
        @Override
        public void mousePressed(MouseEvent e) {
            if (!SwingUtilities.isLeftMouseButton(e)) {
                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 (!SwingUtilities.isLeftMouseButton(e)) {
                return;
            }
            setMouseLocation(e.getPoint());
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            if (!SwingUtilities.isLeftMouseButton(e)) {
                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 (!SwingUtilities.isLeftMouseButton(e)) {
                return;
            }
            finaliseSelectionLocation(e.getPoint());
        }
    }

    private class MyDnDTarget implements DnDTarget {
        private int applicableDropCount = 0;

        private void execute(State state, boolean execute) {
            if (!getStateComponentAssociation().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 = NavigationEditorUtils.sum(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) {
        }
    }
}