/* * 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(); } } }