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