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