diff options
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.java | 1262 |
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(); + } + } +} |