OSDN Git Service

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