Java tutorial
/* * 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) { } } }