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