blob: eb3d6f2901715d489a3fe2eeb742ad18f9488386 [file] [log] [blame]
/*
* 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();
}
}
}