• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
18 
19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
20 import static com.android.ide.common.layout.LayoutConstants.ATTR_CLASS;
21 import static com.android.ide.common.layout.LayoutConstants.ATTR_COLUMN_COUNT;
22 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN;
23 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN_SPAN;
24 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW;
25 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW_SPAN;
26 import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION;
27 import static com.android.ide.common.layout.LayoutConstants.ATTR_ROW_COUNT;
28 import static com.android.ide.common.layout.LayoutConstants.ATTR_SRC;
29 import static com.android.ide.common.layout.LayoutConstants.ATTR_TEXT;
30 import static com.android.ide.common.layout.LayoutConstants.DRAWABLE_PREFIX;
31 import static com.android.ide.common.layout.LayoutConstants.GRID_LAYOUT;
32 import static com.android.ide.common.layout.LayoutConstants.LAYOUT_PREFIX;
33 import static com.android.ide.common.layout.LayoutConstants.LINEAR_LAYOUT;
34 import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL;
35 import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_VIEWTAG;
36 import static com.android.tools.lint.detector.api.LintConstants.AUTO_URI;
37 import static com.android.tools.lint.detector.api.LintConstants.URI_PREFIX;
38 import static org.eclipse.jface.viewers.StyledString.QUALIFIER_STYLER;
39 
40 import com.android.annotations.VisibleForTesting;
41 import com.android.ide.common.api.INode;
42 import com.android.ide.common.api.InsertType;
43 import com.android.ide.common.layout.BaseLayoutRule;
44 import com.android.ide.common.layout.GridLayoutRule;
45 import com.android.ide.eclipse.adt.AdtPlugin;
46 import com.android.ide.eclipse.adt.internal.editors.IconFactory;
47 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
48 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
49 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
50 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
51 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
52 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
53 import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertySheetPage;
54 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
55 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
56 import com.android.ide.eclipse.adt.internal.editors.ui.ErrorImageComposite;
57 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
58 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
59 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
60 import com.android.util.Pair;
61 
62 import org.eclipse.core.resources.IProject;
63 import org.eclipse.jface.action.Action;
64 import org.eclipse.jface.action.ActionContributionItem;
65 import org.eclipse.jface.action.IAction;
66 import org.eclipse.jface.action.IContributionItem;
67 import org.eclipse.jface.action.IMenuListener;
68 import org.eclipse.jface.action.IMenuManager;
69 import org.eclipse.jface.action.IToolBarManager;
70 import org.eclipse.jface.action.MenuManager;
71 import org.eclipse.jface.action.Separator;
72 import org.eclipse.jface.preference.JFacePreferences;
73 import org.eclipse.jface.viewers.DoubleClickEvent;
74 import org.eclipse.jface.viewers.IDoubleClickListener;
75 import org.eclipse.jface.viewers.IElementComparer;
76 import org.eclipse.jface.viewers.ISelection;
77 import org.eclipse.jface.viewers.ITreeContentProvider;
78 import org.eclipse.jface.viewers.ITreeSelection;
79 import org.eclipse.jface.viewers.StyledCellLabelProvider;
80 import org.eclipse.jface.viewers.StyledString;
81 import org.eclipse.jface.viewers.StyledString.Styler;
82 import org.eclipse.jface.viewers.TreePath;
83 import org.eclipse.jface.viewers.TreeSelection;
84 import org.eclipse.jface.viewers.TreeViewer;
85 import org.eclipse.jface.viewers.Viewer;
86 import org.eclipse.jface.viewers.ViewerCell;
87 import org.eclipse.swt.SWT;
88 import org.eclipse.swt.dnd.DND;
89 import org.eclipse.swt.dnd.Transfer;
90 import org.eclipse.swt.events.DisposeEvent;
91 import org.eclipse.swt.events.DisposeListener;
92 import org.eclipse.swt.events.KeyEvent;
93 import org.eclipse.swt.events.KeyListener;
94 import org.eclipse.swt.events.MenuDetectEvent;
95 import org.eclipse.swt.events.MenuDetectListener;
96 import org.eclipse.swt.graphics.Image;
97 import org.eclipse.swt.widgets.Composite;
98 import org.eclipse.swt.widgets.Control;
99 import org.eclipse.swt.widgets.Tree;
100 import org.eclipse.swt.widgets.TreeItem;
101 import org.eclipse.ui.IActionBars;
102 import org.eclipse.ui.IEditorPart;
103 import org.eclipse.ui.INullSelectionListener;
104 import org.eclipse.ui.IWorkbenchPart;
105 import org.eclipse.ui.actions.ActionFactory;
106 import org.eclipse.ui.views.contentoutline.ContentOutlinePage;
107 import org.eclipse.wb.core.controls.SelfOrientingSashForm;
108 import org.eclipse.wb.internal.core.editor.structure.IPage;
109 import org.eclipse.wb.internal.core.editor.structure.PageSiteComposite;
110 import org.w3c.dom.Element;
111 import org.w3c.dom.Node;
112 
113 import java.util.ArrayList;
114 import java.util.HashSet;
115 import java.util.List;
116 import java.util.Set;
117 
118 /**
119  * An outline page for the layout canvas view.
120  * <p/>
121  * The page is created by {@link LayoutEditorDelegate#delegateGetAdapter(Class)}. This means
122  * we have *one* instance of the outline page per open canvas editor.
123  * <p/>
124  * It sets itself as a listener on the site's selection service in order to be
125  * notified of the canvas' selection changes.
126  * The underlying page is also a selection provider (via IContentOutlinePage)
127  * and as such it will broadcast selection changes to the site's selection service
128  * (on which both the layout editor part and the property sheet page listen.)
129  */
130 public class OutlinePage extends ContentOutlinePage
131     implements INullSelectionListener, IPage {
132 
133     /** Label which separates outline text from additional attributes like text prefix or url */
134     private static final String LABEL_SEPARATOR = " - ";
135 
136     /** Max character count in labels, used for truncation */
137     private static final int LABEL_MAX_WIDTH = 50;
138 
139     /**
140      * The graphical editor that created this outline.
141      */
142     private final GraphicalEditorPart mGraphicalEditorPart;
143 
144     /**
145      * RootWrapper is a workaround: we can't set the input of the TreeView to its root
146      * element, so we introduce a fake parent.
147      */
148     private final RootWrapper mRootWrapper = new RootWrapper();
149 
150     /**
151      * Menu manager for the context menu actions.
152      * The actions delegate to the current GraphicalEditorPart.
153      */
154     private MenuManager mMenuManager;
155 
156     private Composite mControl;
157     private PropertySheetPage mPropertySheet;
158     private PageSiteComposite mPropertySheetComposite;
159     private boolean mShowPropertySheet;
160     private boolean mShowHeader;
161     private boolean mIgnoreSelection;
162     private boolean mActive = true;
163 
164     /** Action to Select All in the tree */
165     private final Action mTreeSelectAllAction = new Action() {
166         @Override
167         public void run() {
168             getTreeViewer().getTree().selectAll();
169             OutlinePage.this.fireSelectionChanged(getSelection());
170         }
171 
172         @Override
173         public String getId() {
174             return ActionFactory.SELECT_ALL.getId();
175         }
176     };
177 
178     /** Action for moving items up in the tree */
179     private Action mMoveUpAction = new Action("Move Up\t-",
180             IconFactory.getInstance().getImageDescriptor("up")) { //$NON-NLS-1$
181 
182         @Override
183         public String getId() {
184             return "adt.outline.moveup"; //$NON-NLS-1$
185         }
186 
187         @Override
188         public boolean isEnabled() {
189             return canMove(false);
190         }
191 
192         @Override
193         public void run() {
194             move(false);
195         }
196     };
197 
198     /** Action for moving items down in the tree */
199     private Action mMoveDownAction = new Action("Move Down\t+",
200             IconFactory.getInstance().getImageDescriptor("down")) { //$NON-NLS-1$
201 
202         @Override
203         public String getId() {
204             return "adt.outline.movedown"; //$NON-NLS-1$
205         }
206 
207         @Override
208         public boolean isEnabled() {
209             return canMove(true);
210         }
211 
212         @Override
213         public void run() {
214             move(true);
215         }
216     };
217 
218     /**
219      * Creates a new {@link OutlinePage} associated with the given editor
220      *
221      * @param graphicalEditorPart the editor associated with this outline
222      */
OutlinePage(GraphicalEditorPart graphicalEditorPart)223     public OutlinePage(GraphicalEditorPart graphicalEditorPart) {
224         super();
225         mGraphicalEditorPart = graphicalEditorPart;
226     }
227 
228     @Override
getControl()229     public Control getControl() {
230         // We've injected some controls between the root of the outline page
231         // and the tree control, so return the actual root (a sash form) rather
232         // than the superclass' implementation which returns the tree. If we don't
233         // do this, various checks in the outline page which checks that getControl().getParent()
234         // is the outline window itself will ignore this page.
235         return mControl;
236     }
237 
setActive(boolean active)238     void setActive(boolean active) {
239         if (active != mActive) {
240             mActive = active;
241 
242             // Outlines are by default active when they are created; this is intended
243             // for deactivating a hidden outline and later reactivating it
244             assert mControl != null;
245             if (active) {
246                 getSite().getPage().addSelectionListener(this);
247                 setModel(mGraphicalEditorPart.getCanvasControl().getViewHierarchy().getRoot());
248             } else {
249                 getSite().getPage().removeSelectionListener(this);
250                 mRootWrapper.setRoot(null);
251                 if (mPropertySheet != null) {
252                     mPropertySheet.selectionChanged(null, TreeSelection.EMPTY);
253                 }
254             }
255         }
256     }
257 
258     /**
259      * Set whether the outline should be shown in the header
260      *
261      * @param show whether a header should be shown
262      */
setShowHeader(boolean show)263     public void setShowHeader(boolean show) {
264         mShowHeader = show;
265     }
266 
267     /**
268      * Set whether the property sheet should be shown within this outline
269      *
270      * @param show whether the property sheet should show
271      */
setShowPropertySheet(boolean show)272     public void setShowPropertySheet(boolean show) {
273         if (show != mShowPropertySheet) {
274             mShowPropertySheet = show;
275             if (mControl == null) {
276                 return;
277             }
278 
279             if (show && mPropertySheet == null) {
280                 createPropertySheet();
281             } else if (!show) {
282                 mPropertySheetComposite.dispose();
283                 mPropertySheetComposite = null;
284                 mPropertySheet.dispose();
285                 mPropertySheet = null;
286             }
287 
288             mControl.layout();
289         }
290     }
291 
292     @Override
createControl(Composite parent)293     public void createControl(Composite parent) {
294         mControl = new SelfOrientingSashForm(parent, SWT.VERTICAL);
295 
296         if (mShowHeader) {
297             PageSiteComposite mOutlineComposite = new PageSiteComposite(mControl, SWT.BORDER);
298             mOutlineComposite.setTitleText("Outline");
299             mOutlineComposite.setTitleImage(IconFactory.getInstance().getIcon("components_view"));
300             mOutlineComposite.setPage(new IPage() {
301                 @Override
302                 public void createControl(Composite outlineParent) {
303                     createOutline(outlineParent);
304                 }
305 
306                 @Override
307                 public void dispose() {
308                 }
309 
310                 @Override
311                 public Control getControl() {
312                     return getTreeViewer().getTree();
313                 }
314 
315                 @Override
316                 public void setToolBar(IToolBarManager toolBarManager) {
317                     makeContributions(null, toolBarManager, null);
318                     toolBarManager.update(false);
319                 }
320 
321                 @Override
322                 public void setFocus() {
323                     getControl().setFocus();
324                 }
325             });
326         } else {
327             createOutline(mControl);
328         }
329 
330         if (mShowPropertySheet) {
331             createPropertySheet();
332         }
333     }
334 
createOutline(Composite parent)335     private void createOutline(Composite parent) {
336         super.createControl(parent);
337 
338         TreeViewer tv = getTreeViewer();
339         tv.setAutoExpandLevel(2);
340         tv.setContentProvider(new ContentProvider());
341         tv.setLabelProvider(new LabelProvider());
342         tv.setInput(mRootWrapper);
343         tv.expandToLevel(mRootWrapper.getRoot(), 2);
344 
345         int supportedOperations = DND.DROP_COPY | DND.DROP_MOVE;
346         Transfer[] transfers = new Transfer[] {
347             SimpleXmlTransfer.getInstance()
348         };
349 
350         tv.addDropSupport(supportedOperations, transfers, new OutlineDropListener(this, tv));
351         tv.addDragSupport(supportedOperations, transfers, new OutlineDragListener(this, tv));
352 
353         // The tree viewer will hold CanvasViewInfo instances, however these
354         // change each time the canvas is reloaded. OTOH layoutlib gives us
355         // constant UiView keys which we can use to perform tree item comparisons.
356         tv.setComparer(new IElementComparer() {
357             @Override
358             public int hashCode(Object element) {
359                 if (element instanceof CanvasViewInfo) {
360                     UiViewElementNode key = ((CanvasViewInfo) element).getUiViewNode();
361                     if (key != null) {
362                         return key.hashCode();
363                     }
364                 }
365                 if (element != null) {
366                     return element.hashCode();
367                 }
368                 return 0;
369             }
370 
371             @Override
372             public boolean equals(Object a, Object b) {
373                 if (a instanceof CanvasViewInfo && b instanceof CanvasViewInfo) {
374                     UiViewElementNode keyA = ((CanvasViewInfo) a).getUiViewNode();
375                     UiViewElementNode keyB = ((CanvasViewInfo) b).getUiViewNode();
376                     if (keyA != null) {
377                         return keyA.equals(keyB);
378                     }
379                 }
380                 if (a != null) {
381                     return a.equals(b);
382                 }
383                 return false;
384             }
385         });
386         tv.addDoubleClickListener(new IDoubleClickListener() {
387             @Override
388             public void doubleClick(DoubleClickEvent event) {
389                 // This used to open the property view, but now that properties are docked
390                 // let's use it for something else -- such as showing the editor source
391                 /*
392                 // Front properties panel; its selection is already linked
393                 IWorkbenchPage page = getSite().getPage();
394                 try {
395                     page.showView(IPageLayout.ID_PROP_SHEET, null, IWorkbenchPage.VIEW_ACTIVATE);
396                 } catch (PartInitException e) {
397                     AdtPlugin.log(e, "Could not activate property sheet");
398                 }
399                 */
400 
401                 TreeItem[] selection = getTreeViewer().getTree().getSelection();
402                 if (selection.length > 0) {
403                     CanvasViewInfo vi = getViewInfo(selection[0].getData());
404                     if (vi != null) {
405                         LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
406                         canvas.show(vi);
407                     }
408                 }
409             }
410         });
411 
412         setupContextMenu();
413 
414         // Listen to selection changes from the layout editor
415         getSite().getPage().addSelectionListener(this);
416         getControl().addDisposeListener(new DisposeListener() {
417 
418             @Override
419             public void widgetDisposed(DisposeEvent e) {
420                 dispose();
421             }
422         });
423 
424         Tree tree = tv.getTree();
425         tree.addKeyListener(new KeyListener() {
426 
427             @Override
428             public void keyPressed(KeyEvent e) {
429                 if (e.character == '-') {
430                     if (mMoveUpAction.isEnabled()) {
431                         mMoveUpAction.run();
432                     }
433                 } else if (e.character == '+') {
434                     if (mMoveDownAction.isEnabled()) {
435                         mMoveDownAction.run();
436                     }
437                 }
438             }
439 
440             @Override
441             public void keyReleased(KeyEvent e) {
442             }
443         });
444     }
445 
createPropertySheet()446     private void createPropertySheet() {
447         mPropertySheetComposite = new PageSiteComposite(mControl, SWT.BORDER);
448         mPropertySheetComposite.setTitleText("Properties");
449         mPropertySheetComposite.setTitleImage(IconFactory.getInstance().getIcon("properties_view"));
450         mPropertySheet = new PropertySheetPage(mGraphicalEditorPart);
451         mPropertySheetComposite.setPage(mPropertySheet);
452     }
453 
454     @Override
dispose()455     public void dispose() {
456         mRootWrapper.setRoot(null);
457 
458         getSite().getPage().removeSelectionListener(this);
459         super.dispose();
460         if (mPropertySheet != null) {
461             mPropertySheet.dispose();
462             mPropertySheet = null;
463         }
464     }
465 
466     /**
467      * Invoked by {@link LayoutCanvas} to set the model (a.k.a. the root view info).
468      *
469      * @param rootViewInfo The root of the view info hierarchy. Can be null.
470      */
setModel(CanvasViewInfo rootViewInfo)471     public void setModel(CanvasViewInfo rootViewInfo) {
472         if (!mActive) {
473             return;
474         }
475 
476         mRootWrapper.setRoot(rootViewInfo);
477 
478         TreeViewer tv = getTreeViewer();
479         if (tv != null) {
480             Object[] expanded = tv.getExpandedElements();
481             tv.refresh();
482             tv.setExpandedElements(expanded);
483             // Ensure that the root is expanded
484             tv.expandToLevel(rootViewInfo, 2);
485         }
486     }
487 
488     /**
489      * Returns the current tree viewer selection. Shouldn't be null,
490      * although it can be {@link TreeSelection#EMPTY}.
491      */
492     @Override
getSelection()493     public ISelection getSelection() {
494         return super.getSelection();
495     }
496 
497     /**
498      * Sets the outline selection.
499      *
500      * @param selection Only {@link ITreeSelection} will be used, otherwise the
501      *   selection will be cleared (including a null selection).
502      */
503     @Override
setSelection(ISelection selection)504     public void setSelection(ISelection selection) {
505         // TreeViewer should be able to deal with a null selection, but let's make it safe
506         if (selection == null) {
507             selection = TreeSelection.EMPTY;
508         }
509         if (selection.equals(TreeSelection.EMPTY)) {
510             return;
511         }
512 
513         super.setSelection(selection);
514 
515         TreeViewer tv = getTreeViewer();
516         if (tv == null || !(selection instanceof ITreeSelection) || selection.isEmpty()) {
517             return;
518         }
519 
520         // auto-reveal the selection
521         ITreeSelection treeSel = (ITreeSelection) selection;
522         for (TreePath p : treeSel.getPaths()) {
523             tv.expandToLevel(p, 1);
524         }
525     }
526 
527     @Override
fireSelectionChanged(ISelection selection)528     protected void fireSelectionChanged(ISelection selection) {
529         super.fireSelectionChanged(selection);
530         if (mPropertySheet != null && !mIgnoreSelection) {
531             mPropertySheet.selectionChanged(null, selection);
532         }
533     }
534 
535     /**
536      * Listens to a workbench selection.
537      * Only listen on selection coming from {@link LayoutEditorDelegate}, which avoid
538      * picking up our own selections.
539      */
540     @Override
selectionChanged(IWorkbenchPart part, ISelection selection)541     public void selectionChanged(IWorkbenchPart part, ISelection selection) {
542         if (mIgnoreSelection) {
543             return;
544         }
545 
546         if (part instanceof IEditorPart) {
547             LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor((IEditorPart) part);
548             if (delegate != null) {
549                 try {
550                     mIgnoreSelection = true;
551                     setSelection(selection);
552 
553                     if (mPropertySheet != null) {
554                         mPropertySheet.selectionChanged(part, selection);
555                     }
556                 } finally {
557                     mIgnoreSelection = false;
558                 }
559             }
560         }
561     }
562 
563     // ----
564 
565     /**
566      * In theory, the root of the model should be the input of the {@link TreeViewer},
567      * which would be the root {@link CanvasViewInfo}.
568      * That means in theory {@link ContentProvider#getElements(Object)} should return
569      * its own input as the single root node.
570      * <p/>
571      * However as described in JFace Bug 9262, this case is not properly handled by
572      * a {@link TreeViewer} and leads to an infinite recursion in the tree viewer.
573      * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=9262
574      * <p/>
575      * The solution is to wrap the tree viewer input in a dummy root node that acts
576      * as a parent. This class does just that.
577      */
578     private static class RootWrapper {
579         private CanvasViewInfo mRoot;
580 
setRoot(CanvasViewInfo root)581         public void setRoot(CanvasViewInfo root) {
582             mRoot = root;
583         }
584 
getRoot()585         public CanvasViewInfo getRoot() {
586             return mRoot;
587         }
588     }
589 
590     /** Return the {@link CanvasViewInfo} associated with the given TreeItem's data field */
getViewInfo(Object viewData)591     /* package */ static CanvasViewInfo getViewInfo(Object viewData) {
592         if (viewData instanceof RootWrapper) {
593             return ((RootWrapper) viewData).getRoot();
594         }
595         if (viewData instanceof CanvasViewInfo) {
596             return (CanvasViewInfo) viewData;
597         }
598         return null;
599     }
600 
601     // --- Content and Label Providers ---
602 
603     /**
604      * Content provider for the Outline model.
605      * Objects are going to be {@link CanvasViewInfo}.
606      */
607     private static class ContentProvider implements ITreeContentProvider {
608 
609         @Override
getChildren(Object element)610         public Object[] getChildren(Object element) {
611             if (element instanceof RootWrapper) {
612                 CanvasViewInfo root = ((RootWrapper)element).getRoot();
613                 if (root != null) {
614                     return new Object[] { root };
615                 }
616             }
617             if (element instanceof CanvasViewInfo) {
618                 List<CanvasViewInfo> children = ((CanvasViewInfo) element).getUniqueChildren();
619                 if (children != null) {
620                     return children.toArray();
621                 }
622             }
623             return new Object[0];
624         }
625 
626         @Override
getParent(Object element)627         public Object getParent(Object element) {
628             if (element instanceof CanvasViewInfo) {
629                 return ((CanvasViewInfo) element).getParent();
630             }
631             return null;
632         }
633 
634         @Override
hasChildren(Object element)635         public boolean hasChildren(Object element) {
636             if (element instanceof CanvasViewInfo) {
637                 List<CanvasViewInfo> children = ((CanvasViewInfo) element).getChildren();
638                 if (children != null) {
639                     return children.size() > 0;
640                 }
641             }
642             return false;
643         }
644 
645         /**
646          * Returns the root element.
647          * Semantically, the root element is the single top-level XML element of the XML layout.
648          */
649         @Override
getElements(Object inputElement)650         public Object[] getElements(Object inputElement) {
651             return getChildren(inputElement);
652         }
653 
654         @Override
dispose()655         public void dispose() {
656             // pass
657         }
658 
659         @Override
inputChanged(Viewer viewer, Object oldInput, Object newInput)660         public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
661             // pass
662         }
663     }
664 
665     /**
666      * Label provider for the Outline model.
667      * Objects are going to be {@link CanvasViewInfo}.
668      */
669     private class LabelProvider extends StyledCellLabelProvider {
670         /**
671          * Returns the element's logo with a fallback on the android logo.
672          *
673          * @param element the tree element
674          * @return the image to be used as a logo
675          */
getImage(Object element)676         public Image getImage(Object element) {
677             if (element instanceof CanvasViewInfo) {
678                 element = ((CanvasViewInfo) element).getUiViewNode();
679             }
680 
681             if (element instanceof UiElementNode) {
682                 UiElementNode node = (UiElementNode) element;
683                 ElementDescriptor desc = node.getDescriptor();
684                 if (desc != null) {
685                     Image img = null;
686                     // Special case for the common case of vertical linear layouts:
687                     // show vertical linear icon (the default icon shows horizontal orientation)
688                     String uiName = desc.getUiName();
689                     if (uiName.equals(LINEAR_LAYOUT)) {
690                         Element e = (Element) node.getXmlNode();
691                         if (VALUE_VERTICAL.equals(e.getAttributeNS(ANDROID_URI,
692                                 ATTR_ORIENTATION))) {
693                             IconFactory factory = IconFactory.getInstance();
694                             img = factory.getIcon("VerticalLinearLayout"); //$NON-NLS-1$
695                         }
696                     } else if (uiName.equals(VIEW_VIEWTAG)) {
697                         Node xmlNode = node.getXmlNode();
698                         if (xmlNode instanceof Element) {
699                             String className = ((Element) xmlNode).getAttribute(ATTR_CLASS);
700                             if (className != null && className.length() > 0) {
701                                 int index = className.lastIndexOf('.');
702                                 if (index != -1) {
703                                     className = className.substring(index + 1);
704                                 }
705                                 img = IconFactory.getInstance().getIcon(className);
706                             }
707                         }
708                     }
709                     if (img == null) {
710                         img = desc.getGenericIcon();
711                     }
712                     if (img != null) {
713                         if (node.hasError()) {
714                             return new ErrorImageComposite(img).createImage();
715                         } else {
716                             return img;
717                         }
718                     }
719                 }
720             }
721 
722             return AdtPlugin.getAndroidLogo();
723         }
724 
725         /**
726          * Uses {@link UiElementNode#getStyledDescription} for the label for this tree item.
727          */
728         @Override
update(ViewerCell cell)729         public void update(ViewerCell cell) {
730             Object element = cell.getElement();
731             StyledString styledString = null;
732 
733             CanvasViewInfo vi = null;
734             if (element instanceof CanvasViewInfo) {
735                 vi = (CanvasViewInfo) element;
736                 element = vi.getUiViewNode();
737             }
738 
739             Image image = getImage(element);
740 
741             if (element instanceof UiElementNode) {
742                 UiElementNode node = (UiElementNode) element;
743                 styledString = node.getStyledDescription();
744                 Node xmlNode = node.getXmlNode();
745                 if (xmlNode instanceof Element) {
746                     Element e = (Element) xmlNode;
747 
748                     // Temporary diagnostics code when developing GridLayout
749                     if (GridLayoutRule.sDebugGridLayout) {
750                         String namespace;
751                         if (e.getParentNode().getNodeName().equals(GRID_LAYOUT)) {
752                             namespace = ANDROID_URI;
753                         } else {
754                             IProject project = mGraphicalEditorPart.getProject();
755                             ProjectState projectState = Sdk.getProjectState(project);
756                             if (projectState != null && projectState.isLibrary()) {
757                                 namespace = AUTO_URI;
758                             } else {
759                                 ManifestInfo info = ManifestInfo.get(project);
760                                 namespace = URI_PREFIX + info.getPackage();
761                             }
762                         }
763 
764                         if (e.getNodeName() != null && e.getNodeName().endsWith(GRID_LAYOUT)) {
765                             // Attach rowCount/columnCount info
766                             String rowCount = e.getAttributeNS(namespace, ATTR_ROW_COUNT);
767                             if (rowCount.length() == 0) {
768                                 rowCount = "?";
769                             }
770                             String columnCount = e.getAttributeNS(namespace, ATTR_COLUMN_COUNT);
771                             if (columnCount.length() == 0) {
772                                 columnCount = "?";
773                             }
774 
775                             styledString.append(" - columnCount=", QUALIFIER_STYLER);
776                             styledString.append(columnCount, QUALIFIER_STYLER);
777                             styledString.append(", rowCount=", QUALIFIER_STYLER);
778                             styledString.append(rowCount, QUALIFIER_STYLER);
779                         } else if (e.getParentNode() != null
780                             && e.getParentNode().getNodeName() != null
781                             && e.getParentNode().getNodeName().endsWith(GRID_LAYOUT)) {
782                             // Attach row/column info
783                             String row = e.getAttributeNS(namespace, ATTR_LAYOUT_ROW);
784                             if (row.length() == 0) {
785                                 row = "?";
786                             }
787                             Styler colStyle = QUALIFIER_STYLER;
788                             String column = e.getAttributeNS(namespace, ATTR_LAYOUT_COLUMN);
789                             if (column.length() == 0) {
790                                 column = "?";
791                             } else {
792                                 String colCount = ((Element) e.getParentNode()).getAttributeNS(
793                                         namespace, ATTR_COLUMN_COUNT);
794                                 if (colCount.length() > 0 && Integer.parseInt(colCount) <=
795                                         Integer.parseInt(column)) {
796                                     colStyle = StyledString.createColorRegistryStyler(
797                                         JFacePreferences.ERROR_COLOR, null);
798                                 }
799                             }
800                             String rowSpan = e.getAttributeNS(namespace, ATTR_LAYOUT_ROW_SPAN);
801                             String columnSpan = e.getAttributeNS(namespace,
802                                     ATTR_LAYOUT_COLUMN_SPAN);
803                             if (rowSpan.length() == 0) {
804                                 rowSpan = "1";
805                             }
806                             if (columnSpan.length() == 0) {
807                                 columnSpan = "1";
808                             }
809 
810                             styledString.append(" - cell (row=", QUALIFIER_STYLER);
811                             styledString.append(row, QUALIFIER_STYLER);
812                             styledString.append(',', QUALIFIER_STYLER);
813                             styledString.append("col=", colStyle);
814                             styledString.append(column, colStyle);
815                             styledString.append(')', colStyle);
816                             styledString.append(", span=(", QUALIFIER_STYLER);
817                             styledString.append(columnSpan, QUALIFIER_STYLER);
818                             styledString.append(',', QUALIFIER_STYLER);
819                             styledString.append(rowSpan, QUALIFIER_STYLER);
820                             styledString.append(')', QUALIFIER_STYLER);
821                         }
822                     }
823 
824                     if (e.hasAttributeNS(ANDROID_URI, ATTR_TEXT)) {
825                         // Show the text attribute
826                         String text = e.getAttributeNS(ANDROID_URI, ATTR_TEXT);
827                         if (text != null && text.length() > 0
828                                 && !text.contains(node.getDescriptor().getUiName())) {
829                             if (text.charAt(0) == '@') {
830                                 String resolved = mGraphicalEditorPart.findString(text);
831                                 if (resolved != null) {
832                                     text = resolved;
833                                 }
834                             }
835                             styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
836                             styledString.append('"', QUALIFIER_STYLER);
837                             styledString.append(truncate(text, styledString), QUALIFIER_STYLER);
838                             styledString.append('"', QUALIFIER_STYLER);
839                         }
840                     } else if (e.hasAttributeNS(ANDROID_URI, ATTR_SRC)) {
841                         // Show ImageView source attributes etc
842                         String src = e.getAttributeNS(ANDROID_URI, ATTR_SRC);
843                         if (src != null && src.length() > 0) {
844                             if (src.startsWith(DRAWABLE_PREFIX)) {
845                                 src = src.substring(DRAWABLE_PREFIX.length());
846                             }
847                             styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
848                             styledString.append(truncate(src, styledString), QUALIFIER_STYLER);
849                         }
850                     } else if (e.getTagName().equals(LayoutDescriptors.VIEW_INCLUDE)) {
851                         // Show the include reference.
852 
853                         // Note: the layout attribute is NOT in the Android namespace
854                         String src = e.getAttribute(LayoutDescriptors.ATTR_LAYOUT);
855                         if (src != null && src.length() > 0) {
856                             if (src.startsWith(LAYOUT_PREFIX)) {
857                                 src = src.substring(LAYOUT_PREFIX.length());
858                             }
859                             styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
860                             styledString.append(truncate(src, styledString), QUALIFIER_STYLER);
861                         }
862                     }
863                 }
864             } else if (element == null && vi != null) {
865                 // It's an inclusion-context: display it
866                 Reference includedWithin = mGraphicalEditorPart.getIncludedWithin();
867                 if (includedWithin != null) {
868                     styledString = new StyledString();
869                     styledString.append(includedWithin.getDisplayName(), QUALIFIER_STYLER);
870                     image = IconFactory.getInstance().getIcon(LayoutDescriptors.VIEW_INCLUDE);
871                 }
872             }
873 
874             if (styledString == null) {
875                 styledString = new StyledString();
876                 styledString.append(element == null ? "(null)" : element.toString());
877             }
878 
879            cell.setText(styledString.toString());
880            cell.setStyleRanges(styledString.getStyleRanges());
881            cell.setImage(image);
882            super.update(cell);
883        }
884 
885         @Override
isLabelProperty(Object element, String property)886         public boolean isLabelProperty(Object element, String property) {
887             return super.isLabelProperty(element, property);
888         }
889     }
890 
891     // --- Context Menu ---
892 
893     /**
894      * This viewer uses its own actions that delegate to the ones given
895      * by the {@link LayoutCanvas}. All the processing is actually handled
896      * directly by the canvas and this viewer only gets refreshed as a
897      * consequence of the canvas changing the XML model.
898      */
setupContextMenu()899     private void setupContextMenu() {
900 
901         mMenuManager = new MenuManager();
902         mMenuManager.removeAll();
903 
904         mMenuManager.add(mMoveUpAction);
905         mMenuManager.add(mMoveDownAction);
906         mMenuManager.add(new Separator());
907 
908         mMenuManager.add(new SelectionManager.SelectionMenu(mGraphicalEditorPart));
909         mMenuManager.add(new Separator());
910         final String prefix = LayoutCanvas.PREFIX_CANVAS_ACTION;
911         mMenuManager.add(new DelegateAction(prefix + ActionFactory.CUT.getId()));
912         mMenuManager.add(new DelegateAction(prefix + ActionFactory.COPY.getId()));
913         mMenuManager.add(new DelegateAction(prefix + ActionFactory.PASTE.getId()));
914 
915         mMenuManager.add(new Separator());
916 
917         mMenuManager.add(new DelegateAction(prefix + ActionFactory.DELETE.getId()));
918 
919         mMenuManager.addMenuListener(new IMenuListener() {
920             @Override
921             public void menuAboutToShow(IMenuManager manager) {
922                 // Update all actions to match their LayoutCanvas counterparts
923                 for (IContributionItem contrib : manager.getItems()) {
924                     if (contrib instanceof ActionContributionItem) {
925                         IAction action = ((ActionContributionItem) contrib).getAction();
926                         if (action instanceof DelegateAction) {
927                             ((DelegateAction) action).updateFromEditorPart(mGraphicalEditorPart);
928                         }
929                     }
930                 }
931             }
932         });
933 
934         new DynamicContextMenu(
935                 mGraphicalEditorPart.getEditorDelegate(),
936                 mGraphicalEditorPart.getCanvasControl(),
937                 mMenuManager);
938 
939         getTreeViewer().getTree().setMenu(mMenuManager.createContextMenu(getControl()));
940 
941         // Update Move Up/Move Down state only when the menu is opened
942         getTreeViewer().getTree().addMenuDetectListener(new MenuDetectListener() {
943             @Override
944             public void menuDetected(MenuDetectEvent e) {
945                 mMenuManager.update(IAction.ENABLED);
946             }
947         });
948     }
949 
950     /**
951      * An action that delegates its properties and behavior to a target action.
952      * The target action can be null or it can change overtime, typically as the
953      * layout canvas' editor part is activated or closed.
954      */
955     private static class DelegateAction extends Action {
956         private IAction mTargetAction;
957         private final String mCanvasActionId;
958 
DelegateAction(String canvasActionId)959         public DelegateAction(String canvasActionId) {
960             super(canvasActionId);
961             setId(canvasActionId);
962             mCanvasActionId = canvasActionId;
963         }
964 
965         // --- Methods form IAction ---
966 
967         /** Returns the target action's {@link #isEnabled()} if defined, or false. */
968         @Override
isEnabled()969         public boolean isEnabled() {
970             return mTargetAction == null ? false : mTargetAction.isEnabled();
971         }
972 
973         /** Returns the target action's {@link #isChecked()} if defined, or false. */
974         @Override
isChecked()975         public boolean isChecked() {
976             return mTargetAction == null ? false : mTargetAction.isChecked();
977         }
978 
979         /** Returns the target action's {@link #isHandled()} if defined, or false. */
980         @Override
isHandled()981         public boolean isHandled() {
982             return mTargetAction == null ? false : mTargetAction.isHandled();
983         }
984 
985         /** Runs the target action if defined. */
986         @Override
run()987         public void run() {
988             if (mTargetAction != null) {
989                 mTargetAction.run();
990             }
991             super.run();
992         }
993 
994         /**
995          * Updates this action to delegate to its counterpart in the given editor part
996          *
997          * @param editorPart The editor being updated
998          */
updateFromEditorPart(GraphicalEditorPart editorPart)999         public void updateFromEditorPart(GraphicalEditorPart editorPart) {
1000             LayoutCanvas canvas = editorPart == null ? null : editorPart.getCanvasControl();
1001             if (canvas == null) {
1002                 mTargetAction = null;
1003             } else {
1004                 mTargetAction = canvas.getAction(mCanvasActionId);
1005             }
1006 
1007             if (mTargetAction != null) {
1008                 setText(mTargetAction.getText());
1009                 setId(mTargetAction.getId());
1010                 setDescription(mTargetAction.getDescription());
1011                 setImageDescriptor(mTargetAction.getImageDescriptor());
1012                 setHoverImageDescriptor(mTargetAction.getHoverImageDescriptor());
1013                 setDisabledImageDescriptor(mTargetAction.getDisabledImageDescriptor());
1014                 setToolTipText(mTargetAction.getToolTipText());
1015                 setActionDefinitionId(mTargetAction.getActionDefinitionId());
1016                 setHelpListener(mTargetAction.getHelpListener());
1017                 setAccelerator(mTargetAction.getAccelerator());
1018                 setChecked(mTargetAction.isChecked());
1019                 setEnabled(mTargetAction.isEnabled());
1020             } else {
1021                 setEnabled(false);
1022             }
1023         }
1024     }
1025 
1026     /** Returns the associated editor with this outline */
getEditor()1027     /* package */GraphicalEditorPart getEditor() {
1028         return mGraphicalEditorPart;
1029     }
1030 
1031     @Override
setActionBars(IActionBars actionBars)1032     public void setActionBars(IActionBars actionBars) {
1033         super.setActionBars(actionBars);
1034 
1035         // Map Outline actions to canvas actions such that they share Undo context etc
1036         LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
1037         canvas.updateGlobalActions(actionBars);
1038 
1039         // Special handling for Select All since it's different than the canvas (will
1040         // include selecting the root etc)
1041         actionBars.setGlobalActionHandler(mTreeSelectAllAction.getId(), mTreeSelectAllAction);
1042         actionBars.updateActionBars();
1043     }
1044 
1045     // ---- Move Up/Down Support ----
1046 
1047     /** Returns true if the current selected item can be moved */
canMove(boolean forward)1048     private boolean canMove(boolean forward) {
1049         CanvasViewInfo viewInfo = getSingleSelectedItem();
1050         if (viewInfo != null) {
1051             UiViewElementNode node = viewInfo.getUiViewNode();
1052             if (forward) {
1053                 return findNext(node) != null;
1054             } else {
1055                 return findPrevious(node) != null;
1056             }
1057         }
1058 
1059         return false;
1060     }
1061 
1062     /** Moves the current selected item down (forward) or up (not forward) */
move(boolean forward)1063     private void move(boolean forward) {
1064         CanvasViewInfo viewInfo = getSingleSelectedItem();
1065         if (viewInfo != null) {
1066             final Pair<UiViewElementNode, Integer> target;
1067             UiViewElementNode selected = viewInfo.getUiViewNode();
1068             if (forward) {
1069                 target = findNext(selected);
1070             } else {
1071                 target = findPrevious(selected);
1072             }
1073             if (target != null) {
1074                 final LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
1075                 final SelectionManager selectionManager = canvas.getSelectionManager();
1076                 final ArrayList<SelectionItem> dragSelection = new ArrayList<SelectionItem>();
1077                 dragSelection.add(selectionManager.createSelection(viewInfo));
1078                 SelectionManager.sanitize(dragSelection);
1079 
1080                 if (!dragSelection.isEmpty()) {
1081                     final SimpleElement[] elements = SelectionItem.getAsElements(dragSelection);
1082                     UiViewElementNode parentNode = target.getFirst();
1083                     final NodeProxy targetNode = canvas.getNodeFactory().create(parentNode);
1084 
1085                     // Record children of the target right before the drop (such that we
1086                     // can find out after the drop which exact children were inserted)
1087                     Set<INode> children = new HashSet<INode>();
1088                     for (INode node : targetNode.getChildren()) {
1089                         children.add(node);
1090                     }
1091 
1092                     String label = MoveGesture.computeUndoLabel(targetNode,
1093                             elements, DND.DROP_MOVE);
1094                     canvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(label, new Runnable() {
1095                         @Override
1096                         public void run() {
1097                             InsertType insertType = InsertType.MOVE_INTO;
1098                             if (dragSelection.get(0).getNode().getParent() == targetNode) {
1099                                 insertType = InsertType.MOVE_WITHIN;
1100                             }
1101                             canvas.getRulesEngine().setInsertType(insertType);
1102                             int index = target.getSecond();
1103                             BaseLayoutRule.insertAt(targetNode, elements, false, index);
1104                             targetNode.applyPendingChanges();
1105                             canvas.getClipboardSupport().deleteSelection("Remove", dragSelection);
1106                         }
1107                     });
1108 
1109                     // Now find out which nodes were added, and look up their
1110                     // corresponding CanvasViewInfos
1111                     final List<INode> added = new ArrayList<INode>();
1112                     for (INode node : targetNode.getChildren()) {
1113                         if (!children.contains(node)) {
1114                             added.add(node);
1115                         }
1116                     }
1117 
1118                     selectionManager.setOutlineSelection(added);
1119                 }
1120             }
1121         }
1122     }
1123 
1124     /**
1125      * Returns the {@link CanvasViewInfo} for the currently selected item, or null if
1126      * there are no or multiple selected items
1127      *
1128      * @return the current selected item if there is exactly one item selected
1129      */
getSingleSelectedItem()1130     private CanvasViewInfo getSingleSelectedItem() {
1131         TreeItem[] selection = getTreeViewer().getTree().getSelection();
1132         if (selection.length == 1) {
1133             return getViewInfo(selection[0].getData());
1134         }
1135 
1136         return null;
1137     }
1138 
1139 
1140     /** Returns the pair [parent,index] of the next node (when iterating forward) */
1141     @VisibleForTesting
findNext(UiViewElementNode node)1142     /* package */ static Pair<UiViewElementNode, Integer> findNext(UiViewElementNode node) {
1143         UiElementNode parent = node.getUiParent();
1144         if (parent == null) {
1145             return null;
1146         }
1147 
1148         UiElementNode next = node.getUiNextSibling();
1149         if (next != null) {
1150             if (DescriptorsUtils.canInsertChildren(next.getDescriptor(), null)) {
1151                 return getFirstPosition(next);
1152             } else {
1153                 return getPositionAfter(next);
1154             }
1155         }
1156 
1157         next = parent.getUiNextSibling();
1158         if (next != null) {
1159             return getPositionBefore(next);
1160         } else {
1161             UiElementNode grandParent = parent.getUiParent();
1162             if (grandParent != null) {
1163                 return getLastPosition(grandParent);
1164             }
1165         }
1166 
1167         return null;
1168     }
1169 
1170     /** Returns the pair [parent,index] of the previous node (when iterating backward) */
1171     @VisibleForTesting
findPrevious(UiViewElementNode node)1172     /* package */ static Pair<UiViewElementNode, Integer> findPrevious(UiViewElementNode node) {
1173         UiElementNode prev = node.getUiPreviousSibling();
1174         if (prev != null) {
1175             UiElementNode curr = prev;
1176             while (true) {
1177                 List<UiElementNode> children = curr.getUiChildren();
1178                 if (children.size() > 0) {
1179                     curr = children.get(children.size() - 1);
1180                     continue;
1181                 }
1182                 if (DescriptorsUtils.canInsertChildren(curr.getDescriptor(), null)) {
1183                     return getFirstPosition(curr);
1184                 } else {
1185                     if (curr == prev) {
1186                         return getPositionBefore(curr);
1187                     } else {
1188                         return getPositionAfter(curr);
1189                     }
1190                 }
1191             }
1192         }
1193 
1194         return getPositionBefore(node.getUiParent());
1195     }
1196 
1197     /** Returns the pair [parent,index] of the position immediately before the given node  */
getPositionBefore(UiElementNode node)1198     private static Pair<UiViewElementNode, Integer> getPositionBefore(UiElementNode node) {
1199         if (node != null) {
1200             UiElementNode parent = node.getUiParent();
1201             if (parent != null && parent instanceof UiViewElementNode) {
1202                 return Pair.of((UiViewElementNode) parent, node.getUiSiblingIndex());
1203             }
1204         }
1205 
1206         return null;
1207     }
1208 
1209     /** Returns the pair [parent,index] of the position immediately following the given node  */
getPositionAfter(UiElementNode node)1210     private static Pair<UiViewElementNode, Integer> getPositionAfter(UiElementNode node) {
1211         if (node != null) {
1212             UiElementNode parent = node.getUiParent();
1213             if (parent != null && parent instanceof UiViewElementNode) {
1214                 return Pair.of((UiViewElementNode) parent, node.getUiSiblingIndex() + 1);
1215             }
1216         }
1217 
1218         return null;
1219     }
1220 
1221     /** Returns the pair [parent,index] of the first position inside the given parent */
getFirstPosition(UiElementNode parent)1222     private static Pair<UiViewElementNode, Integer> getFirstPosition(UiElementNode parent) {
1223         if (parent != null && parent instanceof UiViewElementNode) {
1224             return Pair.of((UiViewElementNode) parent, 0);
1225         }
1226 
1227         return null;
1228     }
1229 
1230     /**
1231      * Returns the pair [parent,index] of the last position after the given node's
1232      * children
1233      */
getLastPosition(UiElementNode parent)1234     private static Pair<UiViewElementNode, Integer> getLastPosition(UiElementNode parent) {
1235         if (parent != null && parent instanceof UiViewElementNode) {
1236             return Pair.of((UiViewElementNode) parent, parent.getUiChildren().size());
1237         }
1238 
1239         return null;
1240     }
1241 
1242     /**
1243      * Truncates the given text such that it will fit into the given {@link StyledString}
1244      * up to a maximum length of {@link #LABEL_MAX_WIDTH}.
1245      *
1246      * @param text the text to truncate
1247      * @param string the existing string to be appended to
1248      * @return the truncated string
1249      */
truncate(String text, StyledString string)1250     private static String truncate(String text, StyledString string) {
1251         int existingLength = string.length();
1252 
1253         if (text.length() + existingLength > LABEL_MAX_WIDTH) {
1254             int truncatedLength = LABEL_MAX_WIDTH - existingLength - 3;
1255             if (truncatedLength > 0) {
1256                 return String.format("%1$s...", text.substring(0, truncatedLength));
1257             } else {
1258                 return ""; //$NON-NLS-1$
1259             }
1260         }
1261 
1262         return text;
1263     }
1264 
1265     @Override
setToolBar(IToolBarManager toolBarManager)1266     public void setToolBar(IToolBarManager toolBarManager) {
1267         makeContributions(null, toolBarManager, null);
1268         toolBarManager.update(false);
1269     }
1270 }
1271