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 17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2; 18 19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; 20 import static com.android.ide.common.layout.LayoutConstants.ATTR_CLASS; 21 import static com.android.ide.common.layout.LayoutConstants.ATTR_COLUMN_COUNT; 22 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN; 23 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN_SPAN; 24 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW; 25 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW_SPAN; 26 import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION; 27 import static com.android.ide.common.layout.LayoutConstants.ATTR_ROW_COUNT; 28 import static com.android.ide.common.layout.LayoutConstants.ATTR_SRC; 29 import static com.android.ide.common.layout.LayoutConstants.ATTR_TEXT; 30 import static com.android.ide.common.layout.LayoutConstants.DRAWABLE_PREFIX; 31 import static com.android.ide.common.layout.LayoutConstants.GRID_LAYOUT; 32 import static com.android.ide.common.layout.LayoutConstants.LAYOUT_PREFIX; 33 import static com.android.ide.common.layout.LayoutConstants.LINEAR_LAYOUT; 34 import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL; 35 import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_VIEWTAG; 36 import static com.android.tools.lint.detector.api.LintConstants.AUTO_URI; 37 import static com.android.tools.lint.detector.api.LintConstants.URI_PREFIX; 38 import static org.eclipse.jface.viewers.StyledString.QUALIFIER_STYLER; 39 40 import com.android.annotations.VisibleForTesting; 41 import com.android.ide.common.api.INode; 42 import com.android.ide.common.api.InsertType; 43 import com.android.ide.common.layout.BaseLayoutRule; 44 import com.android.ide.common.layout.GridLayoutRule; 45 import com.android.ide.eclipse.adt.AdtPlugin; 46 import com.android.ide.eclipse.adt.internal.editors.IconFactory; 47 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; 48 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; 49 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 50 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; 51 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; 52 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; 53 import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertySheetPage; 54 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; 55 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; 56 import com.android.ide.eclipse.adt.internal.editors.ui.ErrorImageComposite; 57 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 58 import com.android.ide.eclipse.adt.internal.sdk.ProjectState; 59 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 60 import com.android.util.Pair; 61 62 import org.eclipse.core.resources.IProject; 63 import org.eclipse.jface.action.Action; 64 import org.eclipse.jface.action.ActionContributionItem; 65 import org.eclipse.jface.action.IAction; 66 import org.eclipse.jface.action.IContributionItem; 67 import org.eclipse.jface.action.IMenuListener; 68 import org.eclipse.jface.action.IMenuManager; 69 import org.eclipse.jface.action.IToolBarManager; 70 import org.eclipse.jface.action.MenuManager; 71 import org.eclipse.jface.action.Separator; 72 import org.eclipse.jface.preference.JFacePreferences; 73 import org.eclipse.jface.viewers.DoubleClickEvent; 74 import org.eclipse.jface.viewers.IDoubleClickListener; 75 import org.eclipse.jface.viewers.IElementComparer; 76 import org.eclipse.jface.viewers.ISelection; 77 import org.eclipse.jface.viewers.ITreeContentProvider; 78 import org.eclipse.jface.viewers.ITreeSelection; 79 import org.eclipse.jface.viewers.StyledCellLabelProvider; 80 import org.eclipse.jface.viewers.StyledString; 81 import org.eclipse.jface.viewers.StyledString.Styler; 82 import org.eclipse.jface.viewers.TreePath; 83 import org.eclipse.jface.viewers.TreeSelection; 84 import org.eclipse.jface.viewers.TreeViewer; 85 import org.eclipse.jface.viewers.Viewer; 86 import org.eclipse.jface.viewers.ViewerCell; 87 import org.eclipse.swt.SWT; 88 import org.eclipse.swt.dnd.DND; 89 import org.eclipse.swt.dnd.Transfer; 90 import org.eclipse.swt.events.DisposeEvent; 91 import org.eclipse.swt.events.DisposeListener; 92 import org.eclipse.swt.events.KeyEvent; 93 import org.eclipse.swt.events.KeyListener; 94 import org.eclipse.swt.events.MenuDetectEvent; 95 import org.eclipse.swt.events.MenuDetectListener; 96 import org.eclipse.swt.graphics.Image; 97 import org.eclipse.swt.widgets.Composite; 98 import org.eclipse.swt.widgets.Control; 99 import org.eclipse.swt.widgets.Tree; 100 import org.eclipse.swt.widgets.TreeItem; 101 import org.eclipse.ui.IActionBars; 102 import org.eclipse.ui.IEditorPart; 103 import org.eclipse.ui.INullSelectionListener; 104 import org.eclipse.ui.IWorkbenchPart; 105 import org.eclipse.ui.actions.ActionFactory; 106 import org.eclipse.ui.views.contentoutline.ContentOutlinePage; 107 import org.eclipse.wb.core.controls.SelfOrientingSashForm; 108 import org.eclipse.wb.internal.core.editor.structure.IPage; 109 import org.eclipse.wb.internal.core.editor.structure.PageSiteComposite; 110 import org.w3c.dom.Element; 111 import org.w3c.dom.Node; 112 113 import java.util.ArrayList; 114 import java.util.HashSet; 115 import java.util.List; 116 import java.util.Set; 117 118 /** 119 * An outline page for the layout canvas view. 120 * <p/> 121 * The page is created by {@link LayoutEditorDelegate#delegateGetAdapter(Class)}. This means 122 * we have *one* instance of the outline page per open canvas editor. 123 * <p/> 124 * It sets itself as a listener on the site's selection service in order to be 125 * notified of the canvas' selection changes. 126 * The underlying page is also a selection provider (via IContentOutlinePage) 127 * and as such it will broadcast selection changes to the site's selection service 128 * (on which both the layout editor part and the property sheet page listen.) 129 */ 130 public class OutlinePage extends ContentOutlinePage 131 implements INullSelectionListener, IPage { 132 133 /** Label which separates outline text from additional attributes like text prefix or url */ 134 private static final String LABEL_SEPARATOR = " - "; 135 136 /** Max character count in labels, used for truncation */ 137 private static final int LABEL_MAX_WIDTH = 50; 138 139 /** 140 * The graphical editor that created this outline. 141 */ 142 private final GraphicalEditorPart mGraphicalEditorPart; 143 144 /** 145 * RootWrapper is a workaround: we can't set the input of the TreeView to its root 146 * element, so we introduce a fake parent. 147 */ 148 private final RootWrapper mRootWrapper = new RootWrapper(); 149 150 /** 151 * Menu manager for the context menu actions. 152 * The actions delegate to the current GraphicalEditorPart. 153 */ 154 private MenuManager mMenuManager; 155 156 private Composite mControl; 157 private PropertySheetPage mPropertySheet; 158 private PageSiteComposite mPropertySheetComposite; 159 private boolean mShowPropertySheet; 160 private boolean mShowHeader; 161 private boolean mIgnoreSelection; 162 private boolean mActive = true; 163 164 /** Action to Select All in the tree */ 165 private final Action mTreeSelectAllAction = new Action() { 166 @Override 167 public void run() { 168 getTreeViewer().getTree().selectAll(); 169 OutlinePage.this.fireSelectionChanged(getSelection()); 170 } 171 172 @Override 173 public String getId() { 174 return ActionFactory.SELECT_ALL.getId(); 175 } 176 }; 177 178 /** Action for moving items up in the tree */ 179 private Action mMoveUpAction = new Action("Move Up\t-", 180 IconFactory.getInstance().getImageDescriptor("up")) { //$NON-NLS-1$ 181 182 @Override 183 public String getId() { 184 return "adt.outline.moveup"; //$NON-NLS-1$ 185 } 186 187 @Override 188 public boolean isEnabled() { 189 return canMove(false); 190 } 191 192 @Override 193 public void run() { 194 move(false); 195 } 196 }; 197 198 /** Action for moving items down in the tree */ 199 private Action mMoveDownAction = new Action("Move Down\t+", 200 IconFactory.getInstance().getImageDescriptor("down")) { //$NON-NLS-1$ 201 202 @Override 203 public String getId() { 204 return "adt.outline.movedown"; //$NON-NLS-1$ 205 } 206 207 @Override 208 public boolean isEnabled() { 209 return canMove(true); 210 } 211 212 @Override 213 public void run() { 214 move(true); 215 } 216 }; 217 218 /** 219 * Creates a new {@link OutlinePage} associated with the given editor 220 * 221 * @param graphicalEditorPart the editor associated with this outline 222 */ 223 public OutlinePage(GraphicalEditorPart graphicalEditorPart) { 224 super(); 225 mGraphicalEditorPart = graphicalEditorPart; 226 } 227 228 @Override 229 public Control getControl() { 230 // We've injected some controls between the root of the outline page 231 // and the tree control, so return the actual root (a sash form) rather 232 // than the superclass' implementation which returns the tree. If we don't 233 // do this, various checks in the outline page which checks that getControl().getParent() 234 // is the outline window itself will ignore this page. 235 return mControl; 236 } 237 238 void setActive(boolean active) { 239 if (active != mActive) { 240 mActive = active; 241 242 // Outlines are by default active when they are created; this is intended 243 // for deactivating a hidden outline and later reactivating it 244 assert mControl != null; 245 if (active) { 246 getSite().getPage().addSelectionListener(this); 247 setModel(mGraphicalEditorPart.getCanvasControl().getViewHierarchy().getRoot()); 248 } else { 249 getSite().getPage().removeSelectionListener(this); 250 mRootWrapper.setRoot(null); 251 if (mPropertySheet != null) { 252 mPropertySheet.selectionChanged(null, TreeSelection.EMPTY); 253 } 254 } 255 } 256 } 257 258 /** 259 * Set whether the outline should be shown in the header 260 * 261 * @param show whether a header should be shown 262 */ 263 public void setShowHeader(boolean show) { 264 mShowHeader = show; 265 } 266 267 /** 268 * Set whether the property sheet should be shown within this outline 269 * 270 * @param show whether the property sheet should show 271 */ 272 public void setShowPropertySheet(boolean show) { 273 if (show != mShowPropertySheet) { 274 mShowPropertySheet = show; 275 if (mControl == null) { 276 return; 277 } 278 279 if (show && mPropertySheet == null) { 280 createPropertySheet(); 281 } else if (!show) { 282 mPropertySheetComposite.dispose(); 283 mPropertySheetComposite = null; 284 mPropertySheet.dispose(); 285 mPropertySheet = null; 286 } 287 288 mControl.layout(); 289 } 290 } 291 292 @Override 293 public void createControl(Composite parent) { 294 mControl = new SelfOrientingSashForm(parent, SWT.VERTICAL); 295 296 if (mShowHeader) { 297 PageSiteComposite mOutlineComposite = new PageSiteComposite(mControl, SWT.BORDER); 298 mOutlineComposite.setTitleText("Outline"); 299 mOutlineComposite.setTitleImage(IconFactory.getInstance().getIcon("components_view")); 300 mOutlineComposite.setPage(new IPage() { 301 @Override 302 public void createControl(Composite outlineParent) { 303 createOutline(outlineParent); 304 } 305 306 @Override 307 public void dispose() { 308 } 309 310 @Override 311 public Control getControl() { 312 return getTreeViewer().getTree(); 313 } 314 315 @Override 316 public void setToolBar(IToolBarManager toolBarManager) { 317 makeContributions(null, toolBarManager, null); 318 toolBarManager.update(false); 319 } 320 321 @Override 322 public void setFocus() { 323 getControl().setFocus(); 324 } 325 }); 326 } else { 327 createOutline(mControl); 328 } 329 330 if (mShowPropertySheet) { 331 createPropertySheet(); 332 } 333 } 334 335 private void createOutline(Composite parent) { 336 super.createControl(parent); 337 338 TreeViewer tv = getTreeViewer(); 339 tv.setAutoExpandLevel(2); 340 tv.setContentProvider(new ContentProvider()); 341 tv.setLabelProvider(new LabelProvider()); 342 tv.setInput(mRootWrapper); 343 tv.expandToLevel(mRootWrapper.getRoot(), 2); 344 345 int supportedOperations = DND.DROP_COPY | DND.DROP_MOVE; 346 Transfer[] transfers = new Transfer[] { 347 SimpleXmlTransfer.getInstance() 348 }; 349 350 tv.addDropSupport(supportedOperations, transfers, new OutlineDropListener(this, tv)); 351 tv.addDragSupport(supportedOperations, transfers, new OutlineDragListener(this, tv)); 352 353 // The tree viewer will hold CanvasViewInfo instances, however these 354 // change each time the canvas is reloaded. OTOH layoutlib gives us 355 // constant UiView keys which we can use to perform tree item comparisons. 356 tv.setComparer(new IElementComparer() { 357 @Override 358 public int hashCode(Object element) { 359 if (element instanceof CanvasViewInfo) { 360 UiViewElementNode key = ((CanvasViewInfo) element).getUiViewNode(); 361 if (key != null) { 362 return key.hashCode(); 363 } 364 } 365 if (element != null) { 366 return element.hashCode(); 367 } 368 return 0; 369 } 370 371 @Override 372 public boolean equals(Object a, Object b) { 373 if (a instanceof CanvasViewInfo && b instanceof CanvasViewInfo) { 374 UiViewElementNode keyA = ((CanvasViewInfo) a).getUiViewNode(); 375 UiViewElementNode keyB = ((CanvasViewInfo) b).getUiViewNode(); 376 if (keyA != null) { 377 return keyA.equals(keyB); 378 } 379 } 380 if (a != null) { 381 return a.equals(b); 382 } 383 return false; 384 } 385 }); 386 tv.addDoubleClickListener(new IDoubleClickListener() { 387 @Override 388 public void doubleClick(DoubleClickEvent event) { 389 // This used to open the property view, but now that properties are docked 390 // let's use it for something else -- such as showing the editor source 391 /* 392 // Front properties panel; its selection is already linked 393 IWorkbenchPage page = getSite().getPage(); 394 try { 395 page.showView(IPageLayout.ID_PROP_SHEET, null, IWorkbenchPage.VIEW_ACTIVATE); 396 } catch (PartInitException e) { 397 AdtPlugin.log(e, "Could not activate property sheet"); 398 } 399 */ 400 401 TreeItem[] selection = getTreeViewer().getTree().getSelection(); 402 if (selection.length > 0) { 403 CanvasViewInfo vi = getViewInfo(selection[0].getData()); 404 if (vi != null) { 405 LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl(); 406 canvas.show(vi); 407 } 408 } 409 } 410 }); 411 412 setupContextMenu(); 413 414 // Listen to selection changes from the layout editor 415 getSite().getPage().addSelectionListener(this); 416 getControl().addDisposeListener(new DisposeListener() { 417 418 @Override 419 public void widgetDisposed(DisposeEvent e) { 420 dispose(); 421 } 422 }); 423 424 Tree tree = tv.getTree(); 425 tree.addKeyListener(new KeyListener() { 426 427 @Override 428 public void keyPressed(KeyEvent e) { 429 if (e.character == '-') { 430 if (mMoveUpAction.isEnabled()) { 431 mMoveUpAction.run(); 432 } 433 } else if (e.character == '+') { 434 if (mMoveDownAction.isEnabled()) { 435 mMoveDownAction.run(); 436 } 437 } 438 } 439 440 @Override 441 public void keyReleased(KeyEvent e) { 442 } 443 }); 444 } 445 446 private void createPropertySheet() { 447 mPropertySheetComposite = new PageSiteComposite(mControl, SWT.BORDER); 448 mPropertySheetComposite.setTitleText("Properties"); 449 mPropertySheetComposite.setTitleImage(IconFactory.getInstance().getIcon("properties_view")); 450 mPropertySheet = new PropertySheetPage(mGraphicalEditorPart); 451 mPropertySheetComposite.setPage(mPropertySheet); 452 } 453 454 @Override 455 public void dispose() { 456 mRootWrapper.setRoot(null); 457 458 getSite().getPage().removeSelectionListener(this); 459 super.dispose(); 460 if (mPropertySheet != null) { 461 mPropertySheet.dispose(); 462 mPropertySheet = null; 463 } 464 } 465 466 /** 467 * Invoked by {@link LayoutCanvas} to set the model (a.k.a. the root view info). 468 * 469 * @param rootViewInfo The root of the view info hierarchy. Can be null. 470 */ 471 public void setModel(CanvasViewInfo rootViewInfo) { 472 if (!mActive) { 473 return; 474 } 475 476 mRootWrapper.setRoot(rootViewInfo); 477 478 TreeViewer tv = getTreeViewer(); 479 if (tv != null) { 480 Object[] expanded = tv.getExpandedElements(); 481 tv.refresh(); 482 tv.setExpandedElements(expanded); 483 // Ensure that the root is expanded 484 tv.expandToLevel(rootViewInfo, 2); 485 } 486 } 487 488 /** 489 * Returns the current tree viewer selection. Shouldn't be null, 490 * although it can be {@link TreeSelection#EMPTY}. 491 */ 492 @Override 493 public ISelection getSelection() { 494 return super.getSelection(); 495 } 496 497 /** 498 * Sets the outline selection. 499 * 500 * @param selection Only {@link ITreeSelection} will be used, otherwise the 501 * selection will be cleared (including a null selection). 502 */ 503 @Override 504 public void setSelection(ISelection selection) { 505 // TreeViewer should be able to deal with a null selection, but let's make it safe 506 if (selection == null) { 507 selection = TreeSelection.EMPTY; 508 } 509 if (selection.equals(TreeSelection.EMPTY)) { 510 return; 511 } 512 513 super.setSelection(selection); 514 515 TreeViewer tv = getTreeViewer(); 516 if (tv == null || !(selection instanceof ITreeSelection) || selection.isEmpty()) { 517 return; 518 } 519 520 // auto-reveal the selection 521 ITreeSelection treeSel = (ITreeSelection) selection; 522 for (TreePath p : treeSel.getPaths()) { 523 tv.expandToLevel(p, 1); 524 } 525 } 526 527 @Override 528 protected void fireSelectionChanged(ISelection selection) { 529 super.fireSelectionChanged(selection); 530 if (mPropertySheet != null && !mIgnoreSelection) { 531 mPropertySheet.selectionChanged(null, selection); 532 } 533 } 534 535 /** 536 * Listens to a workbench selection. 537 * Only listen on selection coming from {@link LayoutEditorDelegate}, which avoid 538 * picking up our own selections. 539 */ 540 @Override 541 public void selectionChanged(IWorkbenchPart part, ISelection selection) { 542 if (mIgnoreSelection) { 543 return; 544 } 545 546 if (part instanceof IEditorPart) { 547 LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor((IEditorPart) part); 548 if (delegate != null) { 549 try { 550 mIgnoreSelection = true; 551 setSelection(selection); 552 553 if (mPropertySheet != null) { 554 mPropertySheet.selectionChanged(part, selection); 555 } 556 } finally { 557 mIgnoreSelection = false; 558 } 559 } 560 } 561 } 562 563 // ---- 564 565 /** 566 * In theory, the root of the model should be the input of the {@link TreeViewer}, 567 * which would be the root {@link CanvasViewInfo}. 568 * That means in theory {@link ContentProvider#getElements(Object)} should return 569 * its own input as the single root node. 570 * <p/> 571 * However as described in JFace Bug 9262, this case is not properly handled by 572 * a {@link TreeViewer} and leads to an infinite recursion in the tree viewer. 573 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=9262 574 * <p/> 575 * The solution is to wrap the tree viewer input in a dummy root node that acts 576 * as a parent. This class does just that. 577 */ 578 private static class RootWrapper { 579 private CanvasViewInfo mRoot; 580 581 public void setRoot(CanvasViewInfo root) { 582 mRoot = root; 583 } 584 585 public CanvasViewInfo getRoot() { 586 return mRoot; 587 } 588 } 589 590 /** Return the {@link CanvasViewInfo} associated with the given TreeItem's data field */ 591 /* package */ static CanvasViewInfo getViewInfo(Object viewData) { 592 if (viewData instanceof RootWrapper) { 593 return ((RootWrapper) viewData).getRoot(); 594 } 595 if (viewData instanceof CanvasViewInfo) { 596 return (CanvasViewInfo) viewData; 597 } 598 return null; 599 } 600 601 // --- Content and Label Providers --- 602 603 /** 604 * Content provider for the Outline model. 605 * Objects are going to be {@link CanvasViewInfo}. 606 */ 607 private static class ContentProvider implements ITreeContentProvider { 608 609 @Override 610 public Object[] getChildren(Object element) { 611 if (element instanceof RootWrapper) { 612 CanvasViewInfo root = ((RootWrapper)element).getRoot(); 613 if (root != null) { 614 return new Object[] { root }; 615 } 616 } 617 if (element instanceof CanvasViewInfo) { 618 List<CanvasViewInfo> children = ((CanvasViewInfo) element).getUniqueChildren(); 619 if (children != null) { 620 return children.toArray(); 621 } 622 } 623 return new Object[0]; 624 } 625 626 @Override 627 public Object getParent(Object element) { 628 if (element instanceof CanvasViewInfo) { 629 return ((CanvasViewInfo) element).getParent(); 630 } 631 return null; 632 } 633 634 @Override 635 public boolean hasChildren(Object element) { 636 if (element instanceof CanvasViewInfo) { 637 List<CanvasViewInfo> children = ((CanvasViewInfo) element).getChildren(); 638 if (children != null) { 639 return children.size() > 0; 640 } 641 } 642 return false; 643 } 644 645 /** 646 * Returns the root element. 647 * Semantically, the root element is the single top-level XML element of the XML layout. 648 */ 649 @Override 650 public Object[] getElements(Object inputElement) { 651 return getChildren(inputElement); 652 } 653 654 @Override 655 public void dispose() { 656 // pass 657 } 658 659 @Override 660 public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { 661 // pass 662 } 663 } 664 665 /** 666 * Label provider for the Outline model. 667 * Objects are going to be {@link CanvasViewInfo}. 668 */ 669 private class LabelProvider extends StyledCellLabelProvider { 670 /** 671 * Returns the element's logo with a fallback on the android logo. 672 * 673 * @param element the tree element 674 * @return the image to be used as a logo 675 */ 676 public Image getImage(Object element) { 677 if (element instanceof CanvasViewInfo) { 678 element = ((CanvasViewInfo) element).getUiViewNode(); 679 } 680 681 if (element instanceof UiElementNode) { 682 UiElementNode node = (UiElementNode) element; 683 ElementDescriptor desc = node.getDescriptor(); 684 if (desc != null) { 685 Image img = null; 686 // Special case for the common case of vertical linear layouts: 687 // show vertical linear icon (the default icon shows horizontal orientation) 688 String uiName = desc.getUiName(); 689 if (uiName.equals(LINEAR_LAYOUT)) { 690 Element e = (Element) node.getXmlNode(); 691 if (VALUE_VERTICAL.equals(e.getAttributeNS(ANDROID_URI, 692 ATTR_ORIENTATION))) { 693 IconFactory factory = IconFactory.getInstance(); 694 img = factory.getIcon("VerticalLinearLayout"); //$NON-NLS-1$ 695 } 696 } else if (uiName.equals(VIEW_VIEWTAG)) { 697 Node xmlNode = node.getXmlNode(); 698 if (xmlNode instanceof Element) { 699 String className = ((Element) xmlNode).getAttribute(ATTR_CLASS); 700 if (className != null && className.length() > 0) { 701 int index = className.lastIndexOf('.'); 702 if (index != -1) { 703 className = className.substring(index + 1); 704 } 705 img = IconFactory.getInstance().getIcon(className); 706 } 707 } 708 } 709 if (img == null) { 710 img = desc.getGenericIcon(); 711 } 712 if (img != null) { 713 if (node.hasError()) { 714 return new ErrorImageComposite(img).createImage(); 715 } else { 716 return img; 717 } 718 } 719 } 720 } 721 722 return AdtPlugin.getAndroidLogo(); 723 } 724 725 /** 726 * Uses {@link UiElementNode#getStyledDescription} for the label for this tree item. 727 */ 728 @Override 729 public void update(ViewerCell cell) { 730 Object element = cell.getElement(); 731 StyledString styledString = null; 732 733 CanvasViewInfo vi = null; 734 if (element instanceof CanvasViewInfo) { 735 vi = (CanvasViewInfo) element; 736 element = vi.getUiViewNode(); 737 } 738 739 Image image = getImage(element); 740 741 if (element instanceof UiElementNode) { 742 UiElementNode node = (UiElementNode) element; 743 styledString = node.getStyledDescription(); 744 Node xmlNode = node.getXmlNode(); 745 if (xmlNode instanceof Element) { 746 Element e = (Element) xmlNode; 747 748 // Temporary diagnostics code when developing GridLayout 749 if (GridLayoutRule.sDebugGridLayout) { 750 String namespace; 751 if (e.getParentNode().getNodeName().equals(GRID_LAYOUT)) { 752 namespace = ANDROID_URI; 753 } else { 754 IProject project = mGraphicalEditorPart.getProject(); 755 ProjectState projectState = Sdk.getProjectState(project); 756 if (projectState != null && projectState.isLibrary()) { 757 namespace = AUTO_URI; 758 } else { 759 ManifestInfo info = ManifestInfo.get(project); 760 namespace = URI_PREFIX + info.getPackage(); 761 } 762 } 763 764 if (e.getNodeName() != null && e.getNodeName().endsWith(GRID_LAYOUT)) { 765 // Attach rowCount/columnCount info 766 String rowCount = e.getAttributeNS(namespace, ATTR_ROW_COUNT); 767 if (rowCount.length() == 0) { 768 rowCount = "?"; 769 } 770 String columnCount = e.getAttributeNS(namespace, ATTR_COLUMN_COUNT); 771 if (columnCount.length() == 0) { 772 columnCount = "?"; 773 } 774 775 styledString.append(" - columnCount=", QUALIFIER_STYLER); 776 styledString.append(columnCount, QUALIFIER_STYLER); 777 styledString.append(", rowCount=", QUALIFIER_STYLER); 778 styledString.append(rowCount, QUALIFIER_STYLER); 779 } else if (e.getParentNode() != null 780 && e.getParentNode().getNodeName() != null 781 && e.getParentNode().getNodeName().endsWith(GRID_LAYOUT)) { 782 // Attach row/column info 783 String row = e.getAttributeNS(namespace, ATTR_LAYOUT_ROW); 784 if (row.length() == 0) { 785 row = "?"; 786 } 787 Styler colStyle = QUALIFIER_STYLER; 788 String column = e.getAttributeNS(namespace, ATTR_LAYOUT_COLUMN); 789 if (column.length() == 0) { 790 column = "?"; 791 } else { 792 String colCount = ((Element) e.getParentNode()).getAttributeNS( 793 namespace, ATTR_COLUMN_COUNT); 794 if (colCount.length() > 0 && Integer.parseInt(colCount) <= 795 Integer.parseInt(column)) { 796 colStyle = StyledString.createColorRegistryStyler( 797 JFacePreferences.ERROR_COLOR, null); 798 } 799 } 800 String rowSpan = e.getAttributeNS(namespace, ATTR_LAYOUT_ROW_SPAN); 801 String columnSpan = e.getAttributeNS(namespace, 802 ATTR_LAYOUT_COLUMN_SPAN); 803 if (rowSpan.length() == 0) { 804 rowSpan = "1"; 805 } 806 if (columnSpan.length() == 0) { 807 columnSpan = "1"; 808 } 809 810 styledString.append(" - cell (row=", QUALIFIER_STYLER); 811 styledString.append(row, QUALIFIER_STYLER); 812 styledString.append(',', QUALIFIER_STYLER); 813 styledString.append("col=", colStyle); 814 styledString.append(column, colStyle); 815 styledString.append(')', colStyle); 816 styledString.append(", span=(", QUALIFIER_STYLER); 817 styledString.append(columnSpan, QUALIFIER_STYLER); 818 styledString.append(',', QUALIFIER_STYLER); 819 styledString.append(rowSpan, QUALIFIER_STYLER); 820 styledString.append(')', QUALIFIER_STYLER); 821 } 822 } 823 824 if (e.hasAttributeNS(ANDROID_URI, ATTR_TEXT)) { 825 // Show the text attribute 826 String text = e.getAttributeNS(ANDROID_URI, ATTR_TEXT); 827 if (text != null && text.length() > 0 828 && !text.contains(node.getDescriptor().getUiName())) { 829 if (text.charAt(0) == '@') { 830 String resolved = mGraphicalEditorPart.findString(text); 831 if (resolved != null) { 832 text = resolved; 833 } 834 } 835 styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER); 836 styledString.append('"', QUALIFIER_STYLER); 837 styledString.append(truncate(text, styledString), QUALIFIER_STYLER); 838 styledString.append('"', QUALIFIER_STYLER); 839 } 840 } else if (e.hasAttributeNS(ANDROID_URI, ATTR_SRC)) { 841 // Show ImageView source attributes etc 842 String src = e.getAttributeNS(ANDROID_URI, ATTR_SRC); 843 if (src != null && src.length() > 0) { 844 if (src.startsWith(DRAWABLE_PREFIX)) { 845 src = src.substring(DRAWABLE_PREFIX.length()); 846 } 847 styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER); 848 styledString.append(truncate(src, styledString), QUALIFIER_STYLER); 849 } 850 } else if (e.getTagName().equals(LayoutDescriptors.VIEW_INCLUDE)) { 851 // Show the include reference. 852 853 // Note: the layout attribute is NOT in the Android namespace 854 String src = e.getAttribute(LayoutDescriptors.ATTR_LAYOUT); 855 if (src != null && src.length() > 0) { 856 if (src.startsWith(LAYOUT_PREFIX)) { 857 src = src.substring(LAYOUT_PREFIX.length()); 858 } 859 styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER); 860 styledString.append(truncate(src, styledString), QUALIFIER_STYLER); 861 } 862 } 863 } 864 } else if (element == null && vi != null) { 865 // It's an inclusion-context: display it 866 Reference includedWithin = mGraphicalEditorPart.getIncludedWithin(); 867 if (includedWithin != null) { 868 styledString = new StyledString(); 869 styledString.append(includedWithin.getDisplayName(), QUALIFIER_STYLER); 870 image = IconFactory.getInstance().getIcon(LayoutDescriptors.VIEW_INCLUDE); 871 } 872 } 873 874 if (styledString == null) { 875 styledString = new StyledString(); 876 styledString.append(element == null ? "(null)" : element.toString()); 877 } 878 879 cell.setText(styledString.toString()); 880 cell.setStyleRanges(styledString.getStyleRanges()); 881 cell.setImage(image); 882 super.update(cell); 883 } 884 885 @Override 886 public boolean isLabelProperty(Object element, String property) { 887 return super.isLabelProperty(element, property); 888 } 889 } 890 891 // --- Context Menu --- 892 893 /** 894 * This viewer uses its own actions that delegate to the ones given 895 * by the {@link LayoutCanvas}. All the processing is actually handled 896 * directly by the canvas and this viewer only gets refreshed as a 897 * consequence of the canvas changing the XML model. 898 */ 899 private void setupContextMenu() { 900 901 mMenuManager = new MenuManager(); 902 mMenuManager.removeAll(); 903 904 mMenuManager.add(mMoveUpAction); 905 mMenuManager.add(mMoveDownAction); 906 mMenuManager.add(new Separator()); 907 908 mMenuManager.add(new SelectionManager.SelectionMenu(mGraphicalEditorPart)); 909 mMenuManager.add(new Separator()); 910 final String prefix = LayoutCanvas.PREFIX_CANVAS_ACTION; 911 mMenuManager.add(new DelegateAction(prefix + ActionFactory.CUT.getId())); 912 mMenuManager.add(new DelegateAction(prefix + ActionFactory.COPY.getId())); 913 mMenuManager.add(new DelegateAction(prefix + ActionFactory.PASTE.getId())); 914 915 mMenuManager.add(new Separator()); 916 917 mMenuManager.add(new DelegateAction(prefix + ActionFactory.DELETE.getId())); 918 919 mMenuManager.addMenuListener(new IMenuListener() { 920 @Override 921 public void menuAboutToShow(IMenuManager manager) { 922 // Update all actions to match their LayoutCanvas counterparts 923 for (IContributionItem contrib : manager.getItems()) { 924 if (contrib instanceof ActionContributionItem) { 925 IAction action = ((ActionContributionItem) contrib).getAction(); 926 if (action instanceof DelegateAction) { 927 ((DelegateAction) action).updateFromEditorPart(mGraphicalEditorPart); 928 } 929 } 930 } 931 } 932 }); 933 934 new DynamicContextMenu( 935 mGraphicalEditorPart.getEditorDelegate(), 936 mGraphicalEditorPart.getCanvasControl(), 937 mMenuManager); 938 939 getTreeViewer().getTree().setMenu(mMenuManager.createContextMenu(getControl())); 940 941 // Update Move Up/Move Down state only when the menu is opened 942 getTreeViewer().getTree().addMenuDetectListener(new MenuDetectListener() { 943 @Override 944 public void menuDetected(MenuDetectEvent e) { 945 mMenuManager.update(IAction.ENABLED); 946 } 947 }); 948 } 949 950 /** 951 * An action that delegates its properties and behavior to a target action. 952 * The target action can be null or it can change overtime, typically as the 953 * layout canvas' editor part is activated or closed. 954 */ 955 private static class DelegateAction extends Action { 956 private IAction mTargetAction; 957 private final String mCanvasActionId; 958 959 public DelegateAction(String canvasActionId) { 960 super(canvasActionId); 961 setId(canvasActionId); 962 mCanvasActionId = canvasActionId; 963 } 964 965 // --- Methods form IAction --- 966 967 /** Returns the target action's {@link #isEnabled()} if defined, or false. */ 968 @Override 969 public boolean isEnabled() { 970 return mTargetAction == null ? false : mTargetAction.isEnabled(); 971 } 972 973 /** Returns the target action's {@link #isChecked()} if defined, or false. */ 974 @Override 975 public boolean isChecked() { 976 return mTargetAction == null ? false : mTargetAction.isChecked(); 977 } 978 979 /** Returns the target action's {@link #isHandled()} if defined, or false. */ 980 @Override 981 public boolean isHandled() { 982 return mTargetAction == null ? false : mTargetAction.isHandled(); 983 } 984 985 /** Runs the target action if defined. */ 986 @Override 987 public void run() { 988 if (mTargetAction != null) { 989 mTargetAction.run(); 990 } 991 super.run(); 992 } 993 994 /** 995 * Updates this action to delegate to its counterpart in the given editor part 996 * 997 * @param editorPart The editor being updated 998 */ 999 public void updateFromEditorPart(GraphicalEditorPart editorPart) { 1000 LayoutCanvas canvas = editorPart == null ? null : editorPart.getCanvasControl(); 1001 if (canvas == null) { 1002 mTargetAction = null; 1003 } else { 1004 mTargetAction = canvas.getAction(mCanvasActionId); 1005 } 1006 1007 if (mTargetAction != null) { 1008 setText(mTargetAction.getText()); 1009 setId(mTargetAction.getId()); 1010 setDescription(mTargetAction.getDescription()); 1011 setImageDescriptor(mTargetAction.getImageDescriptor()); 1012 setHoverImageDescriptor(mTargetAction.getHoverImageDescriptor()); 1013 setDisabledImageDescriptor(mTargetAction.getDisabledImageDescriptor()); 1014 setToolTipText(mTargetAction.getToolTipText()); 1015 setActionDefinitionId(mTargetAction.getActionDefinitionId()); 1016 setHelpListener(mTargetAction.getHelpListener()); 1017 setAccelerator(mTargetAction.getAccelerator()); 1018 setChecked(mTargetAction.isChecked()); 1019 setEnabled(mTargetAction.isEnabled()); 1020 } else { 1021 setEnabled(false); 1022 } 1023 } 1024 } 1025 1026 /** Returns the associated editor with this outline */ 1027 /* package */GraphicalEditorPart getEditor() { 1028 return mGraphicalEditorPart; 1029 } 1030 1031 @Override 1032 public void setActionBars(IActionBars actionBars) { 1033 super.setActionBars(actionBars); 1034 1035 // Map Outline actions to canvas actions such that they share Undo context etc 1036 LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl(); 1037 canvas.updateGlobalActions(actionBars); 1038 1039 // Special handling for Select All since it's different than the canvas (will 1040 // include selecting the root etc) 1041 actionBars.setGlobalActionHandler(mTreeSelectAllAction.getId(), mTreeSelectAllAction); 1042 actionBars.updateActionBars(); 1043 } 1044 1045 // ---- Move Up/Down Support ---- 1046 1047 /** Returns true if the current selected item can be moved */ 1048 private boolean canMove(boolean forward) { 1049 CanvasViewInfo viewInfo = getSingleSelectedItem(); 1050 if (viewInfo != null) { 1051 UiViewElementNode node = viewInfo.getUiViewNode(); 1052 if (forward) { 1053 return findNext(node) != null; 1054 } else { 1055 return findPrevious(node) != null; 1056 } 1057 } 1058 1059 return false; 1060 } 1061 1062 /** Moves the current selected item down (forward) or up (not forward) */ 1063 private void move(boolean forward) { 1064 CanvasViewInfo viewInfo = getSingleSelectedItem(); 1065 if (viewInfo != null) { 1066 final Pair<UiViewElementNode, Integer> target; 1067 UiViewElementNode selected = viewInfo.getUiViewNode(); 1068 if (forward) { 1069 target = findNext(selected); 1070 } else { 1071 target = findPrevious(selected); 1072 } 1073 if (target != null) { 1074 final LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl(); 1075 final SelectionManager selectionManager = canvas.getSelectionManager(); 1076 final ArrayList<SelectionItem> dragSelection = new ArrayList<SelectionItem>(); 1077 dragSelection.add(selectionManager.createSelection(viewInfo)); 1078 SelectionManager.sanitize(dragSelection); 1079 1080 if (!dragSelection.isEmpty()) { 1081 final SimpleElement[] elements = SelectionItem.getAsElements(dragSelection); 1082 UiViewElementNode parentNode = target.getFirst(); 1083 final NodeProxy targetNode = canvas.getNodeFactory().create(parentNode); 1084 1085 // Record children of the target right before the drop (such that we 1086 // can find out after the drop which exact children were inserted) 1087 Set<INode> children = new HashSet<INode>(); 1088 for (INode node : targetNode.getChildren()) { 1089 children.add(node); 1090 } 1091 1092 String label = MoveGesture.computeUndoLabel(targetNode, 1093 elements, DND.DROP_MOVE); 1094 canvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(label, new Runnable() { 1095 @Override 1096 public void run() { 1097 InsertType insertType = InsertType.MOVE_INTO; 1098 if (dragSelection.get(0).getNode().getParent() == targetNode) { 1099 insertType = InsertType.MOVE_WITHIN; 1100 } 1101 canvas.getRulesEngine().setInsertType(insertType); 1102 int index = target.getSecond(); 1103 BaseLayoutRule.insertAt(targetNode, elements, false, index); 1104 targetNode.applyPendingChanges(); 1105 canvas.getClipboardSupport().deleteSelection("Remove", dragSelection); 1106 } 1107 }); 1108 1109 // Now find out which nodes were added, and look up their 1110 // corresponding CanvasViewInfos 1111 final List<INode> added = new ArrayList<INode>(); 1112 for (INode node : targetNode.getChildren()) { 1113 if (!children.contains(node)) { 1114 added.add(node); 1115 } 1116 } 1117 1118 selectionManager.setOutlineSelection(added); 1119 } 1120 } 1121 } 1122 } 1123 1124 /** 1125 * Returns the {@link CanvasViewInfo} for the currently selected item, or null if 1126 * there are no or multiple selected items 1127 * 1128 * @return the current selected item if there is exactly one item selected 1129 */ 1130 private CanvasViewInfo getSingleSelectedItem() { 1131 TreeItem[] selection = getTreeViewer().getTree().getSelection(); 1132 if (selection.length == 1) { 1133 return getViewInfo(selection[0].getData()); 1134 } 1135 1136 return null; 1137 } 1138 1139 1140 /** Returns the pair [parent,index] of the next node (when iterating forward) */ 1141 @VisibleForTesting 1142 /* package */ static Pair<UiViewElementNode, Integer> findNext(UiViewElementNode node) { 1143 UiElementNode parent = node.getUiParent(); 1144 if (parent == null) { 1145 return null; 1146 } 1147 1148 UiElementNode next = node.getUiNextSibling(); 1149 if (next != null) { 1150 if (DescriptorsUtils.canInsertChildren(next.getDescriptor(), null)) { 1151 return getFirstPosition(next); 1152 } else { 1153 return getPositionAfter(next); 1154 } 1155 } 1156 1157 next = parent.getUiNextSibling(); 1158 if (next != null) { 1159 return getPositionBefore(next); 1160 } else { 1161 UiElementNode grandParent = parent.getUiParent(); 1162 if (grandParent != null) { 1163 return getLastPosition(grandParent); 1164 } 1165 } 1166 1167 return null; 1168 } 1169 1170 /** Returns the pair [parent,index] of the previous node (when iterating backward) */ 1171 @VisibleForTesting 1172 /* package */ static Pair<UiViewElementNode, Integer> findPrevious(UiViewElementNode node) { 1173 UiElementNode prev = node.getUiPreviousSibling(); 1174 if (prev != null) { 1175 UiElementNode curr = prev; 1176 while (true) { 1177 List<UiElementNode> children = curr.getUiChildren(); 1178 if (children.size() > 0) { 1179 curr = children.get(children.size() - 1); 1180 continue; 1181 } 1182 if (DescriptorsUtils.canInsertChildren(curr.getDescriptor(), null)) { 1183 return getFirstPosition(curr); 1184 } else { 1185 if (curr == prev) { 1186 return getPositionBefore(curr); 1187 } else { 1188 return getPositionAfter(curr); 1189 } 1190 } 1191 } 1192 } 1193 1194 return getPositionBefore(node.getUiParent()); 1195 } 1196 1197 /** Returns the pair [parent,index] of the position immediately before the given node */ 1198 private static Pair<UiViewElementNode, Integer> getPositionBefore(UiElementNode node) { 1199 if (node != null) { 1200 UiElementNode parent = node.getUiParent(); 1201 if (parent != null && parent instanceof UiViewElementNode) { 1202 return Pair.of((UiViewElementNode) parent, node.getUiSiblingIndex()); 1203 } 1204 } 1205 1206 return null; 1207 } 1208 1209 /** Returns the pair [parent,index] of the position immediately following the given node */ 1210 private static Pair<UiViewElementNode, Integer> getPositionAfter(UiElementNode node) { 1211 if (node != null) { 1212 UiElementNode parent = node.getUiParent(); 1213 if (parent != null && parent instanceof UiViewElementNode) { 1214 return Pair.of((UiViewElementNode) parent, node.getUiSiblingIndex() + 1); 1215 } 1216 } 1217 1218 return null; 1219 } 1220 1221 /** Returns the pair [parent,index] of the first position inside the given parent */ 1222 private static Pair<UiViewElementNode, Integer> getFirstPosition(UiElementNode parent) { 1223 if (parent != null && parent instanceof UiViewElementNode) { 1224 return Pair.of((UiViewElementNode) parent, 0); 1225 } 1226 1227 return null; 1228 } 1229 1230 /** 1231 * Returns the pair [parent,index] of the last position after the given node's 1232 * children 1233 */ 1234 private static Pair<UiViewElementNode, Integer> getLastPosition(UiElementNode parent) { 1235 if (parent != null && parent instanceof UiViewElementNode) { 1236 return Pair.of((UiViewElementNode) parent, parent.getUiChildren().size()); 1237 } 1238 1239 return null; 1240 } 1241 1242 /** 1243 * Truncates the given text such that it will fit into the given {@link StyledString} 1244 * up to a maximum length of {@link #LABEL_MAX_WIDTH}. 1245 * 1246 * @param text the text to truncate 1247 * @param string the existing string to be appended to 1248 * @return the truncated string 1249 */ 1250 private static String truncate(String text, StyledString string) { 1251 int existingLength = string.length(); 1252 1253 if (text.length() + existingLength > LABEL_MAX_WIDTH) { 1254 int truncatedLength = LABEL_MAX_WIDTH - existingLength - 3; 1255 if (truncatedLength > 0) { 1256 return String.format("%1$s...", text.substring(0, truncatedLength)); 1257 } else { 1258 return ""; //$NON-NLS-1$ 1259 } 1260 } 1261 1262 return text; 1263 } 1264 1265 @Override 1266 public void setToolBar(IToolBarManager toolBarManager) { 1267 makeContributions(null, toolBarManager, null); 1268 toolBarManager.update(false); 1269 } 1270 } 1271