aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java1262
1 files changed, 1262 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java
new file mode 100644
index 000000000..eb3d6f290
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java
@@ -0,0 +1,1262 @@
+/*
+ * 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 static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.FQCN_SPACE;
+import static com.android.SdkConstants.FQCN_SPACE_V7;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.PIXEL_MARGIN;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.PIXEL_RADIUS;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.layout.BaseViewRule;
+import com.android.ide.common.layout.GridLayoutRule;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+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.UiElementNode;
+import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceWizard;
+import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult;
+import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
+import com.android.resources.ResourceType;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.ListenerList;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.dialogs.InputDialog;
+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.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MenuDetectEvent;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.ui.IWorkbenchPartSite;
+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<SelectionItem> mSelections = new LinkedList<SelectionItem>();
+
+ /** An unmodifiable view of {@link #mSelections}. */
+ private final List<SelectionItem> 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) {
+ mCanvas = layoutCanvas;
+ }
+
+ @Override
+ public void addSelectionChangedListener(ISelectionChangedListener listener) {
+ mSelectionListeners.add(listener);
+ }
+
+ @Override
+ public void removeSelectionChangedListener(ISelectionChangedListener listener) {
+ mSelectionListeners.remove(listener);
+ }
+
+ /**
+ * Returns the native {@link SelectionItem} list.
+ *
+ * @return An immutable list of {@link SelectionItem}. Can be empty but not null.
+ */
+ @NonNull
+ List<SelectionItem> 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.
+ */
+ @NonNull
+ public List<SelectionItem> getSnapshot() {
+ if (mSelectionListeners.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ return new ArrayList<SelectionItem>(mSelections);
+ }
+
+ /**
+ * Returns a {@link TreeSelection} where each {@link TreePath} item is
+ * actually a {@link CanvasViewInfo}.
+ */
+ @Override
+ public ISelection getSelection() {
+ if (mSelections.isEmpty()) {
+ return TreeSelection.EMPTY;
+ }
+
+ ArrayList<TreePath> paths = new ArrayList<TreePath>();
+
+ for (SelectionItem cs : mSelections) {
+ CanvasViewInfo vi = cs.getViewInfo();
+ if (vi != null) {
+ paths.add(getTreePath(vi));
+ }
+ }
+
+ return new TreeSelection(paths.toArray(new TreePath[paths.size()]));
+ }
+
+ /**
+ * Create a {@link TreePath} from the given view info
+ *
+ * @param viewInfo the view info to look up a tree path for
+ * @return a {@link TreePath} for the given view info
+ */
+ public static TreePath getTreePath(CanvasViewInfo viewInfo) {
+ ArrayList<Object> segments = new ArrayList<Object>();
+ while (viewInfo != null) {
+ segments.add(0, viewInfo);
+ viewInfo = viewInfo.getParent();
+ }
+
+ return new TreePath(segments.toArray());
+ }
+
+ /**
+ * 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.
+ */
+ @Override
+ public void setSelection(ISelection selection) {
+ if (mInsideUpdateSelection) {
+ return;
+ }
+
+ boolean changed = false;
+ 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;
+ updateActionsFromSelection();
+ redraw();
+ }
+ return;
+ }
+
+ boolean redoLayout = false;
+
+ // Create a list of all currently selected view infos
+ Set<CanvasViewInfo> oldSelected = new HashSet<CanvasViewInfo>();
+ for (SelectionItem 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;
+ }
+ if (newVi.isInvisible()) {
+ redoLayout = true;
+ }
+ } else {
+ // Unrelated selection (e.g. user clicked in the Project Explorer
+ // or something) -- just ignore these
+ return;
+ }
+ }
+
+ // Deselect old selected items that are not in the new one
+ for (CanvasViewInfo vi : oldSelected) {
+ if (vi.isExploded()) {
+ redoLayout = true;
+ }
+ deselect(vi);
+ changed = true;
+ }
+
+ if (redoLayout) {
+ mCanvas.getEditorDelegate().recomputeLayout();
+ }
+ }
+ } finally {
+ mInsideUpdateSelection = false;
+ }
+
+ if (changed) {
+ redraw();
+ fireSelectionChanged();
+ updateActionsFromSelection();
+ }
+ }
+
+ /**
+ * The menu has been activated; ensure that the menu click is over the existing
+ * selection, and if not, update the selection.
+ *
+ * @param e the {@link MenuDetectEvent} which triggered the menu
+ */
+ public void menuClick(MenuDetectEvent e) {
+ LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout();
+
+ // 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.
+
+ for (SelectionItem cs : mSelections) {
+ if (cs.isRoot()) {
+ continue;
+ }
+ if (cs.getRect().contains(p.x, p.y)) {
+ // The cursor is inside the selection. Don't change anything.
+ return;
+ }
+ }
+
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+ selectSingle(vi);
+ }
+
+ /**
+ * 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 (SelectionItem 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 (vi != null && vi.isHidden()) {
+ vi = vi.getParent();
+ }
+
+ 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)) {
+ if (vi.isExploded()) {
+ mCanvas.getEditorDelegate().recomputeLayout();
+ }
+
+ 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 redraw() if the selection changes.
+ *
+ * @param vi The new selected item if non-null. Selection becomes empty if null.
+ * @return the item selected, or null if the selection was cleared (e.g. vi was null)
+ */
+ @Nullable
+ SelectionItem selectSingle(CanvasViewInfo vi) {
+ SelectionItem item = null;
+
+ // reset alternate selection if any
+ mAltSelection = null;
+
+ if (vi == null) {
+ // The user clicked outside the bounds of the root element; in that case, just
+ // select the root element.
+ vi = mCanvas.getViewHierarchy().getRoot();
+ }
+
+ boolean redoLayout = hasExplodedItems();
+
+ // 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.getFirst();
+ }
+ mSelections.clear();
+ }
+
+ if (vi != null) {
+ item = createSelection(vi);
+ mSelections.add(item);
+ if (vi.isInvisible()) {
+ redoLayout = true;
+ }
+ }
+ fireSelectionChanged();
+
+ if (redoLayout) {
+ mCanvas.getEditorDelegate().recomputeLayout();
+ }
+
+ redraw();
+
+ return item;
+ }
+
+ /** Returns true if the view hierarchy is showing exploded items. */
+ private boolean hasExplodedItems() {
+ for (SelectionItem item : mSelections) {
+ if (item.getViewInfo().isExploded()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 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;
+
+ boolean redoLayout = hasExplodedItems();
+
+ mSelections.clear();
+ if (viewInfos != null) {
+ for (CanvasViewInfo viewInfo : viewInfos) {
+ mSelections.add(createSelection(viewInfo));
+ if (viewInfo.isInvisible()) {
+ redoLayout = true;
+ }
+ }
+ }
+
+ fireSelectionChanged();
+
+ if (redoLayout) {
+ mCanvas.getEditorDelegate().recomputeLayout();
+ }
+
+ redraw();
+ }
+
+ public void select(Collection<INode> nodes) {
+ List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>(nodes.size());
+ for (INode node : nodes) {
+ CanvasViewInfo info = mCanvas.getViewHierarchy().findViewInfoFor(node);
+ if (info != null) {
+ infos.add(info);
+ }
+ }
+ selectMultiple(infos);
+ }
+
+ /**
+ * 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) {
+ if (xmlNode == null) {
+ return;
+ } else if (xmlNode.getNodeType() == Node.TEXT_NODE) {
+ xmlNode = xmlNode.getParentNode();
+ }
+
+ 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) {
+ if (viewInfo.isHidden()) {
+ continue;
+ }
+ 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();
+ }
+
+ /** Clears the selection */
+ public void selectNone() {
+ mSelections.clear();
+ mAltSelection = null;
+ fireSelectionChanged();
+ redraw();
+ }
+
+ /** Selects the parent of the current selection */
+ public void selectParent() {
+ if (mSelections.size() == 1) {
+ CanvasViewInfo parent = mSelections.get(0).getViewInfo().getParent();
+ if (parent != null) {
+ selectSingle(parent);
+ }
+ }
+ }
+
+ /** Finds all widgets in the layout that have the same type as the primary */
+ public void selectSameType() {
+ // Find all
+ if (mSelections.size() == 1) {
+ CanvasViewInfo viewInfo = mSelections.get(0).getViewInfo();
+ ElementDescriptor descriptor = viewInfo.getUiViewNode().getDescriptor();
+ mSelections.clear();
+ mAltSelection = null;
+ addSameType(mCanvas.getViewHierarchy().getRoot(), descriptor);
+ fireSelectionChanged();
+ redraw();
+ }
+ }
+
+ /** Helper for {@link #selectSameType} */
+ private void addSameType(CanvasViewInfo root, ElementDescriptor descriptor) {
+ if (root.getUiViewNode().getDescriptor() == descriptor) {
+ mSelections.add(createSelection(root));
+ }
+
+ for (CanvasViewInfo child : root.getChildren()) {
+ addSameType(child, descriptor);
+ }
+ }
+
+ /** Selects the siblings of the primary */
+ public void selectSiblings() {
+ // Find all
+ if (mSelections.size() == 1) {
+ CanvasViewInfo vi = mSelections.get(0).getViewInfo();
+ mSelections.clear();
+ mAltSelection = null;
+ CanvasViewInfo parent = vi.getParent();
+ if (parent == null) {
+ selectNone();
+ } else {
+ for (CanvasViewInfo child : parent.getChildren()) {
+ mSelections.add(createSelection(child));
+ }
+ 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<SelectionItem> it = mSelections.listIterator(); it.hasNext(); ) {
+ SelectionItem 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<SelectionItem> it = mSelections.listIterator(); it.hasNext(); ) {
+ SelectionItem s = it.next();
+ if (canvasViewInfos.contains(s.getViewInfo())) {
+ it.remove();
+ }
+ }
+ }
+
+ /** Sync the selection with an updated view info tree */
+ void sync() {
+ // Check if the selection is still the same (based on the object keys)
+ // and eventually recompute their bounds.
+ for (ListIterator<SelectionItem> it = mSelections.listIterator(); it.hasNext(); ) {
+ SelectionItem s = it.next();
+
+ // Check if the selected object still exists
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ UiViewElementNode key = s.getViewInfo().getUiViewNode();
+ CanvasViewInfo vi = viewHierarchy.findViewInfoFor(key);
+
+ // 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) {
+ vi = findCorresponding(s.getViewInfo(), viewHierarchy.getRoot());
+ }
+ if (vi != null) {
+ it.add(createSelection(vi));
+ }
+ }
+ fireSelectionChanged();
+
+ // remove the current alternate selection views
+ mAltSelection = null;
+ }
+
+ /** Finds the corresponding {@link CanvasViewInfo} in the new hierarchy */
+ private CanvasViewInfo findCorresponding(CanvasViewInfo old, CanvasViewInfo newRoot) {
+ CanvasViewInfo oldParent = old.getParent();
+ if (oldParent != null) {
+ CanvasViewInfo newParent = findCorresponding(oldParent, newRoot);
+ if (newParent == null) {
+ return null;
+ }
+
+ List<CanvasViewInfo> oldSiblings = oldParent.getChildren();
+ List<CanvasViewInfo> newSiblings = newParent.getChildren();
+ Iterator<CanvasViewInfo> oldIterator = oldSiblings.iterator();
+ Iterator<CanvasViewInfo> newIterator = newSiblings.iterator();
+ while (oldIterator.hasNext() && newIterator.hasNext()) {
+ CanvasViewInfo oldSibling = oldIterator.next();
+ CanvasViewInfo newSibling = newIterator.next();
+
+ if (oldSibling.getName().equals(newSibling.getName())) {
+ // Structure has changed: can't do a proper search
+ return null;
+ }
+
+ if (oldSibling == old) {
+ return newSibling;
+ }
+ }
+ } else {
+ return newRoot;
+ }
+
+ return 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() {
+ @Override
+ public void run() {
+ for (Object listener : mSelectionListeners.getListeners()) {
+ ((ISelectionChangedListener) listener).selectionChanged(event);
+ }
+ }
+ });
+
+ updateActionsFromSelection();
+ } finally {
+ mInsideUpdateSelection = false;
+ }
+ }
+
+ /**
+ * Updates menu actions and the layout action bar after a selection change - these are
+ * actions that depend on the selection
+ */
+ private void updateActionsFromSelection() {
+ LayoutEditorDelegate editor = mCanvas.getEditorDelegate();
+ if (editor != null) {
+ // Update menu actions that depend on the selection
+ mCanvas.updateMenuActionState();
+
+ // Update the layout actions bar
+ LayoutActionBar layoutActionBar = editor.getGraphicalEditor().getLayoutActionBar();
+ layoutActionBar.updateSelection();
+ }
+ }
+
+ /**
+ * 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<SelectionItem> selection) {
+ if (selection.isEmpty()) {
+ return;
+ }
+
+ for (Iterator<SelectionItem> it = selection.iterator(); it.hasNext(); ) {
+ SelectionItem cs = it.next();
+ CanvasViewInfo vi = cs.getViewInfo();
+ UiViewElementNode key = vi == null ? null : vi.getUiViewNode();
+ 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<SelectionItem> it2 = selection.iterator();
+ it2.hasNext(); ) {
+ SelectionItem 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;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Selects the given list of nodes in the canvas, and returns true iff the
+ * attempt to select was successful.
+ *
+ * @param nodes The collection of nodes to be selected
+ * @param indices A list of indices within the parent for each node, or null
+ * @return True if and only if all nodes were successfully selected
+ */
+ public boolean selectDropped(List<INode> nodes, List<Integer> indices) {
+ assert indices == null || nodes.size() == indices.size();
+
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+
+ // Look up a list of view infos which correspond to the nodes.
+ final Collection<CanvasViewInfo> newChildren = new ArrayList<CanvasViewInfo>();
+ for (int i = 0, n = nodes.size(); i < n; i++) {
+ INode node = nodes.get(i);
+
+ CanvasViewInfo viewInfo = viewHierarchy.findViewInfoFor(node);
+
+ // There are two scenarios where looking up a view info fails.
+ // The first one is that the node was just added and the render has not yet
+ // happened, so the ViewHierarchy has no record of the node. In this case
+ // there is nothing we can do, and the method will return false (which the
+ // caller will use to schedule a second attempt later).
+ // The second scenario is where the nodes *change identity*. This isn't
+ // common, but when a drop handler makes a lot of changes to its children,
+ // for example when dropping into a GridLayout where attributes are adjusted
+ // on nearly all the other children to update row or column attributes
+ // etc, then in some cases Eclipse's DOM model changes the identities of
+ // the nodes when applying all the edits, so the new Node we created (as
+ // well as possibly other nodes) are no longer the children we observe
+ // after the edit, and there are new copies there instead. In this case
+ // the UiViewModel also fails to map the nodes. To work around this,
+ // we track the *indices* (within the parent) during a drop, such that we
+ // know which children (according to their positions) the given nodes
+ // are supposed to map to, and then we use these view infos instead.
+ if (viewInfo == null && node instanceof NodeProxy && indices != null) {
+ INode parent = node.getParent();
+ CanvasViewInfo parentViewInfo = viewHierarchy.findViewInfoFor(parent);
+ if (parentViewInfo != null) {
+ UiViewElementNode parentUiNode = parentViewInfo.getUiViewNode();
+ if (parentUiNode != null) {
+ List<UiElementNode> children = parentUiNode.getUiChildren();
+ int index = indices.get(i);
+ if (index >= 0 && index < children.size()) {
+ UiElementNode replacedNode = children.get(index);
+ viewInfo = viewHierarchy.findViewInfoFor(replacedNode);
+ }
+ }
+ }
+ }
+
+ if (viewInfo != null) {
+ if (nodes.size() > 1 && viewInfo.isHidden()) {
+ // Skip spacers - unless you're dropping just one
+ continue;
+ }
+ if (GridLayoutRule.sDebugGridLayout && (viewInfo.getName().equals(FQCN_SPACE)
+ || viewInfo.getName().equals(FQCN_SPACE_V7))) {
+ // In debug mode they might not be marked as hidden but we never never
+ // want to select these guys
+ continue;
+ }
+ newChildren.add(viewInfo);
+ }
+ }
+ boolean found = nodes.size() == newChildren.size();
+
+ if (found || newChildren.size() > 0) {
+ mCanvas.getSelectionManager().selectMultiple(newChildren);
+ }
+
+ return found;
+ }
+
+ /**
+ * Update the outline selection to select the given nodes, asynchronously.
+ * @param nodes The nodes to be selected
+ */
+ public void setOutlineSelection(final List<INode> nodes) {
+ Display.getDefault().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ selectDropped(nodes, null /* indices */);
+ syncOutlineSelection();
+ }
+ });
+ }
+
+ /**
+ * Syncs the current selection to the outline, synchronously.
+ */
+ public void syncOutlineSelection() {
+ OutlinePage outlinePage = mCanvas.getOutlinePage();
+ IWorkbenchPartSite site = outlinePage.getEditor().getSite();
+ ISelectionProvider selectionProvider = site.getSelectionProvider();
+ ISelection selection = selectionProvider.getSelection();
+ if (selection != null) {
+ outlinePage.setSelection(selection);
+ }
+ }
+
+ private void redraw() {
+ mCanvas.redraw();
+ }
+
+ SelectionItem createSelection(CanvasViewInfo vi) {
+ return new SelectionItem(mCanvas, vi);
+ }
+
+ /**
+ * Returns true if there is nothing selected
+ *
+ * @return true if there is nothing selected
+ */
+ public boolean isEmpty() {
+ return mSelections.size() == 0;
+ }
+
+ /**
+ * "Select" context menu which lists various menu options related to selection:
+ * <ul>
+ * <li> Select All
+ * <li> Select Parent
+ * <li> Select None
+ * <li> Select Siblings
+ * <li> Select Same Type
+ * </ul>
+ * etc.
+ */
+ public static class SelectionMenu extends SubmenuAction {
+ private final GraphicalEditorPart mEditor;
+
+ public SelectionMenu(GraphicalEditorPart editor) {
+ super("Select");
+ mEditor = editor;
+ }
+
+ @Override
+ public String getId() {
+ return "-selectionmenu"; //$NON-NLS-1$
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ LayoutCanvas canvas = mEditor.getCanvasControl();
+ SelectionManager selectionManager = canvas.getSelectionManager();
+ List<SelectionItem> selections = selectionManager.getSelections();
+ boolean selectedOne = selections.size() == 1;
+ boolean notRoot = selectedOne && !selections.get(0).isRoot();
+ boolean haveSelection = selections.size() > 0;
+
+ Action a;
+ a = selectionManager.new SelectAction("Select Parent\tEsc", SELECT_PARENT);
+ new ActionContributionItem(a).fill(menu, -1);
+ a.setEnabled(notRoot);
+ a.setAccelerator(SWT.ESC);
+
+ a = selectionManager.new SelectAction("Select Siblings", SELECT_SIBLINGS);
+ new ActionContributionItem(a).fill(menu, -1);
+ a.setEnabled(notRoot);
+
+ a = selectionManager.new SelectAction("Select Same Type", SELECT_SAME_TYPE);
+ new ActionContributionItem(a).fill(menu, -1);
+ a.setEnabled(selectedOne);
+
+ new Separator().fill(menu, -1);
+
+ // Special case for Select All: Use global action
+ a = canvas.getSelectAllAction();
+ new ActionContributionItem(a).fill(menu, -1);
+ a.setEnabled(true);
+
+ a = selectionManager.new SelectAction("Deselect All", SELECT_NONE);
+ new ActionContributionItem(a).fill(menu, -1);
+ a.setEnabled(haveSelection);
+ }
+ }
+
+ private static final int SELECT_PARENT = 1;
+ private static final int SELECT_SIBLINGS = 2;
+ private static final int SELECT_SAME_TYPE = 3;
+ private static final int SELECT_NONE = 4; // SELECT_ALL is handled separately
+
+ private class SelectAction extends Action {
+ private final int mType;
+
+ public SelectAction(String title, int type) {
+ super(title, IAction.AS_PUSH_BUTTON);
+ mType = type;
+ }
+
+ @Override
+ public void run() {
+ switch (mType) {
+ case SELECT_NONE:
+ selectNone();
+ break;
+ case SELECT_PARENT:
+ selectParent();
+ break;
+ case SELECT_SAME_TYPE:
+ selectSameType();
+ break;
+ case SELECT_SIBLINGS:
+ selectSiblings();
+ break;
+ }
+
+ List<INode> nodes = new ArrayList<INode>();
+ for (SelectionItem item : getSelections()) {
+ nodes.add(item.getNode());
+ }
+ setOutlineSelection(nodes);
+ }
+ }
+
+ public Pair<SelectionItem, SelectionHandle> findHandle(ControlPoint controlPoint) {
+ if (!isEmpty()) {
+ LayoutPoint layoutPoint = controlPoint.toLayout();
+ int distance = (int) ((PIXEL_MARGIN + PIXEL_RADIUS) / mCanvas.getScale());
+
+ for (SelectionItem item : getSelections()) {
+ SelectionHandles handles = item.getSelectionHandles();
+ // See if it's over the selection handles
+ SelectionHandle handle = handles.findHandle(layoutPoint, distance);
+ if (handle != null) {
+ return Pair.of(item, handle);
+ }
+ }
+
+ }
+ return null;
+ }
+
+ /** Performs the default action provided by the currently selected view */
+ public void performDefaultAction() {
+ final List<SelectionItem> selections = getSelections();
+ if (selections.size() > 0) {
+ NodeProxy primary = selections.get(0).getNode();
+ if (primary != null) {
+ RulesEngine rulesEngine = mCanvas.getRulesEngine();
+ final String id = rulesEngine.callGetDefaultActionId(primary);
+ if (id == null) {
+ return;
+ }
+ final List<RuleAction> actions = rulesEngine.callGetContextMenu(primary);
+ if (actions == null) {
+ return;
+ }
+ RuleAction matching = null;
+ for (RuleAction a : actions) {
+ if (id.equals(a.getId())) {
+ matching = a;
+ break;
+ }
+ }
+ if (matching == null) {
+ return;
+ }
+ final List<INode> selectedNodes = new ArrayList<INode>();
+ for (SelectionItem item : selections) {
+ NodeProxy n = item.getNode();
+ if (n != null) {
+ selectedNodes.add(n);
+ }
+ }
+ final RuleAction action = matching;
+ mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(action.getTitle(),
+ new Runnable() {
+ @Override
+ public void run() {
+ action.getCallback().action(action, selectedNodes,
+ action.getId(), null);
+ LayoutCanvas canvas = mCanvas;
+ CanvasViewInfo root = canvas.getViewHierarchy().getRoot();
+ if (root != null) {
+ UiViewElementNode uiViewNode = root.getUiViewNode();
+ NodeFactory nodeFactory = canvas.getNodeFactory();
+ NodeProxy rootNode = nodeFactory.create(uiViewNode);
+ if (rootNode != null) {
+ rootNode.applyPendingChanges();
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+
+ /** Performs renaming the selected views */
+ public void performRename() {
+ final List<SelectionItem> selections = getSelections();
+ if (selections.size() > 0) {
+ NodeProxy primary = selections.get(0).getNode();
+ if (primary != null) {
+ performRename(primary, selections);
+ }
+ }
+ }
+
+ /**
+ * Performs renaming the given node.
+ *
+ * @param primary the node to be renamed, or the primary node (to get the
+ * current value from if more than one node should be renamed)
+ * @param selections if not null, a list of nodes to apply the setting to
+ * (which should include the primary)
+ * @return the result of the renaming operation
+ */
+ @NonNull
+ public RenameResult performRename(
+ final @NonNull INode primary,
+ final @Nullable List<SelectionItem> selections) {
+ String id = primary.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id != null && !id.isEmpty()) {
+ RenameResult result = RenameResourceWizard.renameResource(
+ mCanvas.getShell(),
+ mCanvas.getEditorDelegate().getGraphicalEditor().getProject(),
+ ResourceType.ID, BaseViewRule.stripIdPrefix(id), null, true /*canClear*/);
+ if (result.isCanceled()) {
+ return result;
+ } else if (!result.isUnavailable()) {
+ return result;
+ }
+ }
+ String currentId = primary.getStringAttr(ANDROID_URI, ATTR_ID);
+ currentId = BaseViewRule.stripIdPrefix(currentId);
+ InputDialog d = new InputDialog(
+ AdtPlugin.getDisplay().getActiveShell(),
+ "Set ID",
+ "New ID:",
+ currentId,
+ ResourceNameValidator.create(false, (IProject) null, ResourceType.ID));
+ if (d.open() == Window.OK) {
+ final String s = d.getValue();
+ mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel("Set ID",
+ new Runnable() {
+ @Override
+ public void run() {
+ String newId = s;
+ newId = NEW_ID_PREFIX + BaseViewRule.stripIdPrefix(s);
+ if (selections != null) {
+ for (SelectionItem item : selections) {
+ NodeProxy node = item.getNode();
+ if (node != null) {
+ node.setAttribute(ANDROID_URI, ATTR_ID, newId);
+ }
+ }
+ } else {
+ primary.setAttribute(ANDROID_URI, ATTR_ID, newId);
+ }
+
+ LayoutCanvas canvas = mCanvas;
+ CanvasViewInfo root = canvas.getViewHierarchy().getRoot();
+ if (root != null) {
+ UiViewElementNode uiViewNode = root.getUiViewNode();
+ NodeFactory nodeFactory = canvas.getNodeFactory();
+ NodeProxy rootNode = nodeFactory.create(uiViewNode);
+ if (rootNode != null) {
+ rootNode.applyPendingChanges();
+ }
+ }
+ }
+ });
+ return RenameResult.name(BaseViewRule.stripIdPrefix(s));
+ } else {
+ return RenameResult.canceled();
+ }
+ }
+}