blob: 001b45a8be2d8296c6a813e85f4027cc66595be8 [file] [log] [blame]
/*
* Copyright (C) 2015 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.uibuilder.model;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.rendering.api.MergeCookie;
import com.android.ide.common.rendering.api.ViewInfo;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.rendering.*;
import com.android.tools.idea.rendering.ResourceNotificationManager.ResourceChangeListener;
import com.android.tools.idea.rendering.ResourceNotificationManager.ResourceVersion;
import com.android.tools.idea.uibuilder.api.*;
import com.android.tools.idea.uibuilder.handlers.ViewEditorImpl;
import com.android.tools.idea.uibuilder.handlers.ViewHandlerManager;
import com.android.tools.idea.uibuilder.lint.LintAnnotationsModel;
import com.android.tools.idea.uibuilder.surface.DesignSurface;
import com.android.tools.idea.uibuilder.surface.ScreenView;
import com.android.utils.XmlUtils;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.intellij.android.designer.model.layout.actions.ToggleRenderModeAction;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.progress.util.ProgressIndicatorBase;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.ModificationTracker;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.XmlElementFactory;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.Alarm;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.Update;
import org.jetbrains.android.facet.AndroidFacet;
import javax.swing.Timer;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.InvalidDnDOperationException;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.util.*;
import java.util.List;
import static com.android.SdkConstants.*;
/**
* Model for an XML file
*/
public class NlModel implements Disposable, ResourceChangeListener, ModificationTracker {
private static final Logger LOG = Logger.getInstance(NlModel.class);
@AndroidCoordinate public static final int EMPTY_COMPONENT_SIZE = 5;
@AndroidCoordinate public static final int VISUAL_EMPTY_COMPONENT_SIZE = 14;
@NonNull private final DesignSurface mySurface;
@NonNull private final AndroidFacet myFacet;
private final XmlFile myFile;
private RenderResult myRenderResult;
private final Configuration myConfiguration;
private final List<ModelListener> myListeners = Lists.newArrayList();
private List<NlComponent> myComponents = Lists.newArrayList();
private final SelectionModel mySelectionModel;
private LintAnnotationsModel myLintAnnotationsModel;
private final long myId;
private Disposable myParent;
private boolean myActive;
private ResourceVersion myRenderedVersion;
private long myModificationCount;
private int myRenderDelay = 10;
private AndroidPreviewProgressIndicator myCurrentIndicator;
private static final Object PROGRESS_LOCK = new Object();
@NonNull
public static NlModel create(@NonNull DesignSurface surface, @Nullable Disposable parent, @NonNull AndroidFacet facet, @NonNull XmlFile file) {
return new NlModel(surface, parent, facet, file);
}
@VisibleForTesting
protected NlModel(@NonNull DesignSurface surface, @Nullable Disposable parent, @NonNull AndroidFacet facet, @NonNull XmlFile file) {
mySurface = surface;
myParent = parent;
myFacet = facet;
myFile = file;
myConfiguration = facet.getConfigurationManager().getConfiguration(myFile.getVirtualFile());
mySelectionModel = new SelectionModel();
myId = System.nanoTime() ^ file.getName().hashCode();
if (parent != null) {
Disposer.register(parent, this);
}
}
public void setParentDisposable(Disposable parent) {
synchronized (myRenderingQueueLock) {
myParent = parent;
}
}
public int getRenderDelay() {
return myRenderDelay;
}
public void setRenderDelay(int renderDelay) {
myRenderDelay = renderDelay;
}
/** Notify model that it's active. A model is active by default. */
public void activate() {
if (!myActive) {
myActive = true;
ResourceNotificationManager manager = ResourceNotificationManager.getInstance(myFile.getProject());
ResourceVersion version = manager.addListener(this, myFacet, myFile, myConfiguration);
if (!version.equals(myRenderedVersion)) {
requestRender();
}
}
}
/** Notify model that it's not active. This means it can stop watching for events etc. It may be activated again in the future. */
public void deactivate() {
if (myActive) {
ResourceNotificationManager manager = ResourceNotificationManager.getInstance(myFile.getProject());
manager.removeListener(this, myFacet, myFile, myConfiguration);
myActive = false;
}
}
public XmlFile getFile() {
return myFile;
}
@NonNull
public SelectionModel getSelectionModel() {
return mySelectionModel;
}
@Nullable
public LintAnnotationsModel getLintAnnotationsModel() {
return myLintAnnotationsModel;
}
public void setLintAnnotationsModel(@Nullable LintAnnotationsModel model) {
myLintAnnotationsModel = model;
requestRender();
}
/** Like {@link #requestRender()}, but tries to do it as quickly as possible (flushes rendering queue) */
public void requestRenderAsap() {
requestRender();
getRenderingQueue().sendFlush();
}
/** Renders immediately and synchronously */
public void renderImmediately() {
getRenderingQueue().cancelAllUpdates();
if (!ApplicationManager.getApplication().isReadAccessAllowed()) {
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
requestRenderAsap();
}
});
return;
}
doRender();
}
public void requestRender() {
ApplicationManager.getApplication().assertIsDispatchThread();
synchronized (PROGRESS_LOCK) {
if (myCurrentIndicator == null) {
myCurrentIndicator = new AndroidPreviewProgressIndicator(0);
myCurrentIndicator.start();
}
}
MergingUpdateQueue renderingQueue = getRenderingQueue();
renderingQueue.cancelAllUpdates();
renderingQueue.queue(new Update("render") {
@Override
public void run() {
DumbService.getInstance(myFacet.getModule().getProject()).waitForSmartMode();
try {
doRender();
}
catch (Throwable e) {
Logger.getInstance(NlModel.class).error(e);
}
synchronized (PROGRESS_LOCK) {
if (myCurrentIndicator != null) {
myCurrentIndicator.stop();
myCurrentIndicator = null;
}
}
}
@Override
public boolean canEat(Update update) {
return true;
}
});
}
@NonNull
private MergingUpdateQueue getRenderingQueue() {
int delay = myRenderDelay;
synchronized (myRenderingQueueLock) {
if (myRenderingQueue == null) {
myRenderingQueue = new MergingUpdateQueue("android.layout.rendering", delay, true, null, myParent, null,
Alarm.ThreadToUse.OWN_THREAD);
}
return myRenderingQueue;
}
}
private final Object myRenderingQueueLock = new Object();
private MergingUpdateQueue myRenderingQueue;
private static final Object RENDERING_LOCK = new Object();
private void doRender() {
if (myFacet.getModule().getProject().isDisposed()) {
return;
}
Configuration configuration = myConfiguration;
if (configuration == null) {
return;
}
// Record the current version we're rendering from; we'll use that in #activate to make sure we're picking up any
// external changes
ResourceNotificationManager resourceNotificationManager = ResourceNotificationManager.getInstance(myFile.getProject());
myRenderedVersion = resourceNotificationManager.getCurrentVersion(myFacet, myFile, myConfiguration);
// Some types of files must be saved to disk first, because layoutlib doesn't
// delegate XML parsers for non-layout files (meaning layoutlib will read the
// disk contents, so we have to push any edits to disk before rendering)
LayoutPullParserFactory.saveFileIfNecessary(myFile);
RenderResult result = null;
synchronized (RENDERING_LOCK) {
RenderService renderService = RenderService.get(myFacet);
RenderLogger logger = renderService.createLogger();
final RenderTask task = renderService.createTask(myFile, configuration, logger, null);
if (task != null) {
if (!ToggleRenderModeAction.isRenderViewPort()) {
task.useDesignMode(myFile);
}
result = task.render();
task.dispose();
}
if (result == null) {
result = RenderResult.createBlank(myFile, logger);
}
}
if (!getRenderingQueue().isEmpty()) {
return;
}
myRenderResult = result;
updateHierarchy(result);
notifyListenersRenderComplete();
}
public void addListener(@NonNull ModelListener listener) {
synchronized (myListeners) {
myListeners.remove(listener); // prevent duplicate registration
myListeners.add(listener);
}
}
public void removeListener(@NonNull ModelListener listener) {
synchronized (myListeners) {
myListeners.remove(listener);
}
}
private void notifyListenersRenderComplete() {
synchronized (myListeners) {
List<ModelListener> listeners = Lists.newArrayList(myListeners);
for (ModelListener listener : listeners) {
listener.modelRendered(this);
}
}
}
@Nullable
public RenderResult getRenderResult() {
return myRenderResult;
}
@NonNull
public AndroidFacet getFacet() {
return myFacet;
}
@NonNull
public Module getModule() {
return myFacet.getModule();
}
@NonNull
public Project getProject() {
return getModule().getProject();
}
@NonNull
public Configuration getConfiguration() {
return myConfiguration;
}
@NonNull
public List<NlComponent> getComponents() {
return myComponents;
}
private final Map<XmlTag,NlComponent> myTagToComponentMap = Maps.newIdentityHashMap();
private final Map<XmlTag,NlComponent> myMergeComponentMap = Maps.newHashMap();
private void updateHierarchy(@Nullable RenderResult result) {
if (result == null || result.getSession() == null || !result.getSession().getResult().isSuccess()) {
myComponents.clear();
return;
}
updateHierarchy(result.getSession().getRootViews());
}
@VisibleForTesting
public void updateHierarchy(@Nullable List<ViewInfo> rootViews) {
for (NlComponent component : myComponents) {
initTagMap(component);
component.children = null;
}
final List<NlComponent> newRoots = Lists.newArrayList();
if (rootViews != null) {
for (ViewInfo info : rootViews) {
NlComponent newRoot = updateHierarchy(null, info, 0, 0);
if (newRoot != null) {
newRoots.add(newRoot);
}
}
}
// TODO: Use result from rendering instead, if available!
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
for (NlComponent root : newRoots) {
TagSnapshot snapshot = TagSnapshot.createTagSnapshot(root.getTag());
updateSnapshot(root, snapshot);
}
}
});
myModificationCount++;
myComponents = newRoots;
}
private static void updateSnapshot(@NonNull NlComponent component, @NonNull TagSnapshot snapshot) {
assert component.getTag() == snapshot.tag;
component.setSnapshot(snapshot);
if (!snapshot.children.isEmpty()) {
if (snapshot.children.size() != component.getChildCount()) {
// TODO: Investigate this; some layouts in iosched triggers this.
return;
}
assert snapshot.children.size() == component.getChildCount();
for (int i = 0, n = component.getChildCount(); i < n; i++) {
NlComponent child = component.getChild(i);
assert child != null;
updateSnapshot(child, snapshot.children.get(i));
}
} else {
assert component.getChildCount() == 0;
}
}
protected void initTagMap(@NonNull NlComponent root) {
myTagToComponentMap.clear();
for (NlComponent component : root.getChildren()) {
gatherTags(myTagToComponentMap, component);
}
}
private static void gatherTags(Map<XmlTag, NlComponent> map, NlComponent component) {
XmlTag tag = component.getTag();
map.put(tag, component);
for (NlComponent child : component.getChildren()) {
gatherTags(map, child);
}
}
@Nullable
private NlComponent updateHierarchy(@Nullable NlComponent parent, ViewInfo view,
@AndroidCoordinate int parentX, @AndroidCoordinate int parentY) {
Object cookie = view.getCookie();
NlComponent component = null;
XmlTag tag = null;
boolean isMerge = false;
if (cookie instanceof XmlTag) {
tag = (XmlTag)cookie;
} else if (cookie instanceof MergeCookie) {
isMerge = true;
cookie = ((MergeCookie)cookie).getCookie();
if (cookie instanceof XmlTag) {
tag = (XmlTag)cookie;
NlComponent mergedComponent = myMergeComponentMap.get(tag);
if (mergedComponent == null && parent != null && tag == parent.getTag()) {
// NumberPicker will render its children with merge cookies pointing to the root
// component (which was not a <merge>)
mergedComponent = parent;
}
if (mergedComponent != null) {
// Just expand the bounds
int left = parentX + view.getLeft();
int top = parentY + view.getTop();
int width = view.getRight() - view.getLeft();
int height = view.getBottom() - view.getTop();
Rectangle rectanglePrev = new Rectangle(mergedComponent.x, mergedComponent.y,
mergedComponent.w, mergedComponent.h);
Rectangle rectangle = new Rectangle(left, top, width, height);
rectangle.add(rectanglePrev);
mergedComponent.setBounds(rectanglePrev.x, rectanglePrev.y, rectanglePrev.width, rectanglePrev.height);
return null;
}
}
}
if (tag != null && parent != null && parent.getTag() == tag) {
tag = null;
}
if (tag != null) {
component = myTagToComponentMap.get(tag);
if (component != null) {
// TODO: Clear out component is the tag is not valid
//if (!tag.isValid()) {
// component = null;
//}
}
if (component == null) {
component = new NlComponent(this, tag);
} else {
component.children = null;
myTagToComponentMap.remove(tag);
component.setTag(tag);
}
component.viewInfo = view;
int left = parentX + view.getLeft();
int top = parentY + view.getTop();
int width = view.getRight() - view.getLeft();
int height = view.getBottom() - view.getTop();
component.setBounds(left, top, Math.max(width, VISUAL_EMPTY_COMPONENT_SIZE), Math.max(height, VISUAL_EMPTY_COMPONENT_SIZE));
if (parent != null && parent != component) {
parent.addChild(component);
if (isMerge) {
myMergeComponentMap.put(tag, component);
}
}
}
if (component != null) {
parent = component;
}
parentX += view.getLeft();
parentY += view.getTop();
for (ViewInfo child : view.getChildren()) {
updateHierarchy(parent, child, parentX, parentY);
}
return component;
}
@Nullable
public List<NlComponent> findByOffset(int offset) {
XmlTag tag = PsiTreeUtil.findElementOfClassAtOffset(myFile, offset, XmlTag.class, false);
return (tag != null) ? findViewsByTag(tag) : null;
}
/**
* Looks up the point at the given pixel coordinates in the Android screen coordinate system, and
* finds the leaf component there and returns it, if any. If the point is outside the screen bounds,
* it will either return null, or the root view if {@code useRootOutsideBounds} is set and there is
* precisely one parent.
*
* @param x the x pixel coordinate
* @param y the y pixel coordinate
* @param useRootOutsideBounds if true, return the root component when pointing outside the screen, otherwise null
* @return the leaf component at the coordinate
*/
@Nullable
public NlComponent findLeafAt(@AndroidCoordinate int x, @AndroidCoordinate int y, boolean useRootOutsideBounds) {
// Search BACKWARDS such that if the children are painted on top of each
// other (as is the case in a FrameLayout) I pick the last one which will
// be topmost!
for (int i = myComponents.size() - 1; i >= 0; i--) {
NlComponent component = myComponents.get(i);
NlComponent leaf = component.findLeafAt(x, y);
if (leaf != null) {
return leaf;
}
}
if (useRootOutsideBounds) {
// If dragging outside of the screen, associate it with the
// root widget (if there is one, and at most one (e.g. not a <merge> tag)
List<NlComponent> components = myComponents;
if (components.size() == 1) {
return components.get(0);
} else {
return null;
}
}
return null;
}
@Nullable
public NlComponent findViewByTag(@NonNull XmlTag tag) {
// TODO: Consider using lookup map
for (NlComponent component : myComponents) {
NlComponent match = component.findViewByTag(tag);
if (match != null) {
return match;
}
}
return null;
}
@Nullable
public List<NlComponent> findViewsByTag(@NonNull XmlTag tag) {
List<NlComponent> result = null;
for (NlComponent view : myComponents) {
List<NlComponent> matches = view.findViewsByTag(tag);
if (matches != null) {
if (result != null) {
result.addAll(matches);
} else {
result = matches;
}
}
}
return result;
}
@Nullable
public NlComponent findViewByPsi(@Nullable PsiElement element) {
assert ApplicationManager.getApplication().isReadAccessAllowed();
while (element != null) {
if (element instanceof XmlTag) {
return findViewByTag((XmlTag)element);
}
element = element.getParent();
}
return null;
}
/**
* Finds any components that overlap the given rectangle.
*
* @param x The top left x corner defining the selection rectangle.
* @param y The top left y corner defining the selection rectangle.
* @param width The w of the selection rectangle
* @param height The h of the selection rectangle
*/
public List<NlComponent> findWithin(@AndroidCoordinate int x,
@AndroidCoordinate int y,
@AndroidCoordinate int width,
@AndroidCoordinate int height) {
List<NlComponent> within = Lists.newArrayList();
for (NlComponent component : myComponents) {
addWithin(within, component, x, y, width, height);
}
return within;
}
private static boolean addWithin(@NonNull List<NlComponent> result,
@NonNull NlComponent component,
@AndroidCoordinate int x,
@AndroidCoordinate int y,
@AndroidCoordinate int width,
@AndroidCoordinate int height) {
if (component.x + component.w <= x ||
x + width <= component.x ||
component.y + component.h <= y ||
y + height <= component.y) {
return false;
}
boolean found = false;
for (NlComponent child : component.getChildren()) {
found |= addWithin(result, child, x, y, width, height);
}
if (!found) {
result.add(component);
}
return true;
}
public void delete(final Collection<NlComponent> components) {
// Group by parent and ask each one to participate
WriteCommandAction<Void> action = new WriteCommandAction<Void>(myFacet.getModule().getProject(), "Delete Component", myFile) {
@Override
protected void run(@NonNull Result<Void> result) throws Throwable {
handleDeletion(components);
}
};
action.execute();
List<NlComponent> remaining = Lists.newArrayList(mySelectionModel.getSelection());
remaining.removeAll(components);
mySelectionModel.setSelection(remaining);
}
private void handleDeletion(@NonNull Collection<NlComponent> components) throws Exception {
// Segment the deleted components into lists of siblings
Map<NlComponent, List<NlComponent>> siblingLists = groupSiblings(components);
ViewHandlerManager viewHandlerManager = ViewHandlerManager.get(myFacet);
// Notify parent components about children getting deleted
for (Map.Entry<NlComponent, List<NlComponent>> entry : siblingLists.entrySet()) {
NlComponent parent = entry.getKey();
if (parent == null) {
continue;
}
List<NlComponent> children = entry.getValue();
boolean finished = false;
ViewHandler handler = viewHandlerManager.getHandler(parent);
if (handler instanceof ViewGroupHandler) {
finished = ((ViewGroupHandler)handler).deleteChildren(parent, children);
}
if (!finished) {
for (NlComponent component : children) {
NlComponent p = component.getParent();
if (p != null) {
p.removeChild(component);
}
component.getTag().delete();
}
}
}
}
/**
* Partitions the given list of components into a map where each value is a list of siblings,
* in the same order as in the original list, and where the keys are the parents (or null
* for the components that do not have a parent).
* <p/>
* The value lists will never be empty. The parent key will be null for components without parents.
*
* @param components the components to be grouped
* @return a map from parents (or null) to a list of components with the corresponding parent
*/
@NonNull
public static Map<NlComponent, List<NlComponent>> groupSiblings(@NonNull Collection<? extends NlComponent> components) {
Map<NlComponent, List<NlComponent>> siblingLists = new HashMap<NlComponent, List<NlComponent>>();
if (components.isEmpty()) {
return siblingLists;
}
if (components.size() == 1) {
NlComponent component = components.iterator().next();
siblingLists.put(component.getParent(), Collections.singletonList(component));
return siblingLists;
}
for (NlComponent component : components) {
NlComponent parent = component.getParent();
List<NlComponent> children = siblingLists.get(parent);
if (children == null) {
children = new ArrayList<NlComponent>();
siblingLists.put(parent, children);
}
children.add(component);
}
return siblingLists;
}
/**
* Creates a new component of the given type. It will optionally insert it as a child of the given parent (and optionally
* right before the given sibling or null to append at the end.)
* <p/>
* Note: This operation can only be called when the caller is already holding a write lock. This will be the
* case from {@link ViewHandler} callbacks such as {@link ViewHandler#onCreate(ViewEditor, NlComponent, NlComponent, InsertType)}
* and {@link com.android.tools.idea.uibuilder.api.DragHandler#commit(int, int, int)}.
*
* @param screenView The target screen, if known. Used to handle pixel to dp computations in view handlers, etc.
* @param fqcn The fully qualified name of the widget to insert, such as {@code android.widget.LinearLayout}.
* You can also pass XML tags here (this is typically the same as the fully qualified class name
* of the custom view, but for Android framework views in the android.view or android.widget packages,
* you can omit the package.)
* @param parent The optional parent to add this component to
* @param before The sibling to insert immediately before, or null to append
* @param insertType The type of insertion
*/
public NlComponent createComponent(@Nullable ScreenView screenView,
@NonNull String fqcn,
@Nullable NlComponent parent,
@Nullable NlComponent before,
@NonNull InsertType insertType) {
String tagName = NlComponent.viewClassToTag(fqcn);
XmlTag tag;
if (parent != null) {
// Creating a component intended to be inserted into an existing layout
tag = parent.getTag().createChildTag(tagName, null, null, false);
} else {
// Creating a component not yet inserted into a layout. Typically done when trying to perform
// a drag from palette, etc.
XmlElementFactory elementFactory = XmlElementFactory.getInstance(getProject());
String text = "<" + fqcn + " xmlns:android=\"http://schemas.android.com/apk/res/android\"/>"; // SIZES?
tag = elementFactory.createTagFromText(text);
}
return createComponent(screenView, tag, parent, before, insertType);
}
public NlComponent createComponent(@Nullable ScreenView screenView,
@NonNull XmlTag tag,
@Nullable NlComponent parent,
@Nullable NlComponent before,
@NonNull InsertType insertType) {
if (parent != null) {
// Creating a component intended to be inserted into an existing layout
XmlTag parentTag = parent.getTag();
if (before != null) {
tag = (XmlTag) parentTag.addBefore(tag, before.getTag());
} else {
tag = parentTag.addSubTag(tag, false);
}
// Required for all views; drop handlers can adjust as necessary
tag.setAttribute(ATTR_LAYOUT_WIDTH, ANDROID_URI, VALUE_WRAP_CONTENT);
tag.setAttribute(ATTR_LAYOUT_HEIGHT, ANDROID_URI, VALUE_WRAP_CONTENT);
} else {
// No namespace yet: use the default prefix instead
tag.setAttribute(ANDROID_NS_NAME_PREFIX + ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
tag.setAttribute(ANDROID_NS_NAME_PREFIX + ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
}
NlComponent child = new NlComponent(this, tag);
if (parent != null) {
parent.addChild(child, before);
}
// Notify view handlers
ViewHandlerManager viewHandlerManager = ViewHandlerManager.get(getProject());
ViewHandler childHandler = viewHandlerManager.getHandler(child);
if (childHandler != null && screenView != null) {
ViewEditor editor = new ViewEditorImpl(screenView);
boolean ok = childHandler.onCreate(editor, parent, child, insertType);
if (!ok) {
if (parent != null) {
parent.removeChild(child);
}
tag.delete();
return null;
}
}
if (parent != null) {
ViewHandler parentHandler = viewHandlerManager.getHandler(parent);
if (parentHandler instanceof ViewGroupHandler) {
((ViewGroupHandler)parentHandler).onChildInserted(parent, child, insertType);
}
}
return child;
}
@NonNull
public Transferable getSelectionAsTransferable() {
return mySelectionModel.getTransferable(myId);
}
/**
* Returns true if the specified components can be added to the specified receiver.
*/
public boolean canAddComponents(@Nullable List<NlComponent> toAdd, @NonNull NlComponent receiver, @Nullable NlComponent before) {
if (before != null && before.getParent() != receiver) {
return false;
}
ViewHandlerManager handlerManager = ViewHandlerManager.get(getProject());
ViewHandler parentHandler = handlerManager.getHandler(receiver);
if (!(parentHandler instanceof ViewGroupHandler)) {
return false;
}
final ViewGroupHandler groupHandler = (ViewGroupHandler)parentHandler;
if (toAdd == null || toAdd.isEmpty()) {
return false;
}
for (NlComponent component : toAdd) {
if (!groupHandler.acceptsChild(receiver, component)) {
return false;
}
ViewHandler handler = handlerManager.getHandler(component);
if (handler != null && !handler.acceptsParent(receiver, component)) {
return false;
}
}
return true;
}
/**
* Adds components to the specified receiver before the given sibling.
* If insertType is a move the components specified should be components from this model.
*/
public void addComponents(@Nullable final List<NlComponent> toAdd,
@NonNull final NlComponent receiver,
@Nullable final NlComponent before,
@NonNull final InsertType insertType) {
if (!canAddComponents(toAdd, receiver, before)) {
return;
}
assert toAdd != null;
WriteCommandAction<Void> action = new WriteCommandAction<Void>(getProject(), insertType.getDragType().getDescription(), myFile) {
@Override
protected void run(@NonNull Result<Void> result) throws Throwable {
handleAddition(toAdd, receiver, before, insertType);
}
};
action.execute();
}
private void handleAddition(@NonNull List<NlComponent> added,
@NonNull NlComponent receiver,
@Nullable NlComponent before,
@NonNull InsertType insertType) {
ViewHandlerManager handlerManager = ViewHandlerManager.get(getProject());
ViewGroupHandler groupHandler = (ViewGroupHandler)handlerManager.getHandler(receiver);
assert groupHandler != null;
for (NlComponent component : added) {
if (insertType.isMove()) {
insertType = component.getParent() == receiver ? InsertType.MOVE_WITHIN : InsertType.MOVE_INTO;
}
if (component.needsDefaultId() && (StringUtil.isEmpty(component.getId()) || !insertType.isMove())) {
component.assignId();
}
groupHandler.onChildInserted(receiver, component, insertType);
NlComponent parent = component.getParent();
if (parent != null) {
parent.removeChild(component);
}
receiver.addChild(component, before);
if (receiver.getTag() != component.getTag()) {
XmlTag prev = component.getTag();
if (before != null) {
component.setTag((XmlTag)receiver.getTag().addBefore(component.getTag(), before.getTag()));
}
else {
component.setTag(receiver.getTag().addSubTag(component.getTag(), false));
}
if (insertType.isMove()) {
prev.delete();
}
}
removeNamespaceAttributes(component);
}
}
private static void removeNamespaceAttributes(NlComponent component) {
for (XmlAttribute attribute : component.getTag().getAttributes()) {
if (attribute.getName().startsWith(XMLNS_PREFIX)) {
attribute.delete();
}
}
}
@Nullable
public static DnDTransferItem getTransferItem(@NonNull Transferable transferable, boolean allowPlaceholder) {
DnDTransferItem item = null;
try {
if (transferable.isDataFlavorSupported(ItemTransferable.DESIGNER_FLAVOR)) {
item = (DnDTransferItem)transferable.getTransferData(ItemTransferable.DESIGNER_FLAVOR);
}
else if (transferable.isDataFlavorSupported(DataFlavor.stringFlavor)) {
String xml = (String)transferable.getTransferData(DataFlavor.stringFlavor);
if (!StringUtil.isEmpty(xml)) {
item = new DnDTransferItem(new DnDTransferComponent("", xml, 200, 100));
}
}
} catch (InvalidDnDOperationException ex) {
if (!allowPlaceholder) {
return null;
}
String defaultXml = "<placeholder xmlns:android=\"http://schemas.android.com/apk/res/android\"/>";
item = new DnDTransferItem(new DnDTransferComponent("", defaultXml, 200, 100));
} catch (IOException ex) {
LOG.warn(ex);
} catch (UnsupportedFlavorException ex) {
LOG.warn(ex);
}
return item;
}
@Nullable
public List<NlComponent> createComponents(@NonNull ScreenView screenView,
@NonNull DnDTransferItem item,
@NonNull InsertType insertType) {
List<NlComponent> components = new ArrayList<NlComponent>(item.getComponents().size());
for (DnDTransferComponent dndComponent : item.getComponents()) {
XmlTag tag = createTagFromTransferItem(screenView, dndComponent.getRepresentation());
NlComponent component = createComponent(screenView, tag, null, null, insertType);
if (component == null) {
return null; // User may have cancelled
}
component.w = dndComponent.getWidth();
component.h = dndComponent.getHeight();
components.add(component);
}
return components;
}
@NonNull
private static XmlTag createTagFromTransferItem(@NonNull ScreenView screenView, @NonNull String text) {
NlModel model = screenView.getModel();
Project project = model.getFacet().getModule().getProject();
XmlElementFactory elementFactory = XmlElementFactory.getInstance(project);
XmlTag tag = null;
if (XmlUtils.parseDocumentSilently(text, false) != null) {
try {
String xml = addAndroidNamespaceIfMissing(text);
tag = elementFactory.createTagFromText(xml);
}
catch (IncorrectOperationException ignore) {
// Thrown by XmlElementFactory if you try to parse non-valid XML. User might have tried
// to drop something like plain text -- insert this as a text view instead.
// However, createTagFromText may not always throw this for invalid XML, so we perform the above parseDocument
// check first instead.
}
}
if (tag == null) {
tag = elementFactory.createTagFromText("<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\" " +
" android:text=\"" + XmlUtils.toXmlAttributeValue(text) + "\"" +
" android:layout_width=\"wrap_content\"" +
" android:layout_height=\"wrap_content\"" +
"/>");
}
return tag;
}
private static String addAndroidNamespaceIfMissing(@NonNull String xml) {
// TODO: Remove this temporary hack, which adds an Android namespace if necessary
// (this is such that the resulting tag is namespace aware, and attempts to manipulate it from
// a component handler will correctly set namespace prefixes)
if (!xml.contains(ANDROID_URI)) {
int index = xml.indexOf('<');
if (index != -1) {
index = xml.indexOf(' ', index);
if (index == -1) {
index = xml.indexOf("/>");
if (index == -1) {
index = xml.indexOf('>');
}
}
if (index != -1) {
xml =
xml.substring(0, index) + " xmlns:android=\"http://schemas.android.com/apk/res/android\"" + xml.substring(index);
}
}
}
return xml;
}
@NonNull
public InsertType determineInsertType(@NonNull DragType dragType, @Nullable DnDTransferItem item, boolean asPreview) {
if (item != null && item.isFromPalette()) {
return asPreview ? InsertType.CREATE_PREVIEW : InsertType.CREATE;
}
switch (dragType) {
case CREATE:
return asPreview ? InsertType.CREATE_PREVIEW : InsertType.CREATE;
case MOVE:
return item != null && myId != item.getModelId() ? InsertType.COPY : InsertType.MOVE_INTO;
case COPY:
return InsertType.COPY;
case PASTE:
default:
return InsertType.PASTE;
}
}
@Override
public void dispose() {
deactivate(); // ensure listeners are unregistered if necessary
}
@Override
public String toString() {
return NlModel.class.getSimpleName() + " for " + myFile;
}
// ---- Implements ResourceNotificationManager.ResourceChangeListener ----
@Override
public void resourcesChanged(@NonNull Set<ResourceNotificationManager.Reason> reason) {
requestRender();
}
// ---- Implements ModificationTracker ----
@Override
public long getModificationCount() {
return myModificationCount;
}
public void notifyModified() {
myModificationCount++;
}
private class AndroidPreviewProgressIndicator extends ProgressIndicatorBase {
private final Object myLock = new Object();
private final int myDelay;
public AndroidPreviewProgressIndicator(int delay) {
myDelay = delay;
}
@Override
public void start() {
super.start();
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
final Timer timer = UIUtil.createNamedTimer("Android rendering progress timer", myDelay, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
synchronized (myLock) {
if (isRunning()) {
mySurface.registerIndicator(AndroidPreviewProgressIndicator.this);
}
}
}
});
timer.setRepeats(false);
timer.start();
}
});
}
@Override
public void stop() {
synchronized (myLock) {
super.stop();
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
mySurface.unregisterIndicator(AndroidPreviewProgressIndicator.this);
}
});
}
}
}
}