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; 18 19 import com.android.ide.eclipse.adt.AdtPlugin; 20 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ILayoutReloadListener; 21 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; 22 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog; 23 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.IConfigListener; 24 import com.android.ide.eclipse.adt.internal.editors.layout.parts.ElementCreateCommand; 25 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; 26 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 27 import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration; 28 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; 29 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFile; 30 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType; 31 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; 32 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 33 import com.android.ide.eclipse.adt.internal.sdk.LoadStatus; 34 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 35 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge; 36 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; 37 import com.android.layoutlib.api.ILayoutBridge; 38 import com.android.layoutlib.api.ILayoutLog; 39 import com.android.layoutlib.api.ILayoutResult; 40 import com.android.layoutlib.api.IProjectCallback; 41 import com.android.layoutlib.api.IResourceValue; 42 import com.android.layoutlib.api.IXmlPullParser; 43 import com.android.sdklib.IAndroidTarget; 44 45 import org.eclipse.core.resources.IFile; 46 import org.eclipse.core.resources.IFolder; 47 import org.eclipse.core.resources.IProject; 48 import org.eclipse.core.resources.IResource; 49 import org.eclipse.core.runtime.CoreException; 50 import org.eclipse.core.runtime.IProgressMonitor; 51 import org.eclipse.core.runtime.IStatus; 52 import org.eclipse.core.runtime.Status; 53 import org.eclipse.core.runtime.jobs.Job; 54 import org.eclipse.draw2d.geometry.Rectangle; 55 import org.eclipse.gef.ui.parts.SelectionSynchronizer; 56 import org.eclipse.jface.dialogs.Dialog; 57 import org.eclipse.swt.SWT; 58 import org.eclipse.swt.custom.SashForm; 59 import org.eclipse.swt.custom.StyledText; 60 import org.eclipse.swt.dnd.Clipboard; 61 import org.eclipse.swt.layout.GridData; 62 import org.eclipse.swt.layout.GridLayout; 63 import org.eclipse.swt.widgets.Composite; 64 import org.eclipse.swt.widgets.Display; 65 import org.eclipse.ui.IEditorInput; 66 import org.eclipse.ui.IEditorSite; 67 import org.eclipse.ui.PartInitException; 68 import org.eclipse.ui.ide.IDE; 69 import org.eclipse.ui.part.EditorPart; 70 import org.eclipse.ui.part.FileEditorInput; 71 72 import java.io.File; 73 import java.io.FileOutputStream; 74 import java.io.IOException; 75 import java.io.InputStream; 76 import java.io.PrintStream; 77 import java.util.Map; 78 79 /** 80 * Graphical layout editor part, version 2. 81 * 82 * @since GLE2 83 * 84 * TODO List: 85 * - display error icon 86 * - finish palette (see palette's todo list) 87 * - finish canvas (see canva's todo list) 88 * - completly rethink the property panel 89 * - link to the existing outline editor (prolly reuse/adapt for simplier model and will need 90 * to adapt the selection synchronizer.) 91 */ 92 public class GraphicalEditorPart extends EditorPart implements IGraphicalLayoutEditor { 93 94 /* 95 * Useful notes: 96 * To understand Drag'n'drop: 97 * http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html 98 */ 99 100 /** Reference to the layout editor */ 101 private final LayoutEditor mLayoutEditor; 102 103 /** reference to the file being edited. */ 104 private IFile mEditedFile; 105 106 /** The current clipboard. Must be disposed later. */ 107 private Clipboard mClipboard; 108 109 /** The configuration composite at the top of the layout editor. */ 110 private ConfigurationComposite mConfigComposite; 111 112 /** The sash that splits the palette from the canvas. */ 113 private SashForm mSashPalette; 114 private SashForm mSashError; 115 116 /** The palette displayed on the left of the sash. */ 117 private PaletteComposite mPalette; 118 119 /** The layout canvas displayed o the right of the sash. */ 120 private LayoutCanvas mLayoutCanvas; 121 122 private StyledText mErrorLabel; 123 124 /** The {@link FolderConfiguration} being edited. */ 125 private FolderConfiguration mEditedConfig; 126 127 private Map<String, Map<String, IResourceValue>> mConfiguredFrameworkRes; 128 private Map<String, Map<String, IResourceValue>> mConfiguredProjectRes; 129 private ProjectCallback mProjectCallback; 130 private ILayoutLog mLogger; 131 132 private boolean mNeedsXmlReload = false; 133 private boolean mNeedsRecompute = false; 134 135 private TargetListener mTargetListener; 136 137 private ConfigListener mConfigListener; 138 139 private ReloadListener mReloadListener; 140 141 GraphicalEditorPart(LayoutEditor layoutEditor)142 public GraphicalEditorPart(LayoutEditor layoutEditor) { 143 mLayoutEditor = layoutEditor; 144 setPartName("Graphical Layout"); 145 } 146 147 // ------------------------------------ 148 // Methods overridden from base classes 149 //------------------------------------ 150 151 /** 152 * Initializes the editor part with a site and input. 153 * {@inheritDoc} 154 */ 155 @Override init(IEditorSite site, IEditorInput input)156 public void init(IEditorSite site, IEditorInput input) throws PartInitException { 157 setSite(site); 158 useNewEditorInput(input); 159 160 if (mTargetListener == null) { 161 mTargetListener = new TargetListener(); 162 AdtPlugin.getDefault().addTargetListener(mTargetListener); 163 } 164 165 if (mReloadListener == null) { 166 mReloadListener = new ReloadListener(); 167 LayoutReloadMonitor.getMonitor().addListener(mEditedFile.getProject(), mReloadListener); 168 } 169 } 170 171 /** 172 * Reloads this editor, by getting the new model from the {@link LayoutEditor}. 173 */ reloadEditor()174 public void reloadEditor() { 175 IEditorInput input = mLayoutEditor.getEditorInput(); 176 177 try { 178 useNewEditorInput(input); 179 } catch (PartInitException e) { 180 // really this shouldn't happen! Log it in case it happens. 181 mEditedFile = null; 182 AdtPlugin.log(e, "Input is not of type FileEditorInput: %1$s", //$NON-NLS-1$ 183 input == null ? "null" : input.toString()); //$NON-NLS-1$ 184 } 185 } 186 useNewEditorInput(IEditorInput input)187 private void useNewEditorInput(IEditorInput input) throws PartInitException { 188 // The contract of init() mentions we need to fail if we can't understand the input. 189 if (!(input instanceof FileEditorInput)) { 190 throw new PartInitException("Input is not of type FileEditorInput: " + //$NON-NLS-1$ 191 input == null ? "null" : input.toString()); //$NON-NLS-1$ 192 } 193 194 FileEditorInput fileInput = (FileEditorInput)input; 195 mEditedFile = fileInput.getFile(); 196 } 197 198 @Override createPartControl(Composite parent)199 public void createPartControl(Composite parent) { 200 201 Display d = parent.getDisplay(); 202 mClipboard = new Clipboard(d); 203 204 GridLayout gl = new GridLayout(1, false); 205 parent.setLayout(gl); 206 gl.marginHeight = gl.marginWidth = 0; 207 208 // create the top part for the configuration control 209 mConfigListener = new ConfigListener(); 210 mConfigComposite = new ConfigurationComposite(mConfigListener, parent, SWT.BORDER); 211 mConfigComposite.updateUIFromResources(); 212 213 mSashPalette = new SashForm(parent, SWT.HORIZONTAL); 214 mSashPalette.setLayoutData(new GridData(GridData.FILL_BOTH)); 215 216 mPalette = new PaletteComposite(mSashPalette); 217 218 mSashError = new SashForm(mSashPalette, SWT.VERTICAL | SWT.BORDER); 219 mSashError.setLayoutData(new GridData(GridData.FILL_BOTH)); 220 221 mLayoutCanvas = new LayoutCanvas(mSashError, SWT.NONE); 222 mErrorLabel = new StyledText(mSashError, SWT.READ_ONLY); 223 mErrorLabel.setEditable(false); 224 mErrorLabel.setBackground(d.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); 225 mErrorLabel.setForeground(d.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); 226 227 mSashPalette.setWeights(new int[] { 20, 80 }); 228 mSashError.setWeights(new int[] { 80, 20 }); 229 mSashError.setMaximizedControl(mLayoutCanvas); 230 231 // Initialize the state 232 reloadPalette(); 233 } 234 235 /** 236 * Switches the stack to display the error label and hide the canvas. 237 * @param errorFormat The new error to display if not null. 238 * @param parameters String.format parameters for the error format. 239 */ displayError(String errorFormat, Object...parameters)240 private void displayError(String errorFormat, Object...parameters) { 241 if (errorFormat != null) { 242 mErrorLabel.setText(String.format(errorFormat, parameters)); 243 } 244 mSashError.setMaximizedControl(null); 245 } 246 247 /** Displays the canvas and hides the error label. */ hideError()248 private void hideError() { 249 mSashError.setMaximizedControl(mLayoutCanvas); 250 } 251 252 @Override dispose()253 public void dispose() { 254 if (mTargetListener != null) { 255 AdtPlugin.getDefault().removeTargetListener(mTargetListener); 256 mTargetListener = null; 257 } 258 259 if (mReloadListener != null) { 260 LayoutReloadMonitor.getMonitor().removeListener(mReloadListener); 261 mReloadListener = null; 262 } 263 264 if (mClipboard != null) { 265 mClipboard.dispose(); 266 mClipboard = null; 267 } 268 269 super.dispose(); 270 } 271 272 /** 273 * Listens to changes from the Configuration UI banner and triggers layout rendering when 274 * changed. Also provide the Configuration UI with the list of resources/layout to display. 275 */ 276 private class ConfigListener implements IConfigListener { 277 278 /** 279 * Looks for a file matching the new {@link FolderConfiguration} and attempts to open it. 280 * <p/>If there is no match, notify the user. 281 */ onConfigurationChange()282 public void onConfigurationChange() { 283 mConfiguredFrameworkRes = mConfiguredProjectRes = null; 284 285 if (mEditedFile == null || mEditedConfig == null) { 286 return; 287 } 288 289 // get the resources of the file's project. 290 ProjectResources resources = ResourceManager.getInstance().getProjectResources( 291 mEditedFile.getProject()); 292 293 // from the resources, look for a matching file 294 ResourceFile match = null; 295 if (resources != null) { 296 match = resources.getMatchingFile(mEditedFile.getName(), 297 ResourceFolderType.LAYOUT, 298 mConfigComposite.getCurrentConfig()); 299 } 300 301 if (match != null) { 302 if (match.getFile().equals(mEditedFile) == false) { 303 try { 304 IDE.openEditor( 305 getSite().getWorkbenchWindow().getActivePage(), 306 match.getFile().getIFile()); 307 308 // we're done! 309 return; 310 } catch (PartInitException e) { 311 // FIXME: do something! 312 } 313 } 314 315 // at this point, we have not opened a new file. 316 317 // update the configuration icons with the new edited config. 318 setConfiguration(mEditedConfig, false /*force*/); 319 320 // enable the create button if the current and edited config are not equals 321 mConfigComposite.setEnabledCreate( 322 mEditedConfig.equals(mConfigComposite.getCurrentConfig()) == false); 323 324 // Even though the layout doesn't change, the config changed, and referenced 325 // resources need to be updated. 326 recomputeLayout(); 327 } else { 328 // enable the Create button 329 mConfigComposite.setEnabledCreate(true); 330 331 // display the error. 332 FolderConfiguration currentConfig = mConfigComposite.getCurrentConfig(); 333 displayError( 334 "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.", 335 currentConfig.toDisplayString(), 336 currentConfig.getFolderName(ResourceFolderType.LAYOUT, 337 Sdk.getCurrent().getTarget(mEditedFile.getProject())), 338 mEditedFile.getName()); 339 } 340 } 341 onThemeChange()342 public void onThemeChange() { 343 recomputeLayout(); 344 } 345 OnClippingChange()346 public void OnClippingChange() { 347 recomputeLayout(); 348 } 349 onCreate()350 public void onCreate() { 351 LayoutCreatorDialog dialog = new LayoutCreatorDialog(mConfigComposite.getShell(), 352 mEditedFile.getName(), 353 Sdk.getCurrent().getTarget(mEditedFile.getProject()), 354 mConfigComposite.getCurrentConfig()); 355 if (dialog.open() == Dialog.OK) { 356 final FolderConfiguration config = new FolderConfiguration(); 357 dialog.getConfiguration(config); 358 359 createAlternateLayout(config); 360 } 361 } 362 getConfiguredFrameworkResources()363 public Map<String, Map<String, IResourceValue>> getConfiguredFrameworkResources() { 364 if (mConfiguredFrameworkRes == null && mConfigComposite != null) { 365 ProjectResources frameworkRes = getFrameworkResources(); 366 367 if (frameworkRes == null) { 368 AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework"); 369 } else { 370 // get the framework resource values based on the current config 371 mConfiguredFrameworkRes = frameworkRes.getConfiguredResources( 372 mConfigComposite.getCurrentConfig()); 373 } 374 } 375 376 return mConfiguredFrameworkRes; 377 } 378 getConfiguredProjectResources()379 public Map<String, Map<String, IResourceValue>> getConfiguredProjectResources() { 380 if (mConfiguredProjectRes == null && mConfigComposite != null) { 381 ProjectResources project = getProjectResources(); 382 383 // make sure they are loaded 384 project.loadAll(); 385 386 // get the project resource values based on the current config 387 mConfiguredProjectRes = project.getConfiguredResources( 388 mConfigComposite.getCurrentConfig()); 389 } 390 391 return mConfiguredProjectRes; 392 } 393 394 /** 395 * Returns a {@link ProjectResources} for the framework resources. 396 * @return the framework resources or null if not found. 397 */ getFrameworkResources()398 public ProjectResources getFrameworkResources() { 399 if (mEditedFile != null) { 400 Sdk currentSdk = Sdk.getCurrent(); 401 if (currentSdk != null) { 402 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject()); 403 404 if (target != null) { 405 AndroidTargetData data = currentSdk.getTargetData(target); 406 407 if (data != null) { 408 return data.getFrameworkResources(); 409 } 410 } 411 } 412 } 413 414 return null; 415 } 416 getProjectResources()417 public ProjectResources getProjectResources() { 418 if (mEditedFile != null) { 419 ResourceManager manager = ResourceManager.getInstance(); 420 return manager.getProjectResources(mEditedFile.getProject()); 421 } 422 423 return null; 424 } 425 426 /** 427 * Creates a new layout file from the specified {@link FolderConfiguration}. 428 */ createAlternateLayout(final FolderConfiguration config)429 private void createAlternateLayout(final FolderConfiguration config) { 430 new Job("Create Alternate Resource") { 431 @Override 432 protected IStatus run(IProgressMonitor monitor) { 433 // get the folder name 434 String folderName = config.getFolderName(ResourceFolderType.LAYOUT, 435 Sdk.getCurrent().getTarget(mEditedFile.getProject())); 436 try { 437 438 // look to see if it exists. 439 // get the res folder 440 IFolder res = (IFolder)mEditedFile.getParent().getParent(); 441 String path = res.getLocation().toOSString(); 442 443 File newLayoutFolder = new File(path + File.separator + folderName); 444 if (newLayoutFolder.isFile()) { 445 // this should not happen since aapt would have complained 446 // before, but if one disable the automatic build, this could 447 // happen. 448 String message = String.format("File 'res/%1$s' is in the way!", 449 folderName); 450 451 AdtPlugin.displayError("Layout Creation", message); 452 453 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message); 454 } else if (newLayoutFolder.exists() == false) { 455 // create it. 456 newLayoutFolder.mkdir(); 457 } 458 459 // now create the file 460 File newLayoutFile = new File(newLayoutFolder.getAbsolutePath() + 461 File.separator + mEditedFile.getName()); 462 463 newLayoutFile.createNewFile(); 464 465 InputStream input = mEditedFile.getContents(); 466 467 FileOutputStream fos = new FileOutputStream(newLayoutFile); 468 469 byte[] data = new byte[512]; 470 int count; 471 while ((count = input.read(data)) != -1) { 472 fos.write(data, 0, count); 473 } 474 475 input.close(); 476 fos.close(); 477 478 // refreshes the res folder to show up the new 479 // layout folder (if needed) and the file. 480 // We use a progress monitor to catch the end of the refresh 481 // to trigger the edit of the new file. 482 res.refreshLocal(IResource.DEPTH_INFINITE, new IProgressMonitor() { 483 public void done() { 484 mConfigComposite.getDisplay().asyncExec(new Runnable() { 485 public void run() { 486 onConfigurationChange(); 487 } 488 }); 489 } 490 491 public void beginTask(String name, int totalWork) { 492 // pass 493 } 494 495 public void internalWorked(double work) { 496 // pass 497 } 498 499 public boolean isCanceled() { 500 // pass 501 return false; 502 } 503 504 public void setCanceled(boolean value) { 505 // pass 506 } 507 508 public void setTaskName(String name) { 509 // pass 510 } 511 512 public void subTask(String name) { 513 // pass 514 } 515 516 public void worked(int work) { 517 // pass 518 } 519 }); 520 } catch (IOException e2) { 521 String message = String.format( 522 "Failed to create File 'res/%1$s/%2$s' : %3$s", 523 folderName, mEditedFile.getName(), e2.getMessage()); 524 525 AdtPlugin.displayError("Layout Creation", message); 526 527 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 528 message, e2); 529 } catch (CoreException e2) { 530 String message = String.format( 531 "Failed to create File 'res/%1$s/%2$s' : %3$s", 532 folderName, mEditedFile.getName(), e2.getMessage()); 533 534 AdtPlugin.displayError("Layout Creation", message); 535 536 return e2.getStatus(); 537 } 538 539 return Status.OK_STATUS; 540 541 } 542 }.schedule(); 543 } 544 } 545 546 /** 547 * Listens to target changed in the current project, to trigger a new layout rendering. 548 */ 549 private class TargetListener implements ITargetChangeListener { 550 onProjectTargetChange(IProject changedProject)551 public void onProjectTargetChange(IProject changedProject) { 552 if (changedProject == getLayoutEditor().getProject()) { 553 onTargetsLoaded(); 554 } 555 } 556 onTargetsLoaded()557 public void onTargetsLoaded() { 558 // because the SDK changed we must reset the configured framework resource. 559 mConfiguredFrameworkRes = null; 560 561 mConfigComposite.updateUIFromResources(); 562 563 // updateUiFromFramework will reset language/region combo, so we must call 564 // setConfiguration after, or the settext on language/region will be lost. 565 if (mEditedConfig != null) { 566 setConfiguration(mEditedConfig, false /*force*/); 567 } 568 569 // make sure we remove the custom view loader, since its parent class loader is the 570 // bridge class loader. 571 mProjectCallback = null; 572 573 recomputeLayout(); 574 } 575 } 576 577 /** 578 * Update the UI controls state with a given {@link FolderConfiguration}. 579 * <p/>If <var>force</var> is set to <code>true</code> the UI will be changed to exactly reflect 580 * <var>config</var>, otherwise, if a qualifier is not present in <var>config</var>, 581 * the UI control is not modified. However if the value in the control is not the default value, 582 * a warning icon is shown. 583 * @param config The {@link FolderConfiguration} to set. 584 * @param force Whether the UI should be changed to exactly match the received configuration. 585 */ setConfiguration(FolderConfiguration config, boolean force)586 void setConfiguration(FolderConfiguration config, boolean force) { 587 mEditedConfig = config; 588 mConfiguredFrameworkRes = mConfiguredProjectRes = null; 589 590 mConfigComposite.setConfiguration(config, force); 591 592 } 593 594 // ---------------- 595 596 /** 597 * Save operation in the Graphical Editor Part. 598 * <p/> 599 * In our workflow, the model is owned by the Structured XML Editor. 600 * The graphical layout editor just displays it -- thus we don't really 601 * save anything here. 602 * <p/> 603 * This must NOT call the parent editor part. At the contrary, the parent editor 604 * part will call this *after* having done the actual save operation. 605 * <p/> 606 * The only action this editor must do is mark the undo command stack as 607 * being no longer dirty. 608 */ 609 @Override doSave(IProgressMonitor monitor)610 public void doSave(IProgressMonitor monitor) { 611 // TODO implement a command stack 612 // getCommandStack().markSaveLocation(); 613 // firePropertyChange(PROP_DIRTY); 614 } 615 616 /** 617 * Save operation in the Graphical Editor Part. 618 * <p/> 619 * In our workflow, the model is owned by the Structured XML Editor. 620 * The graphical layout editor just displays it -- thus we don't really 621 * save anything here. 622 */ 623 @Override doSaveAs()624 public void doSaveAs() { 625 // pass 626 } 627 628 /** 629 * In our workflow, the model is owned by the Structured XML Editor. 630 * The graphical layout editor just displays it -- thus we don't really 631 * save anything here. 632 */ 633 @Override isDirty()634 public boolean isDirty() { 635 return false; 636 } 637 638 /** 639 * In our workflow, the model is owned by the Structured XML Editor. 640 * The graphical layout editor just displays it -- thus we don't really 641 * save anything here. 642 */ 643 @Override isSaveAsAllowed()644 public boolean isSaveAsAllowed() { 645 return false; 646 } 647 648 @Override setFocus()649 public void setFocus() { 650 // TODO Auto-generated method stub 651 652 } 653 654 /** 655 * Responds to a page change that made the Graphical editor page the activated page. 656 */ activated()657 public void activated() { 658 if (mNeedsRecompute || mNeedsXmlReload) { 659 recomputeLayout(); 660 } 661 } 662 663 /** 664 * Responds to a page change that made the Graphical editor page the deactivated page 665 */ deactivated()666 public void deactivated() { 667 // nothing to be done here for now. 668 } 669 670 /** 671 * Sets the UI for the edition of a new file. 672 * @param configuration the configuration of the new file. 673 */ editNewFile(FolderConfiguration configuration)674 public void editNewFile(FolderConfiguration configuration) { 675 // update the configuration UI 676 setConfiguration(configuration, true /*force*/); 677 678 // enable the create button if the current and edited config are not equals 679 mConfigComposite.setEnabledCreate( 680 mEditedConfig.equals(mConfigComposite.getCurrentConfig()) == false); 681 682 reloadConfigurationUi(false /*notifyListener*/); 683 } 684 getClipboard()685 public Clipboard getClipboard() { 686 return mClipboard; 687 } 688 getLayoutEditor()689 public LayoutEditor getLayoutEditor() { 690 return mLayoutEditor; 691 } 692 getModel()693 public UiDocumentNode getModel() { 694 return mLayoutEditor.getUiRootNode(); 695 } 696 getSelectionSynchronizer()697 public SelectionSynchronizer getSelectionSynchronizer() { 698 // TODO Auto-generated method stub 699 return null; 700 } 701 702 /** 703 * Callback for XML model changed. Only update/recompute the layout if the editor is visible 704 */ onXmlModelChanged()705 public void onXmlModelChanged() { 706 if (mLayoutEditor.isGraphicalEditorActive()) { 707 doXmlReload(true /* force */); 708 recomputeLayout(); 709 } else { 710 mNeedsXmlReload = true; 711 } 712 } 713 714 /** 715 * Actually performs the XML reload 716 * @see #onXmlModelChanged() 717 */ doXmlReload(boolean force)718 private void doXmlReload(boolean force) { 719 if (force || mNeedsXmlReload) { 720 721 // TODO : update the mLayoutCanvas, preserving the current selection if possible. 722 723 // GraphicalViewer viewer = getGraphicalViewer(); 724 // 725 // // try to preserve the selection before changing the content 726 // SelectionManager selMan = viewer.getSelectionManager(); 727 // ISelection selection = selMan.getSelection(); 728 // 729 // try { 730 // viewer.setContents(getModel()); 731 // } finally { 732 // selMan.setSelection(selection); 733 // } 734 735 mNeedsXmlReload = false; 736 } 737 } 738 recomputeLayout()739 public void recomputeLayout() { 740 doXmlReload(false /* force */); 741 try { 742 // check that the resource exists. If the file is opened but the project is closed 743 // or deleted for some reason (changed from outside of eclipse), then this will 744 // return false; 745 if (mEditedFile.exists() == false) { 746 displayError("Resource '%1$s' does not exist.", 747 mEditedFile.getFullPath().toString()); 748 return; 749 } 750 751 IProject iProject = mEditedFile.getProject(); 752 753 if (mEditedFile.isSynchronized(IResource.DEPTH_ZERO) == false) { 754 String message = String.format("%1$s is out of sync. Please refresh.", 755 mEditedFile.getName()); 756 757 displayError(message); 758 759 // also print it in the error console. 760 AdtPlugin.printErrorToConsole(iProject.getName(), message); 761 return; 762 } 763 764 Sdk currentSdk = Sdk.getCurrent(); 765 if (currentSdk != null) { 766 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject()); 767 if (target == null) { 768 displayError("The project target is not set."); 769 return; 770 } 771 772 AndroidTargetData data = currentSdk.getTargetData(target); 773 if (data == null) { 774 // It can happen that the workspace refreshes while the SDK is loading its 775 // data, which could trigger a redraw of the opened layout if some resources 776 // changed while Eclipse is closed. 777 // In this case data could be null, but this is not an error. 778 // We can just silently return, as all the opened editors are automatically 779 // refreshed once the SDK finishes loading. 780 if (AdtPlugin.getDefault().getSdkLoadStatus() != LoadStatus.LOADING) { 781 displayError("The project target (%s) was not properly loaded.", 782 target.getName()); 783 } 784 return; 785 } 786 787 // check there is actually a model (maybe the file is empty). 788 UiDocumentNode model = getModel(); 789 790 if (model.getUiChildren().size() == 0) { 791 displayError("No Xml content. Go to the Outline view and add nodes."); 792 return; 793 } 794 795 LayoutBridge bridge = data.getLayoutBridge(); 796 797 if (bridge.bridge != null) { // bridge can never be null. 798 ResourceManager resManager = ResourceManager.getInstance(); 799 800 ProjectResources projectRes = resManager.getProjectResources(iProject); 801 if (projectRes == null) { 802 displayError("Missing project resources."); 803 return; 804 } 805 806 // get the resources of the file's project. 807 Map<String, Map<String, IResourceValue>> configuredProjectRes = 808 mConfigListener.getConfiguredProjectResources(); 809 810 // get the framework resources 811 Map<String, Map<String, IResourceValue>> frameworkResources = 812 mConfigListener.getConfiguredFrameworkResources(); 813 814 if (configuredProjectRes != null && frameworkResources != null) { 815 if (mProjectCallback == null) { 816 mProjectCallback = new ProjectCallback( 817 bridge.classLoader, projectRes, iProject); 818 } 819 820 if (mLogger == null) { 821 mLogger = new ILayoutLog() { 822 public void error(String message) { 823 AdtPlugin.printErrorToConsole(mEditedFile.getName(), message); 824 } 825 826 public void error(Throwable error) { 827 String message = error.getMessage(); 828 if (message == null) { 829 message = error.getClass().getName(); 830 } 831 832 PrintStream ps = new PrintStream(AdtPlugin.getErrorStream()); 833 error.printStackTrace(ps); 834 } 835 836 public void warning(String message) { 837 AdtPlugin.printToConsole(mEditedFile.getName(), message); 838 } 839 }; 840 } 841 842 // get the selected theme 843 String theme = mConfigComposite.getTheme(); 844 if (theme != null) { 845 846 // Compute the layout 847 UiElementPullParser parser = new UiElementPullParser(getModel()); 848 Rectangle rect = getBounds(); 849 boolean isProjectTheme = mConfigComposite.isProjectTheme(); 850 851 int density = mConfigComposite.getDensity().getDpiValue(); 852 float xdpi = mConfigComposite.getXDpi(); 853 float ydpi = mConfigComposite.getYDpi(); 854 855 ILayoutResult result = computeLayout(bridge, parser, 856 iProject /* projectKey */, 857 rect.width, rect.height, !mConfigComposite.getClipping(), 858 density, xdpi, ydpi, 859 theme, isProjectTheme, 860 configuredProjectRes, frameworkResources, mProjectCallback, 861 mLogger); 862 863 mLayoutCanvas.setResult(result); 864 865 // update the UiElementNode with the layout info. 866 if (result.getSuccess() == ILayoutResult.SUCCESS) { 867 hideError(); 868 } else { 869 displayError(result.getErrorMessage()); 870 } 871 872 model.refreshUi(); 873 } 874 } 875 } else { 876 // SDK is loaded but not the layout library! 877 878 // check whether the bridge managed to load, or not 879 if (bridge.status == LoadStatus.LOADING) { 880 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.", 881 mEditedFile.getName()); 882 } else { 883 displayError("Eclipse failed to load the framework information and the layout library!"); 884 } 885 } 886 } else { 887 displayError("Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.", 888 mEditedFile.getName()); 889 } 890 } finally { 891 // no matter the result, we are done doing the recompute based on the latest 892 // resource/code change. 893 mNeedsRecompute = false; 894 } 895 } 896 897 /** 898 * Computes a layout by calling the correct computeLayout method of ILayoutBridge based on 899 * the implementation API level. 900 * 901 * Implementation detail: the bridge's computeLayout() method already returns a newly 902 * allocated ILayourResult. 903 */ 904 @SuppressWarnings("deprecation") computeLayout(LayoutBridge bridge, IXmlPullParser layoutDescription, Object projectKey, int screenWidth, int screenHeight, boolean renderFullSize, int density, float xdpi, float ydpi, String themeName, boolean isProjectTheme, Map<String, Map<String, IResourceValue>> projectResources, Map<String, Map<String, IResourceValue>> frameworkResources, IProjectCallback projectCallback, ILayoutLog logger)905 private static ILayoutResult computeLayout(LayoutBridge bridge, 906 IXmlPullParser layoutDescription, Object projectKey, 907 int screenWidth, int screenHeight, boolean renderFullSize, 908 int density, float xdpi, float ydpi, 909 String themeName, boolean isProjectTheme, 910 Map<String, Map<String, IResourceValue>> projectResources, 911 Map<String, Map<String, IResourceValue>> frameworkResources, 912 IProjectCallback projectCallback, ILayoutLog logger) { 913 914 if (bridge.apiLevel >= ILayoutBridge.API_CURRENT) { 915 // newest API with support for "render full height" 916 // TODO: link boolean to UI. 917 return bridge.bridge.computeLayout(layoutDescription, 918 projectKey, screenWidth, screenHeight, renderFullSize, 919 density, xdpi, ydpi, 920 themeName, isProjectTheme, 921 projectResources, frameworkResources, projectCallback, 922 logger); 923 } else if (bridge.apiLevel == 3) { 924 // newer api with density support. 925 return bridge.bridge.computeLayout(layoutDescription, 926 projectKey, screenWidth, screenHeight, density, xdpi, ydpi, 927 themeName, isProjectTheme, 928 projectResources, frameworkResources, projectCallback, 929 logger); 930 } else if (bridge.apiLevel == 2) { 931 // api with boolean for separation of project/framework theme 932 return bridge.bridge.computeLayout(layoutDescription, 933 projectKey, screenWidth, screenHeight, themeName, isProjectTheme, 934 projectResources, frameworkResources, projectCallback, 935 logger); 936 } else { 937 // oldest api with no density/dpi, and project theme boolean mixed 938 // into the theme name. 939 940 // change the string if it's a custom theme to make sure we can 941 // differentiate them 942 if (isProjectTheme) { 943 themeName = "*" + themeName; //$NON-NLS-1$ 944 } 945 946 return bridge.bridge.computeLayout(layoutDescription, 947 projectKey, screenWidth, screenHeight, themeName, 948 projectResources, frameworkResources, projectCallback, 949 logger); 950 } 951 } 952 getBounds()953 public Rectangle getBounds() { 954 return mConfigComposite.getScreenBounds(); 955 } 956 reloadPalette()957 public void reloadPalette() { 958 if (mPalette != null) { 959 mPalette.reloadPalette(mLayoutEditor.getTargetData()); 960 } 961 } 962 reloadConfigurationUi(boolean notifyListener)963 public void reloadConfigurationUi(boolean notifyListener) { 964 // enable the clipping button if it's supported. 965 Sdk currentSdk = Sdk.getCurrent(); 966 if (currentSdk != null) { 967 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject()); 968 AndroidTargetData data = currentSdk.getTargetData(target); 969 if (data != null) { 970 LayoutBridge bridge = data.getLayoutBridge(); 971 mConfigComposite.reloadDevices(notifyListener); 972 mConfigComposite.setClippingSupport(bridge.apiLevel >= 4); 973 } 974 } 975 } 976 977 /** 978 * Used by LayoutEditor.UiEditorActions.selectUiNode to select a new UI Node 979 * created by {@link ElementCreateCommand#execute()}. 980 * 981 * @param uiNodeModel The {@link UiElementNode} to select. 982 */ selectModel(UiElementNode uiNodeModel)983 public void selectModel(UiElementNode uiNodeModel) { 984 985 // TODO this method was useful for GLE1. We may not need it anymore now. 986 987 // GraphicalViewer viewer = getGraphicalViewer(); 988 // 989 // // Give focus to the graphical viewer (in case the outline has it) 990 // viewer.getControl().forceFocus(); 991 // 992 // Object editPart = viewer.getEditPartRegistry().get(uiNodeModel); 993 // 994 // if (editPart instanceof EditPart) { 995 // viewer.select((EditPart)editPart); 996 // } 997 } 998 999 private class ReloadListener implements ILayoutReloadListener { 1000 /* 1001 * (non-Javadoc) 1002 * @see com.android.ide.eclipse.editors.layout.LayoutReloadMonitor.ILayoutReloadListener#reloadLayout(boolean, boolean, boolean) 1003 * 1004 * Called when the file changes triggered a redraw of the layout 1005 */ reloadLayout(boolean codeChange, boolean rChange, boolean resChange)1006 public void reloadLayout(boolean codeChange, boolean rChange, boolean resChange) { 1007 boolean recompute = rChange; 1008 1009 if (resChange) { 1010 recompute = true; 1011 1012 // TODO: differentiate between single and multi resource file changed, and whether the resource change affects the cache. 1013 1014 // force a reparse in case a value XML file changed. 1015 mConfiguredProjectRes = null; 1016 1017 // clear the cache in the bridge in case a bitmap/9-patch changed. 1018 IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject()); 1019 if (target != null) { 1020 1021 AndroidTargetData data = Sdk.getCurrent().getTargetData(target); 1022 if (data != null) { 1023 LayoutBridge bridge = data.getLayoutBridge(); 1024 1025 if (bridge.bridge != null) { 1026 bridge.bridge.clearCaches(mEditedFile.getProject()); 1027 } 1028 } 1029 } 1030 1031 mLayoutCanvas.getDisplay().asyncExec(new Runnable() { 1032 public void run() { 1033 mConfigComposite.updateUIFromResources(); 1034 } 1035 }); 1036 } 1037 1038 if (codeChange) { 1039 // only recompute if the custom view loader was used to load some code. 1040 if (mProjectCallback != null && mProjectCallback.isUsed()) { 1041 mProjectCallback = null; 1042 recompute = true; 1043 } 1044 } 1045 1046 if (recompute) { 1047 mLayoutCanvas.getDisplay().asyncExec(new Runnable() { 1048 public void run() { 1049 if (mLayoutEditor.isGraphicalEditorActive()) { 1050 recomputeLayout(); 1051 } else { 1052 mNeedsRecompute = true; 1053 } 1054 } 1055 }); 1056 } 1057 } 1058 } 1059 } 1060