Home | History | Annotate | Download | only in gle2
      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