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