Home | History | Annotate | Download | only in gle2
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
     17 
     18 import static com.android.SdkConstants.ANDROID_URI;
     19 import static com.android.SdkConstants.ATTR_ID;
     20 import static com.android.SdkConstants.FQCN_SPACE;
     21 import static com.android.SdkConstants.FQCN_SPACE_V7;
     22 import static com.android.SdkConstants.NEW_ID_PREFIX;
     23 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.PIXEL_MARGIN;
     24 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.PIXEL_RADIUS;
     25 
     26 import com.android.SdkConstants;
     27 import com.android.annotations.NonNull;
     28 import com.android.annotations.Nullable;
     29 import com.android.ide.common.api.INode;
     30 import com.android.ide.common.api.RuleAction;
     31 import com.android.ide.common.layout.BaseViewRule;
     32 import com.android.ide.common.layout.GridLayoutRule;
     33 import com.android.ide.eclipse.adt.AdtPlugin;
     34 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
     35 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
     36 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
     37 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
     38 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
     39 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
     40 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
     41 import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceWizard;
     42 import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult;
     43 import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
     44 import com.android.resources.ResourceType;
     45 import com.android.utils.Pair;
     46 
     47 import org.eclipse.core.resources.IProject;
     48 import org.eclipse.core.runtime.ListenerList;
     49 import org.eclipse.jface.action.Action;
     50 import org.eclipse.jface.action.ActionContributionItem;
     51 import org.eclipse.jface.action.IAction;
     52 import org.eclipse.jface.action.Separator;
     53 import org.eclipse.jface.dialogs.InputDialog;
     54 import org.eclipse.jface.util.SafeRunnable;
     55 import org.eclipse.jface.viewers.ISelection;
     56 import org.eclipse.jface.viewers.ISelectionChangedListener;
     57 import org.eclipse.jface.viewers.ISelectionProvider;
     58 import org.eclipse.jface.viewers.ITreeSelection;
     59 import org.eclipse.jface.viewers.SelectionChangedEvent;
     60 import org.eclipse.jface.viewers.TreePath;
     61 import org.eclipse.jface.viewers.TreeSelection;
     62 import org.eclipse.jface.window.Window;
     63 import org.eclipse.swt.SWT;
     64 import org.eclipse.swt.events.MenuDetectEvent;
     65 import org.eclipse.swt.events.MouseEvent;
     66 import org.eclipse.swt.widgets.Display;
     67 import org.eclipse.swt.widgets.Menu;
     68 import org.eclipse.ui.IWorkbenchPartSite;
     69 import org.w3c.dom.Node;
     70 
     71 import java.util.ArrayList;
     72 import java.util.Collection;
     73 import java.util.Collections;
     74 import java.util.HashSet;
     75 import java.util.Iterator;
     76 import java.util.LinkedList;
     77 import java.util.List;
     78 import java.util.ListIterator;
     79 import java.util.Set;
     80 
     81 /**
     82  * The {@link SelectionManager} manages the selection in the canvas editor.
     83  * It holds (and can be asked about) the set of selected items, and it also has
     84  * operations for manipulating the selection - such as toggling items, copying
     85  * the selection to the clipboard, etc.
     86  * <p/>
     87  * This class implements {@link ISelectionProvider} so that it can delegate
     88  * the selection provider from the {@link LayoutCanvasViewer}.
     89  * <p/>
     90  * Note that {@link LayoutCanvasViewer} sets a selection change listener on this
     91  * manager so that it can invoke its own fireSelectionChanged when the canvas'
     92  * selection changes.
     93  */
     94 public class SelectionManager implements ISelectionProvider {
     95 
     96     private LayoutCanvas mCanvas;
     97 
     98     /** The current selection list. The list is never null, however it can be empty. */
     99     private final LinkedList<SelectionItem> mSelections = new LinkedList<SelectionItem>();
    100 
    101     /** An unmodifiable view of {@link #mSelections}. */
    102     private final List<SelectionItem> mUnmodifiableSelection =
    103         Collections.unmodifiableList(mSelections);
    104 
    105     /** Barrier set when updating the selection to prevent from recursively
    106      * invoking ourselves. */
    107     private boolean mInsideUpdateSelection;
    108 
    109     /**
    110      * The <em>current</em> alternate selection, if any, which changes when the Alt key is
    111      * used during a selection. Can be null.
    112      */
    113     private CanvasAlternateSelection mAltSelection;
    114 
    115     /** List of clients listening to selection changes. */
    116     private final ListenerList mSelectionListeners = new ListenerList();
    117 
    118     /**
    119      * Constructs a new {@link SelectionManager} associated with the given layout canvas.
    120      *
    121      * @param layoutCanvas The layout canvas to create a {@link SelectionManager} for.
    122      */
    123     public SelectionManager(LayoutCanvas layoutCanvas) {
    124         mCanvas = layoutCanvas;
    125     }
    126 
    127     @Override
    128     public void addSelectionChangedListener(ISelectionChangedListener listener) {
    129         mSelectionListeners.add(listener);
    130     }
    131 
    132     @Override
    133     public void removeSelectionChangedListener(ISelectionChangedListener listener) {
    134         mSelectionListeners.remove(listener);
    135     }
    136 
    137     /**
    138      * Returns the native {@link SelectionItem} list.
    139      *
    140      * @return An immutable list of {@link SelectionItem}. Can be empty but not null.
    141      */
    142     @NonNull
    143     List<SelectionItem> getSelections() {
    144         return mUnmodifiableSelection;
    145     }
    146 
    147     /**
    148      * Return a snapshot/copy of the selection. Useful for clipboards etc where we
    149      * don't want the returned copy to be affected by future edits to the selection.
    150      *
    151      * @return A copy of the current selection. Never null.
    152      */
    153     @NonNull
    154     public List<SelectionItem> getSnapshot() {
    155         if (mSelectionListeners.isEmpty()) {
    156             return Collections.emptyList();
    157         }
    158 
    159         return new ArrayList<SelectionItem>(mSelections);
    160     }
    161 
    162     /**
    163      * Returns a {@link TreeSelection} where each {@link TreePath} item is
    164      * actually a {@link CanvasViewInfo}.
    165      */
    166     @Override
    167     public ISelection getSelection() {
    168         if (mSelections.isEmpty()) {
    169             return TreeSelection.EMPTY;
    170         }
    171 
    172         ArrayList<TreePath> paths = new ArrayList<TreePath>();
    173 
    174         for (SelectionItem cs : mSelections) {
    175             CanvasViewInfo vi = cs.getViewInfo();
    176             if (vi != null) {
    177                 paths.add(getTreePath(vi));
    178             }
    179         }
    180 
    181         return new TreeSelection(paths.toArray(new TreePath[paths.size()]));
    182     }
    183 
    184     /**
    185      * Create a {@link TreePath} from the given view info
    186      *
    187      * @param viewInfo the view info to look up a tree path for
    188      * @return a {@link TreePath} for the given view info
    189      */
    190     public static TreePath getTreePath(CanvasViewInfo viewInfo) {
    191         ArrayList<Object> segments = new ArrayList<Object>();
    192         while (viewInfo != null) {
    193             segments.add(0, viewInfo);
    194             viewInfo = viewInfo.getParent();
    195         }
    196 
    197         return new TreePath(segments.toArray());
    198     }
    199 
    200     /**
    201      * Sets the selection. It must be an {@link ITreeSelection} where each segment
    202      * of the tree path is a {@link CanvasViewInfo}. A null selection is considered
    203      * as an empty selection.
    204      * <p/>
    205      * This method is invoked by {@link LayoutCanvasViewer#setSelection(ISelection)}
    206      * in response to an <em>outside</em> selection (compatible with ours) that has
    207      * changed. Typically it means the outline selection has changed and we're
    208      * synchronizing ours to match.
    209      */
    210     @Override
    211     public void setSelection(ISelection selection) {
    212         if (mInsideUpdateSelection) {
    213             return;
    214         }
    215 
    216         boolean changed = false;
    217         try {
    218             mInsideUpdateSelection = true;
    219 
    220             if (selection == null) {
    221                 selection = TreeSelection.EMPTY;
    222             }
    223 
    224             if (selection instanceof ITreeSelection) {
    225                 ITreeSelection treeSel = (ITreeSelection) selection;
    226 
    227                 if (treeSel.isEmpty()) {
    228                     // Clear existing selection, if any
    229                     if (!mSelections.isEmpty()) {
    230                         mSelections.clear();
    231                         mAltSelection = null;
    232                         updateActionsFromSelection();
    233                         redraw();
    234                     }
    235                     return;
    236                 }
    237 
    238                 boolean redoLayout = false;
    239 
    240                 // Create a list of all currently selected view infos
    241                 Set<CanvasViewInfo> oldSelected = new HashSet<CanvasViewInfo>();
    242                 for (SelectionItem cs : mSelections) {
    243                     oldSelected.add(cs.getViewInfo());
    244                 }
    245 
    246                 // Go thru new selection and take care of selecting new items
    247                 // or marking those which are the same as in the current selection
    248                 for (TreePath path : treeSel.getPaths()) {
    249                     Object seg = path.getLastSegment();
    250                     if (seg instanceof CanvasViewInfo) {
    251                         CanvasViewInfo newVi = (CanvasViewInfo) seg;
    252                         if (oldSelected.contains(newVi)) {
    253                             // This view info is already selected. Remove it from the
    254                             // oldSelected list so that we don't deselect it later.
    255                             oldSelected.remove(newVi);
    256                         } else {
    257                             // This view info is not already selected. Select it now.
    258 
    259                             // reset alternate selection if any
    260                             mAltSelection = null;
    261                             // otherwise add it.
    262                             mSelections.add(createSelection(newVi));
    263                             changed = true;
    264                         }
    265                         if (newVi.isInvisible()) {
    266                             redoLayout = true;
    267                         }
    268                     } else {
    269                         // Unrelated selection (e.g. user clicked in the Project Explorer
    270                         // or something) -- just ignore these
    271                         return;
    272                     }
    273                 }
    274 
    275                 // Deselect old selected items that are not in the new one
    276                 for (CanvasViewInfo vi : oldSelected) {
    277                     if (vi.isExploded()) {
    278                         redoLayout = true;
    279                     }
    280                     deselect(vi);
    281                     changed = true;
    282                 }
    283 
    284                 if (redoLayout) {
    285                     mCanvas.getEditorDelegate().recomputeLayout();
    286                 }
    287             }
    288         } finally {
    289             mInsideUpdateSelection = false;
    290         }
    291 
    292         if (changed) {
    293             redraw();
    294             fireSelectionChanged();
    295             updateActionsFromSelection();
    296         }
    297     }
    298 
    299     /**
    300      * The menu has been activated; ensure that the menu click is over the existing
    301      * selection, and if not, update the selection.
    302      *
    303      * @param e the {@link MenuDetectEvent} which triggered the menu
    304      */
    305     public void menuClick(MenuDetectEvent e) {
    306         LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout();
    307 
    308         // Right click button is used to display a context menu.
    309         // If there's an existing selection and the click is anywhere in this selection
    310         // and there are no modifiers being used, we don't want to change the selection.
    311         // Otherwise we select the item under the cursor.
    312 
    313         for (SelectionItem cs : mSelections) {
    314             if (cs.isRoot()) {
    315                 continue;
    316             }
    317             if (cs.getRect().contains(p.x, p.y)) {
    318                 // The cursor is inside the selection. Don't change anything.
    319                 return;
    320             }
    321         }
    322 
    323         CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
    324         selectSingle(vi);
    325     }
    326 
    327     /**
    328      * Performs selection for a mouse event.
    329      * <p/>
    330      * Shift key (or Command on the Mac) is used to toggle in multi-selection.
    331      * Alt key is used to cycle selection through objects at the same level than
    332      * the one pointed at (i.e. click on an object then alt-click to cycle).
    333      *
    334      * @param e The mouse event which triggered the selection. Cannot be null.
    335      *            The modifier key mask will be used to determine whether this
    336      *            is a plain select or a toggle, etc.
    337      */
    338     public void select(MouseEvent e) {
    339         boolean isMultiClick = (e.stateMask & SWT.SHIFT) != 0 ||
    340             // On Mac, the Command key is the normal toggle accelerator
    341             ((SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) &&
    342                     (e.stateMask & SWT.COMMAND) != 0);
    343         boolean isCycleClick   = (e.stateMask & SWT.ALT)   != 0;
    344 
    345         LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout();
    346 
    347         if (e.button == 3) {
    348             // Right click button is used to display a context menu.
    349             // If there's an existing selection and the click is anywhere in this selection
    350             // and there are no modifiers being used, we don't want to change the selection.
    351             // Otherwise we select the item under the cursor.
    352 
    353             if (!isCycleClick && !isMultiClick) {
    354                 for (SelectionItem cs : mSelections) {
    355                     if (cs.getRect().contains(p.x, p.y)) {
    356                         // The cursor is inside the selection. Don't change anything.
    357                         return;
    358                     }
    359                 }
    360             }
    361 
    362         } else if (e.button != 1) {
    363             // Click was done with something else than the left button for normal selection
    364             // or the right button for context menu.
    365             // We don't use mouse button 2 yet (middle mouse, or scroll wheel?) for
    366             // anything, so let's not change the selection.
    367             return;
    368         }
    369 
    370         CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
    371 
    372         if (vi != null && vi.isHidden()) {
    373             vi = vi.getParent();
    374         }
    375 
    376         if (isMultiClick && !isCycleClick) {
    377             // Case where shift is pressed: pointed object is toggled.
    378 
    379             // reset alternate selection if any
    380             mAltSelection = null;
    381 
    382             // If nothing has been found at the cursor, assume it might be a user error
    383             // and avoid clearing the existing selection.
    384 
    385             if (vi != null) {
    386                 // toggle this selection on-off: remove it if already selected
    387                 if (deselect(vi)) {
    388                     if (vi.isExploded()) {
    389                         mCanvas.getEditorDelegate().recomputeLayout();
    390                     }
    391 
    392                     redraw();
    393                     return;
    394                 }
    395 
    396                 // otherwise add it.
    397                 mSelections.add(createSelection(vi));
    398                 fireSelectionChanged();
    399                 redraw();
    400             }
    401 
    402         } else if (isCycleClick) {
    403             // Case where alt is pressed: select or cycle the object pointed at.
    404 
    405             // Note: if shift and alt are pressed, shift is ignored. The alternate selection
    406             // mechanism does not reset the current multiple selection unless they intersect.
    407 
    408             // We need to remember the "origin" of the alternate selection, to be
    409             // able to continue cycling through it later. If there's no alternate selection,
    410             // create one. If there's one but not for the same origin object, create a new
    411             // one too.
    412             if (mAltSelection == null || mAltSelection.getOriginatingView() != vi) {
    413                 mAltSelection = new CanvasAlternateSelection(
    414                         vi, mCanvas.getViewHierarchy().findAltViewInfoAt(p));
    415 
    416                 // deselect them all, in case they were partially selected
    417                 deselectAll(mAltSelection.getAltViews());
    418 
    419                 // select the current one
    420                 CanvasViewInfo vi2 = mAltSelection.getCurrent();
    421                 if (vi2 != null) {
    422                     mSelections.addFirst(createSelection(vi2));
    423                     fireSelectionChanged();
    424                 }
    425             } else {
    426                 // We're trying to cycle through the current alternate selection.
    427                 // First remove the current object.
    428                 CanvasViewInfo vi2 = mAltSelection.getCurrent();
    429                 deselect(vi2);
    430 
    431                 // Now select the next one.
    432                 vi2 = mAltSelection.getNext();
    433                 if (vi2 != null) {
    434                     mSelections.addFirst(createSelection(vi2));
    435                     fireSelectionChanged();
    436                 }
    437             }
    438             redraw();
    439 
    440         } else {
    441             // Case where no modifier is pressed: either select or reset the selection.
    442             selectSingle(vi);
    443         }
    444     }
    445 
    446     /**
    447      * Removes all the currently selected item and only select the given item.
    448      * Issues a redraw() if the selection changes.
    449      *
    450      * @param vi The new selected item if non-null. Selection becomes empty if null.
    451      * @return the item selected, or null if the selection was cleared (e.g. vi was null)
    452      */
    453     @Nullable
    454     SelectionItem selectSingle(CanvasViewInfo vi) {
    455         SelectionItem item = null;
    456 
    457         // reset alternate selection if any
    458         mAltSelection = null;
    459 
    460         if (vi == null) {
    461             // The user clicked outside the bounds of the root element; in that case, just
    462             // select the root element.
    463             vi = mCanvas.getViewHierarchy().getRoot();
    464         }
    465 
    466         boolean redoLayout = hasExplodedItems();
    467 
    468         // reset (multi)selection if any
    469         if (!mSelections.isEmpty()) {
    470             if (mSelections.size() == 1 && mSelections.getFirst().getViewInfo() == vi) {
    471                 // CanvasSelection remains the same, don't touch it.
    472                 return mSelections.getFirst();
    473             }
    474             mSelections.clear();
    475         }
    476 
    477         if (vi != null) {
    478             item = createSelection(vi);
    479             mSelections.add(item);
    480             if (vi.isInvisible()) {
    481                 redoLayout = true;
    482             }
    483         }
    484         fireSelectionChanged();
    485 
    486         if (redoLayout) {
    487             mCanvas.getEditorDelegate().recomputeLayout();
    488         }
    489 
    490         redraw();
    491 
    492         return item;
    493     }
    494 
    495     /** Returns true if the view hierarchy is showing exploded items. */
    496     private boolean hasExplodedItems() {
    497         for (SelectionItem item : mSelections) {
    498             if (item.getViewInfo().isExploded()) {
    499                 return true;
    500             }
    501         }
    502 
    503         return false;
    504     }
    505 
    506     /**
    507      * Selects the given set of {@link CanvasViewInfo}s. This is similar to
    508      * {@link #selectSingle} but allows you to make a multi-selection. Issues a
    509      * {@link #redraw()}.
    510      *
    511      * @param viewInfos A collection of {@link CanvasViewInfo} objects to be
    512      *            selected, or null or empty to clear the selection.
    513      */
    514     /* package */ void selectMultiple(Collection<CanvasViewInfo> viewInfos) {
    515         // reset alternate selection if any
    516         mAltSelection = null;
    517 
    518         boolean redoLayout = hasExplodedItems();
    519 
    520         mSelections.clear();
    521         if (viewInfos != null) {
    522             for (CanvasViewInfo viewInfo : viewInfos) {
    523                 mSelections.add(createSelection(viewInfo));
    524                 if (viewInfo.isInvisible()) {
    525                     redoLayout = true;
    526                 }
    527             }
    528         }
    529 
    530         fireSelectionChanged();
    531 
    532         if (redoLayout) {
    533             mCanvas.getEditorDelegate().recomputeLayout();
    534         }
    535 
    536         redraw();
    537     }
    538 
    539     public void select(Collection<INode> nodes) {
    540         List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>(nodes.size());
    541         for (INode node : nodes) {
    542             CanvasViewInfo info = mCanvas.getViewHierarchy().findViewInfoFor(node);
    543             if (info != null) {
    544                 infos.add(info);
    545             }
    546         }
    547         selectMultiple(infos);
    548     }
    549 
    550     /**
    551      * Selects the visual element corresponding to the given XML node
    552      * @param xmlNode The Node whose element we want to select.
    553      */
    554     /* package */ void select(Node xmlNode) {
    555         if (xmlNode == null) {
    556             return;
    557         } else if (xmlNode.getNodeType() == Node.TEXT_NODE) {
    558             xmlNode = xmlNode.getParentNode();
    559         }
    560 
    561         CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoFor(xmlNode);
    562         if (vi != null && !vi.isRoot()) {
    563             selectSingle(vi);
    564         }
    565     }
    566 
    567     /**
    568      * Selects any views that overlap the given selection rectangle.
    569      *
    570      * @param topLeft The top left corner defining the selection rectangle.
    571      * @param bottomRight The bottom right corner defining the selection
    572      *            rectangle.
    573      * @param toggled A set of {@link CanvasViewInfo}s that should be toggled
    574      *            rather than just added.
    575      */
    576     public void selectWithin(LayoutPoint topLeft, LayoutPoint bottomRight,
    577             Collection<CanvasViewInfo> toggled) {
    578         // reset alternate selection if any
    579         mAltSelection = null;
    580 
    581         ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
    582         Collection<CanvasViewInfo> viewInfos = viewHierarchy.findWithin(topLeft, bottomRight);
    583 
    584         if (toggled.size() > 0) {
    585             // Copy; we're not allowed to touch the passed in collection
    586             Set<CanvasViewInfo> result = new HashSet<CanvasViewInfo>(toggled);
    587             for (CanvasViewInfo viewInfo : viewInfos) {
    588                 if (toggled.contains(viewInfo)) {
    589                     result.remove(viewInfo);
    590                 } else {
    591                     result.add(viewInfo);
    592                 }
    593             }
    594             viewInfos = result;
    595         }
    596 
    597         mSelections.clear();
    598         for (CanvasViewInfo viewInfo : viewInfos) {
    599             if (viewInfo.isHidden()) {
    600                 continue;
    601             }
    602             mSelections.add(createSelection(viewInfo));
    603         }
    604 
    605         fireSelectionChanged();
    606         redraw();
    607     }
    608 
    609     /**
    610      * Clears the selection and then selects everything (all views and all their
    611      * children).
    612      */
    613     public void selectAll() {
    614         // First clear the current selection, if any.
    615         mSelections.clear();
    616         mAltSelection = null;
    617 
    618         // Now select everything if there's a valid layout
    619         for (CanvasViewInfo vi : mCanvas.getViewHierarchy().findAllViewInfos(false)) {
    620             mSelections.add(createSelection(vi));
    621         }
    622 
    623         fireSelectionChanged();
    624         redraw();
    625     }
    626 
    627     /** Clears the selection */
    628     public void selectNone() {
    629         mSelections.clear();
    630         mAltSelection = null;
    631         fireSelectionChanged();
    632         redraw();
    633     }
    634 
    635     /** Selects the parent of the current selection */
    636     public void selectParent() {
    637         if (mSelections.size() == 1) {
    638             CanvasViewInfo parent = mSelections.get(0).getViewInfo().getParent();
    639             if (parent != null) {
    640                 selectSingle(parent);
    641             }
    642         }
    643     }
    644 
    645     /** Finds all widgets in the layout that have the same type as the primary */
    646     public void selectSameType() {
    647         // Find all
    648         if (mSelections.size() == 1) {
    649             CanvasViewInfo viewInfo = mSelections.get(0).getViewInfo();
    650             ElementDescriptor descriptor = viewInfo.getUiViewNode().getDescriptor();
    651             mSelections.clear();
    652             mAltSelection = null;
    653             addSameType(mCanvas.getViewHierarchy().getRoot(), descriptor);
    654             fireSelectionChanged();
    655             redraw();
    656         }
    657     }
    658 
    659     /** Helper for {@link #selectSameType} */
    660     private void addSameType(CanvasViewInfo root, ElementDescriptor descriptor) {
    661         if (root.getUiViewNode().getDescriptor() == descriptor) {
    662             mSelections.add(createSelection(root));
    663         }
    664 
    665         for (CanvasViewInfo child : root.getChildren()) {
    666             addSameType(child, descriptor);
    667         }
    668     }
    669 
    670     /** Selects the siblings of the primary */
    671     public void selectSiblings() {
    672         // Find all
    673         if (mSelections.size() == 1) {
    674             CanvasViewInfo vi = mSelections.get(0).getViewInfo();
    675             mSelections.clear();
    676             mAltSelection = null;
    677             CanvasViewInfo parent = vi.getParent();
    678             if (parent == null) {
    679                 selectNone();
    680             } else {
    681                 for (CanvasViewInfo child : parent.getChildren()) {
    682                     mSelections.add(createSelection(child));
    683                 }
    684                 fireSelectionChanged();
    685                 redraw();
    686             }
    687         }
    688     }
    689 
    690     /**
    691      * Returns true if and only if there is currently more than one selected
    692      * item.
    693      *
    694      * @return True if more than one item is selected
    695      */
    696     public boolean hasMultiSelection() {
    697         return mSelections.size() > 1;
    698     }
    699 
    700     /**
    701      * Deselects a view info. Returns true if the object was actually selected.
    702      * Callers are responsible for calling redraw() and updateOulineSelection()
    703      * after.
    704      * @param canvasViewInfo The item to deselect.
    705      * @return  True if the object was successfully removed from the selection.
    706      */
    707     public boolean deselect(CanvasViewInfo canvasViewInfo) {
    708         if (canvasViewInfo == null) {
    709             return false;
    710         }
    711 
    712         for (ListIterator<SelectionItem> it = mSelections.listIterator(); it.hasNext(); ) {
    713             SelectionItem s = it.next();
    714             if (canvasViewInfo == s.getViewInfo()) {
    715                 it.remove();
    716                 return true;
    717             }
    718         }
    719 
    720         return false;
    721     }
    722 
    723     /**
    724      * Deselects multiple view infos.
    725      * Callers are responsible for calling redraw() and updateOulineSelection() after.
    726      */
    727     private void deselectAll(List<CanvasViewInfo> canvasViewInfos) {
    728         for (ListIterator<SelectionItem> it = mSelections.listIterator(); it.hasNext(); ) {
    729             SelectionItem s = it.next();
    730             if (canvasViewInfos.contains(s.getViewInfo())) {
    731                 it.remove();
    732             }
    733         }
    734     }
    735 
    736     /** Sync the selection with an updated view info tree */
    737     void sync() {
    738         // Check if the selection is still the same (based on the object keys)
    739         // and eventually recompute their bounds.
    740         for (ListIterator<SelectionItem> it = mSelections.listIterator(); it.hasNext(); ) {
    741             SelectionItem s = it.next();
    742 
    743             // Check if the selected object still exists
    744             ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
    745             UiViewElementNode key = s.getViewInfo().getUiViewNode();
    746             CanvasViewInfo vi = viewHierarchy.findViewInfoFor(key);
    747 
    748             // Remove the previous selection -- if the selected object still exists
    749             // we need to recompute its bounds in case it moved so we'll insert a new one
    750             // at the same place.
    751             it.remove();
    752             if (vi == null) {
    753                 vi = findCorresponding(s.getViewInfo(), viewHierarchy.getRoot());
    754             }
    755             if (vi != null) {
    756                 it.add(createSelection(vi));
    757             }
    758         }
    759         fireSelectionChanged();
    760 
    761         // remove the current alternate selection views
    762         mAltSelection = null;
    763     }
    764 
    765     /** Finds the corresponding {@link CanvasViewInfo} in the new hierarchy */
    766     private CanvasViewInfo findCorresponding(CanvasViewInfo old, CanvasViewInfo newRoot) {
    767         CanvasViewInfo oldParent = old.getParent();
    768         if (oldParent != null) {
    769             CanvasViewInfo newParent = findCorresponding(oldParent, newRoot);
    770             if (newParent == null) {
    771                 return null;
    772             }
    773 
    774             List<CanvasViewInfo> oldSiblings = oldParent.getChildren();
    775             List<CanvasViewInfo> newSiblings = newParent.getChildren();
    776             Iterator<CanvasViewInfo> oldIterator = oldSiblings.iterator();
    777             Iterator<CanvasViewInfo> newIterator = newSiblings.iterator();
    778             while (oldIterator.hasNext() && newIterator.hasNext()) {
    779                 CanvasViewInfo oldSibling = oldIterator.next();
    780                 CanvasViewInfo newSibling = newIterator.next();
    781 
    782                 if (oldSibling.getName().equals(newSibling.getName())) {
    783                     // Structure has changed: can't do a proper search
    784                     return null;
    785                 }
    786 
    787                 if (oldSibling == old) {
    788                     return newSibling;
    789                 }
    790             }
    791         } else {
    792             return newRoot;
    793         }
    794 
    795         return null;
    796     }
    797 
    798     /**
    799      * Notifies listeners that the selection has changed.
    800      */
    801     private void fireSelectionChanged() {
    802         if (mInsideUpdateSelection) {
    803             return;
    804         }
    805         try {
    806             mInsideUpdateSelection = true;
    807 
    808             final SelectionChangedEvent event = new SelectionChangedEvent(this, getSelection());
    809 
    810             SafeRunnable.run(new SafeRunnable() {
    811                 @Override
    812                 public void run() {
    813                     for (Object listener : mSelectionListeners.getListeners()) {
    814                         ((ISelectionChangedListener) listener).selectionChanged(event);
    815                     }
    816                 }
    817             });
    818 
    819             updateActionsFromSelection();
    820         } finally {
    821             mInsideUpdateSelection = false;
    822         }
    823     }
    824 
    825     /**
    826      * Updates menu actions and the layout action bar after a selection change - these are
    827      * actions that depend on the selection
    828      */
    829     private void updateActionsFromSelection() {
    830         LayoutEditorDelegate editor = mCanvas.getEditorDelegate();
    831         if (editor != null) {
    832             // Update menu actions that depend on the selection
    833             mCanvas.updateMenuActionState();
    834 
    835             // Update the layout actions bar
    836             LayoutActionBar layoutActionBar = editor.getGraphicalEditor().getLayoutActionBar();
    837             layoutActionBar.updateSelection();
    838         }
    839     }
    840 
    841     /**
    842      * Sanitizes the selection for a copy/cut or drag operation.
    843      * <p/>
    844      * Sanitizes the list to make sure all elements have a valid XML attached to it,
    845      * that is remove element that have no XML to avoid having to make repeated such
    846      * checks in various places after.
    847      * <p/>
    848      * In case of multiple selection, we also need to remove all children when their
    849      * parent is already selected since parents will always be added with all their
    850      * children.
    851      * <p/>
    852      *
    853      * @param selection The selection list to be sanitized <b>in-place</b>.
    854      *      The <code>selection</code> argument should not be {@link #mSelections} -- the
    855      *      given list is going to be altered and we should never alter the user-made selection.
    856      *      Instead the caller should provide its own copy.
    857      */
    858     /* package */ static void sanitize(List<SelectionItem> selection) {
    859         if (selection.isEmpty()) {
    860             return;
    861         }
    862 
    863         for (Iterator<SelectionItem> it = selection.iterator(); it.hasNext(); ) {
    864             SelectionItem cs = it.next();
    865             CanvasViewInfo vi = cs.getViewInfo();
    866             UiViewElementNode key = vi == null ? null : vi.getUiViewNode();
    867             Node node = key == null ? null : key.getXmlNode();
    868             if (node == null) {
    869                 // Missing ViewInfo or view key or XML, discard this.
    870                 it.remove();
    871                 continue;
    872             }
    873 
    874             if (vi != null) {
    875                 for (Iterator<SelectionItem> it2 = selection.iterator();
    876                      it2.hasNext(); ) {
    877                     SelectionItem cs2 = it2.next();
    878                     if (cs != cs2) {
    879                         CanvasViewInfo vi2 = cs2.getViewInfo();
    880                         if (vi.isParent(vi2)) {
    881                             // vi2 is a parent for vi. Remove vi.
    882                             it.remove();
    883                             break;
    884                         }
    885                     }
    886                 }
    887             }
    888         }
    889     }
    890 
    891     /**
    892      * Selects the given list of nodes in the canvas, and returns true iff the
    893      * attempt to select was successful.
    894      *
    895      * @param nodes The collection of nodes to be selected
    896      * @param indices A list of indices within the parent for each node, or null
    897      * @return True if and only if all nodes were successfully selected
    898      */
    899     public boolean selectDropped(List<INode> nodes, List<Integer> indices) {
    900         assert indices == null || nodes.size() == indices.size();
    901 
    902         ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
    903 
    904         // Look up a list of view infos which correspond to the nodes.
    905         final Collection<CanvasViewInfo> newChildren = new ArrayList<CanvasViewInfo>();
    906         for (int i = 0, n = nodes.size(); i < n; i++) {
    907             INode node = nodes.get(i);
    908 
    909             CanvasViewInfo viewInfo = viewHierarchy.findViewInfoFor(node);
    910 
    911             // There are two scenarios where looking up a view info fails.
    912             // The first one is that the node was just added and the render has not yet
    913             // happened, so the ViewHierarchy has no record of the node. In this case
    914             // there is nothing we can do, and the method will return false (which the
    915             // caller will use to schedule a second attempt later).
    916             // The second scenario is where the nodes *change identity*. This isn't
    917             // common, but when a drop handler makes a lot of changes to its children,
    918             // for example when dropping into a GridLayout where attributes are adjusted
    919             // on nearly all the other children to update row or column attributes
    920             // etc, then in some cases Eclipse's DOM model changes the identities of
    921             // the nodes when applying all the edits, so the new Node we created (as
    922             // well as possibly other nodes) are no longer the children we observe
    923             // after the edit, and there are new copies there instead. In this case
    924             // the UiViewModel also fails to map the nodes. To work around this,
    925             // we track the *indices* (within the parent) during a drop, such that we
    926             // know which children (according to their positions) the given nodes
    927             // are supposed to map to, and then we use these view infos instead.
    928             if (viewInfo == null && node instanceof NodeProxy && indices != null) {
    929                 INode parent = node.getParent();
    930                 CanvasViewInfo parentViewInfo = viewHierarchy.findViewInfoFor(parent);
    931                 if (parentViewInfo != null) {
    932                     UiViewElementNode parentUiNode = parentViewInfo.getUiViewNode();
    933                     if (parentUiNode != null) {
    934                         List<UiElementNode> children = parentUiNode.getUiChildren();
    935                         int index = indices.get(i);
    936                         if (index >= 0 && index < children.size()) {
    937                             UiElementNode replacedNode = children.get(index);
    938                             viewInfo = viewHierarchy.findViewInfoFor(replacedNode);
    939                         }
    940                     }
    941                 }
    942             }
    943 
    944             if (viewInfo != null) {
    945                 if (nodes.size() > 1 && viewInfo.isHidden()) {
    946                     // Skip spacers - unless you're dropping just one
    947                     continue;
    948                 }
    949                 if (GridLayoutRule.sDebugGridLayout && (viewInfo.getName().equals(FQCN_SPACE)
    950                         || viewInfo.getName().equals(FQCN_SPACE_V7))) {
    951                     // In debug mode they might not be marked as hidden but we never never
    952                     // want to select these guys
    953                     continue;
    954                 }
    955                 newChildren.add(viewInfo);
    956             }
    957         }
    958         boolean found = nodes.size() == newChildren.size();
    959 
    960         if (found || newChildren.size() > 0) {
    961             mCanvas.getSelectionManager().selectMultiple(newChildren);
    962         }
    963 
    964         return found;
    965     }
    966 
    967     /**
    968      * Update the outline selection to select the given nodes, asynchronously.
    969      * @param nodes The nodes to be selected
    970      */
    971     public void setOutlineSelection(final List<INode> nodes) {
    972         Display.getDefault().asyncExec(new Runnable() {
    973             @Override
    974             public void run() {
    975                 selectDropped(nodes, null /* indices */);
    976                 syncOutlineSelection();
    977             }
    978         });
    979     }
    980 
    981     /**
    982      * Syncs the current selection to the outline, synchronously.
    983      */
    984     public void syncOutlineSelection() {
    985         OutlinePage outlinePage = mCanvas.getOutlinePage();
    986         IWorkbenchPartSite site = outlinePage.getEditor().getSite();
    987         ISelectionProvider selectionProvider = site.getSelectionProvider();
    988         ISelection selection = selectionProvider.getSelection();
    989         if (selection != null) {
    990             outlinePage.setSelection(selection);
    991         }
    992     }
    993 
    994     private void redraw() {
    995         mCanvas.redraw();
    996     }
    997 
    998     SelectionItem createSelection(CanvasViewInfo vi) {
    999         return new SelectionItem(mCanvas, vi);
   1000     }
   1001 
   1002     /**
   1003      * Returns true if there is nothing selected
   1004      *
   1005      * @return true if there is nothing selected
   1006      */
   1007     public boolean isEmpty() {
   1008         return mSelections.size() == 0;
   1009     }
   1010 
   1011     /**
   1012      * "Select" context menu which lists various menu options related to selection:
   1013      * <ul>
   1014      * <li> Select All
   1015      * <li> Select Parent
   1016      * <li> Select None
   1017      * <li> Select Siblings
   1018      * <li> Select Same Type
   1019      * </ul>
   1020      * etc.
   1021      */
   1022     public static class SelectionMenu extends SubmenuAction {
   1023         private final GraphicalEditorPart mEditor;
   1024 
   1025         public SelectionMenu(GraphicalEditorPart editor) {
   1026             super("Select");
   1027             mEditor = editor;
   1028         }
   1029 
   1030         @Override
   1031         public String getId() {
   1032             return "-selectionmenu"; //$NON-NLS-1$
   1033         }
   1034 
   1035         @Override
   1036         protected void addMenuItems(Menu menu) {
   1037             LayoutCanvas canvas = mEditor.getCanvasControl();
   1038             SelectionManager selectionManager = canvas.getSelectionManager();
   1039             List<SelectionItem> selections = selectionManager.getSelections();
   1040             boolean selectedOne = selections.size() == 1;
   1041             boolean notRoot = selectedOne && !selections.get(0).isRoot();
   1042             boolean haveSelection = selections.size() > 0;
   1043 
   1044             Action a;
   1045             a = selectionManager.new SelectAction("Select Parent\tEsc", SELECT_PARENT);
   1046             new ActionContributionItem(a).fill(menu, -1);
   1047             a.setEnabled(notRoot);
   1048             a.setAccelerator(SWT.ESC);
   1049 
   1050             a = selectionManager.new SelectAction("Select Siblings", SELECT_SIBLINGS);
   1051             new ActionContributionItem(a).fill(menu, -1);
   1052             a.setEnabled(notRoot);
   1053 
   1054             a = selectionManager.new SelectAction("Select Same Type", SELECT_SAME_TYPE);
   1055             new ActionContributionItem(a).fill(menu, -1);
   1056             a.setEnabled(selectedOne);
   1057 
   1058             new Separator().fill(menu, -1);
   1059 
   1060             // Special case for Select All: Use global action
   1061             a = canvas.getSelectAllAction();
   1062             new ActionContributionItem(a).fill(menu, -1);
   1063             a.setEnabled(true);
   1064 
   1065             a = selectionManager.new SelectAction("Deselect All", SELECT_NONE);
   1066             new ActionContributionItem(a).fill(menu, -1);
   1067             a.setEnabled(haveSelection);
   1068         }
   1069     }
   1070 
   1071     private static final int SELECT_PARENT = 1;
   1072     private static final int SELECT_SIBLINGS = 2;
   1073     private static final int SELECT_SAME_TYPE = 3;
   1074     private static final int SELECT_NONE = 4; // SELECT_ALL is handled separately
   1075 
   1076     private class SelectAction extends Action {
   1077         private final int mType;
   1078 
   1079         public SelectAction(String title, int type) {
   1080             super(title, IAction.AS_PUSH_BUTTON);
   1081             mType = type;
   1082         }
   1083 
   1084         @Override
   1085         public void run() {
   1086             switch (mType) {
   1087                 case SELECT_NONE:
   1088                     selectNone();
   1089                     break;
   1090                 case SELECT_PARENT:
   1091                     selectParent();
   1092                     break;
   1093                 case SELECT_SAME_TYPE:
   1094                     selectSameType();
   1095                     break;
   1096                 case SELECT_SIBLINGS:
   1097                     selectSiblings();
   1098                     break;
   1099             }
   1100 
   1101             List<INode> nodes = new ArrayList<INode>();
   1102             for (SelectionItem item : getSelections()) {
   1103                 nodes.add(item.getNode());
   1104             }
   1105             setOutlineSelection(nodes);
   1106         }
   1107     }
   1108 
   1109     public Pair<SelectionItem, SelectionHandle> findHandle(ControlPoint controlPoint) {
   1110         if (!isEmpty()) {
   1111             LayoutPoint layoutPoint = controlPoint.toLayout();
   1112             int distance = (int) ((PIXEL_MARGIN + PIXEL_RADIUS) / mCanvas.getScale());
   1113 
   1114             for (SelectionItem item : getSelections()) {
   1115                 SelectionHandles handles = item.getSelectionHandles();
   1116                 // See if it's over the selection handles
   1117                 SelectionHandle handle = handles.findHandle(layoutPoint, distance);
   1118                 if (handle != null) {
   1119                     return Pair.of(item, handle);
   1120                 }
   1121             }
   1122 
   1123         }
   1124         return null;
   1125     }
   1126 
   1127     /** Performs the default action provided by the currently selected view */
   1128     public void performDefaultAction() {
   1129         final List<SelectionItem> selections = getSelections();
   1130         if (selections.size() > 0) {
   1131             NodeProxy primary = selections.get(0).getNode();
   1132             if (primary != null) {
   1133                 RulesEngine rulesEngine = mCanvas.getRulesEngine();
   1134                 final String id = rulesEngine.callGetDefaultActionId(primary);
   1135                 if (id == null) {
   1136                     return;
   1137                 }
   1138                 final List<RuleAction> actions = rulesEngine.callGetContextMenu(primary);
   1139                 if (actions == null) {
   1140                     return;
   1141                 }
   1142                 RuleAction matching = null;
   1143                 for (RuleAction a : actions) {
   1144                     if (id.equals(a.getId())) {
   1145                         matching = a;
   1146                         break;
   1147                     }
   1148                 }
   1149                 if (matching == null) {
   1150                     return;
   1151                 }
   1152                 final List<INode> selectedNodes = new ArrayList<INode>();
   1153                 for (SelectionItem item : selections) {
   1154                     NodeProxy n = item.getNode();
   1155                     if (n != null) {
   1156                         selectedNodes.add(n);
   1157                     }
   1158                 }
   1159                 final RuleAction action = matching;
   1160                 mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(action.getTitle(),
   1161                     new Runnable() {
   1162                         @Override
   1163                         public void run() {
   1164                             action.getCallback().action(action, selectedNodes,
   1165                                     action.getId(), null);
   1166                             LayoutCanvas canvas = mCanvas;
   1167                             CanvasViewInfo root = canvas.getViewHierarchy().getRoot();
   1168                             if (root != null) {
   1169                                 UiViewElementNode uiViewNode = root.getUiViewNode();
   1170                                 NodeFactory nodeFactory = canvas.getNodeFactory();
   1171                                 NodeProxy rootNode = nodeFactory.create(uiViewNode);
   1172                                 if (rootNode != null) {
   1173                                     rootNode.applyPendingChanges();
   1174                                 }
   1175                             }
   1176                         }
   1177                 });
   1178             }
   1179         }
   1180     }
   1181 
   1182     /** Performs renaming the selected views */
   1183     public void performRename() {
   1184         final List<SelectionItem> selections = getSelections();
   1185         if (selections.size() > 0) {
   1186             NodeProxy primary = selections.get(0).getNode();
   1187             if (primary != null) {
   1188                 performRename(primary, selections);
   1189             }
   1190         }
   1191     }
   1192 
   1193     /**
   1194      * Performs renaming the given node.
   1195      *
   1196      * @param primary the node to be renamed, or the primary node (to get the
   1197      *            current value from if more than one node should be renamed)
   1198      * @param selections if not null, a list of nodes to apply the setting to
   1199      *            (which should include the primary)
   1200      * @return the result of the renaming operation
   1201      */
   1202     @NonNull
   1203     public RenameResult performRename(
   1204             final @NonNull INode primary,
   1205             final @Nullable List<SelectionItem> selections) {
   1206         String id = primary.getStringAttr(ANDROID_URI, ATTR_ID);
   1207         if (id != null && !id.isEmpty()) {
   1208             RenameResult result = RenameResourceWizard.renameResource(
   1209                     mCanvas.getShell(),
   1210                     mCanvas.getEditorDelegate().getGraphicalEditor().getProject(),
   1211                     ResourceType.ID, BaseViewRule.stripIdPrefix(id), null, true /*canClear*/);
   1212             if (result.isCanceled()) {
   1213                 return result;
   1214             } else if (!result.isUnavailable()) {
   1215                 return result;
   1216             }
   1217         }
   1218         String currentId = primary.getStringAttr(ANDROID_URI, ATTR_ID);
   1219         currentId = BaseViewRule.stripIdPrefix(currentId);
   1220         InputDialog d = new InputDialog(
   1221                     AdtPlugin.getDisplay().getActiveShell(),
   1222                     "Set ID",
   1223                     "New ID:",
   1224                     currentId,
   1225                     ResourceNameValidator.create(false, (IProject) null, ResourceType.ID));
   1226         if (d.open() == Window.OK) {
   1227             final String s = d.getValue();
   1228             mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel("Set ID",
   1229                     new Runnable() {
   1230                 @Override
   1231                 public void run() {
   1232                     String newId = s;
   1233                     newId = NEW_ID_PREFIX + BaseViewRule.stripIdPrefix(s);
   1234                     if (selections != null) {
   1235                         for (SelectionItem item : selections) {
   1236                             NodeProxy node = item.getNode();
   1237                             if (node != null) {
   1238                                 node.setAttribute(ANDROID_URI, ATTR_ID, newId);
   1239                             }
   1240                         }
   1241                     } else {
   1242                         primary.setAttribute(ANDROID_URI, ATTR_ID, newId);
   1243                     }
   1244 
   1245                     LayoutCanvas canvas = mCanvas;
   1246                     CanvasViewInfo root = canvas.getViewHierarchy().getRoot();
   1247                     if (root != null) {
   1248                         UiViewElementNode uiViewNode = root.getUiViewNode();
   1249                         NodeFactory nodeFactory = canvas.getNodeFactory();
   1250                         NodeProxy rootNode = nodeFactory.create(uiViewNode);
   1251                         if (rootNode != null) {
   1252                             rootNode.applyPendingChanges();
   1253                         }
   1254                     }
   1255                 }
   1256             });
   1257             return RenameResult.name(BaseViewRule.stripIdPrefix(s));
   1258         } else {
   1259             return RenameResult.canceled();
   1260         }
   1261     }
   1262 }
   1263