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