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