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