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