checkbox
classpath
clipboard
+clipboards
codebase
codename
codenames
int
javadoc
layoutlib
+leaky
lifecycle
linestyle
linux
* classes using this Pair by a more dedicated data structure (so we don't have
* to pass around generic signatures as is currently done, though at least the
* construction is helped a bit by the {@link #of} factory method.
+ *
+ * @param <S> The type of the first value
+ * @param <T> The type of the second value
*/
class Pair<S,T> {
private final S mFirst;
return false;
return true;
}
-}
\ No newline at end of file
+}
}
for (String it : data.getCurr().getAttr()) {
- newChild.setAttribute(ANDROID_URI, "layout_" + it, id != null ? id : "true");
+ newChild.setAttribute(ANDROID_URI,
+ "layout_" + it, id != null ? id : "true");
}
addInnerElements(newChild, element, idMap);
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.swt.widgets.Display;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.ui.StructuredTextEditor;
+import org.eclipse.wst.xml.core.internal.document.NodeContainer;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
* Derived classes must implement createFormPages to create the forms before the
* source editor. This can be a no-op if desired.
*/
+@SuppressWarnings("restriction") // Uses XML model, which has no non-restricted replacement yet
public abstract class AndroidXmlEditor extends FormEditor implements IResourceChangeListener {
/** Preference name for the current page of this file */
}
/**
+ * Get the XML text directly from the editor.
+ *
+ * @param xmlNode The node whose XML text we want to obtain.
+ * @return The XML representation of the {@link Node}.
+ */
+ public String getXmlText(Node xmlNode) {
+ String data = null;
+ IStructuredModel model = getModelForRead();
+ try {
+ IStructuredDocument document = getStructuredDocument();
+ if (xmlNode instanceof NodeContainer) {
+ // The easy way to get the source of an SSE XML node.
+ data = ((NodeContainer) xmlNode).getSource();
+ } else if (xmlNode instanceof IndexedRegion && document != null) {
+ // Try harder.
+ IndexedRegion region = (IndexedRegion) xmlNode;
+ int start = region.getStartOffset();
+ int end = region.getEndOffset();
+
+ if (end > start) {
+ data = document.get(start, end - start);
+ }
+ }
+ } catch (BadLocationException e) {
+ // the region offset was invalid. ignore.
+ } finally {
+ model.releaseFromRead();
+ }
+ return data;
+ }
+
+ /**
* Listen to changes in the underlying XML model in the structured editor.
*/
private class XmlModelStateListener implements IModelStateListener {
* Helper method that returns a {@link ViewElementDescriptor} for the requested FQCN.
* Will return null if we can't find that FQCN or we lack the editor/data/descriptors info.
*/
- public ViewElementDescriptor getFqcnViewDescritor(String fqcn) {
+ public ViewElementDescriptor getFqcnViewDescriptor(String fqcn) {
ViewElementDescriptor desc = null;
AndroidTargetData data = getTargetData();
package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
import com.android.ide.common.api.INode;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
import com.android.sdklib.SdkConstants;
import org.eclipse.swt.graphics.Rectangle;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.List;
/**
* Represents one selection in {@link LayoutCanvas}.
/**
* Returns true when this selection item represents the root, the top level
* layout element in the editor.
+ * @return True if and only if this element is at the root of the hierarchy
*/
public boolean isRoot() {
return mNodeProxy.getParent() == null;
if (mNodeProxy != null) {
INode parent = mNodeProxy.getParent();
if (parent instanceof NodeProxy) {
- gre.callOnChildSelected(gcWrapper, (NodeProxy)parent, mNodeProxy);
+ gre.callOnChildSelected(gcWrapper, (NodeProxy) parent, mNodeProxy);
}
}
}
return name;
}
+
+ /**
+ * Gets the XML text from the given selection for a text transfer.
+ * The returned string can be empty but not null.
+ */
+ /* package */ static String getAsText(LayoutCanvas canvas, List<CanvasSelection> selection) {
+ StringBuilder sb = new StringBuilder();
+
+ LayoutEditor layoutEditor = canvas.getLayoutEditor();
+ for (CanvasSelection cs : selection) {
+ CanvasViewInfo vi = cs.getViewInfo();
+ UiViewElementNode key = vi.getUiViewKey();
+ Node node = key.getXmlNode();
+ String t = layoutEditor.getXmlText(node);
+ if (t != null) {
+ if (sb.length() > 0) {
+ sb.append('\n');
+ }
+ sb.append(t);
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Returns elements representing the given selection of canvas items.
+ *
+ * @param items Items to wrap in elements
+ * @return An array of wrapper elements. Never null.
+ */
+ /* package */ static SimpleElement[] getAsElements(List<CanvasSelection> items) {
+ ArrayList<SimpleElement> elements = new ArrayList<SimpleElement>();
+
+ for (CanvasSelection cs : items) {
+ CanvasViewInfo vi = cs.getViewInfo();
+
+ SimpleElement e = vi.toSimpleElement();
+ elements.add(e);
+ }
+
+ return elements.toArray(new SimpleElement[elements.size()]);
+ }
}
package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+import com.android.ide.common.api.Rect;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.layoutlib.api.ILayoutResult;
import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo;
this(viewInfo, null /*parent*/, 0 /*parentX*/, 0 /*parentY*/);
}
- private CanvasViewInfo(ILayoutViewInfo viewInfo, CanvasViewInfo parent, int parentX, int parentY) {
+ private CanvasViewInfo(ILayoutViewInfo viewInfo, CanvasViewInfo parent,
+ int parentX, int parentY) {
mParent = parent;
mName = viewInfo.getName();
return null;
}
+
+ /**
+ * Returns true iff this view info corresponds to a root element.
+ *
+ * @return True iff this is a root view info.
+ */
+ public boolean isRoot() {
+ // Select the visual element -- unless it's the root.
+ // The root element is the one whose GRAND parent
+ // is null (because the parent will be a -document-
+ // node).
+ return mUiViewKey == null || mUiViewKey.getUiParent() == null ||
+ mUiViewKey.getUiParent().getUiParent() == null;
+ }
+
+ /**
+ * Returns the info represented as a {@link SimpleElement}.
+ *
+ * @return A {@link SimpleElement} wrapping this info.
+ */
+ /* package */ SimpleElement toSimpleElement() {
+
+ UiViewElementNode uiNode = getUiViewKey();
+
+ String fqcn = SimpleXmlTransfer.getFqcn(uiNode.getDescriptor());
+ String parentFqcn = null;
+ Rect bounds = new Rect(getAbsRect());
+ Rect parentBounds = null;
+
+ UiElementNode uiParent = uiNode.getUiParent();
+ if (uiParent != null) {
+ parentFqcn = SimpleXmlTransfer.getFqcn(uiParent.getDescriptor());
+ }
+ if (getParent() != null) {
+ parentBounds = new Rect(getParent().getAbsRect());
+ }
+
+ SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds);
+
+ for (UiAttributeNode attr : uiNode.getUiAttributes()) {
+ String value = attr.getCurrentValue();
+ if (value != null && value.length() > 0) {
+ AttributeDescriptor attrDesc = attr.getDescriptor();
+ SimpleAttribute a = new SimpleAttribute(
+ attrDesc.getNamespaceUri(),
+ attrDesc.getXmlLocalName(),
+ value);
+ e.addAttribute(a);
+ }
+ }
+
+ for (CanvasViewInfo childVi : getChildren()) {
+ SimpleElement e2 = childVi.toSimpleElement();
+ if (e2 != null) {
+ e.addInnerElement(e2);
+ }
+ }
+
+ return e;
+ }
+
}
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.IDragElement.IDragAttribute;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.widgets.Composite;
+
+import java.util.List;
+
+/**
+ * The {@link ClipboardSupport} class manages the native clipboard, providing operations
+ * to copy, cut and paste view items, and can answer whether the clipboard contains
+ * a transferable we care about.
+ */
+public class ClipboardSupport {
+ private static final boolean DEBUG = false;
+
+ /** SWT clipboard instance. */
+ private Clipboard mClipboard;
+ private LayoutCanvas mCanvas;
+
+ /**
+ * Constructs a new {@link ClipboardSupport} tied to the given
+ * {@link LayoutCanvas}.
+ *
+ * @param canvas The {@link LayoutCanvas} to provide clipboard support for.
+ * @param parent The parent widget in the SWT hierarchy of the canvas.
+ */
+ public ClipboardSupport(LayoutCanvas canvas, Composite parent) {
+ this.mCanvas = canvas;
+
+ mClipboard = new Clipboard(parent.getDisplay());
+ }
+
+ /**
+ * Frees up any resources held by the {@link ClipboardSupport}.
+ */
+ public void dispose() {
+ if (mClipboard != null) {
+ mClipboard.dispose();
+ mClipboard = null;
+ }
+ }
+
+ /**
+ * Perform the "Copy" action, either from the Edit menu or from the context
+ * menu.
+ * <p/>
+ * This sanitizes the selection, so it must be a copy. It then inserts the
+ * selection both as text and as {@link SimpleElement}s in the clipboard.
+ *
+ * @param selection A list of selection items to add to the clipboard;
+ * <b>this should be a copy already - this method will not make a
+ * copy</b>
+ */
+ public void copySelectionToClipboard(List<CanvasSelection> selection) {
+ SelectionManager.sanitize(selection);
+
+ if (selection.isEmpty()) {
+ return;
+ }
+
+ Object[] data = new Object[] {
+ CanvasSelection.getAsElements(selection),
+ CanvasSelection.getAsText(mCanvas, selection)
+ };
+
+ Transfer[] types = new Transfer[] {
+ SimpleXmlTransfer.getInstance(),
+ TextTransfer.getInstance()
+ };
+
+ mClipboard.setContents(data, types);
+ }
+
+ /**
+ * Perform the "Cut" action, either from the Edit menu or from the context
+ * menu.
+ * <p/>
+ * This sanitizes the selection, so it must be a copy. It uses the
+ * {@link #copySelectionToClipboard(List)} method to copy the selection to
+ * the clipboard. Finally it uses {@link #deleteSelection(String, List)} to
+ * delete the selection with a "Cut" verb for the title.
+ *
+ * @param selection A list of selection items to add to the clipboard;
+ * <b>this should be a copy already - this method will not make a
+ * copy</b>
+ */
+ public void cutSelectionToClipboard(List<CanvasSelection> selection) {
+ copySelectionToClipboard(selection);
+ deleteSelection(
+ mCanvas.getCutLabel(),
+ selection);
+ }
+
+ /**
+ * Deletes the given selection.
+ *
+ * @param verb A translated verb for the action. Will be used for the
+ * undo/redo title. Typically this should be
+ * {@link Action#getText()} for either the cut or the delete
+ * actions in the canvas.
+ * @param selection The selection. Must not be null. Can be empty, in which
+ * case nothing happens. The selection list will be sanitized so
+ * the caller should pass in a copy.
+ */
+ public void deleteSelection(String verb, final List<CanvasSelection> selection) {
+ SelectionManager.sanitize(selection);
+
+ if (selection.isEmpty()) {
+ return;
+ }
+
+ // If all selected items have the same *kind* of parent, display that in the undo title.
+ String title = null;
+ for (CanvasSelection cs : selection) {
+ CanvasViewInfo vi = cs.getViewInfo();
+ if (vi != null && vi.getParent() != null) {
+ if (title == null) {
+ title = vi.getParent().getName();
+ } else if (!title.equals(vi.getParent().getName())) {
+ // More than one kind of parent selected.
+ title = null;
+ break;
+ }
+ }
+ }
+
+ if (title != null) {
+ // Typically the name is an FQCN. Just get the last segment.
+ int pos = title.lastIndexOf('.');
+ if (pos > 0 && pos < title.length() - 1) {
+ title = title.substring(pos + 1);
+ }
+ }
+ boolean multiple = mCanvas.getSelectionManager().hasMultiSelection();
+ if (title == null) {
+ title = String.format(
+ multiple ? "%1$s elements" : "%1$s element",
+ verb);
+ } else {
+ title = String.format(
+ multiple ? "%1$s elements from %2$s" : "%1$s element from %2$s",
+ verb, title);
+ }
+
+ // Implementation note: we don't clear the internal selection after removing
+ // the elements. An update XML model event should happen when the model gets released
+ // which will trigger a recompute of the layout, thus reloading the model thus
+ // resetting the selection.
+ mCanvas.getLayoutEditor().wrapUndoEditXmlModel(title, new Runnable() {
+ public void run() {
+ for (CanvasSelection cs : selection) {
+ CanvasViewInfo vi = cs.getViewInfo();
+ // You can't delete the root element
+ if (vi != null && !vi.isRoot()) {
+ UiViewElementNode ui = vi.getUiViewKey();
+ if (ui != null) {
+ ui.deleteXmlNode();
+ }
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Perform the "Paste" action, either from the Edit menu or from the context
+ * menu.
+ *
+ * @param selection A list of selection items to add to the clipboard;
+ * <b>this should be a copy already - this method will not make a
+ * copy</b>
+ */
+ public void pasteSelection(List<CanvasSelection> selection) {
+
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+ SimpleElement[] pasted = (SimpleElement[]) mClipboard.getContents(sxt);
+
+ if (pasted == null || pasted.length == 0) {
+ return;
+ }
+
+ CanvasViewInfo lastRoot = mCanvas.getViewHierarchy().getRoot();
+ if (lastRoot == null) {
+ // Pasting in an empty document. Only paste the first element.
+ pasteInEmptyDocument(pasted[0]);
+ return;
+ }
+
+ // Otherwise use the current selection, if any, as a guide where to paste
+ // using the first selected element only. If there's no selection use
+ // the root as the insertion point.
+ SelectionManager.sanitize(selection);
+ CanvasViewInfo target = lastRoot;
+ if (selection.size() > 0) {
+ CanvasSelection cs = selection.get(0);
+ target = cs.getViewInfo();
+ }
+
+ NodeProxy targetNode = mCanvas.getNodeFactory().create(target);
+
+ mCanvas.getRulesEngine().callOnPaste(targetNode, pasted);
+ }
+
+ /**
+ * Paste a new root into an empty XML layout.
+ * <p/>
+ * In case of error (unknown FQCN, document not empty), silently do nothing.
+ * In case of success, the new element will have some default attributes set (xmlns:android,
+ * layout_width and height). The edit is wrapped in a proper undo.
+ * <p/>
+ * Implementation is similar to {@link #createDocumentRoot(String)} except we also
+ * copy all the attributes and inner elements recursively.
+ */
+ private void pasteInEmptyDocument(final IDragElement pastedElement) {
+ String rootFqcn = pastedElement.getFqcn();
+
+ // Need a valid empty document to create the new root
+ final LayoutEditor layoutEditor = mCanvas.getLayoutEditor();
+ final UiDocumentNode uiDoc = layoutEditor.getUiRootNode();
+ if (uiDoc == null || uiDoc.getUiChildren().size() > 0) {
+ debugPrintf("Failed to paste document root for %1$s: document is not empty", rootFqcn);
+ return;
+ }
+
+ // Find the view descriptor matching our FQCN
+ final ViewElementDescriptor viewDesc = layoutEditor.getFqcnViewDescriptor(rootFqcn);
+ if (viewDesc == null) {
+ // TODO this could happen if pasting a custom view not known in this project
+ debugPrintf("Failed to paste document root, unknown FQCN %1$s", rootFqcn);
+ return;
+ }
+
+ // Get the last segment of the FQCN for the undo title
+ String title = rootFqcn;
+ int pos = title.lastIndexOf('.');
+ if (pos > 0 && pos < title.length() - 1) {
+ title = title.substring(pos + 1);
+ }
+ title = String.format("Paste root %1$s in document", title);
+
+ layoutEditor.wrapUndoEditXmlModel(title, new Runnable() {
+ public void run() {
+ UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc);
+
+ // A root node requires the Android XMLNS
+ uiNew.setAttributeValue(
+ "android", //$NON-NLS-1$
+ XmlnsAttributeDescriptor.XMLNS_URI,
+ SdkConstants.NS_RESOURCES,
+ true /*override*/);
+
+ // Copy all the attributes from the pasted element
+ for (IDragAttribute attr : pastedElement.getAttributes()) {
+ uiNew.setAttributeValue(
+ attr.getName(),
+ attr.getUri(),
+ attr.getValue(),
+ true /*override*/);
+ }
+
+ // Adjust the attributes, adding the default layout_width/height
+ // only if they are not present (the original element should have
+ // them though.)
+ DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);
+
+ uiNew.createXmlNode();
+
+ // Now process all children
+ for (IDragElement childElement : pastedElement.getInnerElements()) {
+ addChild(uiNew, childElement);
+ }
+ }
+
+ private void addChild(UiElementNode uiParent, IDragElement childElement) {
+ String childFqcn = childElement.getFqcn();
+ final ViewElementDescriptor childDesc =
+ layoutEditor.getFqcnViewDescriptor(childFqcn);
+ if (childDesc == null) {
+ // TODO this could happen if pasting a custom view
+ debugPrintf("Failed to paste element, unknown FQCN %1$s", childFqcn);
+ return;
+ }
+
+ UiElementNode uiChild = uiParent.appendNewUiChild(childDesc);
+
+ // Copy all the attributes from the pasted element
+ for (IDragAttribute attr : childElement.getAttributes()) {
+ uiChild.setAttributeValue(
+ attr.getName(),
+ attr.getUri(),
+ attr.getValue(),
+ true /*override*/);
+ }
+
+ // Adjust the attributes, adding the default layout_width/height
+ // only if they are not present (the original element should have
+ // them though.)
+ DescriptorsUtils.setDefaultLayoutAttributes(
+ uiChild, false /*updateLayout*/);
+
+ uiChild.createXmlNode();
+
+ // Now process all grand children
+ for (IDragElement grandChildElement : childElement.getInnerElements()) {
+ addChild(uiChild, grandChildElement);
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns true if we have a a simple xml transfer data object on the
+ * clipboard.
+ *
+ * @return True if and only if the clipboard contains one of XML element
+ * objects.
+ */
+ public boolean hasSxtOnClipboard() {
+ // The paste operation is only available if we can paste our custom type.
+ // We do not currently support pasting random text (e.g. XML). Maybe later.
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+ for (TransferData td : mClipboard.getAvailableTypes()) {
+ if (sxt.isSupportedType(td)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void debugPrintf(String message, Object... params) {
+ if (DEBUG) AdtPlugin.printToConsole("Clipboard", String.format(message, params));
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+
+/**
+ * A {@link ControlPoint} is a coordinate in the canvas control which corresponds
+ * exactly to (0,0) at the top left of the canvas. It is unaffected by canvas
+ * zooming.
+ */
+public final class ControlPoint {
+ /** Containing canvas which the point is relative to. */
+ private final LayoutCanvas mCanvas;
+
+ /** The X coordinate of the mouse coordinate. */
+ public final int x;
+
+ /** The Y coordinate of the mouse coordinate. */
+ public final int y;
+
+ /**
+ * Constructs a new {@link ControlPoint} from the given event. The event
+ * must be from a {@link MouseListener} associated with the
+ * {@link LayoutCanvas} such that the {@link MouseEvent#x} and
+ * {@link MouseEvent#y} fields are relative to the canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The mouse event to construct the {@link ControlPoint}
+ * from.
+ * @return A {@link ControlPoint} which corresponds to the given
+ * {@link MouseEvent}.
+ */
+ public static ControlPoint create(LayoutCanvas canvas, MouseEvent event) {
+ // The mouse event coordinates should already be relative to the canvas
+ // widget.
+ assert event.widget == canvas : event.widget;
+ return new ControlPoint(canvas, event.x, event.y);
+ }
+
+ /**
+ * Constructs a new {@link ControlPoint} from the given event. The event
+ * must be from a {@link DragSourceListener} associated with the
+ * {@link LayoutCanvas} such that the {@link DragSourceEvent#x} and
+ * {@link DragSourceEvent#y} fields are relative to the canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The mouse event to construct the {@link ControlPoint}
+ * from.
+ * @return A {@link ControlPoint} which corresponds to the given
+ * {@link DragSourceEvent}.
+ */
+ public static ControlPoint create(LayoutCanvas canvas, DragSourceEvent event) {
+ // The drag source event coordinates should already be relative to the
+ // canvas widget.
+ return new ControlPoint(canvas, event.x, event.y);
+ }
+
+ /**
+ * Constructs a new {@link ControlPoint} from the given event.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The mouse event to construct the {@link ControlPoint}
+ * from.
+ * @return A {@link ControlPoint} which corresponds to the given
+ * {@link DropTargetEvent}.
+ */
+ public static ControlPoint create(LayoutCanvas canvas, DropTargetEvent event) {
+ // The drop target events are always relative to the display, so we must
+ // first convert them to be canvas relative.
+ org.eclipse.swt.graphics.Point p = canvas.toControl(event.x, event.y);
+ return new ControlPoint(canvas, p.x, p.y);
+ }
+
+ /**
+ * Constructs a new {@link ControlPoint} from the given x,y coordinates,
+ * which must be relative to the given {@link LayoutCanvas}.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param x The mouse event x coordinate relative to the canvas
+ * @param y The mouse event x coordinate relative to the canvas
+ * @return A {@link ControlPoint} which corresponds to the given
+ * coordinates.
+ */
+ public static ControlPoint create(LayoutCanvas canvas, int x, int y) {
+ return new ControlPoint(canvas, x, y);
+ }
+
+ /**
+ * Constructs a new canvas control coordinate with the given X and Y
+ * coordinates. This is private; use one of the factory methods
+ * {@link #create(LayoutCanvas, MouseEvent)},
+ * {@link #create(LayoutCanvas, DragSourceEvent)} or
+ * {@link #create(LayoutCanvas, DropTargetEvent)} instead.
+ *
+ * @param canvas The canvas which contains this coordinate
+ * @param x The mouse x coordinate
+ * @param y The mouse y coordinate
+ */
+ private ControlPoint(LayoutCanvas canvas, int x, int y) {
+ this.mCanvas = canvas;
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * Returns the equivalent {@link LayoutPoint} to this
+ * {@link ControlPoint}.
+ *
+ * @return The equivalent {@link LayoutPoint} to this
+ * {@link ControlPoint}.
+ */
+ public LayoutPoint toLayout() {
+ int lx = mCanvas.getHorizontalTransform().inverseTranslate(x);
+ int ly = mCanvas.getVerticalTransform().inverseTranslate(y);
+
+ return LayoutPoint.create(mCanvas, lx, ly);
+ }
+
+ @Override
+ public String toString() {
+ return "ControlPoint [x=" + x + ", y=" + y + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + x;
+ result = prime * result + y;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ ControlPoint other = (ControlPoint) obj;
+ if (x != other.x)
+ return false;
+ if (y != other.y)
+ return false;
+ if (mCanvas != other.mCanvas) {
+ return false;
+ }
+ return true;
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.DropTargetListener;
+
+/**
+ * A {@link DropGesture} is a {@link Gesture} which deals with drag and drop, so
+ * it has additional hooks for indicating whether the current position is
+ * "valid", and in general gets access to the system drag and drop data
+ * structures. See the {@link Gesture} documentation for more details on whether
+ * you should choose a plain {@link Gesture} or a {@link DropGesture}.
+ */
+public abstract class DropGesture extends Gesture {
+ /**
+ * The cursor has entered the drop target boundaries.
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#dragEnter(DropTargetEvent)
+ */
+ public void dragEnter(DropTargetEvent event) {
+ }
+
+ /**
+ * The cursor is moving over the drop target.
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#dragOver(DropTargetEvent)
+ */
+ public void dragOver(DropTargetEvent event) {
+ }
+
+ /**
+ * The operation being performed has changed (usually due to the user
+ * changing the selected modifier key(s) while dragging).
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#dragOperationChanged(DropTargetEvent)
+ */
+ public void dragOperationChanged(DropTargetEvent event) {
+ }
+
+ /**
+ * The cursor has left the drop target boundaries OR the drop has been
+ * canceled OR the data is about to be dropped.
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#dragLeave(DropTargetEvent)
+ */
+ public void dragLeave(DropTargetEvent event) {
+ }
+
+ /**
+ * The drop is about to be performed. The drop target is given a last chance
+ * to change the nature of the drop.
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#dropAccept(DropTargetEvent)
+ */
+ public void dropAccept(DropTargetEvent event) {
+ }
+
+ /**
+ * The data is being dropped. The data field contains java format of the
+ * data being dropped.
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#drop(DropTargetEvent)
+ */
+ public void drop(final DropTargetEvent event) {
+ }
+}
final TreeMap<String, ArrayList<MenuAction>> outActionsMap,
final TreeMap<String, MenuAction.Group> outGroupsMap) {
int maxMenuSelection = 0;
- for (CanvasSelection selection : mCanvas.getCanvasSelections()) {
+ for (CanvasSelection selection : mCanvas.getSelectionManager().getSelections()) {
List<MenuAction> viewActions = null;
if (selection != null) {
CanvasViewInfo vi = selection.getViewInfo();
/**
* Wraps an SWT {@link GC} into an {@link IGraphics} interface so that {@link IViewRule} objects
* can directly draw on the canvas.
- * </p>
+ * <p/>
* The actual wrapped GC object is only non-null during the context of a paint operation.
*/
public class GCWrapper implements IGraphics {
case LINE_DASHDOTDOT:
swtStyle = SWT.LINE_DASHDOTDOT;
break;
+ default:
+ assert false : style;
+ break;
}
if (swtStyle != 0) {
mCurrentStyle = swtStyle;
}
- /** Use the stroke alpha for subsequent drawing operations */
+ /** Uses the stroke alpha for subsequent drawing operations. */
private void useStrokeAlpha() {
mGc.setAlpha(mCurrentStyle.getStrokeAlpha());
}
- /** Use the fill alpha for subsequent drawing operations */
+ /** Uses the fill alpha for subsequent drawing operations. */
private void useFillAlpha() {
mGc.setAlpha(mCurrentStyle.getFillAlpha());
}
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.events.KeyEvent;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A gesture is a mouse or keyboard driven user operation, such as a
+ * swipe-select or a resize. It can be thought of as a session, since it is
+ * initiated, updated during user manipulation, and finally completed or
+ * canceled. A gesture is associated with a single undo transaction (although
+ * some gestures don't actually edit anything, such as a selection), and a
+ * gesture can have a number of graphics {@link Overlay}s which are added and
+ * cleaned up on behalf of the gesture by the system.
+ * <p/>
+ * Gestures are typically mouse oriented. If a mouse wishes to integrate
+ * with the native drag & drop support, it should also implement
+ * the {@link DropGesture} interface, which is a sub interface of this
+ * {@link Gesture} interface. There are pros and cons to using native drag
+ * & drop, so various gestures will differ in whether they use it.
+ * In particular, you should use drag & drop if your gesture should:
+ * <ul>
+ * <li> Show a native drag & drop cursor
+ * <li> Copy or move data, especially if this applies outside the canvas
+ * control window or even the application itself
+ * </ul>
+ * You might want to avoid using native drag & drop if your gesture should:
+ * <ul>
+ * <li> Continue updating itself even when the mouse cursor leaves the
+ * canvas window (in a drag & gesture, as soon as you leave the canvas
+ * the drag source is no longer informed of mouse updates, whereas a regular
+ * mouse listener is)
+ * <li> Respond to modifier keys (for example, if toggling the Shift key
+ * should constrain motion as is common during resizing, and so on)
+ * <li> Use no special cursor (for example, during a marquee selection gesture we
+ * don't want a native drag & drop cursor)
+ * </ul>
+ * <p/>
+ * Examples of gestures:
+ * <ul>
+ * <li>Move (dragging to reorder or change hierarchy of views or change visual
+ * layout attributes)
+ * <li>Marquee (swiping out a rectangle to make a selection)
+ * <li>Resize (dragging some edge or corner of a widget to change its size, for
+ * example to some new fixed size, or to "attach" it to some other edge.)
+ * <li>Inline Editing (editing the text of some text-oriented widget like a
+ * label or a button)
+ * <li>Link (associate two or more widgets in some way, such as an
+ * "is required" widget linked to a text field)
+ * </ul>
+ */
+public abstract class Gesture {
+ /** Start mouse coordinate, in control coordinates. */
+ protected ControlPoint mStart;
+
+ /** Initial SWT mask when the gesture started. */
+ protected int mStartMask;
+
+ /**
+ * Returns a list of overlays, from bottom to top (where the later overlays
+ * are painted on top of earlier ones if they overlap).
+ *
+ * @return A list of overlays to paint for this gesture, if applicable.
+ * Should not be null, but can be empty.
+ */
+ public List<Overlay> createOverlays() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Handles initialization of this gesture. Called when the gesture is
+ * starting.
+ *
+ * @param pos The most recent mouse coordinate applicable to this
+ * gesture, relative to the canvas control.
+ * @param startMask The initial SWT mask for the gesture, if known, or
+ * otherwise 0.
+ */
+ public void begin(ControlPoint pos, int startMask) {
+ this.mStart = pos;
+ this.mStartMask = startMask;
+ }
+
+ /**
+ * Handles updating of the gesture state for a new mouse position.
+ *
+ * @param pos The most recent mouse coordinate applicable to this
+ * gesture, relative to the canvas control.
+ */
+ public void update(ControlPoint pos) {
+ }
+
+ /**
+ * Handles termination of the gesture. This method is called when the
+ * gesture has terminated (either through successful completion, or because
+ * it was canceled).
+ *
+ * @param pos The most recent mouse coordinate applicable to this
+ * gesture, relative to the canvas control.
+ * @param canceled True if the gesture was canceled, and false otherwise.
+ */
+ public void end(ControlPoint pos, boolean canceled) {
+ }
+
+ /**
+ * Handles a key press during the gesture. May be called repeatedly when the
+ * user is holding the key for several seconds.
+ *
+ * @param event The SWT event for the key press,
+ */
+ public void keyPressed(KeyEvent event) {
+ }
+
+ /**
+ * Handles a key release during the gesture.
+ *
+ * @param event The SWT event for the key release,
+ */
+ public void keyReleased(KeyEvent event) {
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DragSource;
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.dnd.DropTarget;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.DropTargetListener;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.TypedEvent;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The {@link GestureManager} is is the central manager of gestures; it is responsible
+ * for recognizing when particular gestures should begin and terminate. It
+ * listens to the drag, mouse and keyboard systems to find out when to start
+ * gestures and in order to update the gestures along the way.
+ */
+public class GestureManager {
+ /** The canvas which owns this GestureManager. */
+ private final LayoutCanvas mCanvas;
+
+ /** The currently executing gesture, or null. */
+ private Gesture mCurrentGesture;
+
+ /** A listener for drop target events. */
+ private final DropTargetListener mDropListener = new CanvasDropListener();
+
+ /** A listener for drag source events. */
+ private final DragSourceListener mDragSourceListener = new CanvasDragSourceListener();
+
+ /**
+ * The list of overlays associated with {@link #mCurrentGesture}. Will be
+ * null before it has been initialized lazily by the paint routine (the
+ * initialized value can never be null, but it can be an empty collection).
+ */
+ private List<Overlay> mOverlays;
+
+ /**
+ * Most recently seen mouse position (x coordinate). We keep a copy of this
+ * value since we sometimes need to know it when we aren't told about the
+ * mouse position (such as when a keystroke is received, such as an arrow
+ * key in order to tweak the current drop position)
+ */
+ protected int mLastMouseX;
+
+ /**
+ * Most recently seen mouse position (y coordinate). We keep a copy of this
+ * value since we sometimes need to know it when we aren't told about the
+ * mouse position (such as when a keystroke is received, such as an arrow
+ * key in order to tweak the current drop position)
+ */
+ protected int mLastMouseY;
+
+ /**
+ * Most recently seen mouse mask. We keep a copy of this since in some
+ * scenarios (such as on a drag gesture) we don't get access to it.
+ */
+ protected int mLastStateMask;
+
+ /**
+ * Listener for mouse motion, click and keyboard events.
+ */
+ private Listener mListener;
+
+ /**
+ * When we the drag leaves, we don't know if that's the last we'll see of
+ * this drag or if it's just temporarily outside the canvas and it will
+ * return. We want to restore it if it comes back. This is also necessary
+ * because even on a drop we'll receive a
+ * {@link DropTargetListener#dragLeave} right before the drop, and we need
+ * to restore it in the drop. Therefore, when we lose a {@link DropGesture}
+ * to a {@link DropTargetListener#dragLeave}, we store a reference to the
+ * current gesture as a {@link #mZombieGesture}, since the gesture is dead
+ * but might be brought back to life if we see a subsequent
+ * {@link DropTargetListener#dragEnter} before another gesture begins.
+ */
+ private DropGesture mZombieGesture;
+
+ /**
+ * Constructs a new {@link GestureManager} for the given
+ * {@link LayoutCanvas}.
+ *
+ * @param canvas The canvas which controls this {@link GestureManager}
+ */
+ public GestureManager(LayoutCanvas canvas) {
+ this.mCanvas = canvas;
+ }
+
+ /**
+ * Returns the canvas associated with this GestureManager.
+ *
+ * @return The {@link LayoutCanvas} associated with this GestureManager.
+ * Never null.
+ */
+ public LayoutCanvas getCanvas() {
+ return mCanvas;
+ }
+
+ /**
+ * Returns the current gesture, if one is in progress, and otherwise returns
+ * null.
+ *
+ * @return The current gesture or null.
+ */
+ public Gesture getCurrentGesture() {
+ return mCurrentGesture;
+ }
+
+ /**
+ * Paints the overlays associated with the current gesture, if any.
+ *
+ * @param gc The graphics object to paint into.
+ */
+ public void paint(GC gc) {
+ if (mCurrentGesture == null) {
+ return;
+ }
+
+ if (mOverlays == null) {
+ mOverlays = mCurrentGesture.createOverlays();
+ Device device = gc.getDevice();
+ for (Overlay overlay : mOverlays) {
+ overlay.create(device);
+ }
+ }
+ for (Overlay overlay : mOverlays) {
+ overlay.paint(gc);
+ }
+ }
+
+ /**
+ * Returns the {@link DropTargetListener} used by the GestureManager. This
+ * is a bit leaky, but the Outline is reusing all this code... This should
+ * be separated out.
+ */
+ /* package */DropTargetListener getDropTargetListener() {
+ return mDropListener;
+ }
+
+ /**
+ * Returns the {@link DragSourceListener} used by the GestureManager. This
+ * is a bit leaky, but the Outline is reusing all this code... This should
+ * be separated out.
+ */
+ /* package */DragSourceListener getDragSourceListener() {
+ return mDragSourceListener;
+ }
+
+ /**
+ * Registers all the listeners needed by the {@link GestureManager}.
+ *
+ * @param dragSource The drag source in the {@link LayoutCanvas} to listen
+ * to.
+ * @param dropTarget The drop target in the {@link LayoutCanvas} to listen
+ * to.
+ */
+ public void registerListeners(DragSource dragSource, DropTarget dropTarget) {
+ assert mListener == null;
+ mListener = new Listener();
+ mCanvas.addMouseMoveListener(mListener);
+ mCanvas.addMouseListener(mListener);
+ mCanvas.addKeyListener(mListener);
+
+ if (dragSource != null) {
+ dragSource.addDragListener(mDragSourceListener);
+ }
+ if (dropTarget != null) {
+ dropTarget.addDropListener(mDropListener);
+ }
+ }
+
+ /**
+ * Unregisters all the listeners previously registered by
+ * {@link #registerListeners}.
+ *
+ * @param dragSource The drag source in the {@link LayoutCanvas} to stop
+ * listening to.
+ * @param dropTarget The drop target in the {@link LayoutCanvas} to stop
+ * listening to.
+ */
+ public void unregisterListeners(DragSource dragSource, DropTarget dropTarget) {
+ if (mListener != null) {
+ mCanvas.removeMouseMoveListener(mListener);
+ mCanvas.removeMouseListener(mListener);
+ mCanvas.removeKeyListener(mListener);
+ mListener = null;
+ }
+
+ if (dragSource != null) {
+ dragSource.removeDragListener(mDragSourceListener);
+ }
+ if (dropTarget != null) {
+ dropTarget.removeDropListener(mDropListener);
+ }
+ }
+
+ /**
+ * Starts the given gesture.
+ *
+ * @param mousePos The most recent mouse coordinate applicable to the new
+ * gesture, in control coordinates.
+ * @param gesture The gesture to initiate
+ */
+ private void startGesture(ControlPoint mousePos, Gesture gesture, int mask) {
+ if (mCurrentGesture != null) {
+ finishGesture(mousePos, true);
+ assert mCurrentGesture == null;
+ }
+
+ if (gesture != null) {
+ mCurrentGesture = gesture;
+ mCurrentGesture.begin(mousePos, mask);
+ }
+ }
+
+ /**
+ * Updates the current gesture, if any, for the given event.
+ *
+ * @param mousePos The most recent mouse coordinate applicable to the new
+ * gesture, in control coordinates.
+ * @param event The event corresponding to this update. May be null. Don't
+ * make any assumptions about the type of this event - for
+ * example, it may not always be a MouseEvent, it could be a
+ * DragSourceEvent, etc.
+ */
+ private void updateMouse(ControlPoint mousePos, TypedEvent event) {
+ if (mCurrentGesture != null) {
+ mCurrentGesture.update(mousePos);
+ }
+ }
+
+ /**
+ * Finish the given gesture, either from successful completion or from
+ * cancellation.
+ *
+ * @param mousePos The most recent mouse coordinate applicable to the new
+ * gesture, in control coordinates.
+ * @param canceled True if and only if the gesture was canceled.
+ */
+ private void finishGesture(ControlPoint mousePos, boolean canceled) {
+ if (mCurrentGesture != null) {
+ mCurrentGesture.end(mousePos, canceled);
+ if (mOverlays != null) {
+ for (Overlay overlay : mOverlays) {
+ overlay.dispose();
+ }
+ mOverlays = null;
+ }
+ mCurrentGesture = null;
+ mZombieGesture = null;
+ mLastStateMask = 0;
+ }
+ }
+
+ /**
+ * Helper class which implements the {@link MouseMoveListener},
+ * {@link MouseListener} and {@link KeyListener} interfaces.
+ */
+ private class Listener implements MouseMoveListener, MouseListener, KeyListener {
+
+ // --- MouseMoveListener ---
+
+ public void mouseMove(MouseEvent e) {
+ mLastMouseX = e.x;
+ mLastMouseY = e.y;
+ mLastStateMask = e.stateMask;
+
+ if ((e.stateMask & SWT.BUTTON_MASK) != 0) {
+ if (mCurrentGesture != null) {
+ ControlPoint controlPoint = ControlPoint.create(mCanvas, e);
+ updateMouse(controlPoint, e);
+ mCanvas.redraw();
+ }
+ } else {
+ mCanvas.hover(e);
+ }
+ }
+
+ // --- MouseListener ---
+
+ public void mouseUp(MouseEvent e) {
+ if (mCurrentGesture == null) {
+ // Just a click, select
+ mCanvas.getSelectionManager().select(e);
+ }
+ finishGesture(ControlPoint.create(mCanvas, e), false);
+ mCanvas.redraw();
+ }
+
+ public void mouseDown(MouseEvent e) {
+ mLastMouseX = e.x;
+ mLastMouseY = e.y;
+ mLastStateMask = e.stateMask;
+
+ // Not yet used. Should be, for Mac and Linux.
+ }
+
+ public void mouseDoubleClick(MouseEvent e) {
+ mCanvas.showXml(e);
+ }
+
+ // --- KeyListener ---
+
+ public void keyPressed(KeyEvent e) {
+ if (mCurrentGesture != null) {
+ mCurrentGesture.keyPressed(e);
+ } else {
+ if (e.keyCode == SWT.ESC) {
+ // It appears that SWT does NOT (on the Mac) pass any
+ // key strokes other than modifier keys when the mouse
+ // button is pressed!!
+ ControlPoint controlPoint = ControlPoint.create(mCanvas,
+ mLastMouseX, mLastMouseY);
+ finishGesture(controlPoint, true);
+ return;
+ }
+ }
+ }
+
+ public void keyReleased(KeyEvent e) {
+ if (mCurrentGesture != null) {
+ mCurrentGesture.keyReleased(e);
+ }
+ }
+
+ }
+
+ /** Listener for Drag & Drop events. */
+ private class CanvasDropListener implements DropTargetListener {
+ public CanvasDropListener() {
+ }
+
+ /**
+ * The cursor has entered the drop target boundaries. {@inheritDoc}
+ */
+ public void dragEnter(DropTargetEvent event) {
+ if (mCurrentGesture == null) {
+ Gesture newGesture = mZombieGesture;
+ if (newGesture == null) {
+ newGesture = new MoveGesture(mCanvas);
+ } else {
+ mZombieGesture = null;
+ }
+ startGesture(ControlPoint.create(mCanvas, event),
+ newGesture, 0);
+ }
+
+ if (mCurrentGesture instanceof DropGesture) {
+ ((DropGesture) mCurrentGesture).dragEnter(event);
+ }
+ }
+
+ /**
+ * The cursor is moving over the drop target. {@inheritDoc}
+ */
+ public void dragOver(DropTargetEvent event) {
+ if (mCurrentGesture instanceof DropGesture) {
+ ((DropGesture) mCurrentGesture).dragOver(event);
+ }
+ }
+
+ /**
+ * The cursor has left the drop target boundaries OR data is about to be
+ * dropped. {@inheritDoc}
+ */
+ public void dragLeave(DropTargetEvent event) {
+ if (mCurrentGesture instanceof DropGesture) {
+ DropGesture dropGesture = (DropGesture) mCurrentGesture;
+ dropGesture.dragLeave(event);
+ finishGesture(ControlPoint.create(mCanvas, event), true);
+ mZombieGesture = dropGesture;
+ }
+ }
+
+ /**
+ * The drop is about to be performed. The drop target is given a last
+ * chance to change the nature of the drop. {@inheritDoc}
+ */
+ public void dropAccept(DropTargetEvent event) {
+ Gesture gesture = mCurrentGesture != null ? mCurrentGesture : mZombieGesture;
+ if (gesture instanceof DropGesture) {
+ ((DropGesture) gesture).dropAccept(event);
+ }
+ }
+
+ /**
+ * The data is being dropped. {@inheritDoc}
+ */
+ public void drop(final DropTargetEvent event) {
+ // See if we had a gesture just prior to the drop (we receive a dragLeave
+ // right before the drop which we don't know whether means the cursor has
+ // left the canvas for good or just before a drop)
+ Gesture gesture = mCurrentGesture != null ? mCurrentGesture : mZombieGesture;
+ mZombieGesture = null;
+
+ if (gesture instanceof DropGesture) {
+ ((DropGesture) gesture).drop(event);
+
+ finishGesture(ControlPoint.create(mCanvas, event), true);
+ }
+ }
+
+ /**
+ * The operation being performed has changed (e.g. modifier key).
+ * {@inheritDoc}
+ */
+ public void dragOperationChanged(DropTargetEvent event) {
+ if (mCurrentGesture instanceof DropGesture) {
+ ((DropGesture) mCurrentGesture).dragOperationChanged(event);
+ }
+ }
+ }
+
+ /**
+ * Our canvas {@link DragSourceListener}. Handles drag being started and
+ * finished and generating the drag data.
+ */
+ private class CanvasDragSourceListener implements DragSourceListener {
+
+ /**
+ * The current selection being dragged. This may be a subset of the
+ * canvas selection due to the "sanitize" pass. Can be empty but never
+ * null.
+ */
+ private final ArrayList<CanvasSelection> mDragSelection = new ArrayList<CanvasSelection>();
+
+ private SimpleElement[] mDragElements;
+
+ /**
+ * The user has begun the actions required to drag the widget.
+ * <p/>
+ * Initiate a drag only if there is one or more item selected. If
+ * there's none, try to auto-select the one under the cursor.
+ * {@inheritDoc}
+ */
+ public void dragStart(DragSourceEvent e) {
+ // We need a selection (simple or multiple) to do any transfer.
+ // If there's a selection *and* the cursor is over this selection,
+ // use all the currently selected elements.
+ // If there is no selection or the cursor is not over a selected
+ // element, *change* the selection to match the element under the
+ // cursor and use that. If nothing can be selected, abort the drag
+ // operation.
+
+ List<CanvasSelection> selections = mCanvas.getSelectionManager().getSelections();
+ mDragSelection.clear();
+
+ if (!selections.isEmpty()) {
+ // Is the cursor on top of a selected element?
+ LayoutPoint p = LayoutPoint.create(mCanvas, e);
+
+ boolean insideSelection = false;
+
+ for (CanvasSelection cs : selections) {
+ if (!cs.isRoot() && cs.getRect().contains(p.x, p.y)) {
+ insideSelection = true;
+ break;
+ }
+ }
+
+ if (!insideSelection) {
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+ if (vi != null && !vi.isRoot()) {
+ mCanvas.getSelectionManager().selectSingle(vi);
+ insideSelection = true;
+ }
+ }
+
+ if (insideSelection) {
+ // We should now have a proper selection that matches the
+ // cursor. Let's use this one. We make a copy of it since
+ // the "sanitize" pass below might remove some of the
+ // selected objects.
+ if (selections.size() == 1) {
+ // You are dragging just one element - this might or
+ // might not be the root, but if it's the root that is
+ // fine since we will let you drag the root if it is the
+ // only thing you are dragging.
+ mDragSelection.addAll(selections);
+ } else {
+ // Only drag non-root items.
+ for (CanvasSelection cs : selections) {
+ if (!cs.isRoot()) {
+ mDragSelection.add(cs);
+ }
+ }
+ }
+ }
+ }
+
+ // If you are dragging a non-selected item, select it
+ if (mDragSelection.isEmpty()) {
+ LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout();
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+ if (vi != null && !vi.isRoot()) {
+ mCanvas.getSelectionManager().selectSingle(vi);
+ mDragSelection.addAll(selections);
+ }
+ }
+
+ SelectionManager.sanitize(mDragSelection);
+
+ e.doit = !mDragSelection.isEmpty();
+ if (e.doit) {
+ mDragElements = CanvasSelection.getAsElements(mDragSelection);
+ GlobalCanvasDragInfo.getInstance().startDrag(mDragElements,
+ mDragSelection.toArray(new CanvasSelection[mDragSelection.size()]),
+ mCanvas, new Runnable() {
+ public void run() {
+ mCanvas.getClipboardSupport().deleteSelection("Remove",
+ mDragSelection);
+ }
+ });
+ }
+
+ // If you drag on the -background-, we make that into a marquee
+ // selection
+ if (!e.doit || (mDragSelection.size() == 1 && mDragSelection.get(0).isRoot())) {
+ boolean toggle = (mLastStateMask & (SWT.CTRL | SWT.SHIFT | SWT.COMMAND)) != 0;
+ startGesture(ControlPoint.create(mCanvas, e),
+ new MarqueeGesture(mCanvas, toggle), mLastStateMask);
+ e.detail = DND.DROP_NONE;
+ e.doit = false;
+ } else {
+ // Otherwise, the drag means you are moving something
+ startGesture(ControlPoint.create(mCanvas, e), new MoveGesture(mCanvas), 0);
+ }
+
+ // No hover during drag (since no mouse over events are delivered
+ // during a drag to keep the hovers up to date anyway)
+ mCanvas.clearHover();
+
+ mCanvas.redraw();
+ }
+
+ /**
+ * Callback invoked when data is needed for the event, typically right
+ * before drop. The drop side decides what type of transfer to use and
+ * this side must now provide the adequate data. {@inheritDoc}
+ */
+ public void dragSetData(DragSourceEvent e) {
+ if (TextTransfer.getInstance().isSupportedType(e.dataType)) {
+ e.data = CanvasSelection.getAsText(mCanvas, mDragSelection);
+ return;
+ }
+
+ if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) {
+ e.data = mDragElements;
+ return;
+ }
+
+ // otherwise we failed
+ e.detail = DND.DROP_NONE;
+ e.doit = false;
+ }
+
+ /**
+ * Callback invoked when the drop has been finished either way. On a
+ * successful move, remove the originating elements.
+ */
+ public void dragFinished(DragSourceEvent e) {
+ // Clear the selection
+ mDragSelection.clear();
+ mDragElements = null;
+ GlobalCanvasDragInfo.getInstance().stopDrag();
+
+ finishGesture(ControlPoint.create(mCanvas, e), e.detail == DND.DROP_NONE);
+ mCanvas.redraw();
+ }
+ }
+}
* off a canvas or its palette and then set back to null when the drag'n'drop is finished.
* <p/>
* Note that when a drag starts in one instance of Eclipse and the dragOver/drop is done
- * in a <em>separate</em> instance of Eclipse, the tragged FQCN won't be registered here
+ * in a <em>separate</em> instance of Eclipse, the dragged FQCN won't be registered here
* and will be null.
*/
-class GlobalCanvasDragInfo {
+final class GlobalCanvasDragInfo {
private static final GlobalCanvasDragInfo sInstance = new GlobalCanvasDragInfo();
* @param xmlNode The Node whose element we want to select
*/
public void select(Node xmlNode) {
- mCanvasViewer.getCanvas().select(xmlNode);
+ mCanvasViewer.getCanvas().getSelectionManager().select(xmlNode);
}
/**
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Rectangle;
+
+/**
+ * The {@link HoverOverlay} paints an optional hover on top of the layout,
+ * highlighting the currently hovered view.
+ */
+public class HoverOverlay extends Overlay {
+ /** Hover border color. Must be disposed, it's NOT a system color. */
+ private Color mHoverStrokeColor;
+
+ /** Hover fill color. Must be disposed, it's NOT a system color. */
+ private Color mHoverFillColor;
+
+ /** Vertical scaling & scrollbar information. */
+ private ScaleInfo mVScale;
+
+ /** Horizontal scaling & scrollbar information. */
+ private ScaleInfo mHScale;
+
+ /**
+ * Current mouse hover border rectangle. Null when there's no mouse hover.
+ * The rectangle coordinates do not take account of the translation, which
+ * must be applied to the rectangle when drawing.
+ */
+ private Rectangle mHoverRect;
+
+ /**
+ * Constructs a new {@link HoverOverlay} linked to the given view hierarchy.
+ *
+ * @param hScale The {@link ScaleInfo} to use to transfer horizontal layout
+ * coordinates to screen coordinates.
+ * @param vScale The {@link ScaleInfo} to use to transfer vertical layout
+ * coordinates to screen coordinates.
+ */
+ public HoverOverlay(ScaleInfo hScale, ScaleInfo vScale) {
+ super();
+ this.mHScale = hScale;
+ this.mVScale = vScale;
+ }
+
+ @Override
+ public void create(Device device) {
+ if (SwtDrawingStyle.HOVER.getStrokeColor() != null) {
+ mHoverStrokeColor = new Color(device, SwtDrawingStyle.HOVER.getStrokeColor());
+ }
+ if (SwtDrawingStyle.HOVER.getFillColor() != null) {
+ mHoverFillColor = new Color(device, SwtDrawingStyle.HOVER.getFillColor());
+ }
+ }
+
+ @Override
+ public void dispose() {
+ if (mHoverStrokeColor != null) {
+ mHoverStrokeColor.dispose();
+ mHoverStrokeColor = null;
+ }
+
+ if (mHoverFillColor != null) {
+ mHoverFillColor.dispose();
+ mHoverFillColor = null;
+ }
+ }
+
+ /**
+ * Sets the hover rectangle. The coordinates of the rectangle are in layout
+ * coordinates. The recipient is will own this rectangle.
+ * <p/>
+ * TODO: Consider switching input arguments to two {@link LayoutPoint}s so
+ * we don't have ambiguity about the coordinate system of these input
+ * parameters.
+ * <p/>
+ *
+ * @param x The top left x coordinate, in layout coordinates, of the hover.
+ * @param y The top left y coordinate, in layout coordinates, of the hover.
+ * @param w The width of the hover (in layout coordinates).
+ * @param h The height of the hover (in layout coordinates).
+ */
+ public void setHover(int x, int y, int w, int h) {
+ mHoverRect = new Rectangle(x, y, w, h);
+ }
+
+ /**
+ * Removes the hover for the next paint.
+ */
+ public void clearHover() {
+ mHoverRect = null;
+ }
+
+ @Override
+ public void paint(GC gc) {
+ if (mHoverRect != null) {
+ // Translate the hover rectangle (in canvas coordinates) to control
+ // coordinates
+ int x = mHScale.translate(mHoverRect.x);
+ int y = mVScale.translate(mHoverRect.y);
+ int w = mHScale.scale(mHoverRect.width);
+ int h = mVScale.scale(mHoverRect.height);
+
+ if (mHoverStrokeColor != null) {
+ int oldAlpha = gc.getAlpha();
+ gc.setForeground(mHoverStrokeColor);
+ gc.setLineStyle(SwtDrawingStyle.HOVER.getLineStyle());
+ gc.setAlpha(SwtDrawingStyle.HOVER.getStrokeAlpha());
+ gc.drawRectangle(x, y, w, h);
+ gc.setAlpha(oldAlpha);
+ }
+
+ if (mHoverFillColor != null) {
+ int oldAlpha = gc.getAlpha();
+ gc.setAlpha(SwtDrawingStyle.HOVER.getFillAlpha());
+ gc.setBackground(mHoverFillColor);
+ gc.fillRectangle(x, y, w, h);
+ gc.setAlpha(oldAlpha);
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.awt.image.Raster;
+
+/**
+ * The {@link ImageOverlay} class renders an image as an overlay.
+ */
+public class ImageOverlay extends Overlay {
+ /** Current background image. Null when there's no image. */
+ private Image mImage;
+
+ /** The associated {@link LayoutCanvas}. */
+ private LayoutCanvas mCanvas;
+
+ /** Vertical scaling & scrollbar information. */
+ private ScaleInfo mVScale;
+
+ /** Horizontal scaling & scrollbar information. */
+ private ScaleInfo mHScale;
+
+ /**
+ * Constructs an {@link ImageOverlay} tied to the given canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} to paint the overlay over.
+ * @param hScale The horizontal scale information.
+ * @param vScale The vertical scale information.
+ */
+ public ImageOverlay(LayoutCanvas canvas, ScaleInfo hScale, ScaleInfo vScale) {
+ this.mCanvas = canvas;
+ this.mHScale = hScale;
+ this.mVScale = vScale;
+ }
+
+ @Override
+ public void create(Device device) {
+ super.create(device);
+ }
+
+ @Override
+ public void dispose() {
+ if (mImage != null) {
+ mImage.dispose();
+ mImage = null;
+ }
+ }
+
+ /**
+ * Sets the image to be drawn as an overlay from the passed in AWT
+ * {@link BufferedImage} (which will be converted to an SWT image).
+ * <p/>
+ * The image <b>can</b> be null, which is the case when we are dealing with
+ * an empty document.
+ *
+ * @param awtImage The AWT image to be rendered as an SWT image.
+ * @return The corresponding SWT image, or null.
+ */
+ public Image setImage(BufferedImage awtImage) {
+ if (mImage != null) {
+ mImage.dispose();
+ }
+ if (awtImage == null) {
+ mImage = null;
+
+ } else {
+ int width = awtImage.getWidth();
+ int height = awtImage.getHeight();
+
+ Raster raster = awtImage.getData(new java.awt.Rectangle(width, height));
+ int[] imageDataBuffer = ((DataBufferInt) raster.getDataBuffer()).getData();
+
+ ImageData imageData = new ImageData(width, height, 32, new PaletteData(0x00FF0000,
+ 0x0000FF00, 0x000000FF));
+
+ imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
+
+ mImage = new Image(mCanvas.getDisplay(), imageData);
+ }
+
+ return mImage;
+ }
+
+ @Override
+ public void paint(GC gc) {
+ if (mImage != null) {
+ boolean valid = mCanvas.getViewHierarchy().isValid();
+ if (!valid) {
+ gc_setAlpha(gc, 128); // half-transparent
+ }
+
+ ScaleInfo hi = mHScale;
+ ScaleInfo vi = mVScale;
+
+ // we only anti-alias when reducing the image size.
+ int oldAlias = -2;
+ if (hi.getScale() < 1.0) {
+ oldAlias = gc_setAntialias(gc, SWT.ON);
+ }
+
+ gc.drawImage(
+ mImage,
+ 0, // srcX
+ 0, // srcY
+ hi.getImgSize(), // srcWidth
+ vi.getImgSize(), // srcHeight
+ hi.translate(0), // destX
+ vi.translate(0), // destY
+ hi.getScalledImgSize(), // destWidth
+ vi.getScalledImgSize()); // destHeight
+
+ if (oldAlias != -2) {
+ gc_setAntialias(gc, oldAlias);
+ }
+
+ if (!valid) {
+ gc_setAlpha(gc, 255); // opaque
+ }
+ }
+ }
+
+ /**
+ * Sets the alpha for the given GC.
+ * <p/>
+ * Alpha may not work on all platforms and may fail with an exception, which
+ * is hidden here (false is returned in that case).
+ *
+ * @param gc the GC to change
+ * @param alpha the new alpha, 0 for transparent, 255 for opaque.
+ * @return True if the operation worked, false if it failed with an
+ * exception.
+ * @see GC#setAlpha(int)
+ */
+ private boolean gc_setAlpha(GC gc, int alpha) {
+ try {
+ gc.setAlpha(alpha);
+ return true;
+ } catch (SWTException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Sets the non-text antialias flag for the given GC.
+ * <p/>
+ * Antialias may not work on all platforms and may fail with an exception,
+ * which is hidden here (-2 is returned in that case).
+ *
+ * @param gc the GC to change
+ * @param alias One of {@link SWT#DEFAULT}, {@link SWT#ON}, {@link SWT#OFF}.
+ * @return The previous aliasing mode if the operation worked, or -2 if it
+ * failed with an exception.
+ * @see GC#setAntialias(int)
+ */
+ private int gc_setAntialias(GC gc, int alias) {
+ try {
+ int old = gc.getAntialias();
+ gc.setAntialias(alias);
+ return old;
+ } catch (SWTException e) {
+ return -2;
+ }
+ }
+
+}
package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
-import com.android.ide.common.api.IDragElement;
import com.android.ide.common.api.INode;
import com.android.ide.common.api.Point;
-import com.android.ide.common.api.Rect;
-import com.android.ide.common.api.IDragElement.IDragAttribute;
import com.android.ide.eclipse.adt.AdtPlugin;
-import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
-import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
-import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
-import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.layoutlib.api.ILayoutResult;
-import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo;
import com.android.sdklib.SdkConstants;
-import org.eclipse.core.runtime.ListenerList;
-import org.eclipse.gef.ui.parts.TreeViewer;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ActionContributionItem;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
-import org.eclipse.jface.text.BadLocationException;
-import org.eclipse.jface.util.SafeRunnable;
-import org.eclipse.jface.viewers.ISelection;
-import org.eclipse.jface.viewers.ISelectionChangedListener;
-import org.eclipse.jface.viewers.ISelectionProvider;
-import org.eclipse.jface.viewers.ITreeSelection;
-import org.eclipse.jface.viewers.SelectionChangedEvent;
-import org.eclipse.jface.viewers.TreePath;
-import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.swt.SWT;
-import org.eclipse.swt.SWTException;
-import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSource;
-import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DragSourceListener;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetListener;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
-import org.eclipse.swt.dnd.TransferData;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseEvent;
-import org.eclipse.swt.events.MouseListener;
-import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
-import org.eclipse.swt.events.SelectionAdapter;
-import org.eclipse.swt.events.SelectionEvent;
-import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
-import org.eclipse.swt.graphics.ImageData;
-import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
-import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Menu;
-import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.actions.ContributionItemFactory;
import org.eclipse.ui.actions.TextActionHandler;
import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction;
import org.eclipse.ui.internal.ide.IDEWorkbenchMessages;
-import org.eclipse.ui.internal.registry.ViewDescriptor;
import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
-import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
-import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
-import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
-import org.eclipse.wst.xml.core.internal.document.NodeContainer;
import org.w3c.dom.Node;
-import java.awt.image.BufferedImage;
-import java.awt.image.DataBufferInt;
-import java.awt.image.Raster;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.Set;
-
/**
* Displays the image rendered by the {@link GraphicalEditorPart} and handles
* the interaction with the widgets.
* actually uses the {@link LayoutCanvasViewer}, which is a JFace viewer wrapper
* around this control.
* <p/>
- * This class implements {@link ISelectionProvider} so that it can delegate
- * the selection provider from the {@link LayoutCanvasViewer}.
- * <p/>
- * Note that {@link LayoutCanvasViewer} sets a selection change listener on this
- * control so that it can invoke its own fireSelectionChanged when the control's
- * selection changes.
+ * The LayoutCanvas contains the painting logic for the canvas. Selection,
+ * clipboard, view management etc. is handled in separate helper classes.
*
* @since GLE2
*/
-/*
- * TODO list:
- * - gray on error, keep select but disable d'n'd.
- * - context menu: enum clear, flag values, toggles as tri-states
- * - context menu: impl custom layout width/height
- * - properly handle custom views
- */
-class LayoutCanvas extends Canvas implements ISelectionProvider {
+@SuppressWarnings("restriction") // For WorkBench "Show In" support
+class LayoutCanvas extends Canvas {
- private final static boolean DEBUG = false;
+ private static final boolean DEBUG = false;
/* package */ static final String PREFIX_CANVAS_ACTION = "canvas_action_";
/** The Rules Engine, associated with the current project. */
private RulesEngine mRulesEngine;
- /** SWT clipboard instance. */
- private Clipboard mClipboard;
-
- /**
- * The CanvasViewInfo root created by the last call to {@link #setResult(ILayoutResult)}
- * with a valid layout.
- * <p/>
- * This <em>can</em> be null to indicate we're dealing with an empty document with
- * no root node. Null here does not mean the result was invalid, merely that the XML
- * had no content to display -- we need to treat an empty document as valid so that
- * we can drop new items in it.
- */
- private CanvasViewInfo mLastValidViewInfoRoot;
-
- /**
- * True when the last {@link #setResult(ILayoutResult)} provided a valid {@link ILayoutResult}.
- * <p/>
- * When false this means the canvas is displaying an out-dated result image & bounds and some
- * features should be disabled accordingly such a drag'n'drop.
- * <p/>
- * Note that an empty document (with a null {@link #mLastValidViewInfoRoot}) is considered
- * valid since it is an acceptable drop target.
- */
- private boolean mIsResultValid;
-
- /** Current background image. Null when there's no image. */
- private Image mImage;
-
- /** The current selection list. The list is never null, however it can be empty. */
- private final LinkedList<CanvasSelection> mSelections = new LinkedList<CanvasSelection>();
-
- /** An unmodifiable view of {@link #mSelections}. */
- private List<CanvasSelection> mUnmodifiableSelection;
-
- /** CanvasSelection border color. Do not dispose, it's a system color. */
- private Color mSelectionFgColor;
-
/** GC wrapper given to the IViewRule methods. The GC itself is only defined in the
* context of {@link #onPaint(PaintEvent)}; otherwise it is null. */
private GCWrapper mGCWrapper;
/** Current hover view info. Null when no mouse hover. */
private CanvasViewInfo mHoverViewInfo;
- /** Current mouse hover border rectangle. Null when there's no mouse hover.
- * The rectangle coordinates do not take account of the translation, which must
- * be applied to the rectangle when drawing.
- */
- private Rectangle mHoverRect;
-
- /** Hover border color. Must be disposed, it's NOT a system color. */
- private Color mHoverStrokeColor;
-
- /** Hover fill color. Must be disposed, it's NOT a system color. */
- private Color mHoverFillColor;
-
- /** Outline color. Must be disposed, it's NOT a system color. */
- private Color mOutlineColor;
-
- /**
- * The <em>current</em> alternate selection, if any, which changes when the Alt key is
- * used during a selection. Can be null.
- */
- private CanvasAlternateSelection mAltSelection;
-
/** When true, always display the outline of all views. */
private boolean mShowOutline;
/** Drop target associated with this composite. */
private DropTarget mDropTarget;
- /** Drop listener, with feedback from current drop */
- private CanvasDropListener mDropListener;
-
/** Factory that can create {@link INode} proxies. */
private final NodeFactory mNodeFactory = new NodeFactory();
/** Drag source associated with this canvas. */
private DragSource mDragSource;
- /** List of clients listening to selection changes. */
- private final ListenerList mSelectionListeners = new ListenerList();
-
/**
* The current Outline Page, to set its model.
* It isn't possible to call OutlinePage2.dispose() in this.dispose().
**/
private OutlinePage2 mOutlinePage;
- /** Barrier set when updating the selection to prevent from recursively
- * invoking ourselves. */
- private boolean mInsideUpdateSelection;
-
/** Delete action for the Edit or context menu. */
private Action mDeleteAction;
/** Root of the context menu. */
private MenuManager mMenuManager;
- private CanvasDragSourceListener mDragSourceListener;
+ /** The view hierarchy associated with this canvas. */
+ private final ViewHierarchy mViewHierarchy = new ViewHierarchy(this);
+
+ /** The selection in the canvas. */
+ private final SelectionManager mSelectionManager = new SelectionManager(this);
+
+ /** The overlay which paints the optional outline. */
+ private OutlineOverlay mOutlineOverlay;
+
+ /** The overlay which paints the mouse hover. */
+ private HoverOverlay mHoverOverlay;
+ /** The overlay which paints the selection. */
+ private SelectionOverlay mSelectionOverlay;
+
+ /** The overlay which paints the rendered layout image. */
+ private ImageOverlay mImageOverlay;
+
+ /**
+ * Gesture Manager responsible for identifying mouse, keyboard and drag and
+ * drop events.
+ */
+ private final GestureManager mGestureManager = new GestureManager(this);
+
+ /**
+ * Native clipboard support.
+ */
+ private ClipboardSupport mClipboardSupport;
public LayoutCanvas(LayoutEditor layoutEditor,
RulesEngine rulesEngine,
mLayoutEditor = layoutEditor;
mRulesEngine = rulesEngine;
- mClipboard = new Clipboard(parent.getDisplay());
-
- mHScale = new ScaleInfo(getHorizontalBar());
- mVScale = new ScaleInfo(getVerticalBar());
+ mClipboardSupport = new ClipboardSupport(this, parent);
+ mHScale = new ScaleInfo(this, getHorizontalBar());
+ mVScale = new ScaleInfo(this, getVerticalBar());
mGCWrapper = new GCWrapper(mHScale, mVScale);
- Display d = getDisplay();
- mSelectionFgColor = new Color(d, SwtDrawingStyle.SELECTION.getStrokeColor());
- if (SwtDrawingStyle.HOVER.getStrokeColor() != null) {
- mHoverStrokeColor = new Color(d, SwtDrawingStyle.HOVER.getStrokeColor());
- }
- if (SwtDrawingStyle.HOVER.getFillColor() != null) {
- mHoverFillColor = new Color(d, SwtDrawingStyle.HOVER.getFillColor());
- }
- mOutlineColor = new Color(d, SwtDrawingStyle.OUTLINE.getStrokeColor());
+ Display display = getDisplay();
+ mFont = display.getSystemFont();
- mFont = d.getSystemFont();
+ // --- Set up graphic overlays
+ // mOutlineOverlay is initialized lazily
+ mHoverOverlay = new HoverOverlay(mHScale, mVScale);
+ mHoverOverlay.create(display);
+ mSelectionOverlay = new SelectionOverlay();
+ mSelectionOverlay.create(display);
+ mImageOverlay = new ImageOverlay(this, mHScale, mVScale);
+ mImageOverlay.create(display);
+ // --- Set up listeners
addPaintListener(new PaintListener() {
public void paintControl(PaintEvent e) {
onPaint(e);
}
});
- addMouseMoveListener(new MouseMoveListener() {
- public void mouseMove(MouseEvent e) {
- onMouseMove(e);
- }
- });
-
- addMouseListener(new MouseListener() {
- public void mouseUp(MouseEvent e) {
- onMouseUp(e);
- }
-
- public void mouseDown(MouseEvent e) {
- onMouseDown(e);
- }
-
- public void mouseDoubleClick(MouseEvent e) {
- onDoubleClick(e);
- }
- });
-
addKeyListener(new KeyListener() {
public void keyPressed(KeyEvent e) {
// --- setup drag'n'drop ---
// DND Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html
- mDropListener = new CanvasDropListener(this);
- mDropTarget = createDropTarget(this, mDropListener);
+ mDropTarget = createDropTarget(this);
+ mDragSource = createDragSource(this);
+ mGestureManager.registerListeners(mDragSource, mDropTarget);
- mDragSourceListener = new CanvasDragSourceListener();
- mDragSource = createDragSource(this, mDragSourceListener);
+ if (mLayoutEditor == null) {
+ // TODO: In another CL we should use EasyMock/objgen to provide an editor.
+ return; // Unit test
+ }
// --- setup context menu ---
setupGlobalActionHandlers();
public void dispose() {
super.dispose();
- if (mSelectionFgColor != null) {
- mSelectionFgColor.dispose();
- mSelectionFgColor = null;
- }
-
- if (mOutlineColor != null) {
- mOutlineColor.dispose();
- mOutlineColor = null;
- }
-
- if (mHoverStrokeColor != null) {
- mHoverStrokeColor.dispose();
- mHoverStrokeColor = null;
- }
-
- if (mHoverFillColor != null) {
- mHoverFillColor.dispose();
- mHoverFillColor = null;
- }
+ mGestureManager.unregisterListeners(mDragSource, mDropTarget);
if (mDropTarget != null) {
mDropTarget.dispose();
mDragSource = null;
}
- if (mClipboard != null) {
- mClipboard.dispose();
- mClipboard = null;
- }
-
- if (mImage != null) {
- mImage.dispose();
- mImage = null;
+ if (mClipboardSupport != null) {
+ mClipboardSupport.dispose();
+ mClipboardSupport = null;
}
if (mGCWrapper != null) {
mGCWrapper.dispose();
mGCWrapper = null;
}
- }
- /**
- * Returns true when the last {@link #setResult(ILayoutResult)} provided a valid
- * {@link ILayoutResult}.
- * <p/>
- * When false this means the canvas is displaying an out-dated result image & bounds and some
- * features should be disabled accordingly such a drag'n'drop.
- * <p/>
- * Note that an empty document (with a null {@link #mLastValidViewInfoRoot}) is considered
- * valid since it is an acceptable drop target.
- */
- /* package */ boolean isResultValid() {
- return mIsResultValid;
- }
+ if (mOutlineOverlay != null) {
+ mOutlineOverlay.dispose();
+ mOutlineOverlay = null;
+ }
- /**
- * Returns true if the last valid content of the canvas represents an empty document.
- */
- /* package */ boolean isEmptyDocument() {
- return mLastValidViewInfoRoot == null;
+ if (mHoverOverlay != null) {
+ mHoverOverlay.dispose();
+ mHoverOverlay = null;
+ }
+
+ if (mSelectionOverlay != null) {
+ mSelectionOverlay.dispose();
+ mSelectionOverlay = null;
+ }
+
+ if (mImageOverlay != null) {
+ mImageOverlay.dispose();
+ mImageOverlay = null;
+ }
}
/** Returns the Rules Engine, associated with the current project. */
* This is used by {@link OutlinePage2} to delegate drag source events.
*/
/* package */ DragSourceListener getDragListener() {
- return mDragSourceListener;
+ return mGestureManager.getDragSourceListener();
}
/**
* This is used by {@link OutlinePage2} to delegate drop target events.
*/
/* package */ DropTargetListener getDropListener() {
- return mDropListener;
+ return mGestureManager.getDropTargetListener();
}
/**
- * Returns the native {@link CanvasSelection} list.
+ * Returns the GCWrapper used to paint view rules.
*
- * @return An immutable list of {@link CanvasSelection}. Can be empty but not null.
- * @see #getSelection() {@link #getSelection()} to retrieve a {@link TreeViewer}
- * compatible {@link ISelection}.
+ * @return The GCWrapper used to paint view rules
*/
- /* package */ List<CanvasSelection> getCanvasSelections() {
- if (mUnmodifiableSelection == null) {
- mUnmodifiableSelection = Collections.unmodifiableList(mSelections);
- }
- return mUnmodifiableSelection;
+ /* package */ GCWrapper getGcWrapper() {
+ return mGCWrapper;
}
/**
}
/**
+ * Returns the horizontal {@link ScaleInfo} transform object, which can map
+ * a layout point into a control point.
+ *
+ * @return A {@link ScaleInfo} for mapping between layout and control
+ * coordinates in the horizontal dimension.
+ */
+ /* package */ ScaleInfo getHorizontalTransform() {
+ return mHScale;
+ }
+
+ /**
+ * Returns the vertical {@link ScaleInfo} transform object, which can map a
+ * layout point into a control point.
+ *
+ * @return A {@link ScaleInfo} for mapping between layout and control
+ * coordinates in the vertical dimension.
+ */
+ /* package */ ScaleInfo getVerticalTransform() {
+ return mVScale;
+ }
+
+ /**
+ * Returns the {@link SelectionManager} associated with this canvas.
+ *
+ * @return The {@link SelectionManager} holding the selection for this
+ * canvas. Never null.
+ */
+ public SelectionManager getSelectionManager() {
+ return mSelectionManager;
+ }
+
+ /**
+ * Returns the {@link ViewHierarchy} object associated with this canvas,
+ * holding the most recent rendered view of the scene, if valid.
+ *
+ * @return The {@link ViewHierarchy} object associated with this canvas.
+ * Never null.
+ */
+ public ViewHierarchy getViewHierarchy() {
+ return mViewHierarchy;
+ }
+
+ /**
+ * Returns the {@link ClipboardSupport} object associated with this canvas.
+ *
+ * @return The {@link ClipboardSupport} object for this canvas. Null only after dispose.
+ */
+ public ClipboardSupport getClipboardSupport() {
+ return mClipboardSupport;
+ }
+
+ /**
* Sets the result of the layout rendering. The result object indicates if the layout
* rendering succeeded. If it did, it contains a bitmap and the objects rectangles.
*
*/
/* package */ void setResult(ILayoutResult result) {
// disable any hover
- mHoverRect = null;
-
- mIsResultValid = (result != null && result.getSuccess() == ILayoutResult.SUCCESS);
+ clearHover();
- if (mIsResultValid && result != null) {
- ILayoutViewInfo root = result.getRootView();
- if (root == null) {
- mLastValidViewInfoRoot = null;
- } else {
- mLastValidViewInfoRoot = new CanvasViewInfo(result.getRootView());
- }
- setImage(result.getImage());
-
- updateNodeProxies(mLastValidViewInfoRoot);
- mOutlinePage.setModel(mLastValidViewInfoRoot);
-
- // Check if the selection is still the same (based on the object keys)
- // and eventually recompute their bounds.
- for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
- CanvasSelection s = it.next();
-
- // Check if the selected object still exists
- Object key = s.getViewInfo().getUiViewKey();
- CanvasViewInfo vi = findViewInfoKey(key, mLastValidViewInfoRoot);
-
- // Remove the previous selection -- if the selected object still exists
- // we need to recompute its bounds in case it moved so we'll insert a new one
- // at the same place.
- it.remove();
- if (vi != null) {
- it.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory));
- }
- }
- fireSelectionChanged();
+ mViewHierarchy.setResult(result);
+ if (mViewHierarchy.isValid() && result != null) {
+ Image image = mImageOverlay.setImage(result.getImage());
- // remove the current alternate selection views
- mAltSelection = null;
+ mOutlinePage.setModel(mViewHierarchy.getRoot());
- if (mImage != null) {
- mHScale.setSize(mImage.getImageData().width, getClientArea().width);
- mVScale.setSize(mImage.getImageData().height, getClientArea().height);
+ if (image != null) {
+ mHScale.setSize(image.getImageData().width, getClientArea().width);
+ mVScale.setSize(image.getImageData().height, getClientArea().height);
}
// Pre-load the android.view.View rule in the Rules Engine. Doing it here means
}
/**
- * Transforms a point, expressed in SWT display coordinates
- * (e.g. from a Drag'n'Drop {@link Event}, not local {@link Control} coordinates)
- * into the canvas' image coordinates according to the current zoom and scroll.
- *
- * @param displayX X in SWT display coordinates
- * @param displayY Y in SWT display coordinates
- * @return A new {@link Point} in canvas coordinates
- */
- /* package */ Point displayToCanvasPoint(int displayX, int displayY) {
- // convert screen coordinates to local SWT control coordinates
- org.eclipse.swt.graphics.Point p = this.toControl(displayX, displayY);
-
- int x = mHScale.inverseTranslate(p.x);
- int y = mVScale.inverseTranslate(p.y);
- return new Point(x, y);
- }
-
- /**
- * Transforms a point, expressed in canvas coordinates, into "client" coordinates
- * relative to the control (and not relative to the display.)
+ * Transforms a point, expressed in layout coordinates, into "client" coordinates
+ * relative to the control (and not relative to the display).
*
* @param canvasX X in the canvas coordinates
* @param canvasY Y in the canvas coordinates
* @return A new {@link Point} in control client coordinates (not display coordinates)
*/
- /* package */ Point canvasToControlPoint(int canvasX, int canvasY) {
+ /* package */ Point layoutToControlPoint(int canvasX, int canvasY) {
int x = mHScale.translate(canvasX);
int y = mVScale.translate(canvasY);
return new Point(x, y);
}
- //----
- // Implementation of ISelectionProvider
-
- /**
- * Returns a {@link TreeSelection} compatible with a TreeViewer
- * where each {@link TreePath} item is actually a {@link CanvasViewInfo}.
- */
- public ISelection getSelection() {
- if (mSelections.isEmpty()) {
- return TreeSelection.EMPTY;
- }
-
- ArrayList<TreePath> paths = new ArrayList<TreePath>();
-
- for (CanvasSelection cs : mSelections) {
- CanvasViewInfo vi = cs.getViewInfo();
- if (vi != null) {
- ArrayList<Object> segments = new ArrayList<Object>();
- while (vi != null) {
- segments.add(0, vi);
- vi = vi.getParent();
- }
- paths.add(new TreePath(segments.toArray()));
- }
- }
-
- return new TreeSelection(paths.toArray(new TreePath[paths.size()]));
- }
-
- /**
- * Sets the selection. It must be an {@link ITreeSelection} where each segment
- * of the tree path is a {@link CanvasViewInfo}. A null selection is considered
- * as an empty selection.
- * <p/>
- * This method is invoked by {@link LayoutCanvasViewer#setSelection(ISelection)}
- * in response to an <em>outside</em> selection (compatible with ours) that has
- * changed. Typically it means the outline selection has changed and we're
- * synchronizing ours to match.
- */
- public void setSelection(ISelection selection) {
- if (mInsideUpdateSelection) {
- return;
- }
-
- try {
- mInsideUpdateSelection = true;
-
- if (selection == null) {
- selection = TreeSelection.EMPTY;
- }
-
- if (selection instanceof ITreeSelection) {
- ITreeSelection treeSel = (ITreeSelection) selection;
-
- if (treeSel.isEmpty()) {
- // Clear existing selection, if any
- if (!mSelections.isEmpty()) {
- mSelections.clear();
- mAltSelection = null;
- redraw();
- }
- return;
- }
-
- boolean changed = false;
-
- // Create a list of all currently selected view infos
- Set<CanvasViewInfo> oldSelected = new HashSet<CanvasViewInfo>();
- for (CanvasSelection cs : mSelections) {
- oldSelected.add(cs.getViewInfo());
- }
-
- // Go thru new selection and take care of selecting new items
- // or marking those which are the same as in the current selection
- for (TreePath path : treeSel.getPaths()) {
- Object seg = path.getLastSegment();
- if (seg instanceof CanvasViewInfo) {
- CanvasViewInfo newVi = (CanvasViewInfo) seg;
- if (oldSelected.contains(newVi)) {
- // This view info is already selected. Remove it from the
- // oldSelected list so that we don't de-select it later.
- oldSelected.remove(newVi);
- } else {
- // This view info is not already selected. Select it now.
-
- // reset alternate selection if any
- mAltSelection = null;
- // otherwise add it.
- mSelections.add(
- new CanvasSelection(newVi, mRulesEngine, mNodeFactory));
- changed = true;
- }
- }
- }
-
- // De-select old selected items that are not in the new one
- for (CanvasViewInfo vi : oldSelected) {
- deselect(vi);
- changed = true;
- }
-
- if (changed) {
- redraw();
- updateMenuActions();
- }
-
- }
- } finally {
- mInsideUpdateSelection = false;
- }
- }
-
- public void addSelectionChangedListener(ISelectionChangedListener listener) {
- mSelectionListeners.add(listener);
- }
-
- public void removeSelectionChangedListener(ISelectionChangedListener listener) {
- mSelectionListeners.remove(listener);
- }
-
/**
* Returns the action for the context menu corresponding to the given action id.
* <p/>
return null;
}
- //---
+ //---------------
/**
- * Helper class to convert between control pixel coordinates and canvas coordinates.
- * Takes care of the zooming and offset of the canvas.
+ * Paints the canvas in response to paint events.
*/
- private class ScaleInfo implements ICanvasTransform {
- /** Canvas image size (original, before zoom), in pixels */
- private int mImgSize;
-
- /** Client size, in pixels */
- private int mClientSize;
-
- /** Left-top offset in client pixel coordinates */
- private int mTranslate;
-
- /** Scaling factor, > 0 */
- private double mScale;
-
- /** Scrollbar widget */
- ScrollBar mScrollbar;
-
- public ScaleInfo(ScrollBar scrollbar) {
- mScrollbar = scrollbar;
- mScale = 1.0;
- mTranslate = 0;
+ private void onPaint(PaintEvent e) {
+ GC gc = e.gc;
+ gc.setFont(mFont);
+ mGCWrapper.setGC(gc);
+ try {
+ mImageOverlay.paint(gc);
- mScrollbar.addSelectionListener(new SelectionAdapter() {
- @Override
- public void widgetSelected(SelectionEvent e) {
- // User requested scrolling. Changes translation and redraw canvas.
- mTranslate = mScrollbar.getSelection();
- redraw();
+ if (mShowOutline) {
+ if (mOutlineOverlay == null) {
+ mOutlineOverlay = new OutlineOverlay(mViewHierarchy, mHScale, mVScale);
+ mOutlineOverlay.create(getDisplay());
}
- });
- }
-
- /**
- * Sets the new scaling factor. Recomputes scrollbars.
- * @param scale Scaling factor, > 0.
- */
- public void setScale(double scale) {
- if (mScale != scale) {
- mScale = scale;
- resizeScrollbar();
+ mOutlineOverlay.paint(gc);
}
- }
-
- /** Returns current scaling factor. */
- public double getScale() {
- return mScale;
- }
- /** Returns Canvas image size (original, before zoom), in pixels. */
- public int getImgSize() {
- return mImgSize;
- }
+ mHoverOverlay.paint(gc);
+ mSelectionOverlay.paint(mSelectionManager, gc, mGCWrapper, mRulesEngine);
+ mGestureManager.paint(gc);
- /** Returns the scaled image size in pixels. */
- public int getScalledImgSize() {
- return (int) (mImgSize * mScale);
+ } finally {
+ mGCWrapper.setGC(null);
}
+ }
- /** Changes the size of the canvas image and the client size. Recomputes scrollbars. */
- public void setSize(int imgSize, int clientSize) {
- mImgSize = imgSize;
- setClientSize(clientSize);
- }
+ /**
+ * Clears the hover.
+ */
+ /* package */ void clearHover() {
+ mHoverOverlay.clearHover();
+ }
- /** Changes the size of the client size. Recomputes scrollbars. */
- public void setClientSize(int clientSize) {
- mClientSize = clientSize;
- resizeScrollbar();
+ /**
+ * Hover on top of a known child.
+ */
+ /* package */ void hover(MouseEvent e) {
+ // Check if a button is pressed; no hovers during drags
+ if ((e.stateMask & SWT.BUTTON_MASK) != 0) {
+ clearHover();
+ return;
}
- private void resizeScrollbar() {
- // scaled image size
- int sx = (int) (mImgSize * mScale);
-
- // actual client area is always reduced by the margins
- int cx = mClientSize - 2 * IMAGE_MARGIN;
+ LayoutPoint p = ControlPoint.create(this, e).toLayout();
+ CanvasViewInfo vi = mViewHierarchy.findViewInfoAt(p);
- if (sx < cx) {
- mScrollbar.setEnabled(false);
- } else {
- mScrollbar.setEnabled(true);
-
- // max scroll value is the scaled image size
- // thumb value is the actual viewable area out of the scaled img size
- mScrollbar.setMaximum(sx);
- mScrollbar.setThumb(cx);
- }
+ // We don't hover on the root since it's not a widget per see and it is always there.
+ if (vi != null && vi.isRoot()) {
+ vi = null;
}
- public int translate(int canvasX) {
- return IMAGE_MARGIN - mTranslate + (int)(mScale * canvasX);
- }
+ boolean needsUpdate = vi != mHoverViewInfo;
+ mHoverViewInfo = vi;
- public int scale(int canwasW) {
- return (int)(mScale * canwasW);
+ if (vi == null) {
+ clearHover();
+ } else {
+ Rectangle r = vi.getSelectionRect();
+ mHoverOverlay.setHover(r.x, r.y, r.width, r.height);
}
- public int inverseTranslate(int screenX) {
- return (int) ((screenX - IMAGE_MARGIN + mTranslate) / mScale);
+ if (needsUpdate) {
+ redraw();
}
}
/**
- * Creates or updates the node proxy for this canvas view info.
- * <p/>
- * Since proxies are reused, this will update the bounds of an existing proxy when the
- * canvas is refreshed and a view changes position or size.
- * <p/>
- * This is a recursive call that updates the whole hierarchy starting at the given
- * view info.
+ * Show the XML element corresponding to the point under the mouse event
+ * (unless it's a root).
+ *
+ * @param e A mouse event pointing on the screen whose underlying XML
+ * element we want to view
*/
- private void updateNodeProxies(CanvasViewInfo vi) {
- if (vi == null) {
+ public void showXml(MouseEvent e) {
+ // Warp to the text editor and show the corresponding XML for the
+ // double-clicked widget
+ LayoutPoint p = ControlPoint.create(this, e).toLayout();
+ CanvasViewInfo vi = mViewHierarchy.findViewInfoAt(p);
+ if (vi == null || vi.isRoot()) {
return;
}
- UiViewElementNode key = vi.getUiViewKey();
-
- if (key != null) {
- mNodeFactory.create(vi);
- }
-
- for (CanvasViewInfo child : vi.getChildren()) {
- updateNodeProxies(child);
+ Node xmlNode = vi.getXmlNode();
+ if (xmlNode != null) {
+ boolean found = mLayoutEditor.show(xmlNode);
+ if (!found) {
+ getDisplay().beep();
+ }
}
}
+ //---------------
+
/**
- * Sets the image of the last *successful* rendering.
- * Converts the AWT image into an SWT image.
+ * Helper to create the drag source for the given control.
* <p/>
- * The image *can* be null, which is the case when we are dealing with an empty document.
+ * This is static with package-access so that {@link OutlinePage2} can also
+ * create an exact copy of the source with the same attributes.
*/
- private void setImage(BufferedImage awtImage) {
- if (mImage != null) {
- mImage.dispose();
- }
- if (awtImage == null) {
- mImage = null;
-
- } else {
- int width = awtImage.getWidth();
- int height = awtImage.getHeight();
-
- Raster raster = awtImage.getData(new java.awt.Rectangle(width, height));
- int[] imageDataBuffer = ((DataBufferInt)raster.getDataBuffer()).getData();
-
- ImageData imageData = new ImageData(width, height, 32,
- new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF));
-
- imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
-
- mImage = new Image(getDisplay(), imageData);
- }
+ /* package */static DragSource createDragSource(Control control) {
+ DragSource source = new DragSource(control, DND.DROP_COPY | DND.DROP_MOVE);
+ source.setTransfer(new Transfer[] {
+ TextTransfer.getInstance(),
+ SimpleXmlTransfer.getInstance()
+ });
+ return source;
}
/**
- * Sets the alpha for the given GC.
+ * Helper to create the drop target for the given control.
* <p/>
- * Alpha may not work on all platforms and may fail with an exception, which is
- * hidden here (false is returned in that case).
- *
- * @param gc the GC to change
- * @param alpha the new alpha, 0 for transparent, 255 for opaque.
- * @return True if the operation worked, false if it failed with an exception.
- *
- * @see GC#setAlpha(int)
+ * This is static with package-access so that {@link OutlinePage2} can also
+ * create an exact copy of the drop target with the same attributes.
*/
- private boolean gc_setAlpha(GC gc, int alpha) {
- try {
- gc.setAlpha(alpha);
- return true;
- } catch (SWTException e) {
- return false;
- }
+ /* package */static DropTarget createDropTarget(Control control) {
+ DropTarget dropTarget = new DropTarget(
+ control, DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_DEFAULT);
+ dropTarget.setTransfer(new Transfer[] {
+ SimpleXmlTransfer.getInstance()
+ });
+ return dropTarget;
}
+ //---------------
+
/**
- * Sets the non-text antialias flag for the given GC.
+ * Invoked by the constructor to add our cut/copy/paste/delete/select-all
+ * handlers in the global action handlers of this editor's site.
* <p/>
- * Antialias may not work on all platforms and may fail with an exception, which is
- * hidden here (-2 is returned in that case).
- *
- * @param gc the GC to change
- * @param alias One of {@link SWT#DEFAULT}, {@link SWT#ON}, {@link SWT#OFF}.
- * @return The previous aliasing mode if the operation worked,
- * or -2 if it failed with an exception.
- *
- * @see GC#setAntialias(int)
+ * This will enable the menu items under the global Edit menu and make them
+ * invoke our actions as needed. As a benefit, the corresponding shortcut
+ * accelerators will do what one would expect.
*/
- private int gc_setAntialias(GC gc, int alias) {
- try {
- int old = gc.getAntialias();
- gc.setAntialias(alias);
- return old;
- } catch (SWTException e) {
- return -2;
- }
- }
-
- /**
- * Paints the canvas in response to paint events.
- */
- private void onPaint(PaintEvent e) {
- GC gc = e.gc;
- gc.setFont(mFont);
- mGCWrapper.setGC(gc);
- try {
-
- if (mImage != null) {
- if (!mIsResultValid) {
- gc_setAlpha(gc, 128); // half-transparent
- }
-
- ScaleInfo hi = mHScale;
- ScaleInfo vi = mVScale;
-
- // we only anti-alias when reducing the image size.
- int oldAlias = -2;
- if (hi.getScale() < 1.0) {
- oldAlias = gc_setAntialias(gc, SWT.ON);
- }
-
- gc.drawImage(mImage,
- 0, // srcX
- 0, // srcY
- hi.getImgSize(), // srcWidth
- vi.getImgSize(), // srcHeight
- hi.translate(0), // destX
- vi.translate(0), // destY
- hi.getScalledImgSize(), // destWidth
- vi.getScalledImgSize() // destHeight
- );
-
- if (oldAlias != -2) {
- gc_setAntialias(gc, oldAlias);
- }
-
- if (!mIsResultValid) {
- gc_setAlpha(gc, 255); // opaque
- }
- }
-
- if (mShowOutline && mLastValidViewInfoRoot != null) {
- gc.setForeground(mOutlineColor);
- gc.setLineStyle(SwtDrawingStyle.OUTLINE.getLineStyle());
- int oldAlpha = gc.getAlpha();
- gc.setAlpha(SwtDrawingStyle.OUTLINE.getStrokeAlpha());
- drawOutline(gc, mLastValidViewInfoRoot);
- gc.setAlpha(oldAlpha);
- }
-
- if (mHoverRect != null) {
- int x = mHScale.translate(mHoverRect.x);
- int y = mVScale.translate(mHoverRect.y);
- int w = mHScale.scale(mHoverRect.width);
- int h = mVScale.scale(mHoverRect.height);
-
- if (mHoverStrokeColor != null) {
- int oldAlpha = gc.getAlpha();
- gc.setForeground(mHoverStrokeColor);
- gc.setLineStyle(SwtDrawingStyle.HOVER.getLineStyle());
- gc.setAlpha(SwtDrawingStyle.HOVER.getStrokeAlpha());
- gc.drawRectangle(x, y, w, h);
- gc.setAlpha(oldAlpha);
- }
-
- if (mHoverFillColor != null) {
- int oldAlpha = gc.getAlpha();
- gc.setAlpha(SwtDrawingStyle.HOVER.getFillAlpha());
- gc.setBackground(mHoverFillColor);
- gc.fillRectangle(x, y, w, h);
- gc.setAlpha(oldAlpha);
- }
- }
-
- int n = mSelections.size();
- if (n > 0) {
- boolean isMultipleSelection = n > 1;
-
- if (n == 1) {
- gc.setForeground(mSelectionFgColor);
- mSelections.get(0).paintParentSelection(mRulesEngine, mGCWrapper);
- }
-
- for (CanvasSelection s : mSelections) {
- gc.setForeground(mSelectionFgColor);
- s.paintSelection(mRulesEngine, mGCWrapper, isMultipleSelection);
- }
- }
-
- if (mDropListener != null) {
- mDropListener.paintFeedback(mGCWrapper);
- }
-
- } finally {
- mGCWrapper.setGC(null);
- }
- }
-
- private void drawOutline(GC gc, CanvasViewInfo info) {
-
- Rectangle r = info.getAbsRect();
-
- int x = mHScale.translate(r.x);
- int y = mVScale.translate(r.y);
- int w = mHScale.scale(r.width);
- int h = mVScale.scale(r.height);
-
- // Add +1 to the width and +1 to the height such that when you have a
- // series of boxes (in say a LinearLayout), instead of the bottom of one
- // box and the top of the next box being -adjacent-, they -overlap-.
- // This makes the outline nicer visually since you don't get
- // "double thickness" lines for all adjacent boxes.
- gc.drawRectangle(x, y, w + 1, h + 1);
-
- for (CanvasViewInfo vi : info.getChildren()) {
- drawOutline(gc, vi);
- }
- }
-
- /**
- * Hover on top of a known child.
- */
- private void onMouseMove(MouseEvent e) {
- CanvasViewInfo root = mLastValidViewInfoRoot;
-
- int x = mHScale.inverseTranslate(e.x);
- int y = mVScale.inverseTranslate(e.y);
-
- CanvasViewInfo vi = findViewInfoAt(x, y);
-
- // We don't hover on the root since it's not a widget per see and it is always there.
- if (vi == root) {
- vi = null;
- }
-
- boolean needsUpdate = vi != mHoverViewInfo;
- mHoverViewInfo = vi;
-
- if (vi == null) {
- mHoverRect = null;
- } else {
- Rectangle r = vi.getSelectionRect();
- mHoverRect = new Rectangle(r.x, r.y, r.width, r.height);
- }
-
- if (needsUpdate) {
- redraw();
- }
- }
-
- private void onMouseDown(MouseEvent e) {
- // Pass, not used yet. We do everything on mouse up.
- }
-
- /**
- * Performs selection on mouse up (not mouse down).
- * <p/>
- * Shift key is used to toggle in multi-selection.
- * Alt key is used to cycle selection through objects at the same level than the one
- * pointed at (i.e. click on an object then alt-click to cycle).
- */
- private void onMouseUp(MouseEvent e) {
-
- boolean isShift = (e.stateMask & SWT.SHIFT) != 0;
- boolean isAlt = (e.stateMask & SWT.ALT) != 0;
-
- int x = mHScale.inverseTranslate(e.x);
- int y = mVScale.inverseTranslate(e.y);
-
- if (e.button == 3) {
- // Right click button is used to display a context menu.
- // If there's an existing selection and the click is anywhere in this selection
- // and there are no modifiers being used, we don't want to change the selection.
- // Otherwise we select the item under the cursor.
-
- if (!isAlt && !isShift) {
- for (CanvasSelection cs : mSelections) {
- if (cs.getRect().contains(x, y)) {
- // The cursor is inside the selection. Don't change anything.
- return;
- }
- }
- }
-
- } else if (e.button != 1) {
- // Click was done with something else than the left button for normal selection
- // or the right button for context menu.
- // We don't use mouse button 2 yet (middle mouse, or scroll wheel?) for
- // anything, so let's not change the selection.
- return;
- }
-
- CanvasViewInfo vi = findViewInfoAt(x, y);
-
- if (isShift && !isAlt) {
- // Case where shift is pressed: pointed object is toggled.
-
- // reset alternate selection if any
- mAltSelection = null;
-
- // If nothing has been found at the cursor, assume it might be a user error
- // and avoid clearing the existing selection.
-
- if (vi != null) {
- // toggle this selection on-off: remove it if already selected
- if (deselect(vi)) {
- redraw();
- return;
- }
-
- // otherwise add it.
- mSelections.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory));
- fireSelectionChanged();
- redraw();
- }
-
- } else if (isAlt) {
- // Case where alt is pressed: select or cycle the object pointed at.
-
- // Note: if shift and alt are pressed, shift is ignored. The alternate selection
- // mechanism does not reset the current multiple selection unless they intersect.
-
- // We need to remember the "origin" of the alternate selection, to be
- // able to continue cycling through it later. If there's no alternate selection,
- // create one. If there's one but not for the same origin object, create a new
- // one too.
- if (mAltSelection == null || mAltSelection.getOriginatingView() != vi) {
- mAltSelection = new CanvasAlternateSelection(
- vi, findAltViewInfoAt(x, y, mLastValidViewInfoRoot));
-
- // deselect them all, in case they were partially selected
- deselectAll(mAltSelection.getAltViews());
-
- // select the current one
- CanvasViewInfo vi2 = mAltSelection.getCurrent();
- if (vi2 != null) {
- mSelections.addFirst(new CanvasSelection(vi2, mRulesEngine, mNodeFactory));
- fireSelectionChanged();
- }
- } else {
- // We're trying to cycle through the current alternate selection.
- // First remove the current object.
- CanvasViewInfo vi2 = mAltSelection.getCurrent();
- deselect(vi2);
-
- // Now select the next one.
- vi2 = mAltSelection.getNext();
- if (vi2 != null) {
- mSelections.addFirst(new CanvasSelection(vi2, mRulesEngine, mNodeFactory));
- fireSelectionChanged();
- }
- }
- redraw();
-
- } else {
- // Case where no modifier is pressed: either select or reset the selection.
-
- selectSingle(vi);
- }
- }
-
- /**
- * Removes all the currently selected item and only select the given item.
- * Issues a {@link #redraw()} if the selection changes.
- *
- * @param vi The new selected item if non-null. Selection becomes empty if null.
- */
- private void selectSingle(CanvasViewInfo vi) {
- // reset alternate selection if any
- mAltSelection = null;
-
- // reset (multi)selection if any
- if (!mSelections.isEmpty()) {
- if (mSelections.size() == 1 && mSelections.getFirst().getViewInfo() == vi) {
- // CanvasSelection remains the same, don't touch it.
- return;
- }
- mSelections.clear();
- }
-
- if (vi != null) {
- mSelections.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory));
- }
- fireSelectionChanged();
- redraw();
- }
-
- /**
- * Selects the given set of {@link CanvasViewInfo}s. This is similar to
- * {@link #selectSingle} but allows you to make a multi-selection. Issues a
- * {@link #redraw()}.
- *
- * @param viewInfos A collection of {@link CanvasViewInfo} objects to be
- * selected, or null or empty to clear the selection.
- */
- /* package */ void selectMultiple(Collection<CanvasViewInfo> viewInfos) {
- // reset alternate selection if any
- mAltSelection = null;
-
- mSelections.clear();
- if (viewInfos != null) {
- for (CanvasViewInfo viewInfo : viewInfos) {
- mSelections.add(new CanvasSelection(viewInfo, mRulesEngine, mNodeFactory));
- }
- }
-
- fireSelectionChanged();
- redraw();
- }
-
- /**
- * Select the visual element corresponding to the given XML node
- * @param xmlNode The Node whose element we want to select
- */
- public void select(Node xmlNode) {
- CanvasViewInfo vi = findViewInfoFor(xmlNode);
- if (vi != null) {
- // Select the visual element -- unless it's the root.
- // The root element is the one whose GRAND parent
- // is null (because the parent will be a -document-
- // node).
- UiViewElementNode key = vi.getUiViewKey();
- if (key != null && key.getUiParent() != null &&
- key.getUiParent().getUiParent() != null) {
- selectSingle(vi);
- }
- }
- }
-
- /**
- * Deselects a view info.
- * Returns true if the object was actually selected.
- * Callers are responsible for calling redraw() and updateOulineSelection() after.
- */
- private boolean deselect(CanvasViewInfo canvasViewInfo) {
- if (canvasViewInfo == null) {
- return false;
- }
-
- for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
- CanvasSelection s = it.next();
- if (canvasViewInfo == s.getViewInfo()) {
- it.remove();
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Deselects multiple view infos.
- * Callers are responsible for calling redraw() and updateOulineSelection() after.
- */
- private void deselectAll(List<CanvasViewInfo> canvasViewInfos) {
- for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
- CanvasSelection s = it.next();
- if (canvasViewInfos.contains(s.getViewInfo())) {
- it.remove();
- }
- }
- }
-
- private void onDoubleClick(MouseEvent e) {
- // Warp to the text editor and show the corresponding XML for the
- // double-clicked widget
- int x = mHScale.inverseTranslate(e.x);
- int y = mVScale.inverseTranslate(e.y);
- CanvasViewInfo vi = findViewInfoAt(x, y);
- if (vi == null) {
- return;
- }
-
- Node xmlNode = vi.getXmlNode();
- if (xmlNode != null) {
- boolean found = mLayoutEditor.show(xmlNode);
- if (!found) {
- getDisplay().beep();
- }
- }
- }
-
- /**
- * Tries to find a child with the same view key in the view info sub-tree.
- * Returns null if not found.
- */
- private CanvasViewInfo findViewInfoKey(Object viewKey, CanvasViewInfo canvasViewInfo) {
- if (canvasViewInfo == null) {
- return null;
- }
- if (canvasViewInfo.getUiViewKey() == viewKey) {
- return canvasViewInfo;
- }
-
- // try to find a matching child
- for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
- CanvasViewInfo v = findViewInfoKey(viewKey, child);
- if (v != null) {
- return v;
- }
- }
-
- return null;
- }
-
- /**
- * Locates and returns the {@link CanvasViewInfo} corresponding to the given
- * node, or null if it cannot be found.
- *
- * @param node The node we want to find a corresponding
- * {@link CanvasViewInfo} for.
- * @return The {@link CanvasViewInfo} corresponding to the given node, or
- * null if no match was found.
- */
- /* package */ CanvasViewInfo findViewInfoFor(Node node) {
- if (mLastValidViewInfoRoot != null) {
- return findViewInfoForNode(node, mLastValidViewInfoRoot);
- } else {
- return null;
- }
- }
-
- /**
- * Tries to find a child with the same view XML node in the view info sub-tree.
- * Returns null if not found.
- */
- private CanvasViewInfo findViewInfoForNode(Node xmlNode, CanvasViewInfo canvasViewInfo) {
- if (canvasViewInfo == null) {
- return null;
- }
- if (canvasViewInfo.getXmlNode() == xmlNode) {
- return canvasViewInfo;
- }
-
- // Try to find a matching child
- for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
- CanvasViewInfo v = findViewInfoForNode(xmlNode, child);
- if (v != null) {
- return v;
- }
- }
-
- return null;
- }
-
-
- /**
- * Tries to find the inner most child matching the given x,y coordinates
- * in the view info sub-tree, starting at the last know view info root.
- * This uses the potentially-expanded selection bounds.
- * <p/>
- * Returns null if not found or if there's no view info root.
- */
- /* package */ CanvasViewInfo findViewInfoAt(int x, int y) {
- if (mLastValidViewInfoRoot == null) {
- return null;
- } else {
- return findViewInfoAt_Recursive(x, y, mLastValidViewInfoRoot);
- }
- }
-
- /**
- * Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
- * <p/>
- * Tries to find the inner most child matching the given x,y coordinates in the view
- * info sub-tree. This uses the potentially-expanded selection bounds.
- *
- * Returns null if not found.
- */
- private CanvasViewInfo findViewInfoAt_Recursive(int x, int y, CanvasViewInfo canvasViewInfo) {
- if (canvasViewInfo == null) {
- return null;
- }
- Rectangle r = canvasViewInfo.getSelectionRect();
- if (r.contains(x, y)) {
-
- // try to find a matching child first
- for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
- CanvasViewInfo v = findViewInfoAt_Recursive(x, y, child);
- if (v != null) {
- return v;
- }
- }
-
- // if no children matched, this is the view that we're looking for
- return canvasViewInfo;
- }
-
- return null;
- }
-
- /**
- * Returns a list of all the possible alternatives for a given view at the given
- * position. This is used to build and manage the "alternate" selection that cycles
- * around the parents or children of the currently selected element.
- */
- private List<CanvasViewInfo> findAltViewInfoAt(int x, int y, CanvasViewInfo parent) {
- return findAltViewInfoAt_Recursive(x, y, parent, null);
- }
-
- /**
- * Internal recursive version of {@link #findAltViewInfoAt(int, int, CanvasViewInfo)}.
- * Please don't use directly.
- */
- private List<CanvasViewInfo> findAltViewInfoAt_Recursive(
- int x, int y, CanvasViewInfo parent, List<CanvasViewInfo> outList) {
- Rectangle r;
-
- if (outList == null) {
- outList = new ArrayList<CanvasViewInfo>();
-
- if (parent != null) {
- // add the parent root only once
- r = parent.getSelectionRect();
- if (r.contains(x, y)) {
- outList.add(parent);
- }
- }
- }
-
- if (parent != null && !parent.getChildren().isEmpty()) {
- // then add all children that match the position
- for (CanvasViewInfo child : parent.getChildren()) {
- r = child.getSelectionRect();
- if (r.contains(x, y)) {
- outList.add(child);
- }
- }
-
- // finally recurse in the children
- for (CanvasViewInfo child : parent.getChildren()) {
- r = child.getSelectionRect();
- if (r.contains(x, y)) {
- findAltViewInfoAt_Recursive(x, y, child, outList);
- }
- }
- }
-
- return outList;
- }
-
- /**
- * Locates and returns the {@link CanvasViewInfo} corresponding to the given
- * node, or null if it cannot be found.
- *
- * @param node The node we want to find a corresponding
- * {@link CanvasViewInfo} for.
- * @return The {@link CanvasViewInfo} corresponding to the given node, or
- * null if no match was found.
- */
- /* package */ CanvasViewInfo findViewInfoFor(INode node) {
- if (mLastValidViewInfoRoot != null && node instanceof NodeProxy) {
- return findViewInfoKey(((NodeProxy) node).getNode(), mLastValidViewInfoRoot);
- } else {
- return null;
- }
- }
-
- /**
- * Used by {@link #onSelectAll()} to add all current view infos to the selection list.
- *
- * @param canvasViewInfo The root to add. This info and all its children will be added to the
- * selection list.
- */
- private void selectAllViewInfos(CanvasViewInfo canvasViewInfo) {
- if (canvasViewInfo != null) {
- mSelections.add(new CanvasSelection(canvasViewInfo, mRulesEngine, mNodeFactory));
- for (CanvasViewInfo vi : canvasViewInfo.getChildren()) {
- selectAllViewInfos(vi);
- }
- }
- }
-
- /**
- * Notifies listeners that the selection has changed.
- */
- private void fireSelectionChanged() {
- if (mInsideUpdateSelection) {
- return;
- }
- try {
- mInsideUpdateSelection = true;
-
- final SelectionChangedEvent event = new SelectionChangedEvent(this, getSelection());
-
- SafeRunnable.run(new SafeRunnable() {
- public void run() {
- for (Object listener : mSelectionListeners.getListeners()) {
- ((ISelectionChangedListener)listener).selectionChanged(event);
- }
- }
- });
-
- // Update menu actions that depend on the selection
- updateMenuActions();
-
- } finally {
- mInsideUpdateSelection = false;
- }
- }
-
-
- //---------------
-
- /**
- * Helper to create the drag source for the given control.
- * <p/>
- * This is static with package-access so that {@link OutlinePage2} can also
- * create an exact copy of the source with the same attributes.
- */
- /* package */ static DragSource createDragSource(
- Control control,
- DragSourceListener dragSourceListener) {
- DragSource source = new DragSource(control, DND.DROP_COPY | DND.DROP_MOVE);
- source.setTransfer(new Transfer[] {
- TextTransfer.getInstance(),
- SimpleXmlTransfer.getInstance()
- } );
- source.addDragListener(dragSourceListener);
- return source;
- }
-
- /**
- * Helper to create the drop target for the given control.
- * <p/>
- * This is static with package-access so that {@link OutlinePage2} can also
- * create an exact copy of the drop target with the same attributes.
- */
- /* package */ static DropTarget createDropTarget(
- Control control,
- DropTargetListener dropListener) {
- DropTarget dropTarget = new DropTarget(
- control, DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_DEFAULT);
- dropTarget.setTransfer(new Transfer[] { SimpleXmlTransfer.getInstance() } );
- dropTarget.addDropListener(dropListener);
- return dropTarget;
- }
-
- /**
- * Our canvas {@link DragSourceListener}. Handles drag being started and finished
- * and generating the drag data.
- */
- private class CanvasDragSourceListener implements DragSourceListener {
-
- /**
- * The current selection being dragged.
- * This may be a subset of the canvas selection due to the "sanitize" pass.
- * Can be empty but never null.
- */
- private final ArrayList<CanvasSelection> mDragSelection = new ArrayList<CanvasSelection>();
- private SimpleElement[] mDragElements;
-
- /**
- * The user has begun the actions required to drag the widget.
- * <p/>
- * Initiate a drag only if there is one or more item selected.
- * If there's none, try to auto-select the one under the cursor.
- *
- * {@inheritDoc}
- */
- public void dragStart(DragSourceEvent e) {
- // We need a selection (simple or multiple) to do any transfer.
- // If there's a selection *and* the cursor is over this selection, use all the
- // currently selected elements.
- // If there is no selection or the cursor is not over a selected element, *change*
- // the selection to match the element under the cursor and use that.
- // If nothing can be selected, abort the drag operation.
-
- mDragSelection.clear();
-
- if (!mSelections.isEmpty()) {
- // Is the cursor on top of a selected element?
- int x = mHScale.inverseTranslate(e.x);
- int y = mVScale.inverseTranslate(e.y);
-
- boolean insideSelection = false;
-
- for (CanvasSelection cs : mSelections) {
- if (!cs.isRoot() && cs.getRect().contains(x, y)) {
- insideSelection = true;
- break;
- }
- }
-
- if (!insideSelection) {
- CanvasViewInfo vi = findViewInfoAt(x, y);
- if (vi != null) {
- selectSingle(vi);
- insideSelection = true;
- }
- }
-
- if (insideSelection) {
- // We should now have a proper selection that matches the cursor.
- // Let's use this one. We make a copy of it since the "sanitize" pass
- // below might remove some of the selected objects.
- if (mSelections.size() == 1) {
- // You are dragging just one element - this might or might not be
- // the root, but if it's the root that is fine since we will let you
- // drag the root if it is the only thing you are dragging.
- mDragSelection.addAll(mSelections);
- } else {
- // Only drag non-root items.
- for (CanvasSelection cs : mSelections) {
- if (!cs.isRoot()) {
- mDragSelection.add(cs);
- }
- }
- }
- }
- }
-
- // If you are dragging a non-selected item, select it
- if (mDragSelection.isEmpty()) {
- int x = mHScale.inverseTranslate(e.x);
- int y = mVScale.inverseTranslate(e.y);
- CanvasViewInfo vi = findViewInfoAt(x, y);
- if (vi != null) {
- selectSingle(vi);
- mDragSelection.addAll(mSelections);
- }
- }
-
- sanitizeSelection(mDragSelection);
-
- e.doit = !mDragSelection.isEmpty();
- if (e.doit) {
- mDragElements = getSelectionAsElements(mDragSelection);
- GlobalCanvasDragInfo.getInstance().startDrag(
- mDragElements,
- mDragSelection.toArray(new CanvasSelection[mDragSelection.size()]),
- LayoutCanvas.this,
- new Runnable() {
- public void run() {
- deleteSelection("Remove", mDragSelection);
- }
- }
- );
- }
- }
-
- /**
- * Callback invoked when data is needed for the event, typically right before drop.
- * The drop side decides what type of transfer to use and this side must now provide
- * the adequate data.
- *
- * {@inheritDoc}
- */
- public void dragSetData(DragSourceEvent e) {
- if (TextTransfer.getInstance().isSupportedType(e.dataType)) {
- e.data = getSelectionAsText(mDragSelection);
- return;
- }
-
- if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) {
- e.data = mDragElements;
- return;
- }
-
- // otherwise we failed
- e.detail = DND.DROP_NONE;
- e.doit = false;
- }
-
- /**
- * Callback invoked when the drop has been finished either way.
- * On a successful move, remove the originating elements.
- */
- public void dragFinished(DragSourceEvent e) {
- // Clear the selection
- mDragSelection.clear();
- mDragElements = null;
- GlobalCanvasDragInfo.getInstance().stopDrag();
- }
- }
-
- /**
- * Sanitizes the selection for a copy/cut or drag operation.
- * <p/>
- * Sanitizes the list to make sure all elements have a valid XML attached to it,
- * that is remove element that have no XML to avoid having to make repeated such
- * checks in various places after.
- * <p/>
- * In case of multiple selection, we also need to remove all children when their
- * parent is already selected since parents will always be added with all their
- * children.
- * <p/>
- *
- * @param selection The selection list to be sanitized <b>in-place</b>.
- * The <code>selection</code> argument should not be {@link #mSelections} -- the
- * given list is going to be altered and we should never alter the user-made selection.
- * Instead the caller should provide its own copy.
- */
- private void sanitizeSelection(List<CanvasSelection> selection) {
- if (selection.isEmpty()) {
- return;
- }
-
- for (Iterator<CanvasSelection> it = selection.iterator(); it.hasNext(); ) {
- CanvasSelection cs = it.next();
- CanvasViewInfo vi = cs.getViewInfo();
- UiViewElementNode key = vi == null ? null : vi.getUiViewKey();
- Node node = key == null ? null : key.getXmlNode();
- if (node == null) {
- // Missing ViewInfo or view key or XML, discard this.
- it.remove();
- continue;
- }
-
- if (vi != null) {
- for (Iterator<CanvasSelection> it2 = selection.iterator();
- it2.hasNext(); ) {
- CanvasSelection cs2 = it2.next();
- if (cs != cs2) {
- CanvasViewInfo vi2 = cs2.getViewInfo();
- if (vi.isParent(vi2)) {
- // vi2 is a parent for vi. Remove vi.
- it.remove();
- break;
- }
- }
- }
- }
- }
- }
-
- /**
- * Get the XML text from the given selection for a text transfer.
- * The returned string can be empty but not null.
- */
- private String getSelectionAsText(List<CanvasSelection> selection) {
- StringBuilder sb = new StringBuilder();
-
- for (CanvasSelection cs : selection) {
- CanvasViewInfo vi = cs.getViewInfo();
- UiViewElementNode key = vi.getUiViewKey();
- Node node = key.getXmlNode();
- String t = getXmlTextFromEditor(mLayoutEditor, node);
- if (t != null) {
- if (sb.length() > 0) {
- sb.append('\n');
- }
- sb.append(t);
- }
- }
-
- return sb.toString();
- }
-
- /**
- * Get the XML text directly from the editor.
- */
- private String getXmlTextFromEditor(AndroidXmlEditor editor, Node xml_node) {
- String data = null;
- IStructuredModel model = editor.getModelForRead();
- try {
- IStructuredDocument sse_doc = editor.getStructuredDocument();
- if (xml_node instanceof NodeContainer) {
- // The easy way to get the source of an SSE XML node.
- data = ((NodeContainer) xml_node).getSource();
- } else if (xml_node instanceof IndexedRegion && sse_doc != null) {
- // Try harder.
- IndexedRegion region = (IndexedRegion) xml_node;
- int start = region.getStartOffset();
- int end = region.getEndOffset();
-
- if (end > start) {
- data = sse_doc.get(start, end - start);
- }
- }
- } catch (BadLocationException e) {
- // the region offset was invalid. ignore.
- } finally {
- model.releaseFromRead();
- }
- return data;
- }
-
- private SimpleElement[] getSelectionAsElements(List<CanvasSelection> mDragSelection) {
- ArrayList<SimpleElement> elements = new ArrayList<SimpleElement>();
-
- for (CanvasSelection cs : mDragSelection) {
- CanvasViewInfo vi = cs.getViewInfo();
-
- SimpleElement e = transformToSimpleElement(vi);
- elements.add(e);
- }
-
- return elements.toArray(new SimpleElement[elements.size()]);
- }
-
- private SimpleElement transformToSimpleElement(CanvasViewInfo vi) {
-
- UiViewElementNode uiNode = vi.getUiViewKey();
-
- String fqcn = SimpleXmlTransfer.getFqcn(uiNode.getDescriptor());
- String parentFqcn = null;
- Rect bounds = new Rect(vi.getAbsRect());
- Rect parentBounds = null;
-
- UiElementNode uiParent = uiNode.getUiParent();
- if (uiParent != null) {
- parentFqcn = SimpleXmlTransfer.getFqcn(uiParent.getDescriptor());
- }
- if (vi.getParent() != null) {
- parentBounds = new Rect(vi.getParent().getAbsRect());
- }
-
- SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds);
-
- for (UiAttributeNode attr : uiNode.getUiAttributes()) {
- String value = attr.getCurrentValue();
- if (value != null && value.length() > 0) {
- AttributeDescriptor attrDesc = attr.getDescriptor();
- SimpleAttribute a = new SimpleAttribute(
- attrDesc.getNamespaceUri(),
- attrDesc.getXmlLocalName(),
- value);
- e.addAttribute(a);
- }
- }
-
- for (CanvasViewInfo childVi : vi.getChildren()) {
- SimpleElement e2 = transformToSimpleElement(childVi);
- if (e2 != null) {
- e.addInnerElement(e2);
- }
- }
-
- return e;
- }
-
- //---------------
-
- /**
- * Invoked by the constructor to add our cut/copy/paste/delete/select-all
- * handlers in the global action handlers of this editor's site.
- * <p/>
- * This will enable the menu items under the global Edit menu and make them
- * invoke our actions as needed. As a benefit, the corresponding shortcut
- * accelerators will do what one would expect.
- */
- private void setupGlobalActionHandlers() {
- // Get the global action bar for this editor (i.e. the menu bar)
- IActionBars actionBars = mLayoutEditor.getEditorSite().getActionBars();
+ private void setupGlobalActionHandlers() {
+ // Get the global action bar for this editor (i.e. the menu bar)
+ IActionBars actionBars = mLayoutEditor.getEditorSite().getActionBars();
TextActionHandler tah = new TextActionHandler(actionBars);
mCutAction = new Action() {
@Override
public void run() {
- cutSelectionToClipboard(new ArrayList<CanvasSelection>(mSelections));
+ mClipboardSupport.cutSelectionToClipboard(mSelectionManager.getSnapshot());
}
};
mCopyAction = new Action() {
@Override
public void run() {
- copySelectionToClipboard(new ArrayList<CanvasSelection>(mSelections));
+ mClipboardSupport.copySelectionToClipboard(mSelectionManager.getSnapshot());
}
};
mPasteAction = new Action() {
@Override
public void run() {
- pasteSelection(new ArrayList<CanvasSelection>(mSelections));
+ mClipboardSupport.pasteSelection(mSelectionManager.getSnapshot());
}
};
mDeleteAction = new Action() {
@Override
public void run() {
- deleteSelection(
- mDeleteAction.getText(), // verb "Delete" from the DELETE action's title
- new ArrayList<CanvasSelection>(mSelections));
+ mClipboardSupport.deleteSelection(
+ getDeleteLabel(),
+ mSelectionManager.getSnapshot());
}
};
mSelectAllAction = new Action() {
@Override
public void run() {
- onSelectAll();
+ mSelectionManager.selectAll();
}
};
copyActionAttributes(mSelectAllAction, ActionFactory.SELECT_ALL);
}
- /** Update menu actions that depends on the selection. */
- private void updateMenuActions() {
+ /* package */ String getCutLabel() {
+ return mCutAction.getText();
+ }
- boolean hasSelection = !mSelections.isEmpty();
+ /* package */ String getDeleteLabel() {
+ // verb "Delete" from the DELETE action's title
+ return mDeleteAction.getText();
+ }
+ /**
+ * Updates menu actions that depends on the selection.
+ *
+ * @param hasSelection True iff we have a non-empty selection
+ */
+ /* package */ void updateMenuActions(boolean hasSelection) {
mCutAction.setEnabled(hasSelection);
mCopyAction.setEnabled(hasSelection);
mDeleteAction.setEnabled(hasSelection);
// The paste operation is only available if we can paste our custom type.
// We do not currently support pasting random text (e.g. XML). Maybe later.
- SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
- boolean hasSxt = false;
- for (TransferData td : mClipboard.getAvailableTypes()) {
- if (sxt.isSupportedType(td)) {
- hasSxt = true;
- break;
- }
- }
+ boolean hasSxt = mClipboardSupport.hasSxtOnClipboard();
mPasteAction.setEnabled(hasSxt);
}
// Create a "Show In" sub-menu and automatically populate it using standard
// actions contributed by the workbench.
String showInLabel = IDEWorkbenchMessages.Workbench_showIn;
- MenuManager showInSubMenu= new MenuManager(showInLabel);
+ MenuManager showInSubMenu = new MenuManager(showInLabel);
showInSubMenu.add(
ContributionItemFactory.VIEWS_SHOW_IN.create(
mLayoutEditor.getSite().getWorkbenchWindow()));
}
/**
- * Invoked by {@link #mSelectAllAction}. It clears the selection and then
- * selects everything (all views and all their children).
- */
- private void onSelectAll() {
- // First clear the current selection, if any.
- mSelections.clear();
- mAltSelection = null;
-
- // Now select everything if there's a valid layout
- if (mIsResultValid && mLastValidViewInfoRoot != null) {
- selectAllViewInfos(mLastValidViewInfoRoot);
- redraw();
- }
-
- fireSelectionChanged();
- }
-
- /**
- * Perform the "Copy" action, either from the Edit menu or from the context menu.
- * Invoked by {@link #mCopyAction}.
- * <p/>
- * This sanitizes the selection, so it must be a copy. It then inserts the selection
- * both as text and as {@link SimpleElement}s in the clipboard.
- */
- private void copySelectionToClipboard(List<CanvasSelection> selection) {
- sanitizeSelection(selection);
-
- if (selection.isEmpty()) {
- return;
- }
-
- Object[] data = new Object[] {
- getSelectionAsElements(selection),
- getSelectionAsText(selection)
- };
-
- Transfer[] types = new Transfer[] {
- SimpleXmlTransfer.getInstance(),
- TextTransfer.getInstance()
- };
-
- mClipboard.setContents(data, types);
- }
-
- /**
- * Perform the "Cut" action, either from the Edit menu or from the context menu.
- * Invoked by {@link #mCutAction}.
- * <p/>
- * This sanitizes the selection, so it must be a copy.
- * It uses the {@link #copySelectionToClipboard(List)} method to copy the selection
- * to the clipboard.
- * Finally it uses {@link #deleteSelection(String, List)} to delete the selection
- * with a "Cut" verb for the title.
+ * Deletes the selection. Equivalent to pressing the Delete key.
*/
- private void cutSelectionToClipboard(List<CanvasSelection> selection) {
- copySelectionToClipboard(selection);
- deleteSelection(
- mCutAction.getText(), // verb "Cut" from the CUT action's title
- selection);
- }
-
- /**
- * Deletes the given selection.
- * <p/>
- * This can either be invoked directly by {@link #mDeleteAction}, or as
- * an implementation detail as part of {@link #mCutAction} or also when removing
- * the elements after a successful "MOVE" drag'n'drop.
- *
- * @param verb A translated verb for the action. Will be used for the undo/redo title.
- * Typically this should be {@link Action#getText()} for either
- * {@link #mCutAction} or {@link #mDeleteAction}.
- * @param selection The selection. Must not be null. Can be empty, in which case nothing
- * happens. The selection list will be sanitized so the caller should give a copy of
- * {@link #mSelections}, directly or indirectly.
- */
- private void deleteSelection(String verb, final List<CanvasSelection> selection) {
- sanitizeSelection(selection);
-
- if (selection.isEmpty()) {
- return;
- }
-
- // If all selected items have the same *kind* of parent, display that in the undo title.
- String title = null;
- for (CanvasSelection cs : selection) {
- CanvasViewInfo vi = cs.getViewInfo();
- if (vi != null && vi.getParent() != null) {
- if (title == null) {
- title = vi.getParent().getName();
- } else if (!title.equals(vi.getParent().getName())) {
- // More than one kind of parent selected.
- title = null;
- break;
- }
- }
- }
-
- if (title != null) {
- // Typically the name is an FQCN. Just get the last segment.
- int pos = title.lastIndexOf('.');
- if (pos > 0 && pos < title.length() - 1) {
- title = title.substring(pos + 1);
- }
- }
- boolean multiple = mSelections.size() > 1;
- if (title == null) {
- title = String.format(
- multiple ? "%1$s elements" : "%1$s element",
- verb);
- } else {
- title = String.format(
- multiple ? "%1$s elements from %2$s" : "%1$s element from %2$s",
- verb, title);
- }
-
- // Implementation note: we don't clear the internal selection after removing
- // the elements. An update XML model event should happen when the model gets released
- // which will trigger a recompute of the layout, thus reloading the model thus
- // resetting the selection.
- mLayoutEditor.wrapUndoEditXmlModel(title, new Runnable() {
- public void run() {
- for (CanvasSelection cs : selection) {
- CanvasViewInfo vi = cs.getViewInfo();
- if (vi != null) {
- UiViewElementNode ui = vi.getUiViewKey();
- if (ui != null) {
- ui.deleteXmlNode();
- }
- }
- }
- }
- });
- }
-
- /**
- * Perform the "Paste" action, either from the Edit menu or from the context menu.
- */
- private void pasteSelection(List<CanvasSelection> selection) {
-
- SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
- SimpleElement[] pasted = (SimpleElement[]) mClipboard.getContents(sxt);
-
- if (pasted == null || pasted.length == 0) {
- return;
- }
-
- if (mLastValidViewInfoRoot == null) {
- // Pasting in an empty document. Only paste the first element.
- pasteInEmptyDocument(pasted[0]);
- return;
- }
-
- // Otherwise use the current selection, if any, as a guide where to paste
- // using the first selected element only. If there's no selection use
- // the root as the insertion point.
- sanitizeSelection(selection);
- CanvasViewInfo target = mLastValidViewInfoRoot;
- if (selection.size() > 0) {
- CanvasSelection cs = selection.get(0);
- target = cs.getViewInfo();
- }
-
- NodeProxy targetNode = mNodeFactory.create(target);
-
- getRulesEngine().callOnPaste(targetNode, pasted);
- }
-
- /**
- * Paste a new root into an empty XML layout.
- * <p/>
- * In case of error (unknown FQCN, document not empty), silently do nothing.
- * In case of success, the new element will have some default attributes set (xmlns:android,
- * layout_width and height). The edit is wrapped in a proper undo.
- * <p/>
- * Implementation is similar to {@link #createDocumentRoot(String)} except we also
- * copy all the attributes and inner elements recursively.
- */
- private void pasteInEmptyDocument(final IDragElement pastedElement) {
- String rootFqcn = pastedElement.getFqcn();
-
- // Need a valid empty document to create the new root
- final UiDocumentNode uiDoc = mLayoutEditor.getUiRootNode();
- if (uiDoc == null || uiDoc.getUiChildren().size() > 0) {
- debugPrintf("Failed to paste document root for %1$s: document is not empty", rootFqcn);
- return;
- }
-
- // Find the view descriptor matching our FQCN
- final ViewElementDescriptor viewDesc = mLayoutEditor.getFqcnViewDescritor(rootFqcn);
- if (viewDesc == null) {
- // TODO this could happen if pasting a custom view not known in this project
- debugPrintf("Failed to paste document root, unknown FQCN %1$s", rootFqcn);
- return;
- }
-
- // Get the last segment of the FQCN for the undo title
- String title = rootFqcn;
- int pos = title.lastIndexOf('.');
- if (pos > 0 && pos < title.length() - 1) {
- title = title.substring(pos + 1);
- }
- title = String.format("Paste root %1$s in document", title);
-
- mLayoutEditor.wrapUndoEditXmlModel(title, new Runnable() {
- public void run() {
- UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc);
-
- // A root node requires the Android XMLNS
- uiNew.setAttributeValue(
- "android",
- XmlnsAttributeDescriptor.XMLNS_URI,
- SdkConstants.NS_RESOURCES,
- true /*override*/);
-
- // Copy all the attributes from the pasted element
- for (IDragAttribute attr : pastedElement.getAttributes()) {
- uiNew.setAttributeValue(
- attr.getName(),
- attr.getUri(),
- attr.getValue(),
- true /*override*/);
- }
-
- // Adjust the attributes, adding the default layout_width/height
- // only if they are not present (the original element should have
- // them though.)
- DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);
-
- uiNew.createXmlNode();
-
- // Now process all children
- for (IDragElement childElement : pastedElement.getInnerElements()) {
- addChild(uiNew, childElement);
- }
- }
-
- private void addChild(UiElementNode uiParent, IDragElement childElement) {
- String childFqcn = childElement.getFqcn();
- final ViewElementDescriptor childDesc =
- mLayoutEditor.getFqcnViewDescritor(childFqcn);
- if (childDesc == null) {
- // TODO this could happen if pasting a custom view
- debugPrintf("Failed to paste element, unknown FQCN %1$s", childFqcn);
- return;
- }
-
- UiElementNode uiChild = uiParent.appendNewUiChild(childDesc);
-
- // Copy all the attributes from the pasted element
- for (IDragAttribute attr : childElement.getAttributes()) {
- uiChild.setAttributeValue(
- attr.getName(),
- attr.getUri(),
- attr.getValue(),
- true /*override*/);
- }
-
- // Adjust the attributes, adding the default layout_width/height
- // only if they are not present (the original element should have
- // them though.)
- DescriptorsUtils.setDefaultLayoutAttributes(
- uiChild, false /*updateLayout*/);
-
- uiChild.createXmlNode();
-
- // Now process all grand children
- for (IDragElement grandChildElement : childElement.getInnerElements()) {
- addChild(uiChild, grandChildElement);
- }
- }
- });
+ /* package */ void delete() {
+ mDeleteAction.run();
}
/**
* Add new root in an existing empty XML layout.
* <p/>
* In case of error (unknown FQCN, document not empty), silently do nothing.
- * In case of success, the new element will have some default attributes set (xmlns:android,
- * layout_width and height). The edit is wrapped in a proper undo.
+ * In case of success, the new element will have some default attributes set
+ * (xmlns:android, layout_width and height). The edit is wrapped in a proper
+ * undo.
* <p/>
- * This is invoked by {@link CanvasDropListener#drop(org.eclipse.swt.dnd.DropTargetEvent)}.
+ * This is invoked by
+ * {@link MoveGesture#drop(org.eclipse.swt.dnd.DropTargetEvent)}.
*
- * @param rootFqcn A non-null non-empty FQCN that must match an existing {@link ViewDescriptor}
- * to add as root to the current empty XML document.
+ * @param rootFqcn A non-null non-empty FQCN that must match an existing
+ * {@link ViewElementDescriptor} to add as root to the current
+ * empty XML document.
*/
/* package */ void createDocumentRoot(String rootFqcn) {
}
// Find the view descriptor matching our FQCN
- final ViewElementDescriptor viewDesc = mLayoutEditor.getFqcnViewDescritor(rootFqcn);
+ final ViewElementDescriptor viewDesc = mLayoutEditor.getFqcnViewDescriptor(rootFqcn);
if (viewDesc == null) {
// TODO this could happen if dropping a custom view not known in this project
debugPrintf("Failed to add document root, unknown FQCN %1$s", rootFqcn);
}
private void debugPrintf(String message, Object... params) {
- if (DEBUG) AdtPlugin.printToConsole("Canvas", String.format(message, params));
+ if (DEBUG) {
+ AdtPlugin.printToConsole("Canvas", String.format(message, params));
+ }
}
}
mLayoutEditor = layoutEditor;
mCanvas = new LayoutCanvas(layoutEditor, rulesEngine, parent, style);
- mCanvas.addSelectionChangedListener(new ISelectionChangedListener() {
- public void selectionChanged(SelectionChangedEvent event) {
- fireSelectionChanged(event);
- }
- });
+ mCanvas.getSelectionManager().addSelectionChangedListener(mSelectionListener);
}
+ private ISelectionChangedListener mSelectionListener = new ISelectionChangedListener() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ fireSelectionChanged(event);
+ }
+ };
+
@Override
public Control getControl() {
return mCanvas;
* have it already casted in the right type.
* <p/>
* This can never be null.
+ * @return The underlying {@link LayoutCanvas}.
*/
public LayoutCanvas getCanvas() {
return mCanvas;
*/
@Override
public ISelection getSelection() {
- return mCanvas.getSelection();
+ return mCanvas.getSelectionManager().getSelection();
}
/**
*/
@Override
public void setSelection(ISelection selection, boolean reveal) {
- mCanvas.setSelection(selection);
+ mCanvas.getSelectionManager().setSelection(selection);
}
/** Unused. Refreshing is done solely by the owning {@link LayoutEditor}. */
mCanvas.dispose();
mCanvas = null;
}
+ if (mSelectionListener != null) {
+ mCanvas.getSelectionManager().removeSelectionChangedListener(mSelectionListener);
+ }
}
}
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+
+/**
+ * A {@link LayoutPoint} is a coordinate in the Android canvas (in other words,
+ * it may differ from the canvas control mouse coordinate because the canvas may
+ * be zoomed and scrolled.)
+ */
+public final class LayoutPoint {
+ /** Containing canvas which the point is relative to. */
+ private final LayoutCanvas mCanvas;
+
+ /** The X coordinate of the canvas coordinate. */
+ public final int x;
+
+ /** The Y coordinate of the canvas coordinate. */
+ public final int y;
+
+ /**
+ * Constructs a new {@link LayoutPoint} from the given event. The event
+ * must be from a {@link MouseListener} associated with the
+ * {@link LayoutCanvas} such that the {@link MouseEvent#x} and
+ * {@link MouseEvent#y} fields are relative to the canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The mouse event to construct the {@link LayoutPoint}
+ * from.
+ * @return A {@link LayoutPoint} which corresponds to the given
+ * {@link MouseEvent}.
+ */
+ public static LayoutPoint create(LayoutCanvas canvas, MouseEvent event) {
+ // The mouse event coordinates should already be relative to the canvas
+ // widget.
+ assert event.widget == canvas : event.widget;
+ return ControlPoint.create(canvas, event).toLayout();
+ }
+
+ /**
+ * Constructs a new {@link LayoutPoint} from the given event. The event
+ * must be from a {@link DragSourceListener} associated with the
+ * {@link LayoutCanvas} such that the {@link DragSourceEvent#x} and
+ * {@link DragSourceEvent#y} fields are relative to the canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The mouse event to construct the {@link LayoutPoint}
+ * from.
+ * @return A {@link LayoutPoint} which corresponds to the given
+ * {@link DragSourceEvent}.
+ */
+ public static LayoutPoint create(LayoutCanvas canvas, DragSourceEvent event) {
+ // The drag source event coordinates should already be relative to the
+ // canvas widget.
+ return ControlPoint.create(canvas, event).toLayout();
+ }
+
+ /**
+ * Constructs a new {@link LayoutPoint} from the given x,y coordinates.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param x The mouse event x coordinate relative to the canvas
+ * @param y The mouse event x coordinate relative to the canvas
+ * @return A {@link LayoutPoint} which corresponds to the given
+ * layout coordinates.
+ */
+ public static LayoutPoint create(LayoutCanvas canvas, int x, int y) {
+ return new LayoutPoint(canvas, x, y);
+ }
+
+ /**
+ * Constructs a new {@link LayoutPoint} with the given X and Y coordinates.
+ *
+ * @param canvas The canvas which contains this coordinate
+ * @param x The canvas X coordinate
+ * @param y The canvas Y coordinate
+ */
+ private LayoutPoint(LayoutCanvas canvas, int x, int y) {
+ this.mCanvas = canvas;
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * Returns the equivalent {@link ControlPoint} to this
+ * {@link LayoutPoint}.
+ *
+ * @return The equivalent {@link ControlPoint} to this
+ * {@link LayoutPoint}
+ */
+ public ControlPoint toControl() {
+ int cx = mCanvas.getHorizontalTransform().translate(x);
+ int cy = mCanvas.getVerticalTransform().translate(y);
+
+ return ControlPoint.create(mCanvas, cx, cy);
+ }
+
+ @Override
+ public String toString() {
+ return "LayoutPoint [x=" + x + ", y=" + y + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + x;
+ result = prime * result + y;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ LayoutPoint other = (LayoutPoint) obj;
+ if (x != other.x)
+ return false;
+ if (y != other.y)
+ return false;
+ return true;
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A {@link MarqueeGesture} is a gesture for swiping out a selection rectangle.
+ * With a modifier key, items that intersect the rectangle can be toggled
+ * instead of added to the new selection set.
+ */
+public class MarqueeGesture extends Gesture {
+ /** The {@link Overlay} drawn for the marquee. */
+ private MarqueeOverlay mOverlay;
+
+ /** The canvas associated with this gesture. */
+ private LayoutCanvas mCanvas;
+
+ /** A copy of the initial selection, when we're toggling the marquee. */
+ private Collection<CanvasViewInfo> mInitialSelection;
+
+ /**
+ * Creates a new marquee selection (selection swiping).
+ *
+ * @param canvas The canvas where selection is performed.
+ * @param toggle If true, toggle the membership of contained elements
+ * instead of adding it.
+ */
+ public MarqueeGesture(LayoutCanvas canvas, boolean toggle) {
+ this.mCanvas = canvas;
+
+ if (toggle) {
+ List<CanvasSelection> selection = canvas.getSelectionManager().getSelections();
+ mInitialSelection = new ArrayList<CanvasViewInfo>(selection.size());
+ for (CanvasSelection item : selection) {
+ mInitialSelection.add(item.getViewInfo());
+ }
+ } else {
+ mInitialSelection = Collections.emptySet();
+ }
+ }
+
+ @Override
+ public void update(ControlPoint pos) {
+ int x = Math.min(pos.x, mStart.x);
+ int y = Math.min(pos.y, mStart.y);
+ int w = Math.abs(pos.x - mStart.x);
+ int h = Math.abs(pos.y - mStart.y);
+
+ mOverlay.updateSize(x, y, w, h);
+
+ // Compute selection overlaps
+ LayoutPoint topLeft = ControlPoint.create(mCanvas, x, y).toLayout();
+ LayoutPoint bottomRight = ControlPoint.create(mCanvas, x + w, y + h).toLayout();
+ mCanvas.getSelectionManager().selectWithin(topLeft, bottomRight, mInitialSelection);
+ }
+
+ @Override
+ public List<Overlay> createOverlays() {
+ mOverlay = new MarqueeOverlay();
+ return Collections.<Overlay> singletonList(mOverlay);
+ }
+
+ /**
+ * An {@link Overlay} for the {@link MarqueeGesture}; paints a selection
+ * overlay rectangle matching the mouse coordinate delta between gesture
+ * start and the current position.
+ */
+ private class MarqueeOverlay extends Overlay {
+ /** Rectangle border color. */
+ private Color mStroke;
+
+ /** Rectangle fill color. */
+ private Color mFill;
+
+ /** Current rectangle coordinates (in terms of control coordinates). */
+ private Rectangle mRectangle = new Rectangle(0, 0, 0, 0);
+
+ /** Alpha value of the fill. */
+ private int mFillAlpha;
+
+ /** Alpha value of the border. */
+ private int mStrokeAlpha;
+
+ /** Constructs a new {@link MarqueeOverlay}. */
+ public MarqueeOverlay() {
+ }
+
+ /**
+ * Updates the size of the marquee rectangle.
+ *
+ * @param x The top left corner of the rectangle, x coordinate.
+ * @param y The top left corner of the rectangle, y coordinate.
+ * @param w Rectangle width.
+ * @param h Rectangle height.
+ */
+ public void updateSize(int x, int y, int w, int h) {
+ mRectangle.x = x;
+ mRectangle.y = y;
+ mRectangle.width = w;
+ mRectangle.height = h;
+ }
+
+ @Override
+ public void create(Device device) {
+ // TODO: Integrate DrawingStyles with this?
+ mStroke = new Color(device, 255, 255, 255);
+ mFill = new Color(device, 128, 128, 128);
+ mFillAlpha = 64;
+ mStrokeAlpha = 255;
+ }
+
+ @Override
+ public void dispose() {
+ mStroke.dispose();
+ mFill.dispose();
+ }
+
+ @Override
+ public void paint(GC gc) {
+ if (mRectangle.width > 0 && mRectangle.height > 0) {
+ gc.setLineStyle(SWT.LINE_SOLID);
+ gc.setLineWidth(1);
+ gc.setForeground(mStroke);
+ gc.setBackground(mFill);
+ gc.setAlpha(mStrokeAlpha);
+ gc.drawRectangle(mRectangle);
+ gc.setAlpha(mFillAlpha);
+ gc.fillRectangle(mRectangle);
+ }
+ }
+ }
+}
/*
- * Copyright (C) 2009 The Android Open Source Project
+ * Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
import com.android.ide.common.api.DropFeedback;
import com.android.ide.common.api.Rect;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTargetEvent;
-import org.eclipse.swt.dnd.DropTargetListener;
import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.widgets.Display;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
- * Handles drop operations on top of the canvas.
- * <p/>
- * Reference for d'n'd: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html
+ * The Move gesture provides the operation for moving widgets around in the canvas.
*/
-/* package */ class CanvasDropListener implements DropTargetListener {
+public class MoveGesture extends DropGesture {
+ /** The associated {@link LayoutCanvas}. */
+ private LayoutCanvas mCanvas;
- private static final boolean DEBUG = false;
+ /** Overlay which paints the drag & drop feedback. */
+ private MoveOverlay mOverlay;
- private final LayoutCanvas mCanvas;
+ private static final boolean DEBUG = false;
/**
* The top view right under the drag'n'drop cursor.
* happens next.
*/
private NodeProxy mLeaveTargetNode;
+
/**
* @see #mLeaveTargetNode
*/
private DropFeedback mLeaveFeedback;
+
/**
* @see #mLeaveTargetNode
*/
/** Singleton used to keep track of drag selection in the same Eclipse instance. */
private final GlobalCanvasDragInfo mGlobalDragInfo;
- public CanvasDropListener(LayoutCanvas canvas) {
- mCanvas = canvas;
+ /**
+ * Constructs a new {@link MoveGesture}, tied to the given canvas.
+ *
+ * @param canvas The canvas to associate the {@link MoveGesture} with.
+ */
+ public MoveGesture(LayoutCanvas canvas) {
+ this.mCanvas = canvas;
mGlobalDragInfo = GlobalCanvasDragInfo.getInstance();
}
+ @Override
+ public List<Overlay> createOverlays() {
+ mOverlay = new MoveOverlay();
+ return Collections.<Overlay> singletonList(mOverlay);
+ }
+
/*
* The cursor has entered the drop target boundaries.
* {@inheritDoc}
*/
+ @Override
public void dragEnter(DropTargetEvent event) {
if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag enter", event);
* The operation being performed has changed (e.g. modifier key).
* {@inheritDoc}
*/
+ @Override
public void dragOperationChanged(DropTargetEvent event) {
if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag changed", event);
* The cursor has left the drop target boundaries OR data is about to be dropped.
* {@inheritDoc}
*/
+ @Override
public void dragLeave(DropTargetEvent event) {
if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag leave");
* The cursor is moving over the drop target.
* {@inheritDoc}
*/
+ @Override
public void dragOver(DropTargetEvent event) {
processDropEvent(event);
}
* The drop target is given a last chance to change the nature of the drop.
* {@inheritDoc}
*/
+ @Override
public void dropAccept(DropTargetEvent event) {
if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drop accept");
* The data is being dropped.
* {@inheritDoc}
*/
+ @Override
public void drop(final DropTargetEvent event) {
if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "dropped");
}
if (mTargetNode == null) {
- if (mCanvas.isResultValid() && mCanvas.isEmptyDocument()) {
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ if (viewHierarchy.isValid() && viewHierarchy.isEmpty()) {
// There is no target node because the drop happens on an empty document.
// Attempt to create a root node accordingly.
createDocumentRoot(elements);
return;
}
- final Point where = mCanvas.displayToCanvasPoint(event.x, event.y);
+ final LayoutPoint canvasPoint = ControlPoint.create(mCanvas, event).toLayout();
// Record children of the target right before the drop (such that we can
// find out after the drop which exact children were inserted)
mCanvas.getRulesEngine().callOnDropped(mTargetNode,
elementsFinal,
mFeedback,
- where);
+ new Point(canvasPoint.x, canvasPoint.y));
// Clean up drag if applicable
if (event.detail == DND.DROP_MOVE) {
GlobalCanvasDragInfo.getInstance().removeSource();
private boolean selectDropped(Collection<INode> nodes) {
final Collection<CanvasViewInfo> newChildren = new ArrayList<CanvasViewInfo>();
for (INode node : nodes) {
- CanvasViewInfo viewInfo = mCanvas.findViewInfoFor(node);
+ CanvasViewInfo viewInfo = mCanvas.getViewHierarchy().findViewInfoFor(node);
if (viewInfo != null) {
newChildren.add(viewInfo);
}
}
- mCanvas.selectMultiple(newChildren);
+ mCanvas.getSelectionManager().selectMultiple(newChildren);
return nodes.size() == newChildren.size();
}
/**
* Computes a suitable Undo label to use for a drop operation, such as
- * "Drop Button in LinearLayout" and "Move Widgets in RelativeLayout"
+ * "Drop Button in LinearLayout" and "Move Widgets in RelativeLayout".
*
* @param targetNode The target of the drop
* @param elements The dragged widgets
* Returns simple name (basename, following last dot) of a fully qualified
* class name.
*
- * @param fcqn The fqcn to reduce
+ * @param fqcn The fqcn to reduce
* @return The base name of the fqcn
*/
private String getSimpleName(String fqcn) {
// Note that the following works even when there is no dot, since
// lastIndexOf will return -1 so we get fcqn.substring(-1+1) =
- // fcqn.substring(0) = fcqn
+ // fcqn.substring(0) = fqcn
return fqcn.substring(fqcn.lastIndexOf('.') + 1);
}
}
/**
- * Invoked by the canvas to refresh the display.
- * @param gCWrapper The GC wrapper, never null.
- */
- public void paintFeedback(GCWrapper gCWrapper) {
- if (mTargetNode != null && mFeedback != null && mFeedback.requestPaint) {
- mCanvas.getRulesEngine().callDropFeedbackPaint(gCWrapper, mTargetNode, mFeedback);
- mFeedback.requestPaint = false;
- }
- }
-
- /**
* Verifies that event.currentDataType is of type {@link SimpleXmlTransfer}.
* If not, try to find a valid data type.
* Otherwise set the drop to {@link DND#DROP_NONE} to cancel it.
* selected target node.
*/
private void processDropEvent(DropTargetEvent event) {
- if (!mCanvas.isResultValid()) {
+ if (!mCanvas.getViewHierarchy().isValid()) {
// We don't allow drop on an invalid layout, even if we have some obsolete
// layout info for it.
event.detail = DND.DROP_NONE;
return;
}
- Point p = mCanvas.displayToCanvasPoint(event.x, event.y);
- int x = p.x;
- int y = p.y;
+ LayoutPoint p = ControlPoint.create(mCanvas, event).toLayout();
// Is the mouse currently captured by a DropFeedback.captureArea?
boolean isCaptured = false;
if (mFeedback != null) {
Rect r = mFeedback.captureArea;
- isCaptured = r != null && r.contains(x, y);
+ isCaptured = r != null && r.contains(p.x, p.y);
}
// We can't switch views/nodes when the mouse is captured
if (isCaptured) {
vi = mCurrentView;
} else {
- vi = mCanvas.findViewInfoAt(x, y);
+ vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
}
boolean isMove = true;
// the -position- of the mouse), and we want this computation to happen
// before we ask the view to draw its feedback.
df = mCanvas.getRulesEngine().callOnDropMove(targetNode,
- mCurrentDragElements, df, p);
+ mCurrentDragElements, df, new Point(p.x, p.y));
}
if (df != null &&
if (isMove && mTargetNode != null && mFeedback != null) {
// this is a move inside the same view
com.android.ide.common.api.Point p2 =
- new com.android.ide.common.api.Point(x, y);
+ new com.android.ide.common.api.Point(p.x, p.y);
updateDropFeedback(mFeedback, event);
DropFeedback df = mCanvas.getRulesEngine().callOnDropMove(
mTargetNode, mCurrentDragElements, mFeedback, p2);
mCanvas.createDocumentRoot(rootFqcn);
}
+
+ /**
+ * An {@link Overlay} to paint the move feedback. This just delegates to the
+ * layout rules.
+ */
+ private class MoveOverlay extends Overlay {
+ @Override
+ public void paint(GC gc) {
+ if (mTargetNode != null && mFeedback != null) {
+ RulesEngine rulesEngine = mCanvas.getRulesEngine();
+ rulesEngine.callDropFeedbackPaint(mCanvas.getGcWrapper(), mTargetNode, mFeedback);
+ mFeedback.requestPaint = false;
+ }
+ }
+ }
}
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Rectangle;
+
+/**
+ * The {@link OutlineOverlay} paints an optional outline on top of the layout,
+ * showing the structure of the individual Android View elements.
+ */
+public class OutlineOverlay extends Overlay {
+ /** The {@link ViewHierarchy} this outline visualizes */
+ private final ViewHierarchy mViewHierarchy;
+
+ /** Outline color. Must be disposed, it's NOT a system color. */
+ private Color mOutlineColor;
+
+ /** Vertical scaling & scrollbar information. */
+ private ScaleInfo mVScale;
+
+ /** Horizontal scaling & scrollbar information. */
+ private ScaleInfo mHScale;
+
+ /**
+ * Constructs a new {@link OutlineOverlay} linked to the given view
+ * hierarchy.
+ *
+ * @param viewHierarchy The {@link ViewHierarchy} to render
+ * @param hScale The {@link ScaleInfo} to use to transfer horizontal layout
+ * coordinates to screen coordinates
+ * @param vScale The {@link ScaleInfo} to use to transfer vertical layout
+ * coordinates to screen coordinates
+ */
+ public OutlineOverlay(ViewHierarchy viewHierarchy, ScaleInfo hScale, ScaleInfo vScale) {
+ super();
+ this.mViewHierarchy = viewHierarchy;
+ this.mHScale = hScale;
+ this.mVScale = vScale;
+ }
+
+ @Override
+ public void create(Device device) {
+ mOutlineColor = new Color(device, SwtDrawingStyle.OUTLINE.getStrokeColor());
+ }
+
+ @Override
+ public void dispose() {
+ if (mOutlineColor != null) {
+ mOutlineColor.dispose();
+ mOutlineColor = null;
+ }
+ }
+
+ @Override
+ public void paint(GC gc) {
+ CanvasViewInfo lastRoot = mViewHierarchy.getRoot();
+ if (lastRoot != null) {
+ gc.setForeground(mOutlineColor);
+ gc.setLineStyle(SwtDrawingStyle.OUTLINE.getLineStyle());
+ int oldAlpha = gc.getAlpha();
+ gc.setAlpha(SwtDrawingStyle.OUTLINE.getStrokeAlpha());
+ drawOutline(gc, lastRoot);
+ gc.setAlpha(oldAlpha);
+ }
+ }
+
+ private void drawOutline(GC gc, CanvasViewInfo info) {
+ Rectangle r = info.getAbsRect();
+
+ int x = mHScale.translate(r.x);
+ int y = mVScale.translate(r.y);
+ int w = mHScale.scale(r.width);
+ int h = mVScale.scale(r.height);
+
+ // Add +1 to the width and +1 to the height such that when you have a
+ // series of boxes (in say a LinearLayout), instead of the bottom of one
+ // box and the top of the next box being -adjacent-, they -overlap-.
+ // This makes the outline nicer visually since you don't get
+ // "double thickness" lines for all adjacent boxes.
+ gc.drawRectangle(x, y, w + 1, h + 1);
+
+ for (CanvasViewInfo vi : info.getChildren()) {
+ drawOutline(gc, vi);
+ }
+ }
+
+}
}
});
- mDragSource = LayoutCanvas.createDragSource(getControl(), new DelegateDragListener());
- mDropTarget = LayoutCanvas.createDropTarget(getControl(), new DelegateDropListener());
+ mDragSource = LayoutCanvas.createDragSource(getControl());
+ mDragSource.addDragListener(new DelegateDragListener());
+
+ mDropTarget = LayoutCanvas.createDropTarget(getControl());
+ mDropTarget.addDropListener(new DelegateDropListener());
setupContextMenu();
LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
if (canvas != null) {
com.android.ide.common.api.Point p =
- canvas.canvasToControlPoint(x, y);
+ canvas.layoutToControlPoint(x, y);
inOutXY.x = p.x;
inOutXY.y = p.y;
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+
+/**
+ * An Overlay is a set of graphics which can be painted on top of the visual
+ * editor. Different {@link Gesture}s produce context specific overlays, such as
+ * swiping rectangles from the {@link MarqueeGesture} and guidelines from the
+ * {@link MoveGesture}.
+ */
+public abstract class Overlay {
+ /**
+ * Construct the overlay, using the given graphics context for painting.
+ */
+ public Overlay() {
+ super();
+ }
+
+ /**
+ * Initializes the overlay before the first use, if applicable. This is a
+ * good place to initialize resources like colors.
+ *
+ * @param device The device to allocate resources for; the parameter passed
+ * to {@link #paint} will correspond to this device.
+ */
+ public void create(Device device) {
+ }
+
+ /**
+ * Releases resources held by the overlay. Called by the editor when an
+ * overlay has been removed.
+ */
+ public void dispose() {
+ }
+
+ /**
+ * Paints the overlay.
+ *
+ * @param gc The SWT {@link GC} object to draw into.
+ */
+ public void paint(GC gc) {
+ throw new IllegalArgumentException("paint() not implemented, probably done "
+ + "with specialized paint signature");
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.ScrollBar;
+
+/**
+ * Helper class to convert between control pixel coordinates and canvas coordinates.
+ * Takes care of the zooming and offset of the canvas.
+ */
+public class ScaleInfo implements ICanvasTransform {
+ /**
+ * The canvas which controls the zooming.
+ */
+ private final LayoutCanvas mCanvas;
+
+ /** Canvas image size (original, before zoom), in pixels. */
+ private int mImgSize;
+
+ /** Client size, in pixels. */
+ private int mClientSize;
+
+ /** Left-top offset in client pixel coordinates. */
+ private int mTranslate;
+
+ /** Scaling factor, > 0. */
+ private double mScale;
+
+ /** Scrollbar widget. */
+ private ScrollBar mScrollbar;
+
+ public ScaleInfo(LayoutCanvas layoutCanvas, ScrollBar scrollbar) {
+ mCanvas = layoutCanvas;
+ mScrollbar = scrollbar;
+ mScale = 1.0;
+ mTranslate = 0;
+
+ mScrollbar.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // User requested scrolling. Changes translation and redraw canvas.
+ mTranslate = mScrollbar.getSelection();
+ ScaleInfo.this.mCanvas.redraw();
+ }
+ });
+ }
+
+ /**
+ * Sets the new scaling factor. Recomputes scrollbars.
+ * @param scale Scaling factor, > 0.
+ */
+ public void setScale(double scale) {
+ if (mScale != scale) {
+ mScale = scale;
+ resizeScrollbar();
+ }
+ }
+
+ /**
+ * Returns current scaling factor.
+ *
+ * @return The current scaling factor
+ */
+ public double getScale() {
+ return mScale;
+ }
+
+ /**
+ * Returns Canvas image size (original, before zoom), in pixels.
+ *
+ * @return Canvas image size (original, before zoom), in pixels
+ */
+ public int getImgSize() {
+ return mImgSize;
+ }
+
+ /**
+ * Returns the scaled image size in pixels.
+ *
+ * @return The scaled image size in pixels.
+ */
+ public int getScalledImgSize() {
+ return (int) (mImgSize * mScale);
+ }
+
+ /** Changes the size of the canvas image and the client size. Recomputes scrollbars. */
+ public void setSize(int imgSize, int clientSize) {
+ mImgSize = imgSize;
+ setClientSize(clientSize);
+ }
+
+ /** Changes the size of the client size. Recomputes scrollbars. */
+ public void setClientSize(int clientSize) {
+ mClientSize = clientSize;
+ resizeScrollbar();
+ }
+
+ private void resizeScrollbar() {
+ // scaled image size
+ int sx = (int) (mImgSize * mScale);
+
+ // actual client area is always reduced by the margins
+ int cx = mClientSize - 2 * IMAGE_MARGIN;
+
+ if (sx < cx) {
+ mScrollbar.setEnabled(false);
+ } else {
+ mScrollbar.setEnabled(true);
+
+ // max scroll value is the scaled image size
+ // thumb value is the actual viewable area out of the scaled img size
+ mScrollbar.setMaximum(sx);
+ mScrollbar.setThumb(cx);
+ }
+ }
+
+ public int translate(int canvasX) {
+ return IMAGE_MARGIN - mTranslate + (int) (mScale * canvasX);
+ }
+
+ public int scale(int canwasW) {
+ return (int) (mScale * canwasW);
+ }
+
+ public int inverseTranslate(int screenX) {
+ return (int) ((screenX - IMAGE_MARGIN + mTranslate) / mScale);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.runtime.ListenerList;
+import org.eclipse.gef.ui.parts.TreeViewer;
+import org.eclipse.jface.util.SafeRunnable;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.ISelectionProvider;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MouseEvent;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+/**
+ * The {@link SelectionManager} manages the selection in the canvas editor.
+ * It holds (and can be asked about) the set of selected items, and it also has
+ * operations for manipulating the selection - such as toggling items, copying
+ * the selection to the clipboard, etc.
+ * <p/>
+ * This class implements {@link ISelectionProvider} so that it can delegate
+ * the selection provider from the {@link LayoutCanvasViewer}.
+ * <p/>
+ * Note that {@link LayoutCanvasViewer} sets a selection change listener on this
+ * manager so that it can invoke its own fireSelectionChanged when the canvas'
+ * selection changes.
+ */
+public class SelectionManager implements ISelectionProvider {
+
+ private LayoutCanvas mCanvas;
+
+ /** The current selection list. The list is never null, however it can be empty. */
+ private final LinkedList<CanvasSelection> mSelections = new LinkedList<CanvasSelection>();
+
+ /** An unmodifiable view of {@link #mSelections}. */
+ private final List<CanvasSelection> mUnmodifiableSelection =
+ Collections.unmodifiableList(mSelections);
+
+ /** Barrier set when updating the selection to prevent from recursively
+ * invoking ourselves. */
+ private boolean mInsideUpdateSelection;
+
+ /**
+ * The <em>current</em> alternate selection, if any, which changes when the Alt key is
+ * used during a selection. Can be null.
+ */
+ private CanvasAlternateSelection mAltSelection;
+
+ /** List of clients listening to selection changes. */
+ private final ListenerList mSelectionListeners = new ListenerList();
+
+
+ /**
+ * Constructs a new {@link SelectionManager} associated with the given layout canvas.
+ *
+ * @param layoutCanvas The layout canvas to create a {@link SelectionManager} for.
+ */
+ public SelectionManager(LayoutCanvas layoutCanvas) {
+ this.mCanvas = layoutCanvas;
+ }
+
+ public void addSelectionChangedListener(ISelectionChangedListener listener) {
+ mSelectionListeners.add(listener);
+ }
+
+ public void removeSelectionChangedListener(ISelectionChangedListener listener) {
+ mSelectionListeners.remove(listener);
+ }
+
+ /**
+ * Returns the native {@link CanvasSelection} list.
+ *
+ * @return An immutable list of {@link CanvasSelection}. Can be empty but not null.
+ * @see #getSelection() {@link #getSelection()} to retrieve a {@link TreeViewer}
+ * compatible {@link ISelection}.
+ */
+ /* package */ List<CanvasSelection> getSelections() {
+ return mUnmodifiableSelection;
+ }
+
+ /**
+ * Return a snapshot/copy of the selection. Useful for clipboards etc where we
+ * don't want the returned copy to be affected by future edits to the selection.
+ *
+ * @return A copy of the current selection. Never null.
+ */
+ /* package */ List<CanvasSelection> getSnapshot() {
+ return new ArrayList<CanvasSelection>(mSelections);
+ }
+
+ /**
+ * Returns a {@link TreeSelection} compatible with a TreeViewer
+ * where each {@link TreePath} item is actually a {@link CanvasViewInfo}.
+ */
+ public ISelection getSelection() {
+ if (mSelections.isEmpty()) {
+ return TreeSelection.EMPTY;
+ }
+
+ ArrayList<TreePath> paths = new ArrayList<TreePath>();
+
+ for (CanvasSelection cs : mSelections) {
+ CanvasViewInfo vi = cs.getViewInfo();
+ if (vi != null) {
+ ArrayList<Object> segments = new ArrayList<Object>();
+ while (vi != null) {
+ segments.add(0, vi);
+ vi = vi.getParent();
+ }
+ paths.add(new TreePath(segments.toArray()));
+ }
+ }
+
+ return new TreeSelection(paths.toArray(new TreePath[paths.size()]));
+ }
+
+ /**
+ * Sets the selection. It must be an {@link ITreeSelection} where each segment
+ * of the tree path is a {@link CanvasViewInfo}. A null selection is considered
+ * as an empty selection.
+ * <p/>
+ * This method is invoked by {@link LayoutCanvasViewer#setSelection(ISelection)}
+ * in response to an <em>outside</em> selection (compatible with ours) that has
+ * changed. Typically it means the outline selection has changed and we're
+ * synchronizing ours to match.
+ */
+ public void setSelection(ISelection selection) {
+ if (mInsideUpdateSelection) {
+ return;
+ }
+
+ try {
+ mInsideUpdateSelection = true;
+
+ if (selection == null) {
+ selection = TreeSelection.EMPTY;
+ }
+
+ if (selection instanceof ITreeSelection) {
+ ITreeSelection treeSel = (ITreeSelection) selection;
+
+ if (treeSel.isEmpty()) {
+ // Clear existing selection, if any
+ if (!mSelections.isEmpty()) {
+ mSelections.clear();
+ mAltSelection = null;
+ redraw();
+ }
+ return;
+ }
+
+ boolean changed = false;
+
+ // Create a list of all currently selected view infos
+ Set<CanvasViewInfo> oldSelected = new HashSet<CanvasViewInfo>();
+ for (CanvasSelection cs : mSelections) {
+ oldSelected.add(cs.getViewInfo());
+ }
+
+ // Go thru new selection and take care of selecting new items
+ // or marking those which are the same as in the current selection
+ for (TreePath path : treeSel.getPaths()) {
+ Object seg = path.getLastSegment();
+ if (seg instanceof CanvasViewInfo) {
+ CanvasViewInfo newVi = (CanvasViewInfo) seg;
+ if (oldSelected.contains(newVi)) {
+ // This view info is already selected. Remove it from the
+ // oldSelected list so that we don't deselect it later.
+ oldSelected.remove(newVi);
+ } else {
+ // This view info is not already selected. Select it now.
+
+ // reset alternate selection if any
+ mAltSelection = null;
+ // otherwise add it.
+ mSelections.add(createSelection(newVi));
+ changed = true;
+ }
+ }
+ }
+
+ // Deselect old selected items that are not in the new one
+ for (CanvasViewInfo vi : oldSelected) {
+ deselect(vi);
+ changed = true;
+ }
+
+ if (changed) {
+ redraw();
+ updateMenuActions();
+ }
+
+ }
+ } finally {
+ mInsideUpdateSelection = false;
+ }
+ }
+
+ /**
+ * Performs selection for a mouse event.
+ * <p/>
+ * Shift key (or Command on the Mac) is used to toggle in multi-selection.
+ * Alt key is used to cycle selection through objects at the same level than
+ * the one pointed at (i.e. click on an object then alt-click to cycle).
+ *
+ * @param e The mouse event which triggered the selection. Cannot be null.
+ * The modifier key mask will be used to determine whether this
+ * is a plain select or a toggle, etc.
+ */
+ public void select(MouseEvent e) {
+
+ boolean isMultiClick = (e.stateMask & SWT.SHIFT) != 0 ||
+ // On Mac, the Command key is the normal toggle accelerator
+ ((SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) &&
+ (e.stateMask & SWT.COMMAND) != 0);
+ boolean isCycleClick = (e.stateMask & SWT.ALT) != 0;
+
+ LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout();
+
+ if (e.button == 3) {
+ // Right click button is used to display a context menu.
+ // If there's an existing selection and the click is anywhere in this selection
+ // and there are no modifiers being used, we don't want to change the selection.
+ // Otherwise we select the item under the cursor.
+
+ if (!isCycleClick && !isMultiClick) {
+ for (CanvasSelection cs : mSelections) {
+ if (cs.getRect().contains(p.x, p.y)) {
+ // The cursor is inside the selection. Don't change anything.
+ return;
+ }
+ }
+ }
+
+ } else if (e.button != 1) {
+ // Click was done with something else than the left button for normal selection
+ // or the right button for context menu.
+ // We don't use mouse button 2 yet (middle mouse, or scroll wheel?) for
+ // anything, so let's not change the selection.
+ return;
+ }
+
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+
+ if (isMultiClick && !isCycleClick) {
+ // Case where shift is pressed: pointed object is toggled.
+
+ // reset alternate selection if any
+ mAltSelection = null;
+
+ // If nothing has been found at the cursor, assume it might be a user error
+ // and avoid clearing the existing selection.
+
+ if (vi != null) {
+ // toggle this selection on-off: remove it if already selected
+ if (deselect(vi)) {
+ redraw();
+ return;
+ }
+
+ // otherwise add it.
+ mSelections.add(createSelection(vi));
+ fireSelectionChanged();
+ redraw();
+ }
+
+ } else if (isCycleClick) {
+ // Case where alt is pressed: select or cycle the object pointed at.
+
+ // Note: if shift and alt are pressed, shift is ignored. The alternate selection
+ // mechanism does not reset the current multiple selection unless they intersect.
+
+ // We need to remember the "origin" of the alternate selection, to be
+ // able to continue cycling through it later. If there's no alternate selection,
+ // create one. If there's one but not for the same origin object, create a new
+ // one too.
+ if (mAltSelection == null || mAltSelection.getOriginatingView() != vi) {
+ mAltSelection = new CanvasAlternateSelection(
+ vi, mCanvas.getViewHierarchy().findAltViewInfoAt(p));
+
+ // deselect them all, in case they were partially selected
+ deselectAll(mAltSelection.getAltViews());
+
+ // select the current one
+ CanvasViewInfo vi2 = mAltSelection.getCurrent();
+ if (vi2 != null) {
+ mSelections.addFirst(createSelection(vi2));
+ fireSelectionChanged();
+ }
+ } else {
+ // We're trying to cycle through the current alternate selection.
+ // First remove the current object.
+ CanvasViewInfo vi2 = mAltSelection.getCurrent();
+ deselect(vi2);
+
+ // Now select the next one.
+ vi2 = mAltSelection.getNext();
+ if (vi2 != null) {
+ mSelections.addFirst(createSelection(vi2));
+ fireSelectionChanged();
+ }
+ }
+ redraw();
+
+ } else {
+ // Case where no modifier is pressed: either select or reset the selection.
+
+ selectSingle(vi);
+ }
+ }
+
+ /**
+ * Removes all the currently selected item and only select the given item.
+ * Issues a {@link #redraw()} if the selection changes.
+ *
+ * @param vi The new selected item if non-null. Selection becomes empty if null.
+ */
+ /* package */ void selectSingle(CanvasViewInfo vi) {
+ // reset alternate selection if any
+ mAltSelection = null;
+
+ // reset (multi)selection if any
+ if (!mSelections.isEmpty()) {
+ if (mSelections.size() == 1 && mSelections.getFirst().getViewInfo() == vi) {
+ // CanvasSelection remains the same, don't touch it.
+ return;
+ }
+ mSelections.clear();
+ }
+
+ if (vi != null) {
+ mSelections.add(createSelection(vi));
+ }
+ fireSelectionChanged();
+ redraw();
+ }
+
+ /**
+ * Selects the given set of {@link CanvasViewInfo}s. This is similar to
+ * {@link #selectSingle} but allows you to make a multi-selection. Issues a
+ * {@link #redraw()}.
+ *
+ * @param viewInfos A collection of {@link CanvasViewInfo} objects to be
+ * selected, or null or empty to clear the selection.
+ */
+ /* package */ void selectMultiple(Collection<CanvasViewInfo> viewInfos) {
+ // reset alternate selection if any
+ mAltSelection = null;
+
+ mSelections.clear();
+ if (viewInfos != null) {
+ for (CanvasViewInfo viewInfo : viewInfos) {
+ mSelections.add(createSelection(viewInfo));
+ }
+ }
+
+ fireSelectionChanged();
+ redraw();
+ }
+
+ /**
+ * Selects the visual element corresponding to the given XML node
+ * @param xmlNode The Node whose element we want to select.
+ */
+ /* package */ void select(Node xmlNode) {
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoFor(xmlNode);
+ if (vi != null && !vi.isRoot()) {
+ selectSingle(vi);
+ }
+ }
+
+ /**
+ * Selects any views that overlap the given selection rectangle.
+ *
+ * @param topLeft The top left corner defining the selection rectangle.
+ * @param bottomRight The bottom right corner defining the selection
+ * rectangle.
+ * @param toggled A set of {@link CanvasViewInfo}s that should be toggled
+ * rather than just added.
+ */
+ public void selectWithin(LayoutPoint topLeft, LayoutPoint bottomRight,
+ Collection<CanvasViewInfo> toggled) {
+ // reset alternate selection if any
+ mAltSelection = null;
+
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ Collection<CanvasViewInfo> viewInfos = viewHierarchy.findWithin(topLeft, bottomRight);
+
+ if (toggled.size() > 0) {
+ // Copy; we're not allowed to touch the passed in collection
+ Set<CanvasViewInfo> result = new HashSet<CanvasViewInfo>(toggled);
+ for (CanvasViewInfo viewInfo : viewInfos) {
+ if (toggled.contains(viewInfo)) {
+ result.remove(viewInfo);
+ } else {
+ result.add(viewInfo);
+ }
+ }
+ viewInfos = result;
+ }
+
+ mSelections.clear();
+ for (CanvasViewInfo viewInfo : viewInfos) {
+ mSelections.add(createSelection(viewInfo));
+ }
+
+ fireSelectionChanged();
+ redraw();
+ }
+
+ /**
+ * Clears the selection and then selects everything (all views and all their
+ * children).
+ */
+ public void selectAll() {
+ // First clear the current selection, if any.
+ mSelections.clear();
+ mAltSelection = null;
+
+ // Now select everything if there's a valid layout
+ for (CanvasViewInfo vi : mCanvas.getViewHierarchy().findAllViewInfos(false)) {
+ mSelections.add(createSelection(vi));
+ }
+
+
+ fireSelectionChanged();
+ redraw();
+ }
+
+ /**
+ * Returns true if and only if there is currently more than one selected
+ * item.
+ *
+ * @return True if more than one item is selected
+ */
+ public boolean hasMultiSelection() {
+ return mSelections.size() > 1;
+ }
+
+ /**
+ * Deselects a view info. Returns true if the object was actually selected.
+ * Callers are responsible for calling redraw() and updateOulineSelection()
+ * after.
+ * @param canvasViewInfo The item to deselect.
+ * @return True if the object was successfully removed from the selection.
+ */
+ public boolean deselect(CanvasViewInfo canvasViewInfo) {
+ if (canvasViewInfo == null) {
+ return false;
+ }
+
+ for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
+ CanvasSelection s = it.next();
+ if (canvasViewInfo == s.getViewInfo()) {
+ it.remove();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Deselects multiple view infos.
+ * Callers are responsible for calling redraw() and updateOulineSelection() after.
+ */
+ private void deselectAll(List<CanvasViewInfo> canvasViewInfos) {
+ for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
+ CanvasSelection s = it.next();
+ if (canvasViewInfos.contains(s.getViewInfo())) {
+ it.remove();
+ }
+ }
+ }
+
+ /** Sync the selection with an updated view info tree */
+ /* package */ void sync(CanvasViewInfo lastValidViewInfoRoot) {
+ // Check if the selection is still the same (based on the object keys)
+ // and eventually recompute their bounds.
+ for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
+ CanvasSelection s = it.next();
+
+ // Check if the selected object still exists
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ Object key = s.getViewInfo().getUiViewKey();
+ CanvasViewInfo vi = viewHierarchy.findViewInfoKey(key, lastValidViewInfoRoot);
+
+ // Remove the previous selection -- if the selected object still exists
+ // we need to recompute its bounds in case it moved so we'll insert a new one
+ // at the same place.
+ it.remove();
+ if (vi != null) {
+ it.add(createSelection(vi));
+ }
+ }
+ fireSelectionChanged();
+
+ // remove the current alternate selection views
+ mAltSelection = null;
+ }
+
+ /**
+ * Notifies listeners that the selection has changed.
+ */
+ private void fireSelectionChanged() {
+ if (mInsideUpdateSelection) {
+ return;
+ }
+ try {
+ mInsideUpdateSelection = true;
+
+ final SelectionChangedEvent event = new SelectionChangedEvent(this, getSelection());
+
+ SafeRunnable.run(new SafeRunnable() {
+ public void run() {
+ for (Object listener : mSelectionListeners.getListeners()) {
+ ((ISelectionChangedListener) listener).selectionChanged(event);
+ }
+ }
+ });
+
+ // Update menu actions that depend on the selection
+ updateMenuActions();
+
+ } finally {
+ mInsideUpdateSelection = false;
+ }
+ }
+
+ /**
+ * Sanitizes the selection for a copy/cut or drag operation.
+ * <p/>
+ * Sanitizes the list to make sure all elements have a valid XML attached to it,
+ * that is remove element that have no XML to avoid having to make repeated such
+ * checks in various places after.
+ * <p/>
+ * In case of multiple selection, we also need to remove all children when their
+ * parent is already selected since parents will always be added with all their
+ * children.
+ * <p/>
+ *
+ * @param selection The selection list to be sanitized <b>in-place</b>.
+ * The <code>selection</code> argument should not be {@link #mSelections} -- the
+ * given list is going to be altered and we should never alter the user-made selection.
+ * Instead the caller should provide its own copy.
+ */
+ /* package */ static void sanitize(List<CanvasSelection> selection) {
+ if (selection.isEmpty()) {
+ return;
+ }
+
+ for (Iterator<CanvasSelection> it = selection.iterator(); it.hasNext(); ) {
+ CanvasSelection cs = it.next();
+ CanvasViewInfo vi = cs.getViewInfo();
+ UiViewElementNode key = vi == null ? null : vi.getUiViewKey();
+ Node node = key == null ? null : key.getXmlNode();
+ if (node == null) {
+ // Missing ViewInfo or view key or XML, discard this.
+ it.remove();
+ continue;
+ }
+
+ if (vi != null) {
+ for (Iterator<CanvasSelection> it2 = selection.iterator();
+ it2.hasNext(); ) {
+ CanvasSelection cs2 = it2.next();
+ if (cs != cs2) {
+ CanvasViewInfo vi2 = cs2.getViewInfo();
+ if (vi.isParent(vi2)) {
+ // vi2 is a parent for vi. Remove vi.
+ it.remove();
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void updateMenuActions() {
+ boolean hasSelection = !mSelections.isEmpty();
+ mCanvas.updateMenuActions(hasSelection);
+ }
+
+ private void redraw() {
+ mCanvas.redraw();
+ }
+
+ private CanvasSelection createSelection(CanvasViewInfo vi) {
+ return new CanvasSelection(vi, mCanvas.getRulesEngine(),
+ mCanvas.getNodeFactory());
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+
+import java.util.List;
+
+/**
+ * The {@link SelectionOverlay} paints the current selection as an overlay.
+ */
+public class SelectionOverlay extends Overlay {
+ /** CanvasSelection border color. */
+ private Color mSelectionFgColor;
+
+ /**
+ * Constructs a new {@link SelectionOverlay} tied to the given canvas.
+ */
+ public SelectionOverlay() {
+ }
+
+ @Override
+ public void create(Device device) {
+ mSelectionFgColor = new Color(device, SwtDrawingStyle.SELECTION.getStrokeColor());
+ }
+
+ @Override
+ public void dispose() {
+ if (mSelectionFgColor != null) {
+ mSelectionFgColor.dispose();
+ mSelectionFgColor = null;
+ }
+ }
+
+ /**
+ * Paints the selection.
+ *
+ * @param selectionManager The {@link SelectionManager} holding the
+ * selection.
+ * @param gc The graphics context to draw into.
+ * @param gcWrapper The graphics context wrapper for the layout rules to use.
+ * @param rulesEngine The {@link RulesEngine} holding the rules.
+ */
+ public void paint(SelectionManager selectionManager, GC gc, GCWrapper gcWrapper,
+ RulesEngine rulesEngine) {
+ List<CanvasSelection> selections = selectionManager.getSelections();
+ int n = selections.size();
+ if (n > 0) {
+ boolean isMultipleSelection = n > 1;
+
+ if (n == 1) {
+ gc.setForeground(mSelectionFgColor);
+ selections.get(0).paintParentSelection(rulesEngine, gcWrapper);
+ }
+
+ for (CanvasSelection s : selections) {
+ if (s.isRoot()) {
+ // The root selection is never painted
+ continue;
+ }
+ gc.setForeground(mSelectionFgColor);
+ s.paintSelection(rulesEngine, gcWrapper, isMultipleSelection);
+ }
+ }
+ }
+
+}
* {@link SimpleAttribute}s, all of which have very specific properties that are
* specifically limited to our needs for drag'n'drop.
*/
-class SimpleXmlTransfer extends ByteArrayTransfer {
+final class SimpleXmlTransfer extends ByteArrayTransfer {
// Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html
/**
* The style definition corresponding to {@link DrawingStyle#SELECTION}
*/
- SELECTION(new RGB(0x00, 0x99, 0xFF), 255, new RGB(0x00, 0x99, 0xFF), 64, 2, SWT.LINE_DASH),
+ SELECTION(new RGB(0x00, 0x99, 0xFF), 255, new RGB(0x00, 0x99, 0xFF), 64, 1, SWT.LINE_DASH),
/**
* The style definition corresponding to {@link DrawingStyle#GUIDELINE}
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.common.api.INode;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.layoutlib.api.ILayoutResult;
+import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo;
+
+import org.eclipse.swt.graphics.Rectangle;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * The view hierarchy class manages a set of view info objects and performs find
+ * operations on this set.
+ */
+public class ViewHierarchy {
+ private LayoutCanvas mCanvas;
+
+ /**
+ * Constructs a new {@link ViewHierarchy} tied to the given
+ * {@link LayoutCanvas}.
+ *
+ * @param canvas The {@link LayoutCanvas} to create a {@link ViewHierarchy}
+ * for.
+ */
+ public ViewHierarchy(LayoutCanvas canvas) {
+ this.mCanvas = canvas;
+ }
+
+ /**
+ * The CanvasViewInfo root created by the last call to {@link #setResult(ILayoutResult)}
+ * with a valid layout.
+ * <p/>
+ * This <em>can</em> be null to indicate we're dealing with an empty document with
+ * no root node. Null here does not mean the result was invalid, merely that the XML
+ * had no content to display -- we need to treat an empty document as valid so that
+ * we can drop new items in it.
+ */
+ private CanvasViewInfo mLastValidViewInfoRoot;
+
+ /**
+ * True when the last {@link #setResult(ILayoutResult)} provided a valid {@link ILayoutResult}.
+ * <p/>
+ * When false this means the canvas is displaying an out-dated result image & bounds and some
+ * features should be disabled accordingly such a drag'n'drop.
+ * <p/>
+ * Note that an empty document (with a null {@link #mLastValidViewInfoRoot}) is considered
+ * valid since it is an acceptable drop target.
+ */
+ private boolean mIsResultValid;
+
+ /**
+ * Sets the result of the layout rendering. The result object indicates if the layout
+ * rendering succeeded. If it did, it contains a bitmap and the objects rectangles.
+ *
+ * Implementation detail: the bridge's computeLayout() method already returns a newly
+ * allocated ILayourResult. That means we can keep this result and hold on to it
+ * when it is valid.
+ *
+ * @param result The new rendering result, either valid or not.
+ */
+ /* package */ void setResult(ILayoutResult result) {
+ mIsResultValid = (result != null && result.getSuccess() == ILayoutResult.SUCCESS);
+
+ if (mIsResultValid && result != null) {
+ ILayoutViewInfo root = result.getRootView();
+ if (root == null) {
+ mLastValidViewInfoRoot = null;
+ } else {
+ mLastValidViewInfoRoot = new CanvasViewInfo(result.getRootView());
+ }
+
+ updateNodeProxies(mLastValidViewInfoRoot);
+
+ // Update the selection
+ mCanvas.getSelectionManager().sync(mLastValidViewInfoRoot);
+ }
+ }
+
+ /**
+ * Creates or updates the node proxy for this canvas view info.
+ * <p/>
+ * Since proxies are reused, this will update the bounds of an existing proxy when the
+ * canvas is refreshed and a view changes position or size.
+ * <p/>
+ * This is a recursive call that updates the whole hierarchy starting at the given
+ * view info.
+ */
+ private void updateNodeProxies(CanvasViewInfo vi) {
+ if (vi == null) {
+ return;
+ }
+
+ UiViewElementNode key = vi.getUiViewKey();
+
+ if (key != null) {
+ mCanvas.getNodeFactory().create(vi);
+ }
+
+ for (CanvasViewInfo child : vi.getChildren()) {
+ updateNodeProxies(child);
+ }
+ }
+
+
+
+ /**
+ * Returns true when the last {@link #setResult(ILayoutResult)} provided a valid
+ * {@link ILayoutResult}.
+ * <p/>
+ * When false this means the canvas is displaying an out-dated result image & bounds and some
+ * features should be disabled accordingly such a drag'n'drop.
+ * <p/>
+ * Note that an empty document (with a null {@link #getRoot()}) is considered
+ * valid since it is an acceptable drop target.
+ * @return True when this {@link ViewHierarchy} contains a valid hierarchy of views.
+ */
+ public boolean isValid() {
+ return mIsResultValid;
+ }
+
+ /**
+ * Returns true if the last valid content of the canvas represents an empty document.
+ * @return True if the last valid content of the canvas represents an empty document.
+ */
+ public boolean isEmpty() {
+ return mLastValidViewInfoRoot == null;
+ }
+
+ /** Locates and return any views that overlap the given selection rectangle.
+ * @param topLeft The top left corner of the selection rectangle.
+ * @param bottomRight The bottom right corner of the selection rectangle.
+ * @return A collection of {@link CanvasViewInfo} objects that overlap the
+ * rectangle.
+ */
+ public Collection<CanvasViewInfo> findWithin(
+ LayoutPoint topLeft,
+ LayoutPoint bottomRight) {
+ Rectangle selectionRectangle = new Rectangle(topLeft.x, topLeft.y, bottomRight.x
+ - topLeft.x, bottomRight.y - topLeft.y);
+ List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
+ addWithin(mLastValidViewInfoRoot, selectionRectangle, infos);
+ return infos;
+ }
+
+ /**
+ * Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
+ * <p/>
+ * Tries to find the inner most child matching the given x,y coordinates in the view
+ * info sub-tree. This uses the potentially-expanded selection bounds.
+ *
+ * Returns null if not found.
+ */
+ private void addWithin(
+ CanvasViewInfo canvasViewInfo,
+ Rectangle canvasRectangle,
+ List<CanvasViewInfo> infos) {
+ if (canvasViewInfo == null) {
+ return;
+ }
+ Rectangle r = canvasViewInfo.getSelectionRect();
+ if (canvasRectangle.intersects(r)) {
+
+ // try to find a matching child first
+ for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+ addWithin(child, canvasRectangle, infos);
+ }
+
+ if (canvasViewInfo != mLastValidViewInfoRoot) {
+ infos.add(canvasViewInfo);
+ }
+ }
+ }
+
+ /**
+ * Locates and returns the {@link CanvasViewInfo} corresponding to the given
+ * node, or null if it cannot be found.
+ *
+ * @param node The node we want to find a corresponding
+ * {@link CanvasViewInfo} for.
+ * @return The {@link CanvasViewInfo} corresponding to the given node, or
+ * null if no match was found.
+ */
+ public CanvasViewInfo findViewInfoFor(Node node) {
+ if (mLastValidViewInfoRoot != null) {
+ return findViewInfoForNode(node, mLastValidViewInfoRoot);
+ }
+ return null;
+ }
+
+ /**
+ * Tries to find a child with the same view XML node in the view info sub-tree.
+ * Returns null if not found.
+ */
+ private CanvasViewInfo findViewInfoForNode(Node xmlNode, CanvasViewInfo canvasViewInfo) {
+ if (canvasViewInfo == null) {
+ return null;
+ }
+ if (canvasViewInfo.getXmlNode() == xmlNode) {
+ return canvasViewInfo;
+ }
+
+ // Try to find a matching child
+ for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+ CanvasViewInfo v = findViewInfoForNode(xmlNode, child);
+ if (v != null) {
+ return v;
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Tries to find the inner most child matching the given x,y coordinates in
+ * the view info sub-tree, starting at the last know view info root. This
+ * uses the potentially-expanded selection bounds.
+ * <p/>
+ * Returns null if not found or if there's no view info root.
+ *
+ * @param p The point at which to look for the deepest match in the view
+ * hierarchy
+ * @return A {@link CanvasViewInfo} that intersects the given point, or null
+ * if nothing was found.
+ */
+ public CanvasViewInfo findViewInfoAt(LayoutPoint p) {
+ if (mLastValidViewInfoRoot == null) {
+ return null;
+ }
+
+ return findViewInfoAt_Recursive(p, mLastValidViewInfoRoot);
+ }
+
+ /**
+ * Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
+ * <p/>
+ * Tries to find the inner most child matching the given x,y coordinates in the view
+ * info sub-tree. This uses the potentially-expanded selection bounds.
+ *
+ * Returns null if not found.
+ */
+ private CanvasViewInfo findViewInfoAt_Recursive(LayoutPoint p, CanvasViewInfo canvasViewInfo) {
+ if (canvasViewInfo == null) {
+ return null;
+ }
+ Rectangle r = canvasViewInfo.getSelectionRect();
+ if (r.contains(p.x, p.y)) {
+
+ // try to find a matching child first
+ for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+ CanvasViewInfo v = findViewInfoAt_Recursive(p, child);
+ if (v != null) {
+ return v;
+ }
+ }
+
+ // if no children matched, this is the view that we're looking for
+ return canvasViewInfo;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a list of all the possible alternatives for a given view at the given
+ * position. This is used to build and manage the "alternate" selection that cycles
+ * around the parents or children of the currently selected element.
+ */
+ /* package */ List<CanvasViewInfo> findAltViewInfoAt(LayoutPoint p) {
+ if (mLastValidViewInfoRoot != null) {
+ return findAltViewInfoAt_Recursive(p, mLastValidViewInfoRoot, null);
+ }
+
+ return null;
+ }
+
+ /**
+ * Internal recursive version of {@link #findAltViewInfoAt(int, int, CanvasViewInfo)}.
+ * Please don't use directly.
+ */
+ private List<CanvasViewInfo> findAltViewInfoAt_Recursive(
+ LayoutPoint p, CanvasViewInfo parent, List<CanvasViewInfo> outList) {
+ Rectangle r;
+
+ if (outList == null) {
+ outList = new ArrayList<CanvasViewInfo>();
+
+ if (parent != null) {
+ // add the parent root only once
+ r = parent.getSelectionRect();
+ if (r.contains(p.x, p.y)) {
+ outList.add(parent);
+ }
+ }
+ }
+
+ if (parent != null && !parent.getChildren().isEmpty()) {
+ // then add all children that match the position
+ for (CanvasViewInfo child : parent.getChildren()) {
+ r = child.getSelectionRect();
+ if (r.contains(p.x, p.y)) {
+ outList.add(child);
+ }
+ }
+
+ // finally recurse in the children
+ for (CanvasViewInfo child : parent.getChildren()) {
+ r = child.getSelectionRect();
+ if (r.contains(p.x, p.y)) {
+ findAltViewInfoAt_Recursive(p, child, outList);
+ }
+ }
+ }
+
+ return outList;
+ }
+
+ /**
+ * Locates and returns the {@link CanvasViewInfo} corresponding to the given
+ * node, or null if it cannot be found.
+ *
+ * @param node The node we want to find a corresponding
+ * {@link CanvasViewInfo} for.
+ * @return The {@link CanvasViewInfo} corresponding to the given node, or
+ * null if no match was found.
+ */
+ public CanvasViewInfo findViewInfoFor(INode node) {
+ if (mLastValidViewInfoRoot != null && node instanceof NodeProxy) {
+ return findViewInfoKey(((NodeProxy) node).getNode(), mLastValidViewInfoRoot);
+ }
+ return null;
+ }
+
+ /**
+ * Tries to find a child with the same view key in the view info sub-tree.
+ * Returns null if not found.
+ *
+ * @param viewKey The view key that a matching {@link CanvasViewInfo} should
+ * have as its key.
+ * @param canvasViewInfo A root {@link CanvasViewInfo} to search from.
+ * @return A {@link CanvasViewInfo} matching the given key, or null if not
+ * found.
+ */
+ public CanvasViewInfo findViewInfoKey(Object viewKey, CanvasViewInfo canvasViewInfo) {
+ if (canvasViewInfo == null) {
+ return null;
+ }
+ if (canvasViewInfo.getUiViewKey() == viewKey) {
+ return canvasViewInfo;
+ }
+
+ // try to find a matching child
+ for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+ CanvasViewInfo v = findViewInfoKey(viewKey, child);
+ if (v != null) {
+ return v;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a list of ALL ViewInfos (possibly excluding the root, depending
+ * on the parameter for that).
+ *
+ * @param includeRoot If true, include the root in the list, otherwise
+ * exclude it (but include all its children)
+ * @return A list of canvas view infos.
+ */
+ public List<CanvasViewInfo> findAllViewInfos(boolean includeRoot) {
+ List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
+ if (mIsResultValid && mLastValidViewInfoRoot != null) {
+ findAllViewInfos(infos, mLastValidViewInfoRoot, includeRoot);
+ }
+
+ return infos;
+ }
+
+ private void findAllViewInfos(List<CanvasViewInfo> result, CanvasViewInfo canvasViewInfo,
+ boolean includeRoot) {
+ if (canvasViewInfo != null) {
+ if (includeRoot || !canvasViewInfo.isRoot()) {
+ result.add(canvasViewInfo);
+ }
+ for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+ findAllViewInfos(result, child, true);
+ }
+ }
+ }
+
+ /**
+ * Return the root of the view hierarchy, if any (could be null, for example
+ * on rendering failure).
+ *
+ * @return The current view hierarchy, or null
+ */
+ public CanvasViewInfo getRoot() {
+ return mLastValidViewInfoRoot;
+ }
+
+}
private ViewElementDescriptor getFqcnViewDescritor(String fqcn) {
AndroidXmlEditor editor = mNode.getEditor();
if (editor instanceof LayoutEditor) {
- return ((LayoutEditor) editor).getFqcnViewDescritor(fqcn);
+ return ((LayoutEditor) editor).getFqcnViewDescriptor(fqcn);
}
return null;
}
}
}
- } else if (ui_parent instanceof UiViewElementNode){
+ } else if (ui_parent instanceof UiViewElementNode) {
layout_attrs =
((ViewElementDescriptor) ui_parent.getDescriptor()).getLayoutAttributes();
}
/**
* Return the set of instrumentation names for the Android project.
*
- * @return <code>null</code if error occurred parsing instrumentations, otherwise returns array
+ * @return <code>null</code> if error occurred parsing instrumentations, otherwise returns array
* of instrumentation class names
*/
String[] getInstrumentationNames() {
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.events.MouseEvent;
+
+public class ControlPointTest extends PointTestCases {
+ public void testCreateFromMouseEvent() throws Exception {
+ MouseEvent mouseEvent = canvasMouseEvent(10, 20, 0);
+
+ ControlPoint point = ControlPoint.create(mCanvas, mouseEvent);
+ assertEquals(10, point.x);
+ assertEquals(20, point.y);
+ }
+
+ public void testCreateFromCoordinates() throws Exception {
+ ControlPoint point = ControlPoint.create(mCanvas, 10, 20);
+ assertEquals(10, point.x);
+ assertEquals(20, point.y);
+ }
+
+ public void testConvertToLayout() throws Exception {
+ ControlPoint point = ControlPoint.create(new TestLayoutCanvas(), 10, 20);
+ assertEquals(10, point.x);
+ assertEquals(20, point.y);
+
+ LayoutPoint layoutPoint = point.toLayout();
+ assertNotNull(layoutPoint);
+ assertEquals(40, layoutPoint.x);
+ assertEquals(60, layoutPoint.y);
+
+ // For sanity let's also convert back and verify
+ ControlPoint controlPoint = layoutPoint.toControl();
+ assertNotNull(controlPoint);
+ assertNotSame(controlPoint, point);
+ assertEquals(point, controlPoint);
+ assertEquals(10, controlPoint.x);
+ assertEquals(20, controlPoint.y);
+ }
+
+ public void testEquals() throws Exception {
+ ControlPoint point1 = ControlPoint.create(mCanvas, 1, 1);
+ ControlPoint point2 = ControlPoint.create(mCanvas, 1, 2);
+ ControlPoint point3 = ControlPoint.create(mCanvas, 2, 1);
+ ControlPoint point2b = ControlPoint.create(mCanvas, 1, 2);
+
+ assertFalse(point2.equals(null));
+
+ assertEquals(point2, point2);
+ assertEquals(point2, point2b);
+ assertEquals(point2.hashCode(), point2b.hashCode());
+ assertNotSame(point2, point2b);
+
+ assertFalse(point1.equals(point2));
+ assertFalse(point1.equals(point3));
+ assertFalse(point2.equals(point3));
+ assertFalse(point1.equals(point2));
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+public class LayoutPointTest extends PointTestCases {
+ public void testCreateFromCoordinates() throws Exception {
+ LayoutPoint point = LayoutPoint.create(mCanvas, 10, 20);
+ assertEquals(10, point.x);
+ assertEquals(20, point.y);
+ }
+
+ public void testEquals() throws Exception {
+ LayoutPoint point1 = LayoutPoint.create(mCanvas, 1, 1);
+ LayoutPoint point2 = LayoutPoint.create(mCanvas, 1, 2);
+ LayoutPoint point3 = LayoutPoint.create(mCanvas, 2, 1);
+ LayoutPoint point2b = LayoutPoint.create(mCanvas, 1, 2);
+
+ assertFalse(point2.equals(null));
+
+ assertEquals(point2, point2);
+ assertEquals(point2, point2b);
+ assertEquals(point2.hashCode(), point2b.hashCode());
+ assertNotSame(point2, point2b);
+
+ assertFalse(point1.equals(point2));
+ assertFalse(point1.equals(point3));
+ assertFalse(point2.equals(point3));
+ assertFalse(point1.equals(point2));
+ }
+
+ public void testConvertToControl() throws Exception {
+ LayoutPoint point = LayoutPoint.create(new TestLayoutCanvas(), 10, 20);
+ assertEquals(10, point.x);
+ assertEquals(20, point.y);
+
+ ControlPoint controlPoint = point.toControl();
+ assertNotNull(controlPoint);
+ assertEquals(-5, controlPoint.x);
+ assertEquals(0, controlPoint.y);
+
+ // For sanity let's also convert back and verify
+ LayoutPoint layoutPoint = controlPoint.toLayout();
+ assertNotNull(layoutPoint);
+ assertNotSame(layoutPoint, point);
+ assertEquals(point, layoutPoint);
+ assertEquals(10, layoutPoint.x);
+ assertEquals(20, layoutPoint.y);
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.List;
+import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.Shell;
+
+import junit.framework.TestCase;
+
+/**
+ * Common utilities for the point tests {@link LayoutPointTest} and
+ * {@link ControlPointTest}
+ */
+public abstract class PointTestCases extends TestCase {
+ LayoutCanvas mCanvas = new TestLayoutCanvas();
+
+ protected MouseEvent canvasMouseEvent(int x, int y, int stateMask) {
+ Event event = new Event();
+ event.x = x;
+ event.y = y;
+ event.stateMask = stateMask;
+ event.widget = mCanvas;
+ MouseEvent mouseEvent = new MouseEvent(event);
+ return mouseEvent;
+ }
+
+ /** Mock implementation of LayoutCanvas */
+ protected static class TestLayoutCanvas extends LayoutCanvas {
+ float mScaleX;
+
+ float mScaleY;
+
+ float mTranslateX;
+
+ float mTranslateY;
+
+ public TestLayoutCanvas(float scaleX, float scaleY, float translateX, float translateY) {
+ super(null, null, new Shell(), 0);
+
+ this.mScaleX = scaleX;
+ this.mScaleY = scaleY;
+ this.mTranslateX = translateX;
+ this.mTranslateY = translateY;
+ }
+
+ public TestLayoutCanvas() {
+ this(2.0f, 2.0f, 20.0f, 20.0f);
+ }
+
+ @Override
+ ScaleInfo getHorizontalTransform() {
+ ScrollBar scrollBar = new List(this, SWT.V_SCROLL|SWT.H_SCROLL).getHorizontalBar();
+ return new TestScaleInfo(scrollBar, mScaleX, mTranslateX);
+ }
+
+ @Override
+ ScaleInfo getVerticalTransform() {
+ ScrollBar scrollBar = new List(this, SWT.V_SCROLL|SWT.H_SCROLL).getVerticalBar();
+ return new TestScaleInfo(scrollBar, mScaleY, mTranslateY);
+ }
+ }
+
+ static class TestScaleInfo extends ScaleInfo {
+ float mScale;
+
+ float mTranslate;
+
+ public TestScaleInfo(ScrollBar scrollBar, float scale, float translate) {
+ super(null, scrollBar);
+ this.mScale = scale;
+ this.mTranslate = translate;
+ }
+
+ @Override
+ public int translate(int value) {
+ return (int) ((value - mTranslate) / mScale);
+ }
+
+ @Override
+ public int inverseTranslate(int value) {
+ return (int) (value * mScale + mTranslate);
+ }
+ }
+}