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 static com.android.ide.common.layout.LayoutConstants.ANDROID_STRING_PREFIX; 20 import static com.android.ide.common.layout.LayoutConstants.SCROLL_VIEW; 21 import static com.android.ide.common.layout.LayoutConstants.STRING_PREFIX; 22 import static com.android.ide.eclipse.adt.AdtConstants.ANDROID_PKG; 23 import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor.viewNeedsPackage; 24 import static com.android.sdklib.SdkConstants.FD_GEN_SOURCES; 25 26 import com.android.ide.common.api.Rect; 27 import com.android.ide.common.rendering.LayoutLibrary; 28 import com.android.ide.common.rendering.StaticRenderSession; 29 import com.android.ide.common.rendering.api.Capability; 30 import com.android.ide.common.rendering.api.LayoutLog; 31 import com.android.ide.common.rendering.api.RenderSession; 32 import com.android.ide.common.rendering.api.ResourceValue; 33 import com.android.ide.common.rendering.api.Result; 34 import com.android.ide.common.rendering.api.SessionParams.RenderingMode; 35 import com.android.ide.common.resources.ResourceFile; 36 import com.android.ide.common.resources.ResourceRepository; 37 import com.android.ide.common.resources.ResourceResolver; 38 import com.android.ide.common.resources.configuration.FolderConfiguration; 39 import com.android.ide.common.sdk.LoadStatus; 40 import com.android.ide.eclipse.adt.AdtConstants; 41 import com.android.ide.eclipse.adt.AdtPlugin; 42 import com.android.ide.eclipse.adt.AdtUtils; 43 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; 44 import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider; 45 import com.android.ide.eclipse.adt.internal.editors.IconFactory; 46 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; 47 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor; 48 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ChangeFlags; 49 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ILayoutReloadListener; 50 import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback; 51 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; 52 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.IConfigListener; 53 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog; 54 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; 55 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; 56 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; 57 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; 58 import com.android.ide.eclipse.adt.internal.editors.ui.DecorComposite; 59 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; 60 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 61 import com.android.ide.eclipse.adt.internal.lint.LintEclipseContext; 62 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; 63 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; 64 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 65 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 66 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; 67 import com.android.ide.eclipse.adt.io.IFileWrapper; 68 import com.android.resources.Density; 69 import com.android.resources.ResourceFolderType; 70 import com.android.resources.ResourceType; 71 import com.android.sdklib.IAndroidTarget; 72 import com.android.sdklib.SdkConstants; 73 import com.android.util.Pair; 74 75 import org.eclipse.core.resources.IFile; 76 import org.eclipse.core.resources.IFolder; 77 import org.eclipse.core.resources.IMarker; 78 import org.eclipse.core.resources.IProject; 79 import org.eclipse.core.resources.IResource; 80 import org.eclipse.core.runtime.CoreException; 81 import org.eclipse.core.runtime.IPath; 82 import org.eclipse.core.runtime.IProgressMonitor; 83 import org.eclipse.core.runtime.IStatus; 84 import org.eclipse.core.runtime.NullProgressMonitor; 85 import org.eclipse.core.runtime.Path; 86 import org.eclipse.core.runtime.QualifiedName; 87 import org.eclipse.core.runtime.Status; 88 import org.eclipse.core.runtime.jobs.Job; 89 import org.eclipse.jdt.core.IClasspathEntry; 90 import org.eclipse.jdt.core.IJavaElement; 91 import org.eclipse.jdt.core.IJavaModelMarker; 92 import org.eclipse.jdt.core.IJavaProject; 93 import org.eclipse.jdt.core.IPackageFragment; 94 import org.eclipse.jdt.core.IPackageFragmentRoot; 95 import org.eclipse.jdt.core.JavaCore; 96 import org.eclipse.jdt.core.JavaModelException; 97 import org.eclipse.jdt.internal.ui.preferences.BuildPathsPropertyPage; 98 import org.eclipse.jdt.ui.actions.OpenNewClassWizardAction; 99 import org.eclipse.jdt.ui.wizards.NewClassWizardPage; 100 import org.eclipse.jface.text.BadLocationException; 101 import org.eclipse.jface.text.IDocument; 102 import org.eclipse.jface.text.source.ISourceViewer; 103 import org.eclipse.jface.viewers.ISelection; 104 import org.eclipse.jface.viewers.ISelectionProvider; 105 import org.eclipse.jface.window.Window; 106 import org.eclipse.swt.SWT; 107 import org.eclipse.swt.custom.SashForm; 108 import org.eclipse.swt.custom.StyleRange; 109 import org.eclipse.swt.custom.StyledText; 110 import org.eclipse.swt.events.MouseAdapter; 111 import org.eclipse.swt.events.MouseEvent; 112 import org.eclipse.swt.graphics.Image; 113 import org.eclipse.swt.layout.GridData; 114 import org.eclipse.swt.layout.GridLayout; 115 import org.eclipse.swt.widgets.Composite; 116 import org.eclipse.swt.widgets.Display; 117 import org.eclipse.text.edits.MalformedTreeException; 118 import org.eclipse.text.edits.MultiTextEdit; 119 import org.eclipse.text.edits.ReplaceEdit; 120 import org.eclipse.ui.IEditorInput; 121 import org.eclipse.ui.IEditorSite; 122 import org.eclipse.ui.INullSelectionListener; 123 import org.eclipse.ui.ISelectionListener; 124 import org.eclipse.ui.IWorkbench; 125 import org.eclipse.ui.IWorkbenchPage; 126 import org.eclipse.ui.IWorkbenchPart; 127 import org.eclipse.ui.IWorkbenchWindow; 128 import org.eclipse.ui.PartInitException; 129 import org.eclipse.ui.PlatformUI; 130 import org.eclipse.ui.dialogs.PreferencesUtil; 131 import org.eclipse.ui.ide.IDE; 132 import org.eclipse.ui.part.EditorPart; 133 import org.eclipse.ui.part.FileEditorInput; 134 import org.eclipse.ui.part.IPage; 135 import org.eclipse.ui.part.PageBookView; 136 import org.w3c.dom.Node; 137 138 import java.io.File; 139 import java.io.FileOutputStream; 140 import java.io.IOException; 141 import java.io.InputStream; 142 import java.util.ArrayList; 143 import java.util.Collection; 144 import java.util.Collections; 145 import java.util.List; 146 import java.util.Map; 147 import java.util.Set; 148 149 /** 150 * Graphical layout editor part, version 2. 151 * <p/> 152 * The main component of the editor part is the {@link LayoutCanvasViewer}, which 153 * actually delegates its work to the {@link LayoutCanvas} control. 154 * <p/> 155 * The {@link LayoutCanvasViewer} is set as the site's {@link ISelectionProvider}: 156 * when the selection changes in the canvas, it is thus broadcasted to anyone listening 157 * on the site's selection service. 158 * <p/> 159 * This part is also an {@link ISelectionListener}. It listens to the site's selection 160 * service and thus receives selection changes from itself as well as the associated 161 * outline and property sheet (these are registered by {@link LayoutEditor#getAdapter(Class)}). 162 * 163 * @since GLE2 164 */ 165 public class GraphicalEditorPart extends EditorPart 166 implements IPageImageProvider, ISelectionListener, INullSelectionListener { 167 168 /* 169 * Useful notes: 170 * To understand Drag'n'drop: 171 * http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html 172 * 173 * To understand the site's selection listener, selection provider, and the 174 * confusion of different-yet-similarly-named interfaces, consult this: 175 * http://www.eclipse.org/articles/Article-WorkbenchSelections/article.html 176 * 177 * To summarize the selection mechanism: 178 * - The workbench site selection service can be seen as "centralized" 179 * service that registers selection providers and selection listeners. 180 * - The editor part and the outline are selection providers. 181 * - The editor part, the outline and the property sheet are listeners 182 * which all listen to each others indirectly. 183 */ 184 185 /** 186 * Session-property on files which specifies the initial config state to be used on 187 * this file 188 */ 189 public final static QualifiedName NAME_INITIAL_STATE = 190 new QualifiedName(AdtPlugin.PLUGIN_ID, "initialstate");//$NON-NLS-1$ 191 192 /** 193 * Session-property on files which specifies the inclusion-context (reference to another layout 194 * which should be "including" this layout) when the file is opened 195 */ 196 public final static QualifiedName NAME_INCLUDE = 197 new QualifiedName(AdtPlugin.PLUGIN_ID, "includer");//$NON-NLS-1$ 198 199 /** Reference to the layout editor */ 200 private final LayoutEditor mLayoutEditor; 201 202 /** Reference to the file being edited. Can also be used to access the {@link IProject}. */ 203 private IFile mEditedFile; 204 205 /** The configuration composite at the top of the layout editor. */ 206 private ConfigurationComposite mConfigComposite; 207 208 /** The sash that splits the palette from the canvas. */ 209 private SashForm mSashPalette; 210 211 /** The sash that splits the palette from the error view. 212 * The error view is shown only when needed. */ 213 private SashForm mSashError; 214 215 /** The palette displayed on the left of the sash. */ 216 private PaletteControl mPalette; 217 218 /** The layout canvas displayed to the right of the sash. */ 219 private LayoutCanvasViewer mCanvasViewer; 220 221 /** The Rules Engine associated with this editor. It is project-specific. */ 222 private RulesEngine mRulesEngine; 223 224 /** Styled text displaying the most recent error in the error view. */ 225 private StyledText mErrorLabel; 226 227 /** 228 * The resource reference to a file that should surround this file (e.g. include this file 229 * visually), or null if not applicable 230 */ 231 private Reference mIncludedWithin; 232 233 private Map<ResourceType, Map<String, ResourceValue>> mConfiguredFrameworkRes; 234 private Map<ResourceType, Map<String, ResourceValue>> mConfiguredProjectRes; 235 private ProjectCallback mProjectCallback; 236 private boolean mNeedsRecompute = false; 237 private TargetListener mTargetListener; 238 private ConfigListener mConfigListener; 239 private ResourceResolver mResourceResolver; 240 private ReloadListener mReloadListener; 241 private int mMinSdkVersion; 242 private int mTargetSdkVersion; 243 private LayoutActionBar mActionBar; 244 245 /** 246 * Flags which tracks whether this editor is currently active which is set whenever 247 * {@link #activated()} is called and clear whenever {@link #deactivated()} is called. 248 * This is used to suppress repeated calls to {@link #activate()} to avoid doing 249 * unnecessary work. 250 */ 251 private boolean mActive; 252 253 public GraphicalEditorPart(LayoutEditor layoutEditor) { 254 mLayoutEditor = layoutEditor; 255 setPartName("Graphical Layout"); 256 } 257 258 // ------------------------------------ 259 // Methods overridden from base classes 260 //------------------------------------ 261 262 /** 263 * Initializes the editor part with a site and input. 264 * {@inheritDoc} 265 */ 266 @Override 267 public void init(IEditorSite site, IEditorInput input) throws PartInitException { 268 setSite(site); 269 useNewEditorInput(input); 270 271 if (mTargetListener == null) { 272 mTargetListener = new TargetListener(); 273 AdtPlugin.getDefault().addTargetListener(mTargetListener); 274 } 275 } 276 277 private void useNewEditorInput(IEditorInput input) throws PartInitException { 278 // The contract of init() mentions we need to fail if we can't understand the input. 279 if (!(input instanceof FileEditorInput)) { 280 throw new PartInitException("Input is not of type FileEditorInput: " + //$NON-NLS-1$ 281 input == null ? "null" : input.toString()); //$NON-NLS-1$ 282 } 283 } 284 285 public Image getPageImage() { 286 return IconFactory.getInstance().getIcon("editor_page_design"); //$NON-NLS-1$ 287 } 288 289 @Override 290 public void createPartControl(Composite parent) { 291 292 Display d = parent.getDisplay(); 293 294 GridLayout gl = new GridLayout(1, false); 295 parent.setLayout(gl); 296 gl.marginHeight = gl.marginWidth = 0; 297 298 mConfigListener = new ConfigListener(); 299 300 // Check whether somebody has requested an initial state for the newly opened file. 301 // The initial state is a serialized version of the state compatible with 302 // {@link ConfigurationComposite#CONFIG_STATE}. 303 String initialState = null; 304 IFile file = mEditedFile; 305 if (file == null) { 306 IEditorInput input = mLayoutEditor.getEditorInput(); 307 if (input instanceof FileEditorInput) { 308 file = ((FileEditorInput) input).getFile(); 309 } 310 } 311 312 if (file != null) { 313 try { 314 initialState = (String) file.getSessionProperty(NAME_INITIAL_STATE); 315 if (initialState != null) { 316 // Only use once 317 file.setSessionProperty(NAME_INITIAL_STATE, null); 318 } 319 } catch (CoreException e) { 320 AdtPlugin.log(e, "Can't read session property %1$s", NAME_INITIAL_STATE); 321 } 322 } 323 324 mConfigComposite = new ConfigurationComposite(mConfigListener, parent, 325 SWT.BORDER, initialState); 326 327 mSashPalette = new SashForm(parent, SWT.HORIZONTAL); 328 mSashPalette.setLayoutData(new GridData(GridData.FILL_BOTH)); 329 330 DecorComposite paletteDecor = new DecorComposite(mSashPalette, SWT.BORDER); 331 paletteDecor.setContent(new PaletteControl.PaletteDecor(this)); 332 mPalette = (PaletteControl) paletteDecor.getContentControl(); 333 334 Composite layoutBarAndCanvas = new Composite(mSashPalette, SWT.NONE); 335 GridLayout gridLayout = new GridLayout(1, false); 336 gridLayout.horizontalSpacing = 0; 337 gridLayout.verticalSpacing = 0; 338 gridLayout.marginWidth = 0; 339 gridLayout.marginHeight = 0; 340 layoutBarAndCanvas.setLayout(gridLayout); 341 mActionBar = new LayoutActionBar(layoutBarAndCanvas, SWT.NONE, this); 342 GridData detailsData = new GridData(SWT.FILL, SWT.FILL, false, false, 1, 1); 343 mActionBar.setLayoutData(detailsData); 344 if (file != null) { 345 mActionBar.updateErrorIndicator(LintEclipseContext.hasMarkers(file)); 346 } 347 348 mSashError = new SashForm(layoutBarAndCanvas, SWT.VERTICAL | SWT.BORDER); 349 mSashError.setLayoutData(new GridData(GridData.FILL_BOTH)); 350 351 mCanvasViewer = new LayoutCanvasViewer(mLayoutEditor, mRulesEngine, mSashError, SWT.NONE); 352 mSashError.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); 353 354 mErrorLabel = new StyledText(mSashError, SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL); 355 mErrorLabel.setEditable(false); 356 mErrorLabel.setBackground(d.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); 357 mErrorLabel.setForeground(d.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); 358 mErrorLabel.addMouseListener(new ErrorLabelListener()); 359 360 mSashPalette.setWeights(new int[] { 20, 80 }); 361 mSashError.setWeights(new int[] { 80, 20 }); 362 mSashError.setMaximizedControl(mCanvasViewer.getControl()); 363 364 // Initialize the state 365 reloadPalette(); 366 367 getSite().setSelectionProvider(mCanvasViewer); 368 getSite().getPage().addSelectionListener(this); 369 } 370 371 /** 372 * Listens to workbench selections that does NOT come from {@link LayoutEditor} 373 * (those are generated by ourselves). 374 * <p/> 375 * Selection can be null, as indicated by this class implementing 376 * {@link INullSelectionListener}. 377 */ 378 public void selectionChanged(IWorkbenchPart part, ISelection selection) { 379 if (!(part instanceof LayoutEditor)) { 380 if (part instanceof PageBookView) { 381 PageBookView pbv = (PageBookView) part; 382 IPage currentPage = pbv.getCurrentPage(); 383 if (currentPage instanceof OutlinePage) { 384 LayoutCanvas canvas = getCanvasControl(); 385 if (canvas != null && canvas.getOutlinePage() != currentPage) { 386 // The notification is not for this view; ignore 387 // (can happen when there are multiple pages simultaneously 388 // visible) 389 return; 390 } 391 } 392 } 393 mCanvasViewer.setSelection(selection); 394 } 395 } 396 397 @Override 398 public void dispose() { 399 getSite().getPage().removeSelectionListener(this); 400 getSite().setSelectionProvider(null); 401 402 if (mTargetListener != null) { 403 AdtPlugin.getDefault().removeTargetListener(mTargetListener); 404 mTargetListener = null; 405 } 406 407 if (mReloadListener != null) { 408 LayoutReloadMonitor.getMonitor().removeListener(mReloadListener); 409 mReloadListener = null; 410 } 411 412 if (mCanvasViewer != null) { 413 mCanvasViewer.dispose(); 414 mCanvasViewer = null; 415 } 416 super.dispose(); 417 } 418 419 /** 420 * Select the visual element corresponding to the given XML node 421 * @param xmlNode The Node whose element we want to select 422 */ 423 public void select(Node xmlNode) { 424 mCanvasViewer.getCanvas().getSelectionManager().select(xmlNode); 425 } 426 427 /** 428 * Listens to changes from the Configuration UI banner and triggers layout rendering when 429 * changed. Also provide the Configuration UI with the list of resources/layout to display. 430 */ 431 private class ConfigListener implements IConfigListener { 432 433 /** 434 * Looks for a file matching the new {@link FolderConfiguration} and attempts to open it. 435 * <p/>If there is no match, notify the user. 436 */ 437 public void onConfigurationChange() { 438 mConfiguredFrameworkRes = mConfiguredProjectRes = null; 439 mResourceResolver = null; 440 441 if (mEditedFile == null || mConfigComposite.getEditedConfig() == null) { 442 return; 443 } 444 445 // Before doing the normal process, test for the following case. 446 // - the editor is being opened (or reset for a new input) 447 // - the file being opened is not the best match for any possible configuration 448 // - another random compatible config was chosen in the config composite. 449 // The result is that 'match' will not be the file being edited, but because this is not 450 // due to a config change, we should not trigger opening the actual best match (also, 451 // because the editor is still opening the MatchingStrategy woudln't answer true 452 // and the best match file would open in a different editor). 453 // So the solution is that if the editor is being created, we just call recomputeLayout 454 // without looking for a better matching layout file. 455 if (mLayoutEditor.isCreatingPages()) { 456 recomputeLayout(); 457 } else { 458 // get the resources of the file's project. 459 ProjectResources resources = ResourceManager.getInstance().getProjectResources( 460 mEditedFile.getProject()); 461 462 // from the resources, look for a matching file 463 ResourceFile match = null; 464 if (resources != null) { 465 match = resources.getMatchingFile(mEditedFile.getName(), 466 ResourceFolderType.LAYOUT, 467 mConfigComposite.getCurrentConfig()); 468 } 469 470 if (match != null) { 471 // since this is coming from Eclipse, this is always an instance of IFileWrapper 472 IFileWrapper iFileWrapper = (IFileWrapper) match.getFile(); 473 IFile iFile = iFileWrapper.getIFile(); 474 if (iFile.equals(mEditedFile) == false) { 475 try { 476 // tell the editor that the next replacement file is due to a config 477 // change. 478 mLayoutEditor.setNewFileOnConfigChange(true); 479 480 // ask the IDE to open the replacement file. 481 IDE.openEditor(getSite().getWorkbenchWindow().getActivePage(), iFile); 482 483 // we're done! 484 return; 485 } catch (PartInitException e) { 486 // FIXME: do something! 487 } 488 } 489 490 // at this point, we have not opened a new file. 491 492 // Store the state in the current file 493 mConfigComposite.storeState(); 494 495 // Even though the layout doesn't change, the config changed, and referenced 496 // resources need to be updated. 497 recomputeLayout(); 498 } else { 499 // display the error. 500 FolderConfiguration currentConfig = mConfigComposite.getCurrentConfig(); 501 displayError( 502 "No resources match the configuration\n \n\t%1$s\n \nChange the configuration or create:\n \n\tres/%2$s/%3$s\n \nYou can also click the 'Create' button above.", 503 currentConfig.toDisplayString(), 504 currentConfig.getFolderName(ResourceFolderType.LAYOUT), 505 mEditedFile.getName()); 506 } 507 } 508 509 reloadPalette(); 510 } 511 512 public void onThemeChange() { 513 // Store the state in the current file 514 mConfigComposite.storeState(); 515 mResourceResolver = null; 516 517 recomputeLayout(); 518 519 reloadPalette(); 520 } 521 522 public void onCreate() { 523 LayoutCreatorDialog dialog = new LayoutCreatorDialog(mConfigComposite.getShell(), 524 mEditedFile.getName(), mConfigComposite.getCurrentConfig()); 525 if (dialog.open() == Window.OK) { 526 final FolderConfiguration config = new FolderConfiguration(); 527 dialog.getConfiguration(config); 528 529 createAlternateLayout(config); 530 } 531 } 532 533 public void onRenderingTargetPreChange(IAndroidTarget oldTarget) { 534 preRenderingTargetChangeCleanUp(oldTarget); 535 } 536 537 public void onRenderingTargetPostChange(IAndroidTarget target) { 538 AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); 539 updateCapabilities(targetData); 540 541 mPalette.reloadPalette(target); 542 } 543 544 public Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources() { 545 if (mConfiguredFrameworkRes == null && mConfigComposite != null) { 546 ResourceRepository frameworkRes = getFrameworkResources(); 547 548 if (frameworkRes == null) { 549 AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework"); 550 } else { 551 // get the framework resource values based on the current config 552 mConfiguredFrameworkRes = frameworkRes.getConfiguredResources( 553 mConfigComposite.getCurrentConfig()); 554 } 555 } 556 557 return mConfiguredFrameworkRes; 558 } 559 560 public Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources() { 561 if (mConfiguredProjectRes == null && mConfigComposite != null) { 562 ProjectResources project = getProjectResources(); 563 564 // get the project resource values based on the current config 565 mConfiguredProjectRes = project.getConfiguredResources( 566 mConfigComposite.getCurrentConfig()); 567 } 568 569 return mConfiguredProjectRes; 570 } 571 572 /** 573 * Returns a {@link ProjectResources} for the framework resources based on the current 574 * configuration selection. 575 * @return the framework resources or null if not found. 576 */ 577 public ResourceRepository getFrameworkResources() { 578 return getFrameworkResources(getRenderingTarget()); 579 } 580 581 /** 582 * Returns a {@link ProjectResources} for the framework resources of a given 583 * target. 584 * @param target the target for which to return the framework resources. 585 * @return the framework resources or null if not found. 586 */ 587 public ResourceRepository getFrameworkResources(IAndroidTarget target) { 588 if (target != null) { 589 AndroidTargetData data = Sdk.getCurrent().getTargetData(target); 590 591 if (data != null) { 592 return data.getFrameworkResources(); 593 } 594 } 595 596 return null; 597 } 598 599 public ProjectResources getProjectResources() { 600 if (mEditedFile != null) { 601 ResourceManager manager = ResourceManager.getInstance(); 602 return manager.getProjectResources(mEditedFile.getProject()); 603 } 604 605 return null; 606 } 607 608 /** 609 * Creates a new layout file from the specified {@link FolderConfiguration}. 610 */ 611 private void createAlternateLayout(final FolderConfiguration config) { 612 new Job("Create Alternate Resource") { 613 @Override 614 protected IStatus run(IProgressMonitor monitor) { 615 // get the folder name 616 String folderName = config.getFolderName(ResourceFolderType.LAYOUT); 617 try { 618 619 // look to see if it exists. 620 // get the res folder 621 IFolder res = (IFolder)mEditedFile.getParent().getParent(); 622 String path = res.getLocation().toOSString(); 623 624 File newLayoutFolder = new File(path + File.separator + folderName); 625 if (newLayoutFolder.isFile()) { 626 // this should not happen since aapt would have complained 627 // before, but if one disable the automatic build, this could 628 // happen. 629 String message = String.format("File 'res/%1$s' is in the way!", 630 folderName); 631 632 AdtPlugin.displayError("Layout Creation", message); 633 634 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message); 635 } else if (newLayoutFolder.exists() == false) { 636 // create it. 637 newLayoutFolder.mkdir(); 638 } 639 640 // now create the file 641 File newLayoutFile = new File(newLayoutFolder.getAbsolutePath() + 642 File.separator + mEditedFile.getName()); 643 644 newLayoutFile.createNewFile(); 645 646 InputStream input = mEditedFile.getContents(); 647 648 FileOutputStream fos = new FileOutputStream(newLayoutFile); 649 650 byte[] data = new byte[512]; 651 int count; 652 while ((count = input.read(data)) != -1) { 653 fos.write(data, 0, count); 654 } 655 656 input.close(); 657 fos.close(); 658 659 // refreshes the res folder to show up the new 660 // layout folder (if needed) and the file. 661 // We use a progress monitor to catch the end of the refresh 662 // to trigger the edit of the new file. 663 res.refreshLocal(IResource.DEPTH_INFINITE, new IProgressMonitor() { 664 public void done() { 665 mConfigComposite.getDisplay().asyncExec(new Runnable() { 666 public void run() { 667 onConfigurationChange(); 668 } 669 }); 670 } 671 672 public void beginTask(String name, int totalWork) { 673 // pass 674 } 675 676 public void internalWorked(double work) { 677 // pass 678 } 679 680 public boolean isCanceled() { 681 // pass 682 return false; 683 } 684 685 public void setCanceled(boolean value) { 686 // pass 687 } 688 689 public void setTaskName(String name) { 690 // pass 691 } 692 693 public void subTask(String name) { 694 // pass 695 } 696 697 public void worked(int work) { 698 // pass 699 } 700 }); 701 } catch (IOException e2) { 702 String message = String.format( 703 "Failed to create File 'res/%1$s/%2$s' : %3$s", 704 folderName, mEditedFile.getName(), e2.getMessage()); 705 706 AdtPlugin.displayError("Layout Creation", message); 707 708 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 709 message, e2); 710 } catch (CoreException e2) { 711 String message = String.format( 712 "Failed to create File 'res/%1$s/%2$s' : %3$s", 713 folderName, mEditedFile.getName(), e2.getMessage()); 714 715 AdtPlugin.displayError("Layout Creation", message); 716 717 return e2.getStatus(); 718 } 719 720 return Status.OK_STATUS; 721 722 } 723 }.schedule(); 724 } 725 726 /** 727 * When the device changes, zoom the view to fit, but only up to 100% (e.g. zoom 728 * out to fit the content, or zoom back in if we were zoomed out more from the 729 * previous view, but only up to 100% such that we never blow up pixels 730 */ 731 public void onDevicePostChange() { 732 if (mActionBar.isZoomingAllowed()) { 733 getCanvasControl().setFitScale(true); 734 } 735 } 736 737 public String getIncludedWithin() { 738 return mIncludedWithin != null ? mIncludedWithin.getName() : null; 739 } 740 } 741 742 /** 743 * Listens to target changed in the current project, to trigger a new layout rendering. 744 */ 745 private class TargetListener implements ITargetChangeListener { 746 747 public void onProjectTargetChange(IProject changedProject) { 748 if (changedProject != null && changedProject.equals(getProject())) { 749 updateEditor(); 750 } 751 } 752 753 public void onTargetLoaded(IAndroidTarget loadedTarget) { 754 IAndroidTarget target = getRenderingTarget(); 755 if (target != null && target.equals(loadedTarget)) { 756 updateEditor(); 757 } 758 } 759 760 public void onSdkLoaded() { 761 // get the current rendering target to unload it 762 IAndroidTarget oldTarget = getRenderingTarget(); 763 preRenderingTargetChangeCleanUp(oldTarget); 764 765 computeSdkVersion(); 766 767 // get the project target 768 Sdk currentSdk = Sdk.getCurrent(); 769 if (currentSdk != null) { 770 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject()); 771 if (target != null) { 772 mConfigComposite.onSdkLoaded(target); 773 mConfigListener.onConfigurationChange(); 774 } 775 } 776 } 777 778 private void updateEditor() { 779 mLayoutEditor.commitPages(false /* onSave */); 780 781 // because the target changed we must reset the configured resources. 782 mConfiguredFrameworkRes = mConfiguredProjectRes = null; 783 mResourceResolver = null; 784 785 // make sure we remove the custom view loader, since its parent class loader is the 786 // bridge class loader. 787 mProjectCallback = null; 788 789 // recreate the ui root node always, this will also call onTargetChange 790 // on the config composite 791 mLayoutEditor.initUiRootNode(true /*force*/); 792 } 793 794 private IProject getProject() { 795 return getLayoutEditor().getProject(); 796 } 797 } 798 799 /** Refresh the configured project resources associated with this editor */ 800 public void refreshProjectResources() { 801 mConfiguredProjectRes = null; 802 mResourceResolver = null; 803 } 804 805 /** 806 * Returns the currently edited file 807 * 808 * @return the currently edited file, or null 809 */ 810 public IFile getEditedFile() { 811 return mEditedFile; 812 } 813 814 /** 815 * Returns the project for the currently edited file, or null 816 * 817 * @return the project containing the edited file, or null 818 */ 819 public IProject getProject() { 820 if (mEditedFile != null) { 821 return mEditedFile.getProject(); 822 } else { 823 return null; 824 } 825 } 826 827 // ---------------- 828 829 /** 830 * Save operation in the Graphical Editor Part. 831 * <p/> 832 * In our workflow, the model is owned by the Structured XML Editor. 833 * The graphical layout editor just displays it -- thus we don't really 834 * save anything here. 835 * <p/> 836 * This must NOT call the parent editor part. At the contrary, the parent editor 837 * part will call this *after* having done the actual save operation. 838 * <p/> 839 * The only action this editor must do is mark the undo command stack as 840 * being no longer dirty. 841 */ 842 @Override 843 public void doSave(IProgressMonitor monitor) { 844 // TODO implement a command stack 845 // getCommandStack().markSaveLocation(); 846 // firePropertyChange(PROP_DIRTY); 847 } 848 849 /** 850 * Save operation in the Graphical Editor Part. 851 * <p/> 852 * In our workflow, the model is owned by the Structured XML Editor. 853 * The graphical layout editor just displays it -- thus we don't really 854 * save anything here. 855 */ 856 @Override 857 public void doSaveAs() { 858 // pass 859 } 860 861 /** 862 * In our workflow, the model is owned by the Structured XML Editor. 863 * The graphical layout editor just displays it -- thus we don't really 864 * save anything here. 865 */ 866 @Override 867 public boolean isDirty() { 868 return false; 869 } 870 871 /** 872 * In our workflow, the model is owned by the Structured XML Editor. 873 * The graphical layout editor just displays it -- thus we don't really 874 * save anything here. 875 */ 876 @Override 877 public boolean isSaveAsAllowed() { 878 return false; 879 } 880 881 @Override 882 public void setFocus() { 883 // TODO Auto-generated method stub 884 885 } 886 887 /** 888 * Responds to a page change that made the Graphical editor page the activated page. 889 */ 890 public void activated() { 891 if (!mActive) { 892 mActive = true; 893 894 mActionBar.updateErrorIndicator(); 895 896 boolean changed = mConfigComposite.syncRenderState(); 897 if (changed) { 898 // Will also force recomputeLayout() 899 return; 900 } 901 902 if (mNeedsRecompute) { 903 recomputeLayout(); 904 } 905 } 906 } 907 908 /** 909 * Responds to a page change that made the Graphical editor page the deactivated page 910 */ 911 public void deactivated() { 912 mActive = false; 913 } 914 915 /** 916 * Opens and initialize the editor with a new file. 917 * @param file the file being edited. 918 */ 919 public void openFile(IFile file) { 920 mEditedFile = file; 921 mConfigComposite.setFile(mEditedFile); 922 923 if (mReloadListener == null) { 924 mReloadListener = new ReloadListener(); 925 LayoutReloadMonitor.getMonitor().addListener(mEditedFile.getProject(), mReloadListener); 926 } 927 928 if (mRulesEngine == null) { 929 mRulesEngine = new RulesEngine(this, mEditedFile.getProject()); 930 if (mCanvasViewer != null) { 931 mCanvasViewer.getCanvas().setRulesEngine(mRulesEngine); 932 } 933 } 934 935 // Pick up hand-off data: somebody requesting this file to be opened may have 936 // requested that it should be opened as included within another file 937 if (mEditedFile != null) { 938 try { 939 mIncludedWithin = (Reference) mEditedFile.getSessionProperty(NAME_INCLUDE); 940 if (mIncludedWithin != null) { 941 // Only use once 942 mEditedFile.setSessionProperty(NAME_INCLUDE, null); 943 } 944 } catch (CoreException e) { 945 AdtPlugin.log(e, "Can't access session property %1$s", NAME_INCLUDE); 946 } 947 } 948 949 computeSdkVersion(); 950 } 951 952 /** 953 * Resets the editor with a replacement file. 954 * @param file the replacement file. 955 */ 956 public void replaceFile(IFile file) { 957 mEditedFile = file; 958 mConfigComposite.replaceFile(mEditedFile); 959 computeSdkVersion(); 960 } 961 962 /** 963 * Resets the editor with a replacement file coming from a config change in the config 964 * selector. 965 * @param file the replacement file. 966 */ 967 public void changeFileOnNewConfig(IFile file) { 968 mEditedFile = file; 969 mConfigComposite.changeFileOnNewConfig(mEditedFile); 970 } 971 972 /** 973 * Responds to a target change for the project of the edited file 974 */ 975 public void onTargetChange() { 976 AndroidTargetData targetData = mConfigComposite.onXmlModelLoaded(); 977 updateCapabilities(targetData); 978 979 mConfigListener.onConfigurationChange(); 980 } 981 982 /** Updates the capabilities for the given target data (which may be null) */ 983 private void updateCapabilities(AndroidTargetData targetData) { 984 if (targetData != null) { 985 LayoutLibrary layoutLib = targetData.getLayoutLibrary(); 986 if (mIncludedWithin != null && !layoutLib.supports(Capability.EMBEDDED_LAYOUT)) { 987 showIn(null); 988 } 989 } 990 } 991 992 public LayoutEditor getLayoutEditor() { 993 return mLayoutEditor; 994 } 995 996 /** 997 * Returns the {@link RulesEngine} associated with this editor 998 * 999 * @return the {@link RulesEngine} associated with this editor, never null 1000 */ 1001 public RulesEngine getRulesEngine() { 1002 return mRulesEngine; 1003 } 1004 1005 /** 1006 * Return the {@link LayoutCanvas} associated with this editor 1007 * 1008 * @return the associated {@link LayoutCanvas} 1009 */ 1010 public LayoutCanvas getCanvasControl() { 1011 if (mCanvasViewer != null) { 1012 return mCanvasViewer.getCanvas(); 1013 } 1014 return null; 1015 } 1016 1017 public UiDocumentNode getModel() { 1018 return mLayoutEditor.getUiRootNode(); 1019 } 1020 1021 /** 1022 * Callback for XML model changed. Only update/recompute the layout if the editor is visible 1023 */ 1024 public void onXmlModelChanged() { 1025 // To optimize the rendering when the user is editing in the XML pane, we don't 1026 // refresh the editor if it's not the active part. 1027 // 1028 // This behavior is acceptable when the editor is the single "full screen" part 1029 // (as in this case active means visible.) 1030 // Unfortunately this breaks in 2 cases: 1031 // - when performing a drag'n'drop from one editor to another, the target is not 1032 // properly refreshed before it becomes active. 1033 // - when duplicating the editor window and placing both editors side by side (xml in one 1034 // and canvas in the other one), the canvas may not be refreshed when the XML is edited. 1035 // 1036 // TODO find a way to really query whether the pane is visible, not just active. 1037 1038 if (mLayoutEditor.isGraphicalEditorActive()) { 1039 recomputeLayout(); 1040 } else { 1041 // Remember we want to recompute as soon as the editor becomes active. 1042 mNeedsRecompute = true; 1043 } 1044 } 1045 1046 public void recomputeLayout() { 1047 try { 1048 if (!ensureFileValid()) { 1049 return; 1050 } 1051 1052 UiDocumentNode model = getModel(); 1053 if (!ensureModelValid(model)) { 1054 // Although we display an error, we still treat an empty document as a 1055 // successful layout result so that we can drop new elements in it. 1056 // 1057 // For that purpose, create a special LayoutScene that has no image, 1058 // no root view yet indicates success and then update the canvas with it. 1059 1060 mCanvasViewer.getCanvas().setSession( 1061 new StaticRenderSession( 1062 Result.Status.SUCCESS.createResult(), 1063 null /*rootViewInfo*/, null /*image*/), 1064 null /*explodeNodes*/, true /* layoutlib5 */); 1065 return; 1066 } 1067 1068 LayoutLibrary layoutLib = getReadyLayoutLib(true /*displayError*/); 1069 1070 if (layoutLib != null) { 1071 // if drawing in real size, (re)set the scaling factor. 1072 if (mActionBar.isZoomingRealSize()) { 1073 mActionBar.computeAndSetRealScale(false /* redraw */); 1074 } 1075 1076 IProject project = mEditedFile.getProject(); 1077 renderWithBridge(project, model, layoutLib); 1078 } 1079 } finally { 1080 // no matter the result, we are done doing the recompute based on the latest 1081 // resource/code change. 1082 mNeedsRecompute = false; 1083 } 1084 } 1085 1086 public void reloadPalette() { 1087 if (mPalette != null) { 1088 IAndroidTarget renderingTarget = getRenderingTarget(); 1089 if (renderingTarget != null) { 1090 mPalette.reloadPalette(renderingTarget); 1091 } 1092 } 1093 } 1094 1095 /** 1096 * Returns the {@link LayoutLibrary} associated with this editor, if it has 1097 * been initialized already. May return null if it has not been initialized (or has 1098 * not finished initializing). 1099 * 1100 * @return The {@link LayoutLibrary}, or null 1101 */ 1102 public LayoutLibrary getLayoutLibrary() { 1103 return getReadyLayoutLib(false /*displayError*/); 1104 } 1105 1106 /** 1107 * Returns the current bounds of the Android device screen, in canvas control pixels. 1108 * 1109 * @return the bounds of the screen, never null 1110 */ 1111 public Rect getScreenBounds() { 1112 return mConfigComposite.getScreenBounds(); 1113 } 1114 1115 /** 1116 * Returns the scale to multiply pixels in the layout coordinate space with to obtain 1117 * the corresponding dip (device independent pixel) 1118 * 1119 * @return the scale to multiple layout coordinates with to obtain the dip position 1120 */ 1121 public float getDipScale() { 1122 return Density.DEFAULT_DENSITY / (float) mConfigComposite.getDensity().getDpiValue(); 1123 } 1124 1125 // --- private methods --- 1126 1127 /** 1128 * Ensure that the file associated with this editor is valid (exists and is 1129 * synchronized). Any reasons why it is not are displayed in the editor's error area. 1130 * 1131 * @return True if the editor is valid, false otherwise. 1132 */ 1133 private boolean ensureFileValid() { 1134 // check that the resource exists. If the file is opened but the project is closed 1135 // or deleted for some reason (changed from outside of eclipse), then this will 1136 // return false; 1137 if (mEditedFile.exists() == false) { 1138 displayError("Resource '%1$s' does not exist.", 1139 mEditedFile.getFullPath().toString()); 1140 return false; 1141 } 1142 1143 if (mEditedFile.isSynchronized(IResource.DEPTH_ZERO) == false) { 1144 String message = String.format("%1$s is out of sync. Please refresh.", 1145 mEditedFile.getName()); 1146 1147 displayError(message); 1148 1149 // also print it in the error console. 1150 IProject iProject = mEditedFile.getProject(); 1151 AdtPlugin.printErrorToConsole(iProject.getName(), message); 1152 return false; 1153 } 1154 1155 return true; 1156 } 1157 1158 /** 1159 * Returns a {@link LayoutLibrary} that is ready for rendering, or null if the bridge 1160 * is not available or not ready yet (due to SDK loading still being in progress etc). 1161 * If enabled, any reasons preventing the bridge from being returned are displayed to the 1162 * editor's error area. 1163 * 1164 * @param displayError whether to display the loading error or not. 1165 * 1166 * @return LayoutBridge the layout bridge for rendering this editor's scene 1167 */ 1168 LayoutLibrary getReadyLayoutLib(boolean displayError) { 1169 Sdk currentSdk = Sdk.getCurrent(); 1170 if (currentSdk != null) { 1171 IAndroidTarget target = getRenderingTarget(); 1172 1173 if (target != null) { 1174 AndroidTargetData data = currentSdk.getTargetData(target); 1175 if (data != null) { 1176 LayoutLibrary layoutLib = data.getLayoutLibrary(); 1177 1178 if (layoutLib.getStatus() == LoadStatus.LOADED) { 1179 return layoutLib; 1180 } else if (displayError) { // getBridge() == null 1181 // SDK is loaded but not the layout library! 1182 1183 // check whether the bridge managed to load, or not 1184 if (layoutLib.getStatus() == LoadStatus.LOADING) { 1185 displayError("Eclipse is loading framework information and the layout library from the SDK folder.\n%1$s will refresh automatically once the process is finished.", 1186 mEditedFile.getName()); 1187 } else { 1188 String message = layoutLib.getLoadMessage(); 1189 displayError("Eclipse failed to load the framework information and the layout library!" + 1190 message != null ? "\n" + message : ""); 1191 } 1192 } 1193 } else { // data == null 1194 // It can happen that the workspace refreshes while the SDK is loading its 1195 // data, which could trigger a redraw of the opened layout if some resources 1196 // changed while Eclipse is closed. 1197 // In this case data could be null, but this is not an error. 1198 // We can just silently return, as all the opened editors are automatically 1199 // refreshed once the SDK finishes loading. 1200 LoadStatus targetLoadStatus = currentSdk.checkAndLoadTargetData(target, null); 1201 1202 // display error is asked. 1203 if (displayError) { 1204 String targetName = target.getName(); 1205 switch (targetLoadStatus) { 1206 case LOADING: 1207 String s; 1208 if (currentSdk.getTarget(getProject()) == target) { 1209 s = String.format( 1210 "The project target (%1$s) is still loading.", 1211 targetName); 1212 } else { 1213 s = String.format( 1214 "The rendering target (%1$s) is still loading.", 1215 targetName); 1216 } 1217 s += "\nThe layout will refresh automatically once the process is finished."; 1218 displayError(s); 1219 1220 break; 1221 case FAILED: // known failure 1222 case LOADED: // success but data isn't loaded?!?! 1223 displayError("The project target (%s) was not properly loaded.", 1224 targetName); 1225 break; 1226 } 1227 } 1228 } 1229 1230 } else if (displayError) { // target == null 1231 displayError("The project target is not set."); 1232 } 1233 } else if (displayError) { // currentSdk == null 1234 displayError("Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.", 1235 mEditedFile.getName()); 1236 } 1237 1238 return null; 1239 } 1240 1241 /** 1242 * Returns the {@link IAndroidTarget} used for the rendering. 1243 * <p/> 1244 * This first looks for the rendering target setup in the config UI, and if nothing has 1245 * been setup yet, returns the target of the project. 1246 * 1247 * @return an IAndroidTarget object or null if no target is setup and the project has no 1248 * target set. 1249 * 1250 */ 1251 public IAndroidTarget getRenderingTarget() { 1252 // if the SDK is null no targets are loaded. 1253 Sdk currentSdk = Sdk.getCurrent(); 1254 if (currentSdk == null) { 1255 return null; 1256 } 1257 1258 assert mConfigComposite.getDisplay().getThread() == Thread.currentThread(); 1259 1260 // attempt to get a target from the configuration selector. 1261 IAndroidTarget renderingTarget = mConfigComposite.getRenderingTarget(); 1262 if (renderingTarget != null) { 1263 return renderingTarget; 1264 } 1265 1266 // fall back to the project target 1267 if (mEditedFile != null) { 1268 return currentSdk.getTarget(mEditedFile.getProject()); 1269 } 1270 1271 return null; 1272 } 1273 1274 /** 1275 * Returns whether the current rendering target supports the given capability 1276 * 1277 * @param capability the capability to be looked up 1278 * @return true if the current rendering target supports the given capability 1279 */ 1280 public boolean renderingSupports(Capability capability) { 1281 IAndroidTarget target = getRenderingTarget(); 1282 if (target != null) { 1283 AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); 1284 LayoutLibrary layoutLib = targetData.getLayoutLibrary(); 1285 return layoutLib.supports(capability); 1286 } 1287 1288 return false; 1289 } 1290 1291 private boolean ensureModelValid(UiDocumentNode model) { 1292 // check there is actually a model (maybe the file is empty). 1293 if (model.getUiChildren().size() == 0) { 1294 displayError( 1295 "No XML content. Please add a root view or layout to your document."); 1296 return false; 1297 } 1298 1299 return true; 1300 } 1301 1302 private void renderWithBridge(IProject iProject, UiDocumentNode model, 1303 LayoutLibrary layoutLib) { 1304 LayoutCanvas canvas = getCanvasControl(); 1305 Set<UiElementNode> explodeNodes = canvas.getNodesToExplode(); 1306 Rect rect = getScreenBounds(); 1307 RenderLogger logger = new RenderLogger(mEditedFile.getName()); 1308 RenderingMode renderingMode = RenderingMode.NORMAL; 1309 // FIXME set the rendering mode using ViewRule or something. 1310 List<UiElementNode> children = model.getUiChildren(); 1311 if (children.size() > 0 && 1312 children.get(0).getDescriptor().getXmlLocalName().equals(SCROLL_VIEW)) { 1313 renderingMode = RenderingMode.V_SCROLL; 1314 } 1315 1316 RenderSession session = RenderService.create(this) 1317 .setModel(model) 1318 .setSize(rect.w, rect.h) 1319 .setLog(logger) 1320 .setRenderingMode(renderingMode) 1321 .setIncludedWithin(mIncludedWithin) 1322 .setNodesToExpand(explodeNodes) 1323 .createRenderSession(); 1324 1325 boolean layoutlib5 = layoutLib.supports(Capability.EMBEDDED_LAYOUT); 1326 canvas.setSession(session, explodeNodes, layoutlib5); 1327 1328 // update the UiElementNode with the layout info. 1329 if (session != null && session.getResult().isSuccess() == false) { 1330 // An error was generated. Print it (and any other accumulated warnings) 1331 String errorMessage = session.getResult().getErrorMessage(); 1332 Throwable exception = session.getResult().getException(); 1333 if (exception != null && errorMessage == null) { 1334 errorMessage = exception.toString(); 1335 } 1336 if (exception != null || (errorMessage != null && errorMessage.length() > 0)) { 1337 logger.error(null, errorMessage, exception, null /*data*/); 1338 } else if (!logger.hasProblems()) { 1339 logger.error(null, "Unexpected error in rendering, no details given", 1340 null /*data*/); 1341 } 1342 // These errors will be included in the log warnings which are 1343 // displayed regardless of render success status below 1344 } 1345 1346 // We might have detected some missing classes and swapped them by a mock view, 1347 // or run into fidelity warnings or missing resources, so emit all these 1348 // warnings 1349 Set<String> missingClasses = mProjectCallback.getMissingClasses(); 1350 Set<String> brokenClasses = mProjectCallback.getUninstantiatableClasses(); 1351 if (logger.hasProblems()) { 1352 displayLoggerProblems(iProject, logger); 1353 displayFailingClasses(missingClasses, brokenClasses, true); 1354 } else if (missingClasses.size() > 0 || brokenClasses.size() > 0) { 1355 displayFailingClasses(missingClasses, brokenClasses, false); 1356 } else { 1357 // Nope, no missing or broken classes. Clear success, congrats! 1358 hideError(); 1359 } 1360 1361 model.refreshUi(); 1362 } 1363 1364 /** 1365 * Returns the {@link ResourceResolver} for this editor 1366 * 1367 * @return the resolver used to resolve resources for the current configuration of 1368 * this editor, or null 1369 */ 1370 public ResourceResolver getResourceResolver() { 1371 if (mResourceResolver == null) { 1372 String theme = mConfigComposite.getTheme(); 1373 if (theme == null) { 1374 displayError("Missing theme."); 1375 return null; 1376 } 1377 boolean isProjectTheme = mConfigComposite.isProjectTheme(); 1378 1379 Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes = 1380 mConfigListener.getConfiguredProjectResources(); 1381 1382 // Get the framework resources 1383 Map<ResourceType, Map<String, ResourceValue>> frameworkResources = 1384 mConfigListener.getConfiguredFrameworkResources(); 1385 1386 if (configuredProjectRes == null) { 1387 displayError("Missing project resources for current configuration."); 1388 return null; 1389 } 1390 1391 if (frameworkResources == null) { 1392 displayError("Missing framework resources."); 1393 return null; 1394 } 1395 1396 mResourceResolver = ResourceResolver.create( 1397 configuredProjectRes, frameworkResources, 1398 theme, isProjectTheme); 1399 } 1400 1401 return mResourceResolver; 1402 } 1403 1404 /** Returns a project callback, and optionally resets it */ 1405 ProjectCallback getProjectCallback(boolean reset, LayoutLibrary layoutLibrary) { 1406 // Lazily create the project callback the first time we need it 1407 if (mProjectCallback == null) { 1408 ResourceManager resManager = ResourceManager.getInstance(); 1409 IProject project = getProject(); 1410 ProjectResources projectRes = resManager.getProjectResources(project); 1411 mProjectCallback = new ProjectCallback(layoutLibrary, projectRes, project); 1412 } else if (reset) { 1413 // Also clears the set of missing/broken classes prior to rendering 1414 mProjectCallback.getMissingClasses().clear(); 1415 mProjectCallback.getUninstantiatableClasses().clear(); 1416 } 1417 1418 return mProjectCallback; 1419 } 1420 1421 /** 1422 * Returns the resource name of this layout, NOT including the @layout/ prefix 1423 * 1424 * @return the resource name of this layout, NOT including the @layout/ prefix 1425 */ 1426 public String getLayoutResourceName() { 1427 String name = mEditedFile.getName(); 1428 int dotIndex = name.indexOf('.'); 1429 if (dotIndex != -1) { 1430 name = name.substring(0, dotIndex); 1431 } 1432 return name; 1433 } 1434 1435 /** 1436 * Cleans up when the rendering target is about to change 1437 * @param oldTarget the old rendering target. 1438 */ 1439 private void preRenderingTargetChangeCleanUp(IAndroidTarget oldTarget) { 1440 // first clear the caches related to this file in the old target 1441 Sdk currentSdk = Sdk.getCurrent(); 1442 if (currentSdk != null) { 1443 AndroidTargetData data = currentSdk.getTargetData(oldTarget); 1444 if (data != null) { 1445 LayoutLibrary layoutLib = data.getLayoutLibrary(); 1446 1447 // layoutLib can never be null. 1448 layoutLib.clearCaches(mEditedFile.getProject()); 1449 } 1450 } 1451 1452 // Also remove the ProjectCallback as it caches custom views which must be reloaded 1453 // with the classloader of the new LayoutLib. We also have to clear it out 1454 // because it stores a reference to the layout library which could have changed. 1455 mProjectCallback = null; 1456 1457 // FIXME: get rid of the current LayoutScene if any. 1458 } 1459 1460 private class ReloadListener implements ILayoutReloadListener { 1461 /** 1462 * Called when the file changes triggered a redraw of the layout 1463 */ 1464 public void reloadLayout(final ChangeFlags flags, final boolean libraryChanged) { 1465 if (mConfigComposite.isDisposed()) { 1466 return; 1467 } 1468 Display display = mConfigComposite.getDisplay(); 1469 display.asyncExec(new Runnable() { 1470 public void run() { 1471 reloadLayoutSwt(flags, libraryChanged); 1472 } 1473 }); 1474 } 1475 1476 /** Reload layout. <b>Must be called on the SWT thread</b> */ 1477 private void reloadLayoutSwt(ChangeFlags flags, boolean libraryChanged) { 1478 if (mConfigComposite.isDisposed()) { 1479 return; 1480 } 1481 assert mConfigComposite.getDisplay().getThread() == Thread.currentThread(); 1482 1483 boolean recompute = false; 1484 // we only care about the r class of the main project. 1485 if (flags.rClass && libraryChanged == false) { 1486 recompute = true; 1487 if (mEditedFile != null) { 1488 ResourceManager manager = ResourceManager.getInstance(); 1489 ProjectResources projectRes = manager.getProjectResources( 1490 mEditedFile.getProject()); 1491 1492 if (projectRes != null) { 1493 projectRes.resetDynamicIds(); 1494 } 1495 } 1496 } 1497 1498 if (flags.localeList) { 1499 // the locale list *potentially* changed so we update the locale in the 1500 // config composite. 1501 // However there's no recompute, as it could not be needed 1502 // (for instance a new layout) 1503 // If a resource that's not a layout changed this will trigger a recompute anyway. 1504 mConfigComposite.updateLocales(); 1505 } 1506 1507 // if a resources was modified. 1508 if (flags.resources) { 1509 recompute = true; 1510 1511 // TODO: differentiate between single and multi resource file changed, and whether 1512 // the resource change affects the cache. 1513 1514 // force a reparse in case a value XML file changed. 1515 mConfiguredProjectRes = null; 1516 mResourceResolver = null; 1517 1518 // clear the cache in the bridge in case a bitmap/9-patch changed. 1519 LayoutLibrary layoutLib = getReadyLayoutLib(true /*displayError*/); 1520 if (layoutLib != null) { 1521 layoutLib.clearCaches(mEditedFile.getProject()); 1522 } 1523 } 1524 1525 if (flags.code) { 1526 // only recompute if the custom view loader was used to load some code. 1527 if (mProjectCallback != null && mProjectCallback.isUsed()) { 1528 mProjectCallback = null; 1529 recompute = true; 1530 } 1531 } 1532 1533 if (flags.manifest) { 1534 recompute |= computeSdkVersion(); 1535 } 1536 1537 if (recompute) { 1538 if (mLayoutEditor.isGraphicalEditorActive()) { 1539 recomputeLayout(); 1540 } else { 1541 mNeedsRecompute = true; 1542 } 1543 } 1544 } 1545 } 1546 1547 // ---- Error handling ---- 1548 1549 /** 1550 * Switches the sash to display the error label. 1551 * 1552 * @param errorFormat The new error to display if not null. 1553 * @param parameters String.format parameters for the error format. 1554 */ 1555 private void displayError(String errorFormat, Object...parameters) { 1556 if (errorFormat != null) { 1557 mErrorLabel.setText(String.format(errorFormat, parameters)); 1558 } else { 1559 mErrorLabel.setText(""); 1560 } 1561 mSashError.setMaximizedControl(null); 1562 } 1563 1564 /** Displays the canvas and hides the error label. */ 1565 private void hideError() { 1566 mErrorLabel.setText(""); 1567 mSashError.setMaximizedControl(mCanvasViewer.getControl()); 1568 } 1569 1570 /** 1571 * Switches the sash to display the error label to show a list of 1572 * missing classes and give options to create them. 1573 */ 1574 private void displayFailingClasses(Set<String> missingClasses, Set<String> brokenClasses, 1575 boolean append) { 1576 if (missingClasses.size() == 0 && brokenClasses.size() == 0) { 1577 return; 1578 } 1579 1580 if (!append) { 1581 mErrorLabel.setText(""); //$NON-NLS-1$ 1582 } else { 1583 addText(mErrorLabel, "\n"); //$NON-NLS-1$ 1584 } 1585 1586 if (missingClasses.size() > 0) { 1587 addText(mErrorLabel, "The following classes could not be found:\n"); 1588 for (String clazz : missingClasses) { 1589 addText(mErrorLabel, "- "); 1590 addText(mErrorLabel, clazz); 1591 addText(mErrorLabel, " ("); 1592 1593 IProject project = getProject(); 1594 Collection<String> customViews = getCustomViewClassNames(project); 1595 addTypoSuggestions(clazz, customViews, false); 1596 addTypoSuggestions(clazz, customViews, true); 1597 addTypoSuggestions(clazz, getAndroidViewClassNames(project), false); 1598 1599 addActionLink(mErrorLabel, 1600 ActionLinkStyleRange.LINK_FIX_BUILD_PATH, "Fix Build Path", clazz, null); 1601 addText(mErrorLabel, ", "); 1602 addActionLink(mErrorLabel, 1603 ActionLinkStyleRange.LINK_EDIT_XML, "Edit XML", clazz, null); 1604 if (clazz.indexOf('.') != -1) { 1605 // Add "Create Class" link, but only for custom views 1606 addText(mErrorLabel, ", "); 1607 addActionLink(mErrorLabel, 1608 ActionLinkStyleRange.LINK_CREATE_CLASS, "Create Class", clazz, null); 1609 } 1610 addText(mErrorLabel, ")\n"); 1611 } 1612 } 1613 if (brokenClasses.size() > 0) { 1614 addText(mErrorLabel, "The following classes could not be instantiated:\n"); 1615 1616 // Do we have a custom class (not an Android or add-ons class) 1617 boolean haveCustomClass = false; 1618 1619 for (String clazz : brokenClasses) { 1620 addText(mErrorLabel, "- "); 1621 addText(mErrorLabel, " ("); 1622 addActionLink(mErrorLabel, 1623 ActionLinkStyleRange.LINK_OPEN_CLASS, "Open Class", clazz, null); 1624 addText(mErrorLabel, ", "); 1625 addActionLink(mErrorLabel, 1626 ActionLinkStyleRange.LINK_SHOW_LOG, "Show Error Log", clazz, null); 1627 addText(mErrorLabel, ")\n"); 1628 1629 if (!(clazz.startsWith("android.") || //$NON-NLS-1$ 1630 clazz.startsWith("com.google."))) { //$NON-NLS-1$ 1631 haveCustomClass = true; 1632 } 1633 } 1634 1635 addText(mErrorLabel, "See the Error Log (Window > Show View) for more details.\n"); 1636 1637 if (haveCustomClass) { 1638 addText(mErrorLabel, "Tip: Use View.isInEditMode() in your custom views " 1639 + "to skip code when shown in Eclipse"); 1640 } 1641 } 1642 1643 mSashError.setMaximizedControl(null); 1644 } 1645 1646 private void addTypoSuggestions(String actual, Collection<String> views, 1647 boolean compareWithPackage) { 1648 if (views.size() == 0) { 1649 return; 1650 } 1651 1652 // Look for typos and try to match with custom views and android views 1653 String actualBase = actual.substring(actual.lastIndexOf('.') + 1); 1654 if (views.size() > 0) { 1655 for (String suggested : views) { 1656 String suggestedBase = suggested.substring(suggested.lastIndexOf('.') + 1); 1657 1658 String matchWith = compareWithPackage ? suggested : suggestedBase; 1659 int maxDistance = actualBase.length() >= 4 ? 2 : 1; 1660 if (Math.abs(actualBase.length() - matchWith.length()) > maxDistance) { 1661 // The string lengths differ more than the allowed edit distance; 1662 // no point in even attempting to compute the edit distance (requires 1663 // O(n*m) storage and O(n*m) speed, where n and m are the string lengths) 1664 continue; 1665 } 1666 if (AdtUtils.editDistance(actualBase, matchWith) <= maxDistance) { 1667 // Suggest this class as a typo for the given class 1668 String labelClass = (suggestedBase.equals(actual) || actual.indexOf('.') != -1) 1669 ? suggested : suggestedBase; 1670 addActionLink(mErrorLabel, 1671 ActionLinkStyleRange.LINK_CHANGE_CLASS_TO, 1672 String.format("Change to %1$s", 1673 // Only show full package name if class name 1674 // is the same 1675 labelClass), 1676 actual, 1677 viewNeedsPackage(suggested) ? suggested : suggestedBase 1678 ); 1679 addText(mErrorLabel, ", "); 1680 } 1681 } 1682 } 1683 } 1684 1685 private static Collection<String> getCustomViewClassNames(IProject project) { 1686 CustomViewFinder finder = CustomViewFinder.get(project); 1687 Collection<String> views = finder.getAllViews(); 1688 if (views == null) { 1689 finder.refresh(); 1690 views = finder.getAllViews(); 1691 } 1692 1693 return views; 1694 } 1695 1696 private static Collection<String> getAndroidViewClassNames(IProject project) { 1697 Sdk currentSdk = Sdk.getCurrent(); 1698 IAndroidTarget target = currentSdk.getTarget(project); 1699 if (target != null) { 1700 AndroidTargetData targetData = currentSdk.getTargetData(target); 1701 LayoutDescriptors layoutDescriptors = targetData.getLayoutDescriptors(); 1702 return layoutDescriptors.getAllViewClassNames(); 1703 } 1704 1705 return Collections.emptyList(); 1706 } 1707 1708 /** Add a normal line of text to the styled text widget. */ 1709 private void addText(StyledText styledText, String...string) { 1710 for (String s : string) { 1711 styledText.append(s); 1712 } 1713 } 1714 1715 /** Display the problem list encountered during a render */ 1716 private void displayLoggerProblems(IProject project, RenderLogger logger) { 1717 if (logger.hasProblems()) { 1718 mErrorLabel.setText(""); 1719 // A common source of problems is attempting to open a layout when there are 1720 // compilation errors. In this case, may not have run (or may not be up to date) 1721 // so resources cannot be looked up etc. Explain this situation to the user. 1722 1723 boolean hasAaptErrors = false; 1724 boolean hasJavaErrors = false; 1725 try { 1726 IMarker[] markers; 1727 markers = project.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE); 1728 if (markers.length > 0) { 1729 for (IMarker marker : markers) { 1730 String markerType = marker.getType(); 1731 if (markerType.equals(IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER)) { 1732 int severity = marker.getAttribute(IMarker.SEVERITY, -1); 1733 if (severity == IMarker.SEVERITY_ERROR) { 1734 hasJavaErrors = true; 1735 } 1736 } else if (markerType.equals(AdtConstants.MARKER_AAPT_COMPILE)) { 1737 int severity = marker.getAttribute(IMarker.SEVERITY, -1); 1738 if (severity == IMarker.SEVERITY_ERROR) { 1739 hasAaptErrors = true; 1740 } 1741 } 1742 } 1743 } 1744 } catch (CoreException e) { 1745 AdtPlugin.log(e, null); 1746 } 1747 1748 if (logger.seenTagPrefix(LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR)) { 1749 addBoldText(mErrorLabel, 1750 "Missing styles. Is the correct theme chosen for this layout?\n"); 1751 addText(mErrorLabel, 1752 "Use the Theme combo box above the layout to choose a different layout, " + 1753 "or fix the theme style references.\n\n"); 1754 } 1755 1756 if (hasAaptErrors && logger.seenTagPrefix(LayoutLog.TAG_RESOURCES_PREFIX)) { 1757 // Text will automatically be wrapped by the error widget so no reason 1758 // to insert linebreaks in this error message: 1759 String message = 1760 "NOTE: This project contains resource errors, so aapt did not succeed, " 1761 + "which can cause rendering failures. " 1762 + "Fix resource problems first.\n\n"; 1763 addBoldText(mErrorLabel, message); 1764 } else if (hasJavaErrors && mProjectCallback != null && mProjectCallback.isUsed()) { 1765 // Text will automatically be wrapped by the error widget so no reason 1766 // to insert linebreaks in this error message: 1767 String message = 1768 "NOTE: This project contains Java compilation errors, " 1769 + "which can cause rendering failures for custom views. " 1770 + "Fix compilation problems first.\n\n"; 1771 addBoldText(mErrorLabel, message); 1772 } 1773 1774 String problems = logger.getProblems(false /*includeFidelityWarnings*/); 1775 addText(mErrorLabel, problems); 1776 1777 List<String> fidelityWarnings = logger.getFidelityWarnings(); 1778 if (fidelityWarnings != null && fidelityWarnings.size() > 0) { 1779 addText(mErrorLabel, 1780 "The graphics preview in the layout editor may not be accurate:\n"); 1781 for (String warning : fidelityWarnings) { 1782 addText(mErrorLabel, warning + ' '); 1783 addActionLink(mErrorLabel, 1784 ActionLinkStyleRange.IGNORE_FIDELITY_WARNING, 1785 "(Ignore for this session)\n", warning, null); 1786 } 1787 } 1788 1789 mSashError.setMaximizedControl(null); 1790 } else { 1791 mSashError.setMaximizedControl(mCanvasViewer.getControl()); 1792 } 1793 } 1794 1795 /** Appends the given text as a bold string in the given text widget */ 1796 private void addBoldText(StyledText styledText, String text) { 1797 String s = styledText.getText(); 1798 int start = (s == null ? 0 : s.length()); 1799 1800 styledText.append(text); 1801 StyleRange sr = new StyleRange(); 1802 sr.start = start; 1803 sr.length = text.length(); 1804 sr.fontStyle = SWT.BOLD; 1805 styledText.setStyleRange(sr); 1806 } 1807 1808 /** 1809 * Add a URL-looking link to the styled text widget. 1810 * <p/> 1811 * A mouse-click listener is setup and it interprets the link based on the 1812 * action, corresponding to the value fields in {@link ActionLinkStyleRange}. 1813 */ 1814 private void addActionLink(StyledText styledText, int action, String label, 1815 String data1, String data2) { 1816 String s = styledText.getText(); 1817 int start = (s == null ? 0 : s.length()); 1818 styledText.append(label); 1819 1820 StyleRange sr = new ActionLinkStyleRange(action, data1, data2); 1821 sr.start = start; 1822 sr.length = label.length(); 1823 sr.fontStyle = SWT.NORMAL; 1824 sr.underlineStyle = SWT.UNDERLINE_LINK; 1825 sr.underline = true; 1826 styledText.setStyleRange(sr); 1827 } 1828 1829 /** 1830 * Looks up the resource file corresponding to the given type 1831 * 1832 * @param type The type of resource to look up, such as {@link ResourceType#LAYOUT} 1833 * @param name The name of the resource (not including ".xml") 1834 * @param isFrameworkResource if true, the resource is a framework resource, otherwise 1835 * it's a project resource 1836 * @return the resource file defining the named resource, or null if not found 1837 */ 1838 public IPath findResourceFile(ResourceType type, String name, boolean isFrameworkResource) { 1839 // FIXME: This code does not handle theme value resolution. 1840 // There is code to handle this, but it's in layoutlib; we should 1841 // expose that and use it here. 1842 1843 Map<ResourceType, Map<String, ResourceValue>> map; 1844 map = isFrameworkResource ? mConfiguredFrameworkRes : mConfiguredProjectRes; 1845 if (map == null) { 1846 // Not yet configured 1847 return null; 1848 } 1849 1850 Map<String, ResourceValue> layoutMap = map.get(type); 1851 if (layoutMap != null) { 1852 ResourceValue value = layoutMap.get(name); 1853 if (value != null) { 1854 String valueStr = value.getValue(); 1855 if (valueStr.startsWith("?")) { //$NON-NLS-1$ 1856 // FIXME: It's a reference. We should resolve this properly. 1857 return null; 1858 } 1859 return new Path(valueStr); 1860 } 1861 } 1862 1863 return null; 1864 } 1865 1866 /** 1867 * Looks up the path to the file corresponding to the given attribute value, such as 1868 * @layout/foo, which will return the foo.xml file in res/layout/. (The general format 1869 * of the resource url is {@literal @[<package_name>:]<resource_type>/<resource_name>}. 1870 * 1871 * @param url the attribute url 1872 * @return the path to the file defining this attribute, or null if not found 1873 */ 1874 public IPath findResourceFile(String url) { 1875 if (!url.startsWith("@")) { //$NON-NLS-1$ 1876 return null; 1877 } 1878 int typeEnd = url.indexOf('/', 1); 1879 if (typeEnd == -1) { 1880 return null; 1881 } 1882 int nameBegin = typeEnd + 1; 1883 int typeBegin = 1; 1884 int colon = url.lastIndexOf(':', typeEnd); 1885 boolean isFrameworkResource = false; 1886 if (colon != -1) { 1887 // The URL contains a package name. 1888 // While the url format technically allows other package names, 1889 // the platform apparently only supports @android for now (or if it does, 1890 // there are no usages in the current code base so this is not common). 1891 String packageName = url.substring(typeBegin, colon); 1892 if (ANDROID_PKG.equals(packageName)) { 1893 isFrameworkResource = true; 1894 } 1895 1896 typeBegin = colon + 1; 1897 } 1898 1899 String typeName = url.substring(typeBegin, typeEnd); 1900 ResourceType type = ResourceType.getEnum(typeName); 1901 if (type == null) { 1902 return null; 1903 } 1904 1905 String name = url.substring(nameBegin); 1906 return findResourceFile(type, name, isFrameworkResource); 1907 } 1908 1909 /** 1910 * Resolve the given @string reference into a literal String using the current project 1911 * configuration 1912 * 1913 * @param text the text resource reference to resolve 1914 * @return the resolved string, or null 1915 */ 1916 public String findString(String text) { 1917 if (text.startsWith(STRING_PREFIX)) { 1918 return findString(text.substring(STRING_PREFIX.length()), false); 1919 } else if (text.startsWith(ANDROID_STRING_PREFIX)) { 1920 return findString(text.substring(ANDROID_STRING_PREFIX.length()), true); 1921 } else { 1922 return text; 1923 } 1924 } 1925 1926 private String findString(String name, boolean isFrameworkResource) { 1927 Map<ResourceType, Map<String, ResourceValue>> map; 1928 map = isFrameworkResource ? mConfiguredFrameworkRes : mConfiguredProjectRes; 1929 if (map == null) { 1930 // Not yet configured 1931 return null; 1932 } 1933 1934 Map<String, ResourceValue> layoutMap = map.get(ResourceType.STRING); 1935 if (layoutMap != null) { 1936 ResourceValue value = layoutMap.get(name); 1937 if (value != null) { 1938 // FIXME: This code does not handle theme value resolution. 1939 // There is code to handle this, but it's in layoutlib; we should 1940 // expose that and use it here. 1941 return value.getValue(); 1942 } 1943 } 1944 1945 return null; 1946 } 1947 1948 /** 1949 * This StyleRange represents a clickable link in the render output, where various 1950 * actions can be taken such as creating a class, opening the project chooser to 1951 * adjust the build path, etc. 1952 */ 1953 private class ActionLinkStyleRange extends StyleRange { 1954 /** Create a view class */ 1955 private static final int LINK_CREATE_CLASS = 1; 1956 /** Edit the build path for the current project */ 1957 private static final int LINK_FIX_BUILD_PATH = 2; 1958 /** Show the XML tab */ 1959 private static final int LINK_EDIT_XML = 3; 1960 /** Open the given class */ 1961 private static final int LINK_OPEN_CLASS = 4; 1962 /** Show the error log */ 1963 private static final int LINK_SHOW_LOG = 5; 1964 /** Change the class reference to the given fully qualified name */ 1965 private static final int LINK_CHANGE_CLASS_TO = 6; 1966 /** Ignore the given fidelity warning */ 1967 private static final int IGNORE_FIDELITY_WARNING = 7; 1968 1969 /** Client data 1 - usually the class name */ 1970 private final String mData1; 1971 /** Client data 2 - such as the suggested new name */ 1972 private final String mData2; 1973 /** The action to be taken when the link is clicked */ 1974 private final int mAction; 1975 1976 private ActionLinkStyleRange(int action, String data1, String data2) { 1977 super(); 1978 mAction = action; 1979 mData1 = data1; 1980 mData2 = data2; 1981 } 1982 1983 /** Performs the click action */ 1984 public void onClick() { 1985 switch (mAction) { 1986 case LINK_CREATE_CLASS: 1987 createNewClass(mData1); 1988 break; 1989 case LINK_EDIT_XML: 1990 mLayoutEditor.setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID); 1991 break; 1992 case LINK_FIX_BUILD_PATH: 1993 @SuppressWarnings("restriction") 1994 String id = BuildPathsPropertyPage.PROP_ID; 1995 PreferencesUtil.createPropertyDialogOn( 1996 AdtPlugin.getDisplay().getActiveShell(), 1997 getProject(), id, null, null).open(); 1998 break; 1999 case LINK_OPEN_CLASS: 2000 AdtPlugin.openJavaClass(getProject(), mData1); 2001 break; 2002 case LINK_SHOW_LOG: 2003 IWorkbench workbench = PlatformUI.getWorkbench(); 2004 IWorkbenchWindow workbenchWindow = workbench.getActiveWorkbenchWindow(); 2005 try { 2006 IWorkbenchPage page = workbenchWindow.getActivePage(); 2007 page.showView("org.eclipse.pde.runtime.LogView"); //$NON-NLS-1$ 2008 } catch (PartInitException e) { 2009 AdtPlugin.log(e, null); 2010 } 2011 break; 2012 case LINK_CHANGE_CLASS_TO: 2013 // Change class reference of mData1 to mData2 2014 // TODO: run under undo lock 2015 MultiTextEdit edits = new MultiTextEdit(); 2016 ISourceViewer textViewer = mLayoutEditor.getStructuredSourceViewer(); 2017 IDocument document = textViewer.getDocument(); 2018 String xml = document.get(); 2019 int index = 0; 2020 // Replace <old with <new and </old with </new 2021 String prefix = "<"; //$NON-NLS-1$ 2022 String find = prefix + mData1; 2023 String replaceWith = prefix + mData2; 2024 while (true) { 2025 index = xml.indexOf(find, index); 2026 if (index == -1) { 2027 break; 2028 } 2029 edits.addChild(new ReplaceEdit(index, find.length(), replaceWith)); 2030 index += find.length(); 2031 } 2032 index = 0; 2033 prefix = "</"; //$NON-NLS-1$ 2034 find = prefix + mData1; 2035 replaceWith = prefix + mData2; 2036 while (true) { 2037 index = xml.indexOf(find, index); 2038 if (index == -1) { 2039 break; 2040 } 2041 edits.addChild(new ReplaceEdit(index, find.length(), replaceWith)); 2042 index += find.length(); 2043 } 2044 // Handle <view class="old"> 2045 index = 0; 2046 prefix = "\""; //$NON-NLS-1$ 2047 String suffix = "\""; //$NON-NLS-1$ 2048 find = prefix + mData1 + suffix; 2049 replaceWith = prefix + mData2 + suffix; 2050 while (true) { 2051 index = xml.indexOf(find, index); 2052 if (index == -1) { 2053 break; 2054 } 2055 edits.addChild(new ReplaceEdit(index, find.length(), replaceWith)); 2056 index += find.length(); 2057 } 2058 try { 2059 edits.apply(document); 2060 } catch (MalformedTreeException e) { 2061 AdtPlugin.log(e, null); 2062 } catch (BadLocationException e) { 2063 AdtPlugin.log(e, null); 2064 } 2065 break; 2066 case IGNORE_FIDELITY_WARNING: 2067 RenderLogger.ignoreFidelityWarning(mData1); 2068 recomputeLayout(); 2069 break; 2070 default: 2071 break; 2072 } 2073 } 2074 2075 @Override 2076 public boolean similarTo(StyleRange style) { 2077 // Prevent adjacent link ranges from getting merged 2078 return false; 2079 } 2080 } 2081 2082 /** 2083 * Returns the error label for the graphical editor (which may not be visible 2084 * or showing errors) 2085 * 2086 * @return the error label, never null 2087 */ 2088 StyledText getErrorLabel() { 2089 return mErrorLabel; 2090 } 2091 2092 /** 2093 * Monitor clicks on the error label. 2094 * If the click happens on a style range created by 2095 * {@link GraphicalEditorPart#addClassLink(StyledText, String)}, we assume it's about 2096 * a missing class and we then proceed to display the standard Eclipse class creator wizard. 2097 */ 2098 private class ErrorLabelListener extends MouseAdapter { 2099 2100 @Override 2101 public void mouseUp(MouseEvent event) { 2102 super.mouseUp(event); 2103 2104 if (event.widget != mErrorLabel) { 2105 return; 2106 } 2107 2108 int offset = mErrorLabel.getCaretOffset(); 2109 2110 StyleRange r = null; 2111 StyleRange[] ranges = mErrorLabel.getStyleRanges(); 2112 if (ranges != null && ranges.length > 0) { 2113 for (StyleRange sr : ranges) { 2114 if (sr.start <= offset && sr.start + sr.length > offset) { 2115 r = sr; 2116 break; 2117 } 2118 } 2119 } 2120 2121 if (r instanceof ActionLinkStyleRange) { 2122 ActionLinkStyleRange range = (ActionLinkStyleRange) r; 2123 range.onClick(); 2124 } 2125 2126 LayoutCanvas canvas = getCanvasControl(); 2127 canvas.updateMenuActionState(); 2128 } 2129 } 2130 2131 private void createNewClass(String fqcn) { 2132 2133 int pos = fqcn.lastIndexOf('.'); 2134 String packageName = pos < 0 ? "" : fqcn.substring(0, pos); //$NON-NLS-1$ 2135 String className = pos <= 0 || pos >= fqcn.length() ? "" : fqcn.substring(pos + 1); //$NON-NLS-1$ 2136 2137 // create the wizard page for the class creation, and configure it 2138 NewClassWizardPage page = new NewClassWizardPage(); 2139 2140 // set the parent class 2141 page.setSuperClass(SdkConstants.CLASS_VIEW, true /* canBeModified */); 2142 2143 // get the source folders as java elements. 2144 IPackageFragmentRoot[] roots = getPackageFragmentRoots(mLayoutEditor.getProject(), 2145 false /*includeContainers*/, true /*skipGenFolder*/); 2146 2147 IPackageFragmentRoot currentRoot = null; 2148 IPackageFragment currentFragment = null; 2149 int packageMatchCount = -1; 2150 2151 for (IPackageFragmentRoot root : roots) { 2152 // Get the java element for the package. 2153 // This method is said to always return a IPackageFragment even if the 2154 // underlying folder doesn't exist... 2155 IPackageFragment fragment = root.getPackageFragment(packageName); 2156 if (fragment != null && fragment.exists()) { 2157 // we have a perfect match! we use it. 2158 currentRoot = root; 2159 currentFragment = fragment; 2160 packageMatchCount = -1; 2161 break; 2162 } else { 2163 // we don't have a match. we look for the fragment with the best match 2164 // (ie the closest parent package we can find) 2165 try { 2166 IJavaElement[] children; 2167 children = root.getChildren(); 2168 for (IJavaElement child : children) { 2169 if (child instanceof IPackageFragment) { 2170 fragment = (IPackageFragment)child; 2171 if (packageName.startsWith(fragment.getElementName())) { 2172 // its a match. get the number of segments 2173 String[] segments = fragment.getElementName().split("\\."); //$NON-NLS-1$ 2174 if (segments.length > packageMatchCount) { 2175 packageMatchCount = segments.length; 2176 currentFragment = fragment; 2177 currentRoot = root; 2178 } 2179 } 2180 } 2181 } 2182 } catch (JavaModelException e) { 2183 // Couldn't get the children: we just ignore this package root. 2184 } 2185 } 2186 } 2187 2188 ArrayList<IPackageFragment> createdFragments = null; 2189 2190 if (currentRoot != null) { 2191 // if we have a perfect match, we set it and we're done. 2192 if (packageMatchCount == -1) { 2193 page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/); 2194 page.setPackageFragment(currentFragment, true /* canBeModified */); 2195 } else { 2196 // we have a partial match. 2197 // create the package. We have to start with the first segment so that we 2198 // know what to delete in case of a cancel. 2199 try { 2200 createdFragments = new ArrayList<IPackageFragment>(); 2201 2202 int totalCount = packageName.split("\\.").length; //$NON-NLS-1$ 2203 int count = 0; 2204 int index = -1; 2205 // skip the matching packages 2206 while (count < packageMatchCount) { 2207 index = packageName.indexOf('.', index+1); 2208 count++; 2209 } 2210 2211 // create the rest of the segments, except for the last one as indexOf will 2212 // return -1; 2213 while (count < totalCount - 1) { 2214 index = packageName.indexOf('.', index+1); 2215 count++; 2216 createdFragments.add(currentRoot.createPackageFragment( 2217 packageName.substring(0, index), 2218 true /* force*/, new NullProgressMonitor())); 2219 } 2220 2221 // create the last package 2222 createdFragments.add(currentRoot.createPackageFragment( 2223 packageName, true /* force*/, new NullProgressMonitor())); 2224 2225 // set the root and fragment in the Wizard page 2226 page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/); 2227 page.setPackageFragment(createdFragments.get(createdFragments.size()-1), 2228 true /* canBeModified */); 2229 } catch (JavaModelException e) { 2230 // If we can't create the packages, there's a problem. 2231 // We revert to the default package 2232 for (IPackageFragmentRoot root : roots) { 2233 // Get the java element for the package. 2234 // This method is said to always return a IPackageFragment even if the 2235 // underlying folder doesn't exist... 2236 IPackageFragment fragment = root.getPackageFragment(packageName); 2237 if (fragment != null && fragment.exists()) { 2238 page.setPackageFragmentRoot(root, true /* canBeModified*/); 2239 page.setPackageFragment(fragment, true /* canBeModified */); 2240 break; 2241 } 2242 } 2243 } 2244 } 2245 } else if (roots.length > 0) { 2246 // if we haven't found a valid fragment, we set the root to the first source folder. 2247 page.setPackageFragmentRoot(roots[0], true /* canBeModified*/); 2248 } 2249 2250 // if we have a starting class name we use it 2251 if (className != null) { 2252 page.setTypeName(className, true /* canBeModified*/); 2253 } 2254 2255 // create the action that will open it the wizard. 2256 OpenNewClassWizardAction action = new OpenNewClassWizardAction(); 2257 action.setConfiguredWizardPage(page); 2258 action.run(); 2259 IJavaElement element = action.getCreatedElement(); 2260 2261 if (element == null) { 2262 // lets delete the packages we created just for this. 2263 // we need to start with the leaf and go up 2264 if (createdFragments != null) { 2265 try { 2266 for (int i = createdFragments.size() - 1 ; i >= 0 ; i--) { 2267 createdFragments.get(i).delete(true /* force*/, 2268 new NullProgressMonitor()); 2269 } 2270 } catch (JavaModelException e) { 2271 e.printStackTrace(); 2272 } 2273 } 2274 } 2275 } 2276 2277 /** 2278 * Computes and return the {@link IPackageFragmentRoot}s corresponding to the source 2279 * folders of the specified project. 2280 * 2281 * @param project the project 2282 * @param includeContainers True to include containers 2283 * @param skipGenFolder True to skip the "gen" folder 2284 * @return an array of IPackageFragmentRoot. 2285 */ 2286 private IPackageFragmentRoot[] getPackageFragmentRoots(IProject project, 2287 boolean includeContainers, boolean skipGenFolder) { 2288 ArrayList<IPackageFragmentRoot> result = new ArrayList<IPackageFragmentRoot>(); 2289 try { 2290 IJavaProject javaProject = JavaCore.create(project); 2291 IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots(); 2292 for (int i = 0; i < roots.length; i++) { 2293 if (skipGenFolder) { 2294 IResource resource = roots[i].getResource(); 2295 if (resource != null && resource.getName().equals(FD_GEN_SOURCES)) { 2296 continue; 2297 } 2298 } 2299 IClasspathEntry entry = roots[i].getRawClasspathEntry(); 2300 if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE || 2301 (includeContainers && 2302 entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER)) { 2303 result.add(roots[i]); 2304 } 2305 } 2306 } catch (JavaModelException e) { 2307 } 2308 2309 return result.toArray(new IPackageFragmentRoot[result.size()]); 2310 } 2311 2312 /** 2313 * Reopens this file as included within the given file (this assumes that the given 2314 * file has an include tag referencing this view, and the set of views that have this 2315 * property can be found using the {@link IncludeFinder}. 2316 * 2317 * @param includeWithin reference to a file to include as a surrounding context, 2318 * or null to show the file standalone 2319 */ 2320 public void showIn(Reference includeWithin) { 2321 mIncludedWithin = includeWithin; 2322 2323 if (includeWithin != null) { 2324 IFile file = includeWithin.getFile(); 2325 2326 // Update configuration 2327 if (file != null) { 2328 mConfigComposite.resetConfigFor(file); 2329 } 2330 } 2331 recomputeLayout(); 2332 } 2333 2334 /** 2335 * Returns the resource name of the file that is including this current layout, if any 2336 * (may be null) 2337 * 2338 * @return the resource name of an including layout, or null 2339 */ 2340 public Reference getIncludedWithin() { 2341 return mIncludedWithin; 2342 } 2343 2344 /** 2345 * Return all resource names of a given type, either in the project or in the 2346 * framework. 2347 * 2348 * @param framework if true, return all the framework resource names, otherwise return 2349 * all the project resource names 2350 * @param type the type of resource to look up 2351 * @return a collection of resource names, never null but possibly empty 2352 */ 2353 public Collection<String> getResourceNames(boolean framework, ResourceType type) { 2354 Map<ResourceType, Map<String, ResourceValue>> map = 2355 framework ? mConfiguredFrameworkRes : mConfiguredProjectRes; 2356 Map<String, ResourceValue> animations = map.get(type); 2357 if (animations != null) { 2358 return animations.keySet(); 2359 } else { 2360 return Collections.emptyList(); 2361 } 2362 } 2363 2364 /** 2365 * Return this editor's current configuration 2366 * 2367 * @return the current configuration 2368 */ 2369 public FolderConfiguration getConfiguration() { 2370 return mConfigComposite.getCurrentConfig(); 2371 } 2372 2373 /** 2374 * Figures out the project's minSdkVersion and targetSdkVersion and return whether the values 2375 * have changed. 2376 */ 2377 private boolean computeSdkVersion() { 2378 int oldMinSdkVersion = mMinSdkVersion; 2379 int oldTargetSdkVersion = mTargetSdkVersion; 2380 2381 Pair<Integer, Integer> v = ManifestInfo.computeSdkVersions(mEditedFile.getProject()); 2382 mMinSdkVersion = v.getFirst(); 2383 mTargetSdkVersion = v.getSecond(); 2384 2385 return oldMinSdkVersion != mMinSdkVersion || oldTargetSdkVersion != mTargetSdkVersion; 2386 } 2387 2388 public ConfigurationComposite getConfigurationComposite() { 2389 return mConfigComposite; 2390 } 2391 2392 public LayoutActionBar getLayoutActionBar() { 2393 return mActionBar; 2394 } 2395 2396 /** 2397 * Returns the target SDK version 2398 * 2399 * @return the target SDK version 2400 */ 2401 public int getTargetSdkVersion() { 2402 return mTargetSdkVersion; 2403 } 2404 2405 /** 2406 * Returns the minimum SDK version 2407 * 2408 * @return the minimum SDK version 2409 */ 2410 public int getMinSdkVersion() { 2411 return mMinSdkVersion; 2412 } 2413 } 2414