blob: a0b4f48be9aae4f47d8255bd1ca27d6e1355a256 [file] [log] [blame]
/*
* 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.npw.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) {
}
}
}