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