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