1 /* 2 * Copyright (C) 2009 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 com.android.ide.common.api.INode; 20 import com.android.ide.common.api.Margins; 21 import com.android.ide.common.api.Point; 22 import com.android.ide.common.layout.LayoutConstants; 23 import com.android.ide.common.rendering.api.Capability; 24 import com.android.ide.common.rendering.api.RenderSession; 25 import com.android.ide.eclipse.adt.AdtPlugin; 26 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; 27 import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor; 28 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; 29 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; 30 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; 31 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; 32 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory; 33 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; 34 import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository; 35 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; 36 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; 37 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 38 import com.android.resources.Density; 39 import com.android.sdklib.SdkConstants; 40 41 import org.eclipse.core.filesystem.EFS; 42 import org.eclipse.core.filesystem.IFileStore; 43 import org.eclipse.core.resources.IFile; 44 import org.eclipse.core.resources.IWorkspaceRoot; 45 import org.eclipse.core.resources.ResourcesPlugin; 46 import org.eclipse.core.runtime.CoreException; 47 import org.eclipse.core.runtime.IPath; 48 import org.eclipse.core.runtime.QualifiedName; 49 import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility; 50 import org.eclipse.jface.action.Action; 51 import org.eclipse.jface.action.ActionContributionItem; 52 import org.eclipse.jface.action.IAction; 53 import org.eclipse.jface.action.IContributionItem; 54 import org.eclipse.jface.action.IMenuManager; 55 import org.eclipse.jface.action.IStatusLineManager; 56 import org.eclipse.jface.action.MenuManager; 57 import org.eclipse.jface.action.Separator; 58 import org.eclipse.swt.SWT; 59 import org.eclipse.swt.custom.StyledText; 60 import org.eclipse.swt.dnd.DND; 61 import org.eclipse.swt.dnd.DragSource; 62 import org.eclipse.swt.dnd.DropTarget; 63 import org.eclipse.swt.dnd.TextTransfer; 64 import org.eclipse.swt.dnd.Transfer; 65 import org.eclipse.swt.events.ControlAdapter; 66 import org.eclipse.swt.events.ControlEvent; 67 import org.eclipse.swt.events.KeyEvent; 68 import org.eclipse.swt.events.MenuDetectEvent; 69 import org.eclipse.swt.events.MenuDetectListener; 70 import org.eclipse.swt.events.MouseEvent; 71 import org.eclipse.swt.events.PaintEvent; 72 import org.eclipse.swt.events.PaintListener; 73 import org.eclipse.swt.graphics.Font; 74 import org.eclipse.swt.graphics.GC; 75 import org.eclipse.swt.graphics.Image; 76 import org.eclipse.swt.graphics.ImageData; 77 import org.eclipse.swt.graphics.Rectangle; 78 import org.eclipse.swt.widgets.Canvas; 79 import org.eclipse.swt.widgets.Composite; 80 import org.eclipse.swt.widgets.Control; 81 import org.eclipse.swt.widgets.Display; 82 import org.eclipse.swt.widgets.Menu; 83 import org.eclipse.ui.IActionBars; 84 import org.eclipse.ui.IEditorPart; 85 import org.eclipse.ui.IEditorSite; 86 import org.eclipse.ui.IWorkbenchPage; 87 import org.eclipse.ui.PartInitException; 88 import org.eclipse.ui.actions.ActionFactory; 89 import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction; 90 import org.eclipse.ui.actions.ContributionItemFactory; 91 import org.eclipse.ui.ide.IDE; 92 import org.eclipse.ui.internal.ide.IDEWorkbenchMessages; 93 import org.eclipse.ui.texteditor.ITextEditor; 94 import org.eclipse.ui.views.contentoutline.IContentOutlinePage; 95 import org.w3c.dom.Node; 96 97 import java.util.HashSet; 98 import java.util.List; 99 import java.util.Set; 100 101 /** 102 * Displays the image rendered by the {@link GraphicalEditorPart} and handles 103 * the interaction with the widgets. 104 * <p/> 105 * {@link LayoutCanvas} implements the "Canvas" control. The editor part 106 * actually uses the {@link LayoutCanvasViewer}, which is a JFace viewer wrapper 107 * around this control. 108 * <p/> 109 * The LayoutCanvas contains the painting logic for the canvas. Selection, 110 * clipboard, view management etc. is handled in separate helper classes. 111 * 112 * @since GLE2 113 */ 114 @SuppressWarnings("restriction") // For WorkBench "Show In" support 115 public class LayoutCanvas extends Canvas { 116 private final static QualifiedName NAME_ZOOM = 117 new QualifiedName(AdtPlugin.PLUGIN_ID, "zoom");//$NON-NLS-1$ 118 119 private static final boolean DEBUG = false; 120 121 /* package */ static final String PREFIX_CANVAS_ACTION = "canvas_action_"; 122 123 /** The layout editor that uses this layout canvas. */ 124 private final LayoutEditor mLayoutEditor; 125 126 /** The Rules Engine, associated with the current project. */ 127 private RulesEngine mRulesEngine; 128 129 /** GC wrapper given to the IViewRule methods. The GC itself is only defined in the 130 * context of {@link #onPaint(PaintEvent)}; otherwise it is null. */ 131 private GCWrapper mGCWrapper; 132 133 /** Default font used on the canvas. Do not dispose, it's a system font. */ 134 private Font mFont; 135 136 /** Current hover view info. Null when no mouse hover. */ 137 private CanvasViewInfo mHoverViewInfo; 138 139 /** When true, always display the outline of all views. */ 140 private boolean mShowOutline; 141 142 /** When true, display the outline of all empty parent views. */ 143 private boolean mShowInvisible; 144 145 /** Drop target associated with this composite. */ 146 private DropTarget mDropTarget; 147 148 /** Factory that can create {@link INode} proxies. */ 149 private final NodeFactory mNodeFactory = new NodeFactory(this); 150 151 /** Vertical scaling & scrollbar information. */ 152 private CanvasTransform mVScale; 153 154 /** Horizontal scaling & scrollbar information. */ 155 private CanvasTransform mHScale; 156 157 /** Drag source associated with this canvas. */ 158 private DragSource mDragSource; 159 160 /** 161 * The current Outline Page, to set its model. 162 * It isn't possible to call OutlinePage2.dispose() in this.dispose(). 163 * this.dispose() is called from GraphicalEditorPart.dispose(), 164 * when page's widget is already disposed. 165 * Added the DisposeListener to OutlinePage2 in order to correctly dispose this page. 166 **/ 167 private OutlinePage mOutlinePage; 168 169 /** Delete action for the Edit or context menu. */ 170 private Action mDeleteAction; 171 172 /** Select-All action for the Edit or context menu. */ 173 private Action mSelectAllAction; 174 175 /** Paste action for the Edit or context menu. */ 176 private Action mPasteAction; 177 178 /** Cut action for the Edit or context menu. */ 179 private Action mCutAction; 180 181 /** Copy action for the Edit or context menu. */ 182 private Action mCopyAction; 183 184 /** Root of the context menu. */ 185 private MenuManager mMenuManager; 186 187 /** The view hierarchy associated with this canvas. */ 188 private final ViewHierarchy mViewHierarchy = new ViewHierarchy(this); 189 190 /** The selection in the canvas. */ 191 private final SelectionManager mSelectionManager = new SelectionManager(this); 192 193 /** The overlay which paints the optional outline. */ 194 private OutlineOverlay mOutlineOverlay; 195 196 /** The overlay which paints outlines around empty children */ 197 private EmptyViewsOverlay mEmptyOverlay; 198 199 /** The overlay which paints the mouse hover. */ 200 private HoverOverlay mHoverOverlay; 201 202 /** The overlay which paints the selection. */ 203 private SelectionOverlay mSelectionOverlay; 204 205 /** The overlay which paints the rendered layout image. */ 206 private ImageOverlay mImageOverlay; 207 208 /** The overlay which paints masks hiding everything but included content. */ 209 private IncludeOverlay mIncludeOverlay; 210 211 /** 212 * Gesture Manager responsible for identifying mouse, keyboard and drag and 213 * drop events. 214 */ 215 private final GestureManager mGestureManager = new GestureManager(this); 216 217 /** 218 * When set, performs a zoom-to-fit when the next rendering image arrives. 219 */ 220 private boolean mZoomFitNextImage; 221 222 /** 223 * Native clipboard support. 224 */ 225 private ClipboardSupport mClipboardSupport; 226 227 public LayoutCanvas(LayoutEditor layoutEditor, 228 RulesEngine rulesEngine, 229 Composite parent, 230 int style) { 231 super(parent, style | SWT.DOUBLE_BUFFERED | SWT.V_SCROLL | SWT.H_SCROLL); 232 mLayoutEditor = layoutEditor; 233 mRulesEngine = rulesEngine; 234 235 mClipboardSupport = new ClipboardSupport(this, parent); 236 mHScale = new CanvasTransform(this, getHorizontalBar()); 237 mVScale = new CanvasTransform(this, getVerticalBar()); 238 239 // Unit test suite passes a null here; TODO: Replace with mocking 240 IFile file = layoutEditor != null ? layoutEditor.getInputFile() : null; 241 if (file != null) { 242 String zoom = AdtPlugin.getFileProperty(file, NAME_ZOOM); 243 if (zoom != null) { 244 try { 245 double initialScale = Double.parseDouble(zoom); 246 if (initialScale > 0.1) { 247 mHScale.setScale(initialScale); 248 mVScale.setScale(initialScale); 249 } 250 } catch (NumberFormatException nfe) { 251 // Ignore - use zoom=100% 252 } 253 } else { 254 mZoomFitNextImage = true; 255 } 256 } 257 258 mGCWrapper = new GCWrapper(mHScale, mVScale); 259 260 Display display = getDisplay(); 261 mFont = display.getSystemFont(); 262 263 // --- Set up graphic overlays 264 // mOutlineOverlay and mEmptyOverlay are initialized lazily 265 mHoverOverlay = new HoverOverlay(this, mHScale, mVScale); 266 mHoverOverlay.create(display); 267 mSelectionOverlay = new SelectionOverlay(this); 268 mSelectionOverlay.create(display); 269 mImageOverlay = new ImageOverlay(this, mHScale, mVScale); 270 mIncludeOverlay = new IncludeOverlay(this); 271 mImageOverlay.create(display); 272 273 // --- Set up listeners 274 addPaintListener(new PaintListener() { 275 public void paintControl(PaintEvent e) { 276 onPaint(e); 277 } 278 }); 279 280 addControlListener(new ControlAdapter() { 281 @Override 282 public void controlResized(ControlEvent e) { 283 super.controlResized(e); 284 mHScale.setClientSize(getClientArea().width); 285 mVScale.setClientSize(getClientArea().height); 286 } 287 }); 288 289 // --- setup drag'n'drop --- 290 // DND Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html 291 292 mDropTarget = createDropTarget(this); 293 mDragSource = createDragSource(this); 294 mGestureManager.registerListeners(mDragSource, mDropTarget); 295 296 if (mLayoutEditor == null) { 297 // TODO: In another CL we should use EasyMock/objgen to provide an editor. 298 return; // Unit test 299 } 300 301 // --- setup context menu --- 302 setupGlobalActionHandlers(); 303 createContextMenu(); 304 305 // --- setup outline --- 306 // Get the outline associated with this editor, if any and of the right type. 307 Object outline = layoutEditor.getAdapter(IContentOutlinePage.class); 308 if (outline instanceof OutlinePage) { 309 mOutlinePage = (OutlinePage) outline; 310 } 311 } 312 313 public void handleKeyPressed(KeyEvent e) { 314 // Set up backspace as an alias for the delete action within the canvas. 315 // On most Macs there is no delete key - though there IS a key labeled 316 // "Delete" and it sends a backspace key code! In short, for Macs we should 317 // treat backspace as delete, and it's harmless (and probably useful) to 318 // handle backspace for other platforms as well. 319 if (e.keyCode == SWT.BS) { 320 mDeleteAction.run(); 321 } else if (e.keyCode == SWT.ESC) { 322 mSelectionManager.selectParent(); 323 } else { 324 // Zooming actions 325 char c = e.character; 326 LayoutActionBar actionBar = mLayoutEditor.getGraphicalEditor() 327 .getLayoutActionBar(); 328 if (c == '1' && actionBar.isZoomingAllowed()) { 329 setScale(1, true); 330 } else if (c == '0' && actionBar.isZoomingAllowed()) { 331 setFitScale(true); 332 } else if (e.keyCode == '0' && (e.stateMask & SWT.MOD2) != 0 333 && actionBar.isZoomingAllowed()) { 334 setFitScale(false); 335 } else if (c == '+' && actionBar.isZoomingAllowed()) { 336 actionBar.rescale(1); 337 } else if (c == '-' && actionBar.isZoomingAllowed()) { 338 actionBar.rescale(-1); 339 } 340 } 341 } 342 343 @Override 344 public void dispose() { 345 super.dispose(); 346 347 mGestureManager.unregisterListeners(mDragSource, mDropTarget); 348 349 if (mDropTarget != null) { 350 mDropTarget.dispose(); 351 mDropTarget = null; 352 } 353 354 if (mRulesEngine != null) { 355 mRulesEngine.dispose(); 356 mRulesEngine = null; 357 } 358 359 if (mDragSource != null) { 360 mDragSource.dispose(); 361 mDragSource = null; 362 } 363 364 if (mClipboardSupport != null) { 365 mClipboardSupport.dispose(); 366 mClipboardSupport = null; 367 } 368 369 if (mGCWrapper != null) { 370 mGCWrapper.dispose(); 371 mGCWrapper = null; 372 } 373 374 if (mOutlineOverlay != null) { 375 mOutlineOverlay.dispose(); 376 mOutlineOverlay = null; 377 } 378 379 if (mEmptyOverlay != null) { 380 mEmptyOverlay.dispose(); 381 mEmptyOverlay = null; 382 } 383 384 if (mHoverOverlay != null) { 385 mHoverOverlay.dispose(); 386 mHoverOverlay = null; 387 } 388 389 if (mSelectionOverlay != null) { 390 mSelectionOverlay.dispose(); 391 mSelectionOverlay = null; 392 } 393 394 if (mImageOverlay != null) { 395 mImageOverlay.dispose(); 396 mImageOverlay = null; 397 } 398 399 if (mIncludeOverlay != null) { 400 mIncludeOverlay.dispose(); 401 mIncludeOverlay = null; 402 } 403 404 mViewHierarchy.dispose(); 405 } 406 407 /** Returns the Rules Engine, associated with the current project. */ 408 /* package */ RulesEngine getRulesEngine() { 409 return mRulesEngine; 410 } 411 412 /** Sets the Rules Engine, associated with the current project. */ 413 /* package */ void setRulesEngine(RulesEngine rulesEngine) { 414 mRulesEngine = rulesEngine; 415 } 416 417 /** 418 * Returns the factory to use to convert from {@link CanvasViewInfo} or from 419 * {@link UiViewElementNode} to {@link INode} proxies. 420 */ 421 /* package */ NodeFactory getNodeFactory() { 422 return mNodeFactory; 423 } 424 425 /** 426 * Returns the GCWrapper used to paint view rules. 427 * 428 * @return The GCWrapper used to paint view rules 429 */ 430 /* package */ GCWrapper getGcWrapper() { 431 return mGCWrapper; 432 } 433 434 /** 435 * Returns the {@link LayoutEditor} associated with this canvas. 436 */ 437 public LayoutEditor getLayoutEditor() { 438 return mLayoutEditor; 439 } 440 441 /** 442 * Returns the current {@link ImageOverlay} painting the rendered result 443 * 444 * @return the image overlay responsible for painting the rendered result, never null 445 */ 446 ImageOverlay getImageOverlay() { 447 return mImageOverlay; 448 } 449 450 /** 451 * Returns the current {@link SelectionOverlay} painting the selection highlights 452 * 453 * @return the selection overlay responsible for painting the selection highlights, 454 * never null 455 */ 456 SelectionOverlay getSelectionOverlay() { 457 return mSelectionOverlay; 458 } 459 460 /** 461 * Returns the {@link GestureManager} associated with this canvas. 462 * 463 * @return the {@link GestureManager} associated with this canvas, never null. 464 */ 465 GestureManager getGestureManager() { 466 return mGestureManager; 467 } 468 469 /** 470 * Returns the current {@link HoverOverlay} painting the mouse hover. 471 * 472 * @return the hover overlay responsible for painting the mouse hover, 473 * never null 474 */ 475 HoverOverlay getHoverOverlay() { 476 return mHoverOverlay; 477 } 478 479 /** 480 * Returns the horizontal {@link CanvasTransform} transform object, which can map 481 * a layout point into a control point. 482 * 483 * @return A {@link CanvasTransform} for mapping between layout and control 484 * coordinates in the horizontal dimension. 485 */ 486 /* package */ CanvasTransform getHorizontalTransform() { 487 return mHScale; 488 } 489 490 /** 491 * Returns the vertical {@link CanvasTransform} transform object, which can map a 492 * layout point into a control point. 493 * 494 * @return A {@link CanvasTransform} for mapping between layout and control 495 * coordinates in the vertical dimension. 496 */ 497 /* package */ CanvasTransform getVerticalTransform() { 498 return mVScale; 499 } 500 501 /** 502 * Returns the {@link OutlinePage} associated with this canvas 503 * 504 * @return the {@link OutlinePage} associated with this canvas 505 */ 506 public OutlinePage getOutlinePage() { 507 return mOutlinePage; 508 } 509 510 /** 511 * Returns the {@link SelectionManager} associated with this canvas. 512 * 513 * @return The {@link SelectionManager} holding the selection for this 514 * canvas. Never null. 515 */ 516 public SelectionManager getSelectionManager() { 517 return mSelectionManager; 518 } 519 520 /** 521 * Returns the {@link ViewHierarchy} object associated with this canvas, 522 * holding the most recent rendered view of the scene, if valid. 523 * 524 * @return The {@link ViewHierarchy} object associated with this canvas. 525 * Never null. 526 */ 527 public ViewHierarchy getViewHierarchy() { 528 return mViewHierarchy; 529 } 530 531 /** 532 * Returns the {@link ClipboardSupport} object associated with this canvas. 533 * 534 * @return The {@link ClipboardSupport} object for this canvas. Null only after dispose. 535 */ 536 public ClipboardSupport getClipboardSupport() { 537 return mClipboardSupport; 538 } 539 540 /** Returns the Select All action bound to this canvas */ 541 Action getSelectAllAction() { 542 return mSelectAllAction; 543 } 544 545 /** 546 * Sets the result of the layout rendering. The result object indicates if the layout 547 * rendering succeeded. If it did, it contains a bitmap and the objects rectangles. 548 * 549 * Implementation detail: the bridge's computeLayout() method already returns a newly 550 * allocated ILayourResult. That means we can keep this result and hold on to it 551 * when it is valid. 552 * 553 * @param session The new scene, either valid or not. 554 * @param explodedNodes The set of individual nodes the layout computer was asked to 555 * explode. Note that these are independent of the explode-all mode where 556 * all views are exploded; this is used only for the mode ( 557 * {@link #showInvisibleViews(boolean)}) where individual invisible nodes 558 * are padded during certain interactions. 559 */ 560 /* package */ void setSession(RenderSession session, Set<UiElementNode> explodedNodes, 561 boolean layoutlib5) { 562 // disable any hover 563 clearHover(); 564 565 mViewHierarchy.setSession(session, explodedNodes, layoutlib5); 566 if (mViewHierarchy.isValid() && session != null) { 567 Image image = mImageOverlay.setImage(session.getImage(), session.isAlphaChannelImage()); 568 569 mOutlinePage.setModel(mViewHierarchy.getRoot()); 570 571 if (image != null) { 572 mHScale.setSize(image.getImageData().width, getClientArea().width); 573 mVScale.setSize(image.getImageData().height, getClientArea().height); 574 if (mZoomFitNextImage) { 575 mZoomFitNextImage = false; 576 // Must be run asynchronously because getClientArea() returns 0 bounds 577 // when the editor is being initialized 578 getDisplay().asyncExec(new Runnable() { 579 public void run() { 580 setFitScale(true); 581 } 582 }); 583 } 584 } 585 } 586 587 redraw(); 588 } 589 590 /* package */ void setShowOutline(boolean newState) { 591 mShowOutline = newState; 592 redraw(); 593 } 594 595 public double getScale() { 596 return mHScale.getScale(); 597 } 598 599 /* package */ void setScale(double scale, boolean redraw) { 600 if (scale <= 0.0) { 601 scale = 1.0; 602 } 603 604 if (scale == getScale()) { 605 return; 606 } 607 608 mHScale.setScale(scale); 609 mVScale.setScale(scale); 610 if (redraw) { 611 redraw(); 612 } 613 614 // Clear the zoom setting if it is almost identical to 1.0 615 String zoomValue = (Math.abs(scale - 1.0) < 0.0001) ? null : Double.toString(scale); 616 AdtPlugin.setFileProperty(mLayoutEditor.getInputFile(), NAME_ZOOM, zoomValue); 617 } 618 619 /** 620 * Scales the canvas to best fit 621 * 622 * @param onlyZoomOut if true, then the zooming factor will never be larger than 1, 623 * which means that this function will zoom out if necessary to show the 624 * rendered image, but it will never zoom in. 625 */ 626 void setFitScale(boolean onlyZoomOut) { 627 Image image = getImageOverlay().getImage(); 628 if (image != null) { 629 Rectangle canvasSize = getClientArea(); 630 int canvasWidth = canvasSize.width; 631 int canvasHeight = canvasSize.height; 632 633 ImageData imageData = image.getImageData(); 634 int sceneWidth = imageData.width; 635 int sceneHeight = imageData.height; 636 if (sceneWidth == 0.0 || sceneHeight == 0.0) { 637 return; 638 } 639 640 // Reduce the margins if necessary 641 int hDelta = canvasWidth - sceneWidth; 642 int hMargin = 0; 643 if (hDelta > 2 * CanvasTransform.DEFAULT_MARGIN) { 644 hMargin = CanvasTransform.DEFAULT_MARGIN; 645 } else if (hDelta > 0) { 646 hMargin = hDelta / 2; 647 } 648 649 int vDelta = canvasHeight - sceneHeight; 650 int vMargin = 0; 651 if (vDelta > 2 * CanvasTransform.DEFAULT_MARGIN) { 652 vMargin = CanvasTransform.DEFAULT_MARGIN; 653 } else if (vDelta > 0) { 654 vMargin = vDelta / 2; 655 } 656 657 double hScale = (canvasWidth - 2 * hMargin) / (double) sceneWidth; 658 double vScale = (canvasHeight - 2 * vMargin) / (double) sceneHeight; 659 660 double scale = Math.min(hScale, vScale); 661 662 if (onlyZoomOut) { 663 scale = Math.min(1.0, scale); 664 } 665 666 setScale(scale, true); 667 } 668 } 669 670 /** 671 * Transforms a point, expressed in layout coordinates, into "client" coordinates 672 * relative to the control (and not relative to the display). 673 * 674 * @param canvasX X in the canvas coordinates 675 * @param canvasY Y in the canvas coordinates 676 * @return A new {@link Point} in control client coordinates (not display coordinates) 677 */ 678 /* package */ Point layoutToControlPoint(int canvasX, int canvasY) { 679 int x = mHScale.translate(canvasX); 680 int y = mVScale.translate(canvasY); 681 return new Point(x, y); 682 } 683 684 /** 685 * Returns the action for the context menu corresponding to the given action id. 686 * <p/> 687 * For global actions such as copy or paste, the action id must be composed of 688 * the {@link #PREFIX_CANVAS_ACTION} followed by one of {@link ActionFactory}'s 689 * action ids. 690 * <p/> 691 * Returns null if there's no action for the given id. 692 */ 693 /* package */ IAction getAction(String actionId) { 694 String prefix = PREFIX_CANVAS_ACTION; 695 if (mMenuManager == null || 696 actionId == null || 697 !actionId.startsWith(prefix)) { 698 return null; 699 } 700 701 actionId = actionId.substring(prefix.length()); 702 703 for (IContributionItem contrib : mMenuManager.getItems()) { 704 if (contrib instanceof ActionContributionItem && 705 actionId.equals(contrib.getId())) { 706 return ((ActionContributionItem) contrib).getAction(); 707 } 708 } 709 710 return null; 711 } 712 713 //--------------- 714 715 /** 716 * Paints the canvas in response to paint events. 717 */ 718 private void onPaint(PaintEvent e) { 719 GC gc = e.gc; 720 gc.setFont(mFont); 721 mGCWrapper.setGC(gc); 722 try { 723 if (!mImageOverlay.isHiding()) { 724 mImageOverlay.paint(gc); 725 } 726 727 if (mShowOutline) { 728 if (mOutlineOverlay == null) { 729 mOutlineOverlay = new OutlineOverlay(mViewHierarchy, mHScale, mVScale); 730 mOutlineOverlay.create(getDisplay()); 731 } 732 if (!mOutlineOverlay.isHiding()) { 733 mOutlineOverlay.paint(gc); 734 } 735 } 736 737 if (mShowInvisible) { 738 if (mEmptyOverlay == null) { 739 mEmptyOverlay = new EmptyViewsOverlay(mViewHierarchy, mHScale, mVScale); 740 mEmptyOverlay.create(getDisplay()); 741 } 742 if (!mEmptyOverlay.isHiding()) { 743 mEmptyOverlay.paint(gc); 744 } 745 } 746 747 if (!mHoverOverlay.isHiding()) { 748 mHoverOverlay.paint(gc); 749 } 750 if (!mIncludeOverlay.isHiding()) { 751 mIncludeOverlay.paint(gc); 752 } 753 754 if (!mSelectionOverlay.isHiding()) { 755 mSelectionOverlay.paint(mSelectionManager, mGCWrapper, gc, mRulesEngine); 756 } 757 mGestureManager.paint(gc); 758 759 } finally { 760 mGCWrapper.setGC(null); 761 } 762 } 763 764 /** 765 * Shows or hides invisible parent views, which are views which have empty bounds and 766 * no children. The nodes which will be shown are provided by 767 * {@link #getNodesToExplode()}. 768 * 769 * @param show When true, any invisible parent nodes are padded and highlighted 770 * ("exploded"), and when false any formerly exploded nodes are hidden. 771 */ 772 /* package */ void showInvisibleViews(boolean show) { 773 if (mShowInvisible == show) { 774 return; 775 } 776 mShowInvisible = show; 777 778 // Optimization: Avoid doing work when we don't have invisible parents (on show) 779 // or formerly exploded nodes (on hide). 780 if (show && !mViewHierarchy.hasInvisibleParents()) { 781 return; 782 } else if (!show && !mViewHierarchy.hasExplodedParents()) { 783 return; 784 } 785 786 mLayoutEditor.recomputeLayout(); 787 } 788 789 /** 790 * Returns a set of nodes that should be exploded (forced non-zero padding during render), 791 * or null if no nodes should be exploded. (Note that this is independent of the 792 * explode-all mode, where all nodes are padded -- that facility does not use this 793 * mechanism, which is only intended to be used to expose invisible parent nodes. 794 * 795 * @return The set of invisible parents, or null if no views should be expanded. 796 */ 797 public Set<UiElementNode> getNodesToExplode() { 798 if (mShowInvisible) { 799 return mViewHierarchy.getInvisibleNodes(); 800 } 801 802 // IF we have selection, and IF we have invisible nodes in the view, 803 // see if any of the selected items are among the invisible nodes, and if so 804 // add them to a lazily constructed set which we pass back for rendering. 805 Set<UiElementNode> result = null; 806 List<SelectionItem> selections = mSelectionManager.getSelections(); 807 if (selections.size() > 0) { 808 List<CanvasViewInfo> invisibleParents = mViewHierarchy.getInvisibleViews(); 809 if (invisibleParents.size() > 0) { 810 for (SelectionItem item : selections) { 811 CanvasViewInfo viewInfo = item.getViewInfo(); 812 // O(n^2) here, but both the selection size and especially the 813 // invisibleParents size are expected to be small 814 if (invisibleParents.contains(viewInfo)) { 815 UiViewElementNode node = viewInfo.getUiViewNode(); 816 if (node != null) { 817 if (result == null) { 818 result = new HashSet<UiElementNode>(); 819 } 820 result.add(node); 821 } 822 } 823 } 824 } 825 } 826 827 return result; 828 } 829 830 /** 831 * Clears the hover. 832 */ 833 /* package */ void clearHover() { 834 mHoverOverlay.clearHover(); 835 } 836 837 /** 838 * Hover on top of a known child. 839 */ 840 /* package */ void hover(MouseEvent e) { 841 // Check if a button is pressed; no hovers during drags 842 if ((e.stateMask & SWT.BUTTON_MASK) != 0) { 843 clearHover(); 844 return; 845 } 846 847 LayoutPoint p = ControlPoint.create(this, e).toLayout(); 848 CanvasViewInfo vi = mViewHierarchy.findViewInfoAt(p); 849 850 // We don't hover on the root since it's not a widget per see and it is always there. 851 // We also skip spacers... 852 if (vi != null && (vi.isRoot() || vi.isHidden())) { 853 vi = null; 854 } 855 856 boolean needsUpdate = vi != mHoverViewInfo; 857 mHoverViewInfo = vi; 858 859 if (vi == null) { 860 clearHover(); 861 } else { 862 Rectangle r = vi.getSelectionRect(); 863 mHoverOverlay.setHover(r.x, r.y, r.width, r.height); 864 } 865 866 if (needsUpdate) { 867 redraw(); 868 } 869 } 870 871 /** 872 * Shows the given {@link CanvasViewInfo}, which can mean exposing its XML or if it's 873 * an included element, its corresponding file. 874 * 875 * @param vi the {@link CanvasViewInfo} to be shown 876 */ 877 public void show(CanvasViewInfo vi) { 878 String url = vi.getIncludeUrl(); 879 if (url != null) { 880 showInclude(url); 881 } else { 882 showXml(vi); 883 } 884 } 885 886 /** 887 * Shows the layout file referenced by the given url in the same project. 888 * 889 * @param url The layout attribute url of the form @layout/foo 890 */ 891 private void showInclude(String url) { 892 GraphicalEditorPart graphicalEditor = mLayoutEditor.getGraphicalEditor(); 893 IPath filePath = graphicalEditor.findResourceFile(url); 894 if (filePath == null) { 895 // Should not be possible - if the URL had been bad, then we wouldn't 896 // have been able to render the scene and you wouldn't have been able 897 // to click on it 898 return; 899 } 900 901 // Save the including file, if necessary: without it, the "Show Included In" 902 // facility which is invoked automatically will not work properly if the <include> 903 // tag is not in the saved version of the file, since the outer file is read from 904 // disk rather than from memory. 905 IEditorSite editorSite = graphicalEditor.getEditorSite(); 906 IWorkbenchPage page = editorSite.getPage(); 907 page.saveEditor(mLayoutEditor, false); 908 909 IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot(); 910 IFile xmlFile = null; 911 IPath workspacePath = workspace.getLocation(); 912 if (workspacePath.isPrefixOf(filePath)) { 913 IPath relativePath = filePath.makeRelativeTo(workspacePath); 914 xmlFile = (IFile) workspace.findMember(relativePath); 915 } else if (filePath.isAbsolute()) { 916 xmlFile = workspace.getFileForLocation(filePath); 917 } 918 if (xmlFile != null) { 919 IFile leavingFile = graphicalEditor.getEditedFile(); 920 Reference next = Reference.create(graphicalEditor.getEditedFile()); 921 922 try { 923 IEditorPart openAlready = EditorUtility.isOpenInEditor(xmlFile); 924 925 // Show the included file as included within this click source? 926 if (openAlready != null) { 927 if (openAlready instanceof LayoutEditor) { 928 LayoutEditor editor = (LayoutEditor)openAlready; 929 GraphicalEditorPart gEditor = editor.getGraphicalEditor(); 930 if (gEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) { 931 gEditor.showIn(next); 932 } 933 } 934 } else { 935 try { 936 // Set initial state of a new file 937 // TODO: Only set rendering target portion of the state 938 QualifiedName qname = ConfigurationComposite.NAME_CONFIG_STATE; 939 String state = AdtPlugin.getFileProperty(leavingFile, qname); 940 xmlFile.setSessionProperty(GraphicalEditorPart.NAME_INITIAL_STATE, 941 state); 942 } catch (CoreException e) { 943 // pass 944 } 945 946 if (graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) { 947 try { 948 xmlFile.setSessionProperty(GraphicalEditorPart.NAME_INCLUDE, next); 949 } catch (CoreException e) { 950 // pass - worst that can happen is that we don't 951 //start with inclusion 952 } 953 } 954 } 955 956 EditorUtility.openInEditor(xmlFile, true); 957 return; 958 } catch (PartInitException ex) { 959 AdtPlugin.log(ex, "Can't open %$1s", url); //$NON-NLS-1$ 960 } 961 } else { 962 // It's not a path in the workspace; look externally 963 // (this is probably an @android: path) 964 if (filePath.isAbsolute()) { 965 IFileStore fileStore = EFS.getLocalFileSystem().getStore(filePath); 966 // fileStore = fileStore.getChild(names[i]); 967 if (!fileStore.fetchInfo().isDirectory() && fileStore.fetchInfo().exists()) { 968 try { 969 IDE.openEditorOnFileStore(page, fileStore); 970 return; 971 } catch (PartInitException ex) { 972 AdtPlugin.log(ex, "Can't open %$1s", url); //$NON-NLS-1$ 973 } 974 } 975 } 976 } 977 978 // Failed: display message to the user 979 String message = String.format("Could not find resource %1$s", url); 980 IStatusLineManager status = editorSite.getActionBars().getStatusLineManager(); 981 status.setErrorMessage(message); 982 getDisplay().beep(); 983 } 984 985 /** 986 * Returns the layout resource name of this layout 987 * 988 * @return the layout resource name of this layout 989 */ 990 public String getLayoutResourceName() { 991 GraphicalEditorPart graphicalEditor = mLayoutEditor.getGraphicalEditor(); 992 return graphicalEditor.getLayoutResourceName(); 993 } 994 995 /** 996 * Returns the layout resource url of the current layout 997 * 998 * @return 999 */ 1000 /* 1001 public String getMe() { 1002 GraphicalEditorPart graphicalEditor = mLayoutEditor.getGraphicalEditor(); 1003 IFile editedFile = graphicalEditor.getEditedFile(); 1004 return editedFile.getProjectRelativePath().toOSString(); 1005 } 1006 */ 1007 1008 /** 1009 * Show the XML element corresponding to the given {@link CanvasViewInfo} (unless it's 1010 * a root). 1011 * 1012 * @param vi The clicked {@link CanvasViewInfo} whose underlying XML element we want 1013 * to view 1014 */ 1015 private void showXml(CanvasViewInfo vi) { 1016 // Warp to the text editor and show the corresponding XML for the 1017 // double-clicked widget 1018 if (vi.isRoot()) { 1019 return; 1020 } 1021 1022 Node xmlNode = vi.getXmlNode(); 1023 if (xmlNode != null) { 1024 boolean found = mLayoutEditor.show(xmlNode); 1025 if (!found) { 1026 getDisplay().beep(); 1027 } 1028 } 1029 } 1030 1031 //--------------- 1032 1033 /** 1034 * Helper to create the drag source for the given control. 1035 * <p/> 1036 * This is static with package-access so that {@link OutlinePage} can also 1037 * create an exact copy of the source with the same attributes. 1038 */ 1039 /* package */static DragSource createDragSource(Control control) { 1040 DragSource source = new DragSource(control, DND.DROP_COPY | DND.DROP_MOVE); 1041 source.setTransfer(new Transfer[] { 1042 TextTransfer.getInstance(), 1043 SimpleXmlTransfer.getInstance() 1044 }); 1045 return source; 1046 } 1047 1048 /** 1049 * Helper to create the drop target for the given control. 1050 */ 1051 private static DropTarget createDropTarget(Control control) { 1052 DropTarget dropTarget = new DropTarget( 1053 control, DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_DEFAULT); 1054 dropTarget.setTransfer(new Transfer[] { 1055 SimpleXmlTransfer.getInstance() 1056 }); 1057 return dropTarget; 1058 } 1059 1060 //--------------- 1061 1062 /** 1063 * Invoked by the constructor to add our cut/copy/paste/delete/select-all 1064 * handlers in the global action handlers of this editor's site. 1065 * <p/> 1066 * This will enable the menu items under the global Edit menu and make them 1067 * invoke our actions as needed. As a benefit, the corresponding shortcut 1068 * accelerators will do what one would expect. 1069 */ 1070 private void setupGlobalActionHandlers() { 1071 mCutAction = new Action() { 1072 @Override 1073 public void run() { 1074 mClipboardSupport.cutSelectionToClipboard(mSelectionManager.getSnapshot()); 1075 updateMenuActionState(); 1076 } 1077 }; 1078 1079 copyActionAttributes(mCutAction, ActionFactory.CUT); 1080 1081 mCopyAction = new Action() { 1082 @Override 1083 public void run() { 1084 mClipboardSupport.copySelectionToClipboard(mSelectionManager.getSnapshot()); 1085 updateMenuActionState(); 1086 } 1087 }; 1088 1089 copyActionAttributes(mCopyAction, ActionFactory.COPY); 1090 1091 mPasteAction = new Action() { 1092 @Override 1093 public void run() { 1094 mClipboardSupport.pasteSelection(mSelectionManager.getSnapshot()); 1095 updateMenuActionState(); 1096 } 1097 }; 1098 1099 copyActionAttributes(mPasteAction, ActionFactory.PASTE); 1100 1101 mDeleteAction = new Action() { 1102 @Override 1103 public void run() { 1104 mClipboardSupport.deleteSelection( 1105 getDeleteLabel(), 1106 mSelectionManager.getSnapshot()); 1107 } 1108 }; 1109 1110 copyActionAttributes(mDeleteAction, ActionFactory.DELETE); 1111 1112 mSelectAllAction = new Action() { 1113 @Override 1114 public void run() { 1115 GraphicalEditorPart graphicalEditor = getLayoutEditor().getGraphicalEditor(); 1116 StyledText errorLabel = graphicalEditor.getErrorLabel(); 1117 if (errorLabel.isFocusControl()) { 1118 errorLabel.selectAll(); 1119 return; 1120 } 1121 1122 mSelectionManager.selectAll(); 1123 } 1124 }; 1125 1126 copyActionAttributes(mSelectAllAction, ActionFactory.SELECT_ALL); 1127 } 1128 1129 /* package */ String getCutLabel() { 1130 return mCutAction.getText(); 1131 } 1132 1133 /* package */ String getDeleteLabel() { 1134 // verb "Delete" from the DELETE action's title 1135 return mDeleteAction.getText(); 1136 } 1137 1138 /** 1139 * Updates menu actions that depends on the selection. 1140 */ 1141 void updateMenuActionState() { 1142 List<SelectionItem> selections = getSelectionManager().getSelections(); 1143 boolean hasSelection = !selections.isEmpty(); 1144 if (hasSelection && selections.size() == 1 && selections.get(0).isRoot()) { 1145 hasSelection = false; 1146 } 1147 1148 StyledText errorLabel = mLayoutEditor.getGraphicalEditor().getErrorLabel(); 1149 mCutAction.setEnabled(hasSelection); 1150 mCopyAction.setEnabled(hasSelection || errorLabel.getSelectionCount() > 0); 1151 mDeleteAction.setEnabled(hasSelection); 1152 // Select All should *always* be selectable, regardless of whether anything 1153 // is currently selected. 1154 mSelectAllAction.setEnabled(true); 1155 1156 // The paste operation is only available if we can paste our custom type. 1157 // We do not currently support pasting random text (e.g. XML). Maybe later. 1158 boolean hasSxt = mClipboardSupport.hasSxtOnClipboard(); 1159 mPasteAction.setEnabled(hasSxt); 1160 } 1161 1162 /** 1163 * Update the actions when this editor is activated 1164 * 1165 * @param bars the action bar for this canvas 1166 */ 1167 public void updateGlobalActions(IActionBars bars) { 1168 updateMenuActionState(); 1169 1170 bars.setGlobalActionHandler(ActionFactory.CUT.getId(), mCutAction); 1171 bars.setGlobalActionHandler(ActionFactory.COPY.getId(), mCopyAction); 1172 bars.setGlobalActionHandler(ActionFactory.PASTE.getId(), mPasteAction); 1173 bars.setGlobalActionHandler(ActionFactory.DELETE.getId(), mDeleteAction); 1174 bars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), mSelectAllAction); 1175 1176 ITextEditor editor = mLayoutEditor.getStructuredTextEditor(); 1177 IAction undoAction = editor.getAction(ActionFactory.UNDO.getId()); 1178 bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), undoAction); 1179 IAction redoAction = editor.getAction(ActionFactory.REDO.getId()); 1180 bars.setGlobalActionHandler(ActionFactory.REDO.getId(), redoAction); 1181 1182 bars.updateActionBars(); 1183 } 1184 1185 /** 1186 * Helper for {@link #setupGlobalActionHandlers()}. 1187 * Copies the action attributes form the given {@link ActionFactory}'s action to 1188 * our action. 1189 * <p/> 1190 * {@link ActionFactory} provides access to the standard global actions in Eclipse. 1191 * <p/> 1192 * This allows us to grab the standard labels and icons for the 1193 * global actions such as copy, cut, paste, delete and select-all. 1194 */ 1195 private void copyActionAttributes(Action action, ActionFactory factory) { 1196 IWorkbenchAction wa = factory.create(mLayoutEditor.getEditorSite().getWorkbenchWindow()); 1197 action.setId(wa.getId()); 1198 action.setText(wa.getText()); 1199 action.setEnabled(wa.isEnabled()); 1200 action.setDescription(wa.getDescription()); 1201 action.setToolTipText(wa.getToolTipText()); 1202 action.setAccelerator(wa.getAccelerator()); 1203 action.setActionDefinitionId(wa.getActionDefinitionId()); 1204 action.setImageDescriptor(wa.getImageDescriptor()); 1205 action.setHoverImageDescriptor(wa.getHoverImageDescriptor()); 1206 action.setDisabledImageDescriptor(wa.getDisabledImageDescriptor()); 1207 action.setHelpListener(wa.getHelpListener()); 1208 } 1209 1210 /** 1211 * Creates the context menu for the canvas. This is called once from the canvas' constructor. 1212 * <p/> 1213 * The menu has a static part with actions that are always available such as 1214 * copy, cut, paste and show in > explorer. This is created by 1215 * {@link #setupStaticMenuActions(IMenuManager)}. 1216 * <p/> 1217 * There's also a dynamic part that is populated by the rules of the 1218 * selected elements, created by {@link DynamicContextMenu}. 1219 */ 1220 private void createContextMenu() { 1221 1222 // This manager is the root of the context menu. 1223 mMenuManager = new MenuManager() { 1224 @Override 1225 public boolean isDynamic() { 1226 return true; 1227 } 1228 }; 1229 1230 // Fill the menu manager with the static & dynamic actions 1231 setupStaticMenuActions(mMenuManager); 1232 new DynamicContextMenu(mLayoutEditor, this, mMenuManager); 1233 Menu menu = mMenuManager.createContextMenu(this); 1234 setMenu(menu); 1235 1236 // Add listener to detect when the menu is about to be posted, such that 1237 // we can sync the selection. Without this, you can right click on something 1238 // in the canvas which is NOT selected, and the context menu will show items related 1239 // to the selection, NOT the item you clicked on!! 1240 addMenuDetectListener(new MenuDetectListener() { 1241 public void menuDetected(MenuDetectEvent e) { 1242 mSelectionManager.menuClick(e); 1243 } 1244 }); 1245 } 1246 1247 /** 1248 * Invoked by {@link #createContextMenu()} to create our *static* context menu once. 1249 * <p/> 1250 * The content of the menu itself does not change. However the state of the 1251 * various items is controlled by their associated actions. 1252 * <p/> 1253 * For cut/copy/paste/delete/select-all, we explicitly reuse the actions 1254 * created by {@link #setupGlobalActionHandlers()}, so this method must be 1255 * invoked after that one. 1256 */ 1257 private void setupStaticMenuActions(IMenuManager manager) { 1258 manager.removeAll(); 1259 1260 manager.add(new SelectionManager.SelectionMenu(mLayoutEditor.getGraphicalEditor())); 1261 manager.add(new Separator()); 1262 manager.add(mCutAction); 1263 manager.add(mCopyAction); 1264 manager.add(mPasteAction); 1265 manager.add(new Separator()); 1266 manager.add(mDeleteAction); 1267 manager.add(new Separator()); 1268 manager.add(new PlayAnimationMenu(this)); 1269 manager.add(new Separator()); 1270 1271 // Group "Show Included In" and "Show In" together 1272 manager.add(new ShowWithinMenu(mLayoutEditor)); 1273 1274 // Create a "Show In" sub-menu and automatically populate it using standard 1275 // actions contributed by the workbench. 1276 String showInLabel = IDEWorkbenchMessages.Workbench_showIn; 1277 MenuManager showInSubMenu = new MenuManager(showInLabel); 1278 showInSubMenu.add( 1279 ContributionItemFactory.VIEWS_SHOW_IN.create( 1280 mLayoutEditor.getSite().getWorkbenchWindow())); 1281 manager.add(showInSubMenu); 1282 } 1283 1284 /** 1285 * Deletes the selection. Equivalent to pressing the Delete key. 1286 */ 1287 /* package */ void delete() { 1288 mDeleteAction.run(); 1289 } 1290 1291 /** 1292 * Add new root in an existing empty XML layout. 1293 * <p/> 1294 * In case of error (unknown FQCN, document not empty), silently do nothing. 1295 * In case of success, the new element will have some default attributes set 1296 * (xmlns:android, layout_width and height). The edit is wrapped in a proper 1297 * undo. 1298 * <p/> 1299 * This is invoked by 1300 * {@link MoveGesture#drop(org.eclipse.swt.dnd.DropTargetEvent)}. 1301 * 1302 * @param rootFqcn A non-null non-empty FQCN that must match an existing 1303 * {@link ViewElementDescriptor} to add as root to the current 1304 * empty XML document. 1305 */ 1306 /* package */ void createDocumentRoot(String rootFqcn) { 1307 1308 // Need a valid empty document to create the new root 1309 final UiDocumentNode uiDoc = mLayoutEditor.getUiRootNode(); 1310 if (uiDoc == null || uiDoc.getUiChildren().size() > 0) { 1311 debugPrintf("Failed to create document root for %1$s: document is not empty", rootFqcn); 1312 return; 1313 } 1314 1315 // Find the view descriptor matching our FQCN 1316 final ViewElementDescriptor viewDesc = mLayoutEditor.getFqcnViewDescriptor(rootFqcn); 1317 if (viewDesc == null) { 1318 // TODO this could happen if dropping a custom view not known in this project 1319 debugPrintf("Failed to add document root, unknown FQCN %1$s", rootFqcn); 1320 return; 1321 } 1322 1323 // Get the last segment of the FQCN for the undo title 1324 String title = rootFqcn; 1325 int pos = title.lastIndexOf('.'); 1326 if (pos > 0 && pos < title.length() - 1) { 1327 title = title.substring(pos + 1); 1328 } 1329 title = String.format("Create root %1$s in document", title); 1330 1331 mLayoutEditor.wrapUndoEditXmlModel(title, new Runnable() { 1332 public void run() { 1333 UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc); 1334 1335 // A root node requires the Android XMLNS 1336 uiNew.setAttributeValue( 1337 LayoutConstants.ANDROID_NS_NAME, 1338 XmlnsAttributeDescriptor.XMLNS_URI, 1339 SdkConstants.NS_RESOURCES, 1340 true /*override*/); 1341 1342 // Adjust the attributes 1343 DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/); 1344 1345 uiNew.createXmlNode(); 1346 } 1347 }); 1348 } 1349 1350 /** 1351 * Returns the insets associated with views of the given fully qualified name, for the 1352 * current theme and screen type. 1353 * 1354 * @param fqcn the fully qualified name to the widget type 1355 * @return the insets, or null if unknown 1356 */ 1357 public Margins getInsets(String fqcn) { 1358 if (ViewMetadataRepository.INSETS_SUPPORTED) { 1359 ConfigurationComposite configComposite = 1360 mLayoutEditor.getGraphicalEditor().getConfigurationComposite(); 1361 String theme = configComposite.getTheme(); 1362 Density density = configComposite.getDensity(); 1363 return ViewMetadataRepository.getInsets(fqcn, density, theme); 1364 } else { 1365 return null; 1366 } 1367 } 1368 1369 private void debugPrintf(String message, Object... params) { 1370 if (DEBUG) { 1371 AdtPlugin.printToConsole("Canvas", String.format(message, params)); 1372 } 1373 } 1374 } 1375