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