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 */ 107 public SelectionManager(LayoutCanvas layoutCanvas) { 108 this.mCanvas = layoutCanvas; 109 } 110 111 @Override 112 public void addSelectionChangedListener(ISelectionChangedListener listener) { 113 mSelectionListeners.add(listener); 114 } 115 116 @Override 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 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 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 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 */ 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 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 */ 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 */ 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 */ 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. */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 605 public void selectNone() { 606 mSelections.clear(); 607 mAltSelection = null; 608 fireSelectionChanged(); 609 redraw(); 610 } 611 612 /** Selects the parent of the current selection */ 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 */ 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} */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 971 private void redraw() { 972 mCanvas.redraw(); 973 } 974 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 */ 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 1002 public SelectionMenu(GraphicalEditorPart editor) { 1003 super("Select"); 1004 mEditor = editor; 1005 } 1006 1007 @Override 1008 public String getId() { 1009 return "-selectionmenu"; //$NON-NLS-1$ 1010 } 1011 1012 @Override 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 1056 public SelectAction(String title, int type) { 1057 super(title, IAction.AS_PUSH_BUTTON); 1058 mType = type; 1059 } 1060 1061 @Override 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 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