2 * Copyright (C) 2010 The Android Open Source Project
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
8 * http://www.eclipse.org/org/documents/epl-v10.php
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.
16 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
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;
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;
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;
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;
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.
69 * This class implements {@link ISelectionProvider} so that it can delegate
70 * the selection provider from the {@link LayoutCanvasViewer}.
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'
76 public class SelectionManager implements ISelectionProvider {
78 private LayoutCanvas mCanvas;
80 /** The current selection list. The list is never null, however it can be empty. */
81 private final LinkedList<SelectionItem> mSelections = new LinkedList<SelectionItem>();
83 /** An unmodifiable view of {@link #mSelections}. */
84 private final List<SelectionItem> mUnmodifiableSelection =
85 Collections.unmodifiableList(mSelections);
87 /** Barrier set when updating the selection to prevent from recursively
88 * invoking ourselves. */
89 private boolean mInsideUpdateSelection;
92 * The <em>current</em> alternate selection, if any, which changes when the Alt key is
93 * used during a selection. Can be null.
95 private CanvasAlternateSelection mAltSelection;
97 /** List of clients listening to selection changes. */
98 private final ListenerList mSelectionListeners = new ListenerList();
101 * Constructs a new {@link SelectionManager} associated with the given layout canvas.
103 * @param layoutCanvas The layout canvas to create a {@link SelectionManager} for.
105 public SelectionManager(LayoutCanvas layoutCanvas) {
106 this.mCanvas = layoutCanvas;
109 public void addSelectionChangedListener(ISelectionChangedListener listener) {
110 mSelectionListeners.add(listener);
113 public void removeSelectionChangedListener(ISelectionChangedListener listener) {
114 mSelectionListeners.remove(listener);
118 * Returns the native {@link SelectionItem} list.
120 * @return An immutable list of {@link SelectionItem}. Can be empty but not null.
122 List<SelectionItem> getSelections() {
123 return mUnmodifiableSelection;
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.
130 * @return A copy of the current selection. Never null.
132 /* package */ List<SelectionItem> getSnapshot() {
133 return new ArrayList<SelectionItem>(mSelections);
137 * Returns a {@link TreeSelection} where each {@link TreePath} item is
138 * actually a {@link CanvasViewInfo}.
140 public ISelection getSelection() {
141 if (mSelections.isEmpty()) {
142 return TreeSelection.EMPTY;
145 ArrayList<TreePath> paths = new ArrayList<TreePath>();
147 for (SelectionItem cs : mSelections) {
148 CanvasViewInfo vi = cs.getViewInfo();
150 paths.add(getTreePath(vi));
154 return new TreeSelection(paths.toArray(new TreePath[paths.size()]));
158 * Create a {@link TreePath} from the given view info
160 * @param viewInfo the view info to look up a tree path for
161 * @return a {@link TreePath} for the given view info
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();
170 return new TreePath(segments.toArray());
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.
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.
183 public void setSelection(ISelection selection) {
184 if (mInsideUpdateSelection) {
189 mInsideUpdateSelection = true;
191 if (selection == null) {
192 selection = TreeSelection.EMPTY;
195 if (selection instanceof ITreeSelection) {
196 ITreeSelection treeSel = (ITreeSelection) selection;
198 if (treeSel.isEmpty()) {
199 // Clear existing selection, if any
200 if (!mSelections.isEmpty()) {
202 mAltSelection = null;
203 updateActionsFromSelection();
209 boolean changed = false;
210 boolean redoLayout = false;
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());
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);
229 // This view info is not already selected. Select it now.
231 // reset alternate selection if any
232 mAltSelection = null;
234 mSelections.add(createSelection(newVi));
237 if (newVi.isInvisible()) {
243 // Deselect old selected items that are not in the new one
244 for (CanvasViewInfo vi : oldSelected) {
245 if (vi.isExploded()) {
253 mCanvas.getLayoutEditor().recomputeLayout();
257 updateActionsFromSelection();
262 mInsideUpdateSelection = false;
267 * The menu has been activated; ensure that the menu click is over the existing
268 * selection, and if not, update the selection.
270 * @param e the {@link MenuDetectEvent} which triggered the menu
272 public void menuClick(MenuDetectEvent e) {
273 LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout();
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.
280 for (SelectionItem cs : mSelections) {
284 if (cs.getRect().contains(p.x, p.y)) {
285 // The cursor is inside the selection. Don't change anything.
290 CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
295 * Performs selection for a mouse event.
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).
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.
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;
312 LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout();
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.
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.
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.
337 CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
339 if (vi != null && vi.isHidden()) {
343 if (isMultiClick && !isCycleClick) {
344 // Case where shift is pressed: pointed object is toggled.
346 // reset alternate selection if any
347 mAltSelection = null;
349 // If nothing has been found at the cursor, assume it might be a user error
350 // and avoid clearing the existing selection.
353 // toggle this selection on-off: remove it if already selected
355 if (vi.isExploded()) {
356 mCanvas.getLayoutEditor().recomputeLayout();
364 mSelections.add(createSelection(vi));
365 fireSelectionChanged();
369 } else if (isCycleClick) {
370 // Case where alt is pressed: select or cycle the object pointed at.
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.
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
379 if (mAltSelection == null || mAltSelection.getOriginatingView() != vi) {
380 mAltSelection = new CanvasAlternateSelection(
381 vi, mCanvas.getViewHierarchy().findAltViewInfoAt(p));
383 // deselect them all, in case they were partially selected
384 deselectAll(mAltSelection.getAltViews());
386 // select the current one
387 CanvasViewInfo vi2 = mAltSelection.getCurrent();
389 mSelections.addFirst(createSelection(vi2));
390 fireSelectionChanged();
393 // We're trying to cycle through the current alternate selection.
394 // First remove the current object.
395 CanvasViewInfo vi2 = mAltSelection.getCurrent();
398 // Now select the next one.
399 vi2 = mAltSelection.getNext();
401 mSelections.addFirst(createSelection(vi2));
402 fireSelectionChanged();
408 // Case where no modifier is pressed: either select or reset the selection.
414 * Removes all the currently selected item and only select the given item.
415 * Issues a {@link #redraw()} if the selection changes.
417 * @param vi The new selected item if non-null. Selection becomes empty if null.
419 /* package */ void selectSingle(CanvasViewInfo vi) {
420 // reset alternate selection if any
421 mAltSelection = 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();
429 boolean redoLayout = hasExplodedItems();
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.
441 mSelections.add(createSelection(vi));
442 if (vi.isInvisible()) {
446 fireSelectionChanged();
449 mCanvas.getLayoutEditor().recomputeLayout();
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()) {
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
471 * @param viewInfos A collection of {@link CanvasViewInfo} objects to be
472 * selected, or null or empty to clear the selection.
474 /* package */ void selectMultiple(Collection<CanvasViewInfo> viewInfos) {
475 // reset alternate selection if any
476 mAltSelection = null;
478 boolean redoLayout = hasExplodedItems();
481 if (viewInfos != null) {
482 for (CanvasViewInfo viewInfo : viewInfos) {
483 mSelections.add(createSelection(viewInfo));
484 if (viewInfo.isInvisible()) {
490 fireSelectionChanged();
493 mCanvas.getLayoutEditor().recomputeLayout();
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);
507 selectMultiple(infos);
511 * Selects the visual element corresponding to the given XML node
512 * @param xmlNode The Node whose element we want to select.
514 /* package */ void select(Node xmlNode) {
515 if (xmlNode == null) {
517 } else if (xmlNode.getNodeType() == Node.TEXT_NODE) {
518 xmlNode = xmlNode.getParentNode();
521 CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoFor(xmlNode);
522 if (vi != null && !vi.isRoot()) {
528 * Selects any views that overlap the given selection rectangle.
530 * @param topLeft The top left corner defining the selection rectangle.
531 * @param bottomRight The bottom right corner defining the selection
533 * @param toggled A set of {@link CanvasViewInfo}s that should be toggled
534 * rather than just added.
536 public void selectWithin(LayoutPoint topLeft, LayoutPoint bottomRight,
537 Collection<CanvasViewInfo> toggled) {
538 // reset alternate selection if any
539 mAltSelection = null;
541 ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
542 Collection<CanvasViewInfo> viewInfos = viewHierarchy.findWithin(topLeft, bottomRight);
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);
551 result.add(viewInfo);
558 for (CanvasViewInfo viewInfo : viewInfos) {
559 if (viewInfo.isHidden()) {
562 mSelections.add(createSelection(viewInfo));
565 fireSelectionChanged();
570 * Clears the selection and then selects everything (all views and all their
573 public void selectAll() {
574 // First clear the current selection, if any.
576 mAltSelection = null;
578 // Now select everything if there's a valid layout
579 for (CanvasViewInfo vi : mCanvas.getViewHierarchy().findAllViewInfos(false)) {
580 mSelections.add(createSelection(vi));
583 fireSelectionChanged();
587 /** Clears the selection */
588 public void selectNone() {
590 mAltSelection = null;
591 fireSelectionChanged();
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);
605 /** Finds all widgets in the layout that have the same type as the primary */
606 public void selectSameType() {
608 if (mSelections.size() == 1) {
609 CanvasViewInfo viewInfo = mSelections.get(0).getViewInfo();
610 ElementDescriptor descriptor = viewInfo.getUiViewNode().getDescriptor();
612 mAltSelection = null;
613 addSameType(mCanvas.getViewHierarchy().getRoot(), descriptor);
614 fireSelectionChanged();
619 /** Helper for {@link #selectSameType} */
620 private void addSameType(CanvasViewInfo root, ElementDescriptor descriptor) {
621 if (root.getUiViewNode().getDescriptor() == descriptor) {
622 mSelections.add(createSelection(root));
625 for (CanvasViewInfo child : root.getChildren()) {
626 addSameType(child, descriptor);
630 /** Selects the siblings of the primary */
631 public void selectSiblings() {
633 if (mSelections.size() == 1) {
634 CanvasViewInfo vi = mSelections.get(0).getViewInfo();
636 mAltSelection = null;
637 CanvasViewInfo parent = vi.getParent();
638 if (parent == null) {
641 for (CanvasViewInfo child : parent.getChildren()) {
642 mSelections.add(createSelection(child));
644 fireSelectionChanged();
651 * Returns true if and only if there is currently more than one selected
654 * @return True if more than one item is selected
656 public boolean hasMultiSelection() {
657 return mSelections.size() > 1;
661 * Deselects a view info. Returns true if the object was actually selected.
662 * Callers are responsible for calling redraw() and updateOulineSelection()
664 * @param canvasViewInfo The item to deselect.
665 * @return True if the object was successfully removed from the selection.
667 public boolean deselect(CanvasViewInfo canvasViewInfo) {
668 if (canvasViewInfo == null) {
672 for (ListIterator<SelectionItem> it = mSelections.listIterator(); it.hasNext(); ) {
673 SelectionItem s = it.next();
674 if (canvasViewInfo == s.getViewInfo()) {
684 * Deselects multiple view infos.
685 * Callers are responsible for calling redraw() and updateOulineSelection() after.
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())) {
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();
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);
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.
713 it.add(createSelection(vi));
716 fireSelectionChanged();
718 // remove the current alternate selection views
719 mAltSelection = null;
723 * Notifies listeners that the selection has changed.
725 private void fireSelectionChanged() {
726 if (mInsideUpdateSelection) {
730 mInsideUpdateSelection = true;
732 final SelectionChangedEvent event = new SelectionChangedEvent(this, getSelection());
734 SafeRunnable.run(new SafeRunnable() {
736 for (Object listener : mSelectionListeners.getListeners()) {
737 ((ISelectionChangedListener) listener).selectionChanged(event);
742 updateActionsFromSelection();
744 mInsideUpdateSelection = false;
749 * Updates menu actions and the layout action bar after a selection change - these are
750 * actions that depend on the selection
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();
758 // Update the layout actions bar
759 LayoutActionBar layoutActionBar = editor.getGraphicalEditor().getLayoutActionBar();
760 layoutActionBar.updateSelection();
765 * Sanitizes the selection for a copy/cut or drag operation.
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.
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
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.
781 /* package */ static void sanitize(List<SelectionItem> selection) {
782 if (selection.isEmpty()) {
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();
792 // Missing ViewInfo or view key or XML, discard this.
798 for (Iterator<SelectionItem> it2 = selection.iterator();
800 SelectionItem cs2 = it2.next();
802 CanvasViewInfo vi2 = cs2.getViewInfo();
803 if (vi.isParent(vi2)) {
804 // vi2 is a parent for vi. Remove vi.
815 * Selects the given list of nodes in the canvas, and returns true iff the
816 * attempt to select was successful.
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
822 public boolean selectDropped(List<INode> nodes, List<Integer> indices) {
823 assert indices == null || nodes.size() == indices.size();
825 ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
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);
832 CanvasViewInfo viewInfo = viewHierarchy.findViewInfoFor(node);
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);
867 if (viewInfo != null) {
868 if (nodes.size() > 1 && viewInfo.isHidden()) {
869 // Skip spacers - unless you're dropping just one
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
877 newChildren.add(viewInfo);
880 mCanvas.getSelectionManager().selectMultiple(newChildren);
882 return nodes.size() == newChildren.size();
886 * Update the outline selection to select the given nodes, asynchronously.
887 * @param nodes The nodes to be selected
889 public void setOutlineSelection(final List<INode> nodes) {
890 Display.getDefault().asyncExec(new Runnable() {
892 selectDropped(nodes, null /* indices */);
893 syncOutlineSelection();
899 * Syncs the current selection to the outline, synchronously.
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);
911 private void redraw() {
915 SelectionItem createSelection(CanvasViewInfo vi) {
916 return new SelectionItem(mCanvas, vi);
920 * Returns true if there is nothing selected
922 * @return true if there is nothing selected
924 public boolean isEmpty() {
925 return mSelections.size() == 0;
929 * "Select" context menu which lists various menu options related to selection:
934 * <li> Select Siblings
935 * <li> Select Same Type
939 public static class SelectionMenu extends SubmenuAction {
940 private final GraphicalEditorPart mEditor;
942 public SelectionMenu(GraphicalEditorPart editor) {
948 public String getId() {
949 return "-selectionmenu"; //$NON-NLS-1$
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;
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);
967 a = selectionManager.new SelectAction("Select Siblings", SELECT_SIBLINGS);
968 new ActionContributionItem(a).fill(menu, -1);
969 a.setEnabled(notRoot);
971 a = selectionManager.new SelectAction("Select Same Type", SELECT_SAME_TYPE);
972 new ActionContributionItem(a).fill(menu, -1);
973 a.setEnabled(selectedOne);
975 new Separator().fill(menu, -1);
977 // Special case for Select All: Use global action
978 a = canvas.getSelectAllAction();
979 new ActionContributionItem(a).fill(menu, -1);
982 a = selectionManager.new SelectAction("Select None", SELECT_NONE);
983 new ActionContributionItem(a).fill(menu, -1);
984 a.setEnabled(haveSelection);
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
993 private class SelectAction extends Action {
994 private final int mType;
996 public SelectAction(String title, int type) {
997 super(title, IAction.AS_PUSH_BUTTON);
1010 case SELECT_SAME_TYPE:
1013 case SELECT_SIBLINGS:
1018 List<INode> nodes = new ArrayList<INode>();
1019 for (SelectionItem item : getSelections()) {
1020 nodes.add(item.getNode());
1022 setOutlineSelection(nodes);
1026 public Pair<SelectionItem, SelectionHandle> findHandle(ControlPoint controlPoint) {
1028 LayoutPoint layoutPoint = controlPoint.toLayout();
1029 int distance = (int) ((PIXEL_MARGIN + PIXEL_RADIUS) / mCanvas.getScale());
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);