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