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