1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2; 18 19 import com.android.ide.eclipse.adt.AdtPlugin; 20 import com.android.ide.eclipse.adt.internal.editors.layout.ExplodedRenderingHelper; 21 import com.android.ide.eclipse.adt.internal.editors.layout.IGraphicalLayoutEditor; 22 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; 23 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor; 24 import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback; 25 import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser; 26 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ChangeFlags; 27 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ILayoutReloadListener; 28 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; 29 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog; 30 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.CustomToggle; 31 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.IConfigListener; 32 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; 33 import com.android.ide.eclipse.adt.internal.editors.layout.parts.ElementCreateCommand; 34 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; 35 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 36 import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration; 37 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; 38 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFile; 39 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType; 40 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; 41 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 42 import com.android.ide.eclipse.adt.internal.sdk.LoadStatus; 43 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 44 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge; 45 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; 46 import com.android.ide.eclipse.adt.io.IFileWrapper; 47 import com.android.layoutlib.api.ILayoutBridge; 48 import com.android.layoutlib.api.ILayoutLog; 49 import com.android.layoutlib.api.ILayoutResult; 50 import com.android.layoutlib.api.IProjectCallback; 51 import com.android.layoutlib.api.IResourceValue; 52 import com.android.layoutlib.api.IXmlPullParser; 53 import com.android.sdklib.IAndroidTarget; 54 55 import org.eclipse.core.resources.IFile; 56 import org.eclipse.core.resources.IFolder; 57 import org.eclipse.core.resources.IProject; 58 import org.eclipse.core.resources.IResource; 59 import org.eclipse.core.runtime.CoreException; 60 import org.eclipse.core.runtime.IProgressMonitor; 61 import org.eclipse.core.runtime.IStatus; 62 import org.eclipse.core.runtime.Status; 63 import org.eclipse.core.runtime.jobs.Job; 64 import org.eclipse.draw2d.geometry.Rectangle; 65 import org.eclipse.gef.ui.parts.SelectionSynchronizer; 66 import org.eclipse.jface.action.Action; 67 import org.eclipse.jface.dialogs.Dialog; 68 import org.eclipse.jface.viewers.ISelection; 69 import org.eclipse.jface.viewers.ISelectionProvider; 70 import org.eclipse.swt.SWT; 71 import org.eclipse.swt.custom.SashForm; 72 import org.eclipse.swt.custom.StyledText; 73 import org.eclipse.swt.dnd.Clipboard; 74 import org.eclipse.swt.layout.GridData; 75 import org.eclipse.swt.layout.GridLayout; 76 import org.eclipse.swt.widgets.Composite; 77 import org.eclipse.swt.widgets.Display; 78 import org.eclipse.ui.IActionBars; 79 import org.eclipse.ui.IEditorInput; 80 import org.eclipse.ui.IEditorSite; 81 import org.eclipse.ui.INullSelectionListener; 82 import org.eclipse.ui.ISelectionListener; 83 import org.eclipse.ui.IWorkbenchPart; 84 import org.eclipse.ui.PartInitException; 85 import org.eclipse.ui.actions.ActionFactory; 86 import org.eclipse.ui.ide.IDE; 87 import org.eclipse.ui.part.EditorPart; 88 import org.eclipse.ui.part.FileEditorInput; 89 90 import java.io.File; 91 import java.io.FileOutputStream; 92 import java.io.IOException; 93 import java.io.InputStream; 94 import java.io.PrintStream; 95 import java.util.List; 96 import java.util.Map; 97 98 /** 99 * Graphical layout editor part, version 2. 100 * <p/> 101 * The main component of the editor part is the {@link LayoutCanvasViewer}, which 102 * actually delegates its work to the {@link LayoutCanvas} control. 103 * <p/> 104 * The {@link LayoutCanvasViewer} is set as the site's {@link ISelectionProvider}: 105 * when the selection changes in the canvas, it is thus broadcasted to anyone listening 106 * on the site's selection service. 107 * <p/> 108 * This part is also an {@link ISelectionListener}. It listens to the site's selection 109 * service and thus receives selection changes from itself as well as the associated 110 * outline and property sheet (these are registered by {@link LayoutEditor#getAdapter(Class)}). 111 * 112 * @since GLE2 113 * 114 * TODO List: 115 * - display error icon 116 * - finish palette (see palette's todo list) 117 * - finish canvas (see canvas' todo list) 118 * - completly rethink the property panel 119 */ 120 public class GraphicalEditorPart extends EditorPart 121 implements IGraphicalLayoutEditor, ISelectionListener, INullSelectionListener { 122 123 /* 124 * Useful notes: 125 * To understand Drag'n'drop: 126 * http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html 127 * 128 * To understand the site's selection listener, selection provider, and the 129 * confusion of different-yet-similarly-named interfaces, consult this: 130 * http://www.eclipse.org/articles/Article-WorkbenchSelections/article.html 131 * 132 * To summarize the selection mechanism: 133 * - The workbench site selection service can be seen as "centralized" 134 * service that registers selection providers and selection listeners. 135 * - The editor part and the outline are selection providers. 136 * - The editor part, the outline and the property sheet are listeners 137 * which all listen to each others indirectly. 138 */ 139 140 /** Reference to the layout editor */ 141 private final LayoutEditor mLayoutEditor; 142 143 /** Reference to the file being edited. Can also be used to access the {@link IProject}. */ 144 private IFile mEditedFile; 145 146 /** The current clipboard. Must be disposed later. */ 147 private Clipboard mClipboard; 148 149 /** The configuration composite at the top of the layout editor. */ 150 private ConfigurationComposite mConfigComposite; 151 152 /** The sash that splits the palette from the canvas. */ 153 private SashForm mSashPalette; 154 155 /** The sash that splits the palette from the error view. 156 * The error view is shown only when needed. */ 157 private SashForm mSashError; 158 159 /** The palette displayed on the left of the sash. */ 160 private PaletteComposite mPalette; 161 162 /** The layout canvas displayed to the right of the sash. */ 163 private LayoutCanvasViewer mCanvasViewer; 164 165 /** The Groovy Rules Engine associated with this editor. It is project-specific. */ 166 private RulesEngine mRulesEngine; 167 168 /** Styled text displaying the most recent error in the error view. */ 169 private StyledText mErrorLabel; 170 171 private Map<String, Map<String, IResourceValue>> mConfiguredFrameworkRes; 172 private Map<String, Map<String, IResourceValue>> mConfiguredProjectRes; 173 private ProjectCallback mProjectCallback; 174 private ILayoutLog mLogger; 175 176 private boolean mNeedsXmlReload = false; 177 private boolean mNeedsRecompute = false; 178 179 private TargetListener mTargetListener; 180 181 private ConfigListener mConfigListener; 182 183 private ReloadListener mReloadListener; 184 185 private boolean mUseExplodeMode; 186 187 188 public GraphicalEditorPart(LayoutEditor layoutEditor) { 189 mLayoutEditor = layoutEditor; 190 setPartName("Graphical Layout"); 191 } 192 193 // ------------------------------------ 194 // Methods overridden from base classes 195 //------------------------------------ 196 197 /** 198 * Initializes the editor part with a site and input. 199 * {@inheritDoc} 200 */ 201 @Override 202 public void init(IEditorSite site, IEditorInput input) throws PartInitException { 203 setSite(site); 204 useNewEditorInput(input); 205 206 if (mTargetListener == null) { 207 mTargetListener = new TargetListener(); 208 AdtPlugin.getDefault().addTargetListener(mTargetListener); 209 } 210 } 211 212 private void useNewEditorInput(IEditorInput input) throws PartInitException { 213 // The contract of init() mentions we need to fail if we can't understand the input. 214 if (!(input instanceof FileEditorInput)) { 215 throw new PartInitException("Input is not of type FileEditorInput: " + //$NON-NLS-1$ 216 input == null ? "null" : input.toString()); //$NON-NLS-1$ 217 } 218 } 219 220 @Override 221 public void createPartControl(Composite parent) { 222 223 Display d = parent.getDisplay(); 224 mClipboard = new Clipboard(d); 225 226 GridLayout gl = new GridLayout(1, false); 227 parent.setLayout(gl); 228 gl.marginHeight = gl.marginWidth = 0; 229 230 // create the top part for the configuration control 231 232 CustomToggle[] toggles = new CustomToggle[] { 233 new CustomToggle( 234 "-", 235 null, //image 236 "Canvas zoom out." 237 ) { 238 @Override 239 public void onSelected(boolean newState) { 240 rescale(-1); 241 } 242 }, 243 new CustomToggle( 244 "+", 245 null, //image 246 "Canvas zoom in." 247 ) { 248 @Override 249 public void onSelected(boolean newState) { 250 rescale(+1); 251 } 252 }, 253 new CustomToggle( 254 "Explode", 255 null, //image 256 "Displays extra margins in the layout." 257 ) { 258 @Override 259 public void onSelected(boolean newState) { 260 mUseExplodeMode = newState; 261 recomputeLayout(); 262 } 263 }, 264 new CustomToggle( 265 "Outline", 266 null, //image 267 "Shows the of all views in the layout." 268 ) { 269 @Override 270 public void onSelected(boolean newState) { 271 mCanvasViewer.getCanvas().setShowOutline(newState); 272 } 273 } 274 }; 275 276 mConfigListener = new ConfigListener(); 277 mConfigComposite = new ConfigurationComposite(mConfigListener, toggles, parent, SWT.BORDER); 278 279 mSashPalette = new SashForm(parent, SWT.HORIZONTAL); 280 mSashPalette.setLayoutData(new GridData(GridData.FILL_BOTH)); 281 282 mPalette = new PaletteComposite(mSashPalette); 283 284 mSashError = new SashForm(mSashPalette, SWT.VERTICAL | SWT.BORDER); 285 mSashError.setLayoutData(new GridData(GridData.FILL_BOTH)); 286 287 mCanvasViewer = new LayoutCanvasViewer(mLayoutEditor, mRulesEngine, mSashError, SWT.NONE); 288 289 mErrorLabel = new StyledText(mSashError, SWT.READ_ONLY); 290 mErrorLabel.setEditable(false); 291 mErrorLabel.setBackground(d.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); 292 mErrorLabel.setForeground(d.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); 293 294 mSashPalette.setWeights(new int[] { 20, 80 }); 295 mSashError.setWeights(new int[] { 80, 20 }); 296 mSashError.setMaximizedControl(mCanvasViewer.getControl()); 297 298 setupEditActions(); 299 300 // Initialize the state 301 reloadPalette(); 302 303 getSite().setSelectionProvider(mCanvasViewer); 304 getSite().getPage().addSelectionListener(this); 305 } 306 307 /** 308 * Listens to workbench selections that does NOT come from {@link LayoutEditor} 309 * (those are generated by ourselves). 310 * <p/> 311 * Selection can be null, as indicated by this class implementing 312 * {@link INullSelectionListener}. 313 */ 314 public void selectionChanged(IWorkbenchPart part, ISelection selection) { 315 if (!(part instanceof LayoutEditor)) { 316 mCanvasViewer.setSelection(selection); 317 } 318 } 319 320 /** 321 * Rescales canvas. 322 * @param direction +1 for zoom in, -1 for zoom out 323 */ 324 private void rescale(int direction) { 325 double s = mCanvasViewer.getCanvas().getScale(); 326 327 if (direction > 0) { 328 s = s * 2; 329 } else { 330 s = s / 2; 331 } 332 333 mCanvasViewer.getCanvas().setScale(s); 334 335 } 336 337 private void setupEditActions() { 338 339 IActionBars actionBars = getEditorSite().getActionBars(); 340 341 actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), new Action("Copy") { 342 @Override 343 public void run() { 344 // TODO enable copy only when there's a selection 345 mCanvasViewer.getCanvas().onCopy(mClipboard); 346 } 347 }); 348 349 actionBars.setGlobalActionHandler(ActionFactory.CUT.getId(), new Action("Cut") { 350 @Override 351 public void run() { 352 // TODO enable cut only when there's a selection 353 mCanvasViewer.getCanvas().onCut(mClipboard); 354 } 355 }); 356 357 actionBars.setGlobalActionHandler(ActionFactory.PASTE.getId(), new Action("Paste") { 358 @Override 359 public void run() { 360 mCanvasViewer.getCanvas().onPaste(mClipboard); 361 } 362 }); 363 364 actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), 365 new Action("Select All") { 366 @Override 367 public void run() { 368 mCanvasViewer.getCanvas().onSelectAll(); 369 } 370 }); 371 } 372 373 /** 374 * Switches the stack to display the error label and hide the canvas. 375 * @param errorFormat The new error to display if not null. 376 * @param parameters String.format parameters for the error format. 377 */ 378 private void displayError(String errorFormat, Object...parameters) { 379 if (errorFormat != null) { 380 mErrorLabel.setText(String.format(errorFormat, parameters)); 381 } 382 mSashError.setMaximizedControl(null); 383 } 384 385 /** Displays the canvas and hides the error label. */ 386 private void hideError() { 387 mSashError.setMaximizedControl(mCanvasViewer.getControl()); 388 } 389 390 @Override 391 public void dispose() { 392 393 getSite().getPage().removeSelectionListener(this); 394 getSite().setSelectionProvider(null); 395 396 if (mTargetListener != null) { 397 AdtPlugin.getDefault().removeTargetListener(mTargetListener); 398 mTargetListener = null; 399 } 400 401 if (mReloadListener != null) { 402 LayoutReloadMonitor.getMonitor().removeListener(mReloadListener); 403 mReloadListener = null; 404 } 405 406 if (mClipboard != null) { 407 mClipboard.dispose(); 408 mClipboard = null; 409 } 410 411 super.dispose(); 412 } 413 414 /** 415 * Listens to changes from the Configuration UI banner and triggers layout rendering when 416 * changed. Also provide the Configuration UI with the list of resources/layout to display. 417 */ 418 private class ConfigListener implements IConfigListener { 419 420 /** 421 * Looks for a file matching the new {@link FolderConfiguration} and attempts to open it. 422 * <p/>If there is no match, notify the user. 423 */ 424 public void onConfigurationChange() { 425 mConfiguredFrameworkRes = mConfiguredProjectRes = null; 426 427 if (mEditedFile == null || mConfigComposite.getEditedConfig() == null) { 428 return; 429 } 430 431 // Before doing the normal process, test for the following case. 432 // - the editor is being opened (or reset for a new input) 433 // - the file being opened is not the best match for any possible configuration 434 // - another random compatible config was chosen in the config composite. 435 // The result is that 'match' will not be the file being edited, but because this is not 436 // due to a config change, we should not trigger opening the actual best match (also, 437 // because the editor is still opening the MatchingStrategy woudln't answer true 438 // and the best match file would open in a different editor). 439 // So the solution is that if the editor is being created, we just call recomputeLayout 440 // without looking for a better matching layout file. 441 if (mLayoutEditor.isCreatingPages()) { 442 recomputeLayout(); 443 } else { 444 // get the resources of the file's project. 445 ProjectResources resources = ResourceManager.getInstance().getProjectResources( 446 mEditedFile.getProject()); 447 448 // from the resources, look for a matching file 449 ResourceFile match = null; 450 if (resources != null) { 451 match = resources.getMatchingFile(mEditedFile.getName(), 452 ResourceFolderType.LAYOUT, 453 mConfigComposite.getCurrentConfig()); 454 } 455 456 if (match != null) { 457 // since this is coming from Eclipse, this is always an instance of IFileWrapper 458 IFileWrapper iFileWrapper = (IFileWrapper) match.getFile(); 459 IFile iFile = iFileWrapper.getIFile(); 460 if (iFile.equals(mEditedFile) == false) { 461 try { 462 // tell the editor that the next replacement file is due to a config 463 // change. 464 mLayoutEditor.setNewFileOnConfigChange(true); 465 466 // ask the IDE to open the replacement file. 467 IDE.openEditor(getSite().getWorkbenchWindow().getActivePage(), iFile); 468 469 // we're done! 470 return; 471 } catch (PartInitException e) { 472 // FIXME: do something! 473 } 474 } 475 476 // at this point, we have not opened a new file. 477 478 // Store the state in the current file 479 mConfigComposite.storeState(); 480 481 // Even though the layout doesn't change, the config changed, and referenced 482 // resources need to be updated. 483 recomputeLayout(); 484 } else { 485 // display the error. 486 FolderConfiguration currentConfig = mConfigComposite.getCurrentConfig(); 487 displayError( 488 "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.", 489 currentConfig.toDisplayString(), 490 currentConfig.getFolderName(ResourceFolderType.LAYOUT), 491 mEditedFile.getName()); 492 } 493 } 494 } 495 496 public void onThemeChange() { 497 // Store the state in the current file 498 mConfigComposite.storeState(); 499 500 recomputeLayout(); 501 } 502 503 public void onClippingChange() { 504 recomputeLayout(); 505 } 506 507 public void onCreate() { 508 LayoutCreatorDialog dialog = new LayoutCreatorDialog(mConfigComposite.getShell(), 509 mEditedFile.getName(), mConfigComposite.getCurrentConfig()); 510 if (dialog.open() == Dialog.OK) { 511 final FolderConfiguration config = new FolderConfiguration(); 512 dialog.getConfiguration(config); 513 514 createAlternateLayout(config); 515 } 516 } 517 518 public Map<String, Map<String, IResourceValue>> getConfiguredFrameworkResources() { 519 if (mConfiguredFrameworkRes == null && mConfigComposite != null) { 520 ProjectResources frameworkRes = getFrameworkResources(); 521 522 if (frameworkRes == null) { 523 AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework"); 524 } else { 525 // get the framework resource values based on the current config 526 mConfiguredFrameworkRes = frameworkRes.getConfiguredResources( 527 mConfigComposite.getCurrentConfig()); 528 } 529 } 530 531 return mConfiguredFrameworkRes; 532 } 533 534 public Map<String, Map<String, IResourceValue>> getConfiguredProjectResources() { 535 if (mConfiguredProjectRes == null && mConfigComposite != null) { 536 ProjectResources project = getProjectResources(); 537 538 // make sure they are loaded 539 project.loadAll(); 540 541 // get the project resource values based on the current config 542 mConfiguredProjectRes = project.getConfiguredResources( 543 mConfigComposite.getCurrentConfig()); 544 } 545 546 return mConfiguredProjectRes; 547 } 548 549 /** 550 * Returns a {@link ProjectResources} for the framework resources. 551 * @return the framework resources or null if not found. 552 */ 553 public ProjectResources getFrameworkResources() { 554 if (mEditedFile != null) { 555 Sdk currentSdk = Sdk.getCurrent(); 556 if (currentSdk != null) { 557 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject()); 558 559 if (target != null) { 560 AndroidTargetData data = currentSdk.getTargetData(target); 561 562 if (data != null) { 563 return data.getFrameworkResources(); 564 } 565 } 566 } 567 } 568 569 return null; 570 } 571 572 public ProjectResources getProjectResources() { 573 if (mEditedFile != null) { 574 ResourceManager manager = ResourceManager.getInstance(); 575 return manager.getProjectResources(mEditedFile.getProject()); 576 } 577 578 return null; 579 } 580 581 /** 582 * Creates a new layout file from the specified {@link FolderConfiguration}. 583 */ 584 private void createAlternateLayout(final FolderConfiguration config) { 585 new Job("Create Alternate Resource") { 586 @Override 587 protected IStatus run(IProgressMonitor monitor) { 588 // get the folder name 589 String folderName = config.getFolderName(ResourceFolderType.LAYOUT); 590 try { 591 592 // look to see if it exists. 593 // get the res folder 594 IFolder res = (IFolder)mEditedFile.getParent().getParent(); 595 String path = res.getLocation().toOSString(); 596 597 File newLayoutFolder = new File(path + File.separator + folderName); 598 if (newLayoutFolder.isFile()) { 599 // this should not happen since aapt would have complained 600 // before, but if one disable the automatic build, this could 601 // happen. 602 String message = String.format("File 'res/%1$s' is in the way!", 603 folderName); 604 605 AdtPlugin.displayError("Layout Creation", message); 606 607 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message); 608 } else if (newLayoutFolder.exists() == false) { 609 // create it. 610 newLayoutFolder.mkdir(); 611 } 612 613 // now create the file 614 File newLayoutFile = new File(newLayoutFolder.getAbsolutePath() + 615 File.separator + mEditedFile.getName()); 616 617 newLayoutFile.createNewFile(); 618 619 InputStream input = mEditedFile.getContents(); 620 621 FileOutputStream fos = new FileOutputStream(newLayoutFile); 622 623 byte[] data = new byte[512]; 624 int count; 625 while ((count = input.read(data)) != -1) { 626 fos.write(data, 0, count); 627 } 628 629 input.close(); 630 fos.close(); 631 632 // refreshes the res folder to show up the new 633 // layout folder (if needed) and the file. 634 // We use a progress monitor to catch the end of the refresh 635 // to trigger the edit of the new file. 636 res.refreshLocal(IResource.DEPTH_INFINITE, new IProgressMonitor() { 637 public void done() { 638 mConfigComposite.getDisplay().asyncExec(new Runnable() { 639 public void run() { 640 onConfigurationChange(); 641 } 642 }); 643 } 644 645 public void beginTask(String name, int totalWork) { 646 // pass 647 } 648 649 public void internalWorked(double work) { 650 // pass 651 } 652 653 public boolean isCanceled() { 654 // pass 655 return false; 656 } 657 658 public void setCanceled(boolean value) { 659 // pass 660 } 661 662 public void setTaskName(String name) { 663 // pass 664 } 665 666 public void subTask(String name) { 667 // pass 668 } 669 670 public void worked(int work) { 671 // pass 672 } 673 }); 674 } catch (IOException e2) { 675 String message = String.format( 676 "Failed to create File 'res/%1$s/%2$s' : %3$s", 677 folderName, mEditedFile.getName(), e2.getMessage()); 678 679 AdtPlugin.displayError("Layout Creation", message); 680 681 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 682 message, e2); 683 } catch (CoreException e2) { 684 String message = String.format( 685 "Failed to create File 'res/%1$s/%2$s' : %3$s", 686 folderName, mEditedFile.getName(), e2.getMessage()); 687 688 AdtPlugin.displayError("Layout Creation", message); 689 690 return e2.getStatus(); 691 } 692 693 return Status.OK_STATUS; 694 695 } 696 }.schedule(); 697 } 698 } 699 700 /** 701 * Listens to target changed in the current project, to trigger a new layout rendering. 702 */ 703 private class TargetListener implements ITargetChangeListener { 704 705 public void onProjectTargetChange(IProject changedProject) { 706 if (changedProject != null && changedProject.equals(getProject())) { 707 updateEditor(); 708 } 709 } 710 711 public void onTargetLoaded(IAndroidTarget target) { 712 IProject project = getProject(); 713 if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) { 714 updateEditor(); 715 } 716 } 717 718 public void onSdkLoaded() { 719 Sdk currentSdk = Sdk.getCurrent(); 720 if (currentSdk != null) { 721 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject()); 722 if (target != null) { 723 mConfigComposite.onSdkLoaded(target); 724 mConfigListener.onConfigurationChange(); 725 } 726 } 727 } 728 729 private void updateEditor() { 730 mLayoutEditor.commitPages(false /* onSave */); 731 732 // because the target changed we must reset the configured resources. 733 mConfiguredFrameworkRes = mConfiguredProjectRes = null; 734 735 // make sure we remove the custom view loader, since its parent class loader is the 736 // bridge class loader. 737 mProjectCallback = null; 738 739 // recreate the ui root node always, this will also call onTargetChange 740 // on the config composite 741 mLayoutEditor.initUiRootNode(true /*force*/); 742 } 743 744 private IProject getProject() { 745 return getLayoutEditor().getProject(); 746 } 747 } 748 749 // ---------------- 750 751 /** 752 * Save operation in the Graphical Editor Part. 753 * <p/> 754 * In our workflow, the model is owned by the Structured XML Editor. 755 * The graphical layout editor just displays it -- thus we don't really 756 * save anything here. 757 * <p/> 758 * This must NOT call the parent editor part. At the contrary, the parent editor 759 * part will call this *after* having done the actual save operation. 760 * <p/> 761 * The only action this editor must do is mark the undo command stack as 762 * being no longer dirty. 763 */ 764 @Override 765 public void doSave(IProgressMonitor monitor) { 766 // TODO implement a command stack 767 // getCommandStack().markSaveLocation(); 768 // firePropertyChange(PROP_DIRTY); 769 } 770 771 /** 772 * Save operation in the Graphical Editor Part. 773 * <p/> 774 * In our workflow, the model is owned by the Structured XML Editor. 775 * The graphical layout editor just displays it -- thus we don't really 776 * save anything here. 777 */ 778 @Override 779 public void doSaveAs() { 780 // pass 781 } 782 783 /** 784 * In our workflow, the model is owned by the Structured XML Editor. 785 * The graphical layout editor just displays it -- thus we don't really 786 * save anything here. 787 */ 788 @Override 789 public boolean isDirty() { 790 return false; 791 } 792 793 /** 794 * In our workflow, the model is owned by the Structured XML Editor. 795 * The graphical layout editor just displays it -- thus we don't really 796 * save anything here. 797 */ 798 @Override 799 public boolean isSaveAsAllowed() { 800 return false; 801 } 802 803 @Override 804 public void setFocus() { 805 // TODO Auto-generated method stub 806 807 } 808 809 /** 810 * Responds to a page change that made the Graphical editor page the activated page. 811 */ 812 public void activated() { 813 if (mNeedsRecompute || mNeedsXmlReload) { 814 recomputeLayout(); 815 } 816 } 817 818 /** 819 * Responds to a page change that made the Graphical editor page the deactivated page 820 */ 821 public void deactivated() { 822 // nothing to be done here for now. 823 } 824 825 /** 826 * Opens and initialize the editor with a new file. 827 * @param file the file being edited. 828 */ 829 public void openFile(IFile file) { 830 mEditedFile = file; 831 mConfigComposite.setFile(mEditedFile); 832 833 if (mReloadListener == null) { 834 mReloadListener = new ReloadListener(); 835 LayoutReloadMonitor.getMonitor().addListener(mEditedFile.getProject(), mReloadListener); 836 } 837 838 if (mRulesEngine == null) { 839 mRulesEngine = new RulesEngine(mEditedFile.getProject()); 840 if (mCanvasViewer != null) { 841 mCanvasViewer.getCanvas().setRulesEngine(mRulesEngine); 842 } 843 } 844 } 845 846 /** 847 * Resets the editor with a replacement file. 848 * @param file the replacement file. 849 */ 850 public void replaceFile(IFile file) { 851 mEditedFile = file; 852 mConfigComposite.replaceFile(mEditedFile); 853 } 854 855 /** 856 * Resets the editor with a replacement file coming from a config change in the config 857 * selector. 858 * @param file the replacement file. 859 */ 860 public void changeFileOnNewConfig(IFile file) { 861 mEditedFile = file; 862 mConfigComposite.changeFileOnNewConfig(mEditedFile); 863 } 864 865 public void onTargetChange() { 866 mConfigComposite.onXmlModelLoaded(); 867 mConfigListener.onConfigurationChange(); 868 } 869 870 public void onSdkChange() { 871 Sdk currentSdk = Sdk.getCurrent(); 872 if (currentSdk != null) { 873 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject()); 874 if (target != null) { 875 mConfigComposite.onSdkLoaded(target); 876 mConfigListener.onConfigurationChange(); 877 } 878 } 879 } 880 881 public Clipboard getClipboard() { 882 return mClipboard; 883 } 884 885 public LayoutEditor getLayoutEditor() { 886 return mLayoutEditor; 887 } 888 889 public UiDocumentNode getModel() { 890 return mLayoutEditor.getUiRootNode(); 891 } 892 893 public SelectionSynchronizer getSelectionSynchronizer() { 894 // TODO Auto-generated method stub 895 return null; 896 } 897 898 /** 899 * Callback for XML model changed. Only update/recompute the layout if the editor is visible 900 */ 901 public void onXmlModelChanged() { 902 if (mLayoutEditor.isGraphicalEditorActive()) { 903 doXmlReload(true /* force */); 904 recomputeLayout(); 905 } else { 906 mNeedsXmlReload = true; 907 } 908 } 909 910 /** 911 * Actually performs the XML reload 912 * @see #onXmlModelChanged() 913 */ 914 private void doXmlReload(boolean force) { 915 if (force || mNeedsXmlReload) { 916 917 // TODO : update the mLayoutCanvas, preserving the current selection if possible. 918 919 // GraphicalViewer viewer = getGraphicalViewer(); 920 // 921 // // try to preserve the selection before changing the content 922 // SelectionManager selMan = viewer.getSelectionManager(); 923 // ISelection selection = selMan.getSelection(); 924 // 925 // try { 926 // viewer.setContents(getModel()); 927 // } finally { 928 // selMan.setSelection(selection); 929 // } 930 931 mNeedsXmlReload = false; 932 } 933 } 934 935 public void recomputeLayout() { 936 doXmlReload(false /* force */); 937 try { 938 // check that the resource exists. If the file is opened but the project is closed 939 // or deleted for some reason (changed from outside of eclipse), then this will 940 // return false; 941 if (mEditedFile.exists() == false) { 942 displayError("Resource '%1$s' does not exist.", 943 mEditedFile.getFullPath().toString()); 944 return; 945 } 946 947 IProject iProject = mEditedFile.getProject(); 948 949 if (mEditedFile.isSynchronized(IResource.DEPTH_ZERO) == false) { 950 String message = String.format("%1$s is out of sync. Please refresh.", 951 mEditedFile.getName()); 952 953 displayError(message); 954 955 // also print it in the error console. 956 AdtPlugin.printErrorToConsole(iProject.getName(), message); 957 return; 958 } 959 960 Sdk currentSdk = Sdk.getCurrent(); 961 if (currentSdk != null) { 962 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject()); 963 if (target == null) { 964 displayError("The project target is not set."); 965 return; 966 } 967 968 AndroidTargetData data = currentSdk.getTargetData(target); 969 if (data == null) { 970 // It can happen that the workspace refreshes while the SDK is loading its 971 // data, which could trigger a redraw of the opened layout if some resources 972 // changed while Eclipse is closed. 973 // In this case data could be null, but this is not an error. 974 // We can just silently return, as all the opened editors are automatically 975 // refreshed once the SDK finishes loading. 976 LoadStatus targetLoadStatus = currentSdk.checkAndLoadTargetData(target, null); 977 switch (targetLoadStatus) { 978 case LOADING: 979 displayError("The project target (%1$s) is still loading.\n%2$s will refresh automatically once the process is finished.", 980 target.getName(), mEditedFile.getName()); 981 982 break; 983 case FAILED: // known failure 984 case LOADED: // success but data isn't loaded?!?! 985 displayError("The project target (%s) was not properly loaded.", 986 target.getName()); 987 break; 988 } 989 990 return; 991 } 992 993 // check there is actually a model (maybe the file is empty). 994 UiDocumentNode model = getModel(); 995 996 if (model.getUiChildren().size() == 0) { 997 displayError("No Xml content. Go to the Outline view and add nodes."); 998 return; 999 } 1000 1001 LayoutBridge bridge = data.getLayoutBridge(); 1002 1003 if (bridge.bridge != null) { // bridge can never be null. 1004 ResourceManager resManager = ResourceManager.getInstance(); 1005 1006 ProjectResources projectRes = resManager.getProjectResources(iProject); 1007 if (projectRes == null) { 1008 displayError("Missing project resources."); 1009 return; 1010 } 1011 1012 // get the resources of the file's project. 1013 Map<String, Map<String, IResourceValue>> configuredProjectRes = 1014 mConfigListener.getConfiguredProjectResources(); 1015 1016 // get the framework resources 1017 Map<String, Map<String, IResourceValue>> frameworkResources = 1018 mConfigListener.getConfiguredFrameworkResources(); 1019 1020 if (configuredProjectRes != null && frameworkResources != null) { 1021 if (mProjectCallback == null) { 1022 mProjectCallback = new ProjectCallback( 1023 bridge.classLoader, projectRes, iProject); 1024 } 1025 1026 if (mLogger == null) { 1027 mLogger = new ILayoutLog() { 1028 public void error(String message) { 1029 AdtPlugin.printErrorToConsole(mEditedFile.getName(), message); 1030 } 1031 1032 public void error(Throwable error) { 1033 String message = error.getMessage(); 1034 if (message == null) { 1035 message = error.getClass().getName(); 1036 } 1037 1038 PrintStream ps = new PrintStream(AdtPlugin.getErrorStream()); 1039 error.printStackTrace(ps); 1040 } 1041 1042 public void warning(String message) { 1043 AdtPlugin.printToConsole(mEditedFile.getName(), message); 1044 } 1045 }; 1046 } 1047 1048 // get the selected theme 1049 String theme = mConfigComposite.getTheme(); 1050 if (theme != null) { 1051 // Compute the layout 1052 Rectangle rect = getBounds(); 1053 1054 int width = rect.width; 1055 int height = rect.height; 1056 if (mUseExplodeMode) { 1057 // compute how many padding in x and y will bump the screen size 1058 List<UiElementNode> children = getModel().getUiChildren(); 1059 if (children.size() == 1) { 1060 ExplodedRenderingHelper helper = new ExplodedRenderingHelper( 1061 children.get(0).getXmlNode(), iProject); 1062 1063 // there are 2 paddings for each view 1064 // left and right, or top and bottom. 1065 int paddingValue = ExplodedRenderingHelper.PADDING_VALUE * 2; 1066 1067 width += helper.getWidthPadding() * paddingValue; 1068 height += helper.getHeightPadding() * paddingValue; 1069 } 1070 } 1071 1072 int density = mConfigComposite.getDensity().getDpiValue(); 1073 float xdpi = mConfigComposite.getXDpi(); 1074 float ydpi = mConfigComposite.getYDpi(); 1075 boolean isProjectTheme = mConfigComposite.isProjectTheme(); 1076 1077 UiElementPullParser parser = new UiElementPullParser(getModel(), 1078 mUseExplodeMode, density, xdpi, iProject); 1079 1080 ILayoutResult result = computeLayout(bridge, parser, 1081 iProject /* projectKey */, 1082 width, height, !mConfigComposite.getClipping(), 1083 density, xdpi, ydpi, 1084 theme, isProjectTheme, 1085 configuredProjectRes, frameworkResources, mProjectCallback, 1086 mLogger); 1087 1088 // post rendering clean up 1089 bridge.cleanUp(); 1090 1091 mCanvasViewer.getCanvas().setResult(result); 1092 1093 // update the UiElementNode with the layout info. 1094 if (result.getSuccess() == ILayoutResult.SUCCESS) { 1095 hideError(); 1096 } else { 1097 displayError(result.getErrorMessage()); 1098 } 1099 1100 model.refreshUi(); 1101 } 1102 } 1103 } else { 1104 // SDK is loaded but not the layout library! 1105 1106 // check whether the bridge managed to load, or not 1107 if (bridge.status == LoadStatus.LOADING) { 1108 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.", 1109 mEditedFile.getName()); 1110 } else { 1111 displayError("Eclipse failed to load the framework information and the layout library!"); 1112 } 1113 } 1114 } else { 1115 displayError("Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.", 1116 mEditedFile.getName()); 1117 } 1118 } finally { 1119 // no matter the result, we are done doing the recompute based on the latest 1120 // resource/code change. 1121 mNeedsRecompute = false; 1122 } 1123 } 1124 1125 /** 1126 * Computes a layout by calling the correct computeLayout method of ILayoutBridge based on 1127 * the implementation API level. 1128 * 1129 * Implementation detail: the bridge's computeLayout() method already returns a newly 1130 * allocated ILayoutResult. 1131 */ 1132 @SuppressWarnings("deprecation") 1133 private static ILayoutResult computeLayout(LayoutBridge bridge, 1134 IXmlPullParser layoutDescription, Object projectKey, 1135 int screenWidth, int screenHeight, boolean renderFullSize, 1136 int density, float xdpi, float ydpi, 1137 String themeName, boolean isProjectTheme, 1138 Map<String, Map<String, IResourceValue>> projectResources, 1139 Map<String, Map<String, IResourceValue>> frameworkResources, 1140 IProjectCallback projectCallback, ILayoutLog logger) { 1141 1142 if (bridge.apiLevel >= ILayoutBridge.API_CURRENT) { 1143 // newest API with support for "render full height" 1144 // TODO: link boolean to UI. 1145 return bridge.bridge.computeLayout(layoutDescription, 1146 projectKey, screenWidth, screenHeight, renderFullSize, 1147 density, xdpi, ydpi, 1148 themeName, isProjectTheme, 1149 projectResources, frameworkResources, projectCallback, 1150 logger); 1151 } else if (bridge.apiLevel == 3) { 1152 // newer api with density support. 1153 return bridge.bridge.computeLayout(layoutDescription, 1154 projectKey, screenWidth, screenHeight, density, xdpi, ydpi, 1155 themeName, isProjectTheme, 1156 projectResources, frameworkResources, projectCallback, 1157 logger); 1158 } else if (bridge.apiLevel == 2) { 1159 // api with boolean for separation of project/framework theme 1160 return bridge.bridge.computeLayout(layoutDescription, 1161 projectKey, screenWidth, screenHeight, themeName, isProjectTheme, 1162 projectResources, frameworkResources, projectCallback, 1163 logger); 1164 } else { 1165 // oldest api with no density/dpi, and project theme boolean mixed 1166 // into the theme name. 1167 1168 // change the string if it's a custom theme to make sure we can 1169 // differentiate them 1170 if (isProjectTheme) { 1171 themeName = "*" + themeName; //$NON-NLS-1$ 1172 } 1173 1174 return bridge.bridge.computeLayout(layoutDescription, 1175 projectKey, screenWidth, screenHeight, themeName, 1176 projectResources, frameworkResources, projectCallback, 1177 logger); 1178 } 1179 } 1180 1181 public Rectangle getBounds() { 1182 return mConfigComposite.getScreenBounds(); 1183 } 1184 1185 public void reloadPalette() { 1186 if (mPalette != null) { 1187 mPalette.reloadPalette(mLayoutEditor.getTargetData()); 1188 } 1189 } 1190 1191 /** 1192 * Used by LayoutEditor.UiEditorActions.selectUiNode to select a new UI Node 1193 * created by {@link ElementCreateCommand#execute()}. 1194 * 1195 * @param uiNodeModel The {@link UiElementNode} to select. 1196 */ 1197 public void selectModel(UiElementNode uiNodeModel) { 1198 1199 // TODO this method was useful for GLE1. We may not need it anymore now. 1200 1201 // GraphicalViewer viewer = getGraphicalViewer(); 1202 // 1203 // // Give focus to the graphical viewer (in case the outline has it) 1204 // viewer.getControl().forceFocus(); 1205 // 1206 // Object editPart = viewer.getEditPartRegistry().get(uiNodeModel); 1207 // 1208 // if (editPart instanceof EditPart) { 1209 // viewer.select((EditPart)editPart); 1210 // } 1211 } 1212 1213 private class ReloadListener implements ILayoutReloadListener { 1214 /* 1215 * Called when the file changes triggered a redraw of the layout 1216 */ 1217 public void reloadLayout(ChangeFlags flags, boolean libraryChanged) { 1218 boolean recompute = false; 1219 1220 if (flags.rClass) { 1221 recompute = true; 1222 if (mEditedFile != null) { 1223 ProjectResources projectRes = ResourceManager.getInstance().getProjectResources( 1224 mEditedFile.getProject()); 1225 1226 if (projectRes != null) { 1227 projectRes.resetDynamicIds(); 1228 } 1229 } 1230 } 1231 1232 if (flags.localeList) { 1233 // the locale list *potentially* changed so we update the locale in the 1234 // config composite. 1235 // However there's no recompute, as it could not be needed 1236 // (for instance a new layout) 1237 // If a resource that's not a layout changed this will trigger a recompute anyway. 1238 mCanvasViewer.getControl().getDisplay().asyncExec(new Runnable() { 1239 public void run() { 1240 mConfigComposite.updateLocales(); 1241 } 1242 }); 1243 } 1244 1245 // if a resources was modified. 1246 // also, if a layout in a library was modified. 1247 if (flags.resources || (libraryChanged && flags.layout)) { 1248 recompute = true; 1249 1250 // TODO: differentiate between single and multi resource file changed, and whether the resource change affects the cache. 1251 1252 // force a reparse in case a value XML file changed. 1253 mConfiguredProjectRes = null; 1254 1255 // clear the cache in the bridge in case a bitmap/9-patch changed. 1256 IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject()); 1257 if (target != null) { 1258 1259 AndroidTargetData data = Sdk.getCurrent().getTargetData(target); 1260 if (data != null) { 1261 LayoutBridge bridge = data.getLayoutBridge(); 1262 1263 if (bridge.bridge != null) { 1264 bridge.bridge.clearCaches(mEditedFile.getProject()); 1265 } 1266 } 1267 } 1268 } 1269 1270 if (flags.code) { 1271 // only recompute if the custom view loader was used to load some code. 1272 if (mProjectCallback != null && mProjectCallback.isUsed()) { 1273 mProjectCallback = null; 1274 recompute = true; 1275 } 1276 } 1277 1278 if (recompute) { 1279 mCanvasViewer.getControl().getDisplay().asyncExec(new Runnable() { 1280 public void run() { 1281 if (mLayoutEditor.isGraphicalEditorActive()) { 1282 recomputeLayout(); 1283 } else { 1284 mNeedsRecompute = true; 1285 } 1286 } 1287 }); 1288 } 1289 } 1290 } 1291 } 1292