Home | History | Annotate | Download | only in gle2
      1 /*
      2  * Copyright (C) 2011 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 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
     17 
     18 import static com.android.SdkConstants.ANDROID_LAYOUT_RESOURCE_PREFIX;
     19 import static com.android.SdkConstants.ANDROID_URI;
     20 import static com.android.SdkConstants.ATTR_NUM_COLUMNS;
     21 import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
     22 import static com.android.SdkConstants.GRID_VIEW;
     23 import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
     24 import static com.android.SdkConstants.TOOLS_URI;
     25 import static com.android.SdkConstants.VALUE_AUTO_FIT;
     26 
     27 import com.android.annotations.NonNull;
     28 import com.android.annotations.Nullable;
     29 import com.android.ide.common.rendering.api.AdapterBinding;
     30 import com.android.ide.common.rendering.api.DataBindingItem;
     31 import com.android.ide.common.rendering.api.ResourceReference;
     32 import com.android.ide.eclipse.adt.AdtPlugin;
     33 import com.android.ide.eclipse.adt.AdtUtils;
     34 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
     35 import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
     36 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
     37 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
     38 
     39 import org.eclipse.core.resources.IFile;
     40 import org.eclipse.core.runtime.IProgressMonitor;
     41 import org.eclipse.core.runtime.IStatus;
     42 import org.eclipse.core.runtime.Status;
     43 import org.eclipse.swt.widgets.Display;
     44 import org.eclipse.ui.IEditorPart;
     45 import org.eclipse.ui.progress.WorkbenchJob;
     46 import org.w3c.dom.Document;
     47 import org.w3c.dom.Element;
     48 import org.w3c.dom.Node;
     49 import org.w3c.dom.NodeList;
     50 import org.xmlpull.v1.XmlPullParser;
     51 
     52 import java.util.Collection;
     53 import java.util.List;
     54 import java.util.Map;
     55 
     56 /**
     57  * Design-time metadata lookup for layouts, such as fragment and AdapterView bindings.
     58  */
     59 public class LayoutMetadata {
     60     /** The default layout to use for list items in expandable list views */
     61     public static final String DEFAULT_EXPANDABLE_LIST_ITEM = "simple_expandable_list_item_2"; //$NON-NLS-1$
     62     /** The default layout to use for list items in plain list views */
     63     public static final String DEFAULT_LIST_ITEM = "simple_list_item_2"; //$NON-NLS-1$
     64     /** The default layout to use for list items in spinners */
     65     public static final String DEFAULT_SPINNER_ITEM = "simple_spinner_item"; //$NON-NLS-1$
     66 
     67     /** The string to start metadata comments with */
     68     private static final String COMMENT_PROLOGUE = " Preview: ";
     69     /** The property key, included in comments, which references a list item layout */
     70     public static final String KEY_LV_ITEM = "listitem";        //$NON-NLS-1$
     71     /** The property key, included in comments, which references a list header layout */
     72     public static final String KEY_LV_HEADER = "listheader";    //$NON-NLS-1$
     73     /** The property key, included in comments, which references a list footer layout */
     74     public static final String KEY_LV_FOOTER = "listfooter";    //$NON-NLS-1$
     75     /** The property key, included in comments, which references a fragment layout to show */
     76     public static final String KEY_FRAGMENT_LAYOUT = "layout";        //$NON-NLS-1$
     77     // NOTE: If you add additional keys related to resources, make sure you update the
     78     // ResourceRenameParticipant
     79 
     80     /** Utility class, do not create instances */
     81     private LayoutMetadata() {
     82     }
     83 
     84     /**
     85      * Returns the given property specified in the <b>current</b> element being
     86      * processed by the given pull parser.
     87      *
     88      * @param parser the pull parser, which must be in the middle of processing
     89      *            the target element
     90      * @param name the property name to look up
     91      * @return the property value, or null if not defined
     92      */
     93     @Nullable
     94     public static String getProperty(@NonNull XmlPullParser parser, @NonNull String name) {
     95         String value = parser.getAttributeValue(TOOLS_URI, name);
     96         if (value != null && value.isEmpty()) {
     97             value = null;
     98         }
     99 
    100         return value;
    101     }
    102 
    103     /**
    104      * Clears the old metadata from the given node
    105      *
    106      * @param node the XML node to associate metadata with
    107      * @deprecated this method clears metadata using the old comment-based style;
    108      *             should only be used for migration at this point
    109      */
    110     @Deprecated
    111     public static void clearLegacyComment(Node node) {
    112         NodeList children = node.getChildNodes();
    113         for (int i = 0, n = children.getLength(); i < n; i++) {
    114             Node child = children.item(i);
    115             if (child.getNodeType() == Node.COMMENT_NODE) {
    116                 String text = child.getNodeValue();
    117                 if (text.startsWith(COMMENT_PROLOGUE)) {
    118                     Node commentNode = child;
    119                     // Remove the comment, along with surrounding whitespace if applicable
    120                     Node previous = commentNode.getPreviousSibling();
    121                     if (previous != null && previous.getNodeType() == Node.TEXT_NODE) {
    122                         if (previous.getNodeValue().trim().length() == 0) {
    123                             node.removeChild(previous);
    124                         }
    125                     }
    126                     node.removeChild(commentNode);
    127                     Node first = node.getFirstChild();
    128                     if (first != null && first.getNextSibling() == null
    129                             && first.getNodeType() == Node.TEXT_NODE) {
    130                         if (first.getNodeValue().trim().length() == 0) {
    131                             node.removeChild(first);
    132                         }
    133                     }
    134                 }
    135             }
    136         }
    137     }
    138 
    139     /**
    140      * Returns the given property of the given DOM node, or null
    141      *
    142      * @param node the XML node to associate metadata with
    143      * @param name the name of the property to look up
    144      * @return the value stored with the given node and name, or null
    145      */
    146     @Nullable
    147     public static String getProperty(
    148             @NonNull Node node,
    149             @NonNull String name) {
    150         if (node.getNodeType() == Node.ELEMENT_NODE) {
    151             Element element = (Element) node;
    152             String value = element.getAttributeNS(TOOLS_URI, name);
    153             if (value != null && value.isEmpty()) {
    154                 value = null;
    155             }
    156 
    157             return value;
    158         }
    159 
    160         return null;
    161     }
    162 
    163     /**
    164      * Sets the given property of the given DOM node to a given value, or if null clears
    165      * the property.
    166      *
    167      * @param editor the editor associated with the property
    168      * @param node the XML node to associate metadata with
    169      * @param name the name of the property to set
    170      * @param value the value to store for the given node and name, or null to remove it
    171      */
    172     public static void setProperty(
    173             @NonNull final AndroidXmlEditor editor,
    174             @NonNull final Node node,
    175             @NonNull final String name,
    176             @Nullable final String value) {
    177         // Clear out the old metadata
    178         clearLegacyComment(node);
    179 
    180         if (node.getNodeType() == Node.ELEMENT_NODE) {
    181             final Element element = (Element) node;
    182             final String undoLabel = "Bind View";
    183             AdtUtils.setToolsAttribute(editor, element, undoLabel, name, value,
    184                     false /*reveal*/, false /*append*/);
    185 
    186             // Also apply the same layout to any corresponding elements in other configurations
    187             // of this layout.
    188             final IFile file = editor.getInputFile();
    189             if (file != null) {
    190                 final List<IFile> variations = AdtUtils.getResourceVariations(file, false);
    191                 if (variations.isEmpty()) {
    192                     return;
    193                 }
    194                 Display display = AdtPlugin.getDisplay();
    195                 WorkbenchJob job = new WorkbenchJob(display, "Update alternate views") {
    196                     @Override
    197                     public IStatus runInUIThread(IProgressMonitor monitor) {
    198                         for (IFile variation : variations) {
    199                             if (variation.equals(file)) {
    200                                 continue;
    201                             }
    202                             try {
    203                                 // If the corresponding file is open in the IDE, use the
    204                                 // editor version instead
    205                                 if (!AdtPrefs.getPrefs().isSharedLayoutEditor()) {
    206                                     if (setPropertyInEditor(undoLabel, variation, element, name,
    207                                             value)) {
    208                                         return Status.OK_STATUS;
    209                                     }
    210                                 }
    211 
    212                                 boolean old = editor.getIgnoreXmlUpdate();
    213                                 try {
    214                                     editor.setIgnoreXmlUpdate(true);
    215                                     setPropertyInFile(undoLabel, variation, element, name, value);
    216                                 } finally {
    217                                     editor.setIgnoreXmlUpdate(old);
    218                                 }
    219                             } catch (Exception e) {
    220                                 AdtPlugin.log(e, variation.getFullPath().toOSString());
    221                             }
    222                         }
    223                         return Status.OK_STATUS;
    224                     }
    225 
    226                 };
    227                 job.setSystem(true);
    228                 job.schedule();
    229             }
    230         }
    231     }
    232 
    233     private static boolean setPropertyInEditor(
    234             @NonNull String undoLabel,
    235             @NonNull IFile variation,
    236             @NonNull final Element equivalentElement,
    237             @NonNull final String name,
    238             @Nullable final String value) {
    239         Collection<IEditorPart> editors =
    240                 AdtUtils.findEditorsFor(variation, false /*restore*/);
    241         for (IEditorPart part : editors) {
    242             AndroidXmlEditor editor = AdtUtils.getXmlEditor(part);
    243             if (editor != null) {
    244                 Document doc = DomUtilities.getDocument(editor);
    245                 if (doc != null) {
    246                     Element element = DomUtilities.findCorresponding(equivalentElement, doc);
    247                     if (element != null) {
    248                         AdtUtils.setToolsAttribute(editor, element, undoLabel, name,
    249                                 value, false /*reveal*/, false /*append*/);
    250                         if (part instanceof GraphicalEditorPart) {
    251                             GraphicalEditorPart g = (GraphicalEditorPart) part;
    252                             g.recomputeLayout();
    253                             g.getCanvasControl().redraw();
    254                         }
    255                         return true;
    256                     }
    257                 }
    258             }
    259         }
    260 
    261         return false;
    262     }
    263 
    264     private static boolean setPropertyInFile(
    265             @NonNull String undoLabel,
    266             @NonNull IFile variation,
    267             @NonNull final Element element,
    268             @NonNull final String name,
    269             @Nullable final String value) {
    270         Document doc = DomUtilities.getDocument(variation);
    271         if (doc != null && element.getOwnerDocument() != doc) {
    272             Element other = DomUtilities.findCorresponding(element, doc);
    273             if (other != null) {
    274                 AdtUtils.setToolsAttribute(variation, other, undoLabel,
    275                         name, value, false);
    276 
    277                 return true;
    278             }
    279         }
    280 
    281         return false;
    282     }
    283 
    284     /** Strips out @layout/ or @android:layout/ from the given layout reference */
    285     private static String stripLayoutPrefix(String layout) {
    286         if (layout.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX)) {
    287             layout = layout.substring(ANDROID_LAYOUT_RESOURCE_PREFIX.length());
    288         } else if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) {
    289             layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length());
    290         }
    291 
    292         return layout;
    293     }
    294 
    295     /**
    296      * Creates an {@link AdapterBinding} for the given view object, or null if the user
    297      * has not yet chosen a target layout to use for the given AdapterView.
    298      *
    299      * @param viewObject the view object to create an adapter binding for
    300      * @param map a map containing tools attribute metadata
    301      * @return a binding, or null
    302      */
    303     @Nullable
    304     public static AdapterBinding getNodeBinding(
    305             @Nullable Object viewObject,
    306             @NonNull Map<String, String> map) {
    307         String header = map.get(KEY_LV_HEADER);
    308         String footer = map.get(KEY_LV_FOOTER);
    309         String layout = map.get(KEY_LV_ITEM);
    310         if (layout != null || header != null || footer != null) {
    311             int count = 12;
    312             return getNodeBinding(viewObject, header, footer, layout, count);
    313         }
    314 
    315         return null;
    316     }
    317 
    318     /**
    319      * Creates an {@link AdapterBinding} for the given view object, or null if the user
    320      * has not yet chosen a target layout to use for the given AdapterView.
    321      *
    322      * @param viewObject the view object to create an adapter binding for
    323      * @param uiNode the ui node corresponding to the view object
    324      * @return a binding, or null
    325      */
    326     @Nullable
    327     public static AdapterBinding getNodeBinding(
    328             @Nullable Object viewObject,
    329             @NonNull UiViewElementNode uiNode) {
    330         Node xmlNode = uiNode.getXmlNode();
    331 
    332         String header = getProperty(xmlNode, KEY_LV_HEADER);
    333         String footer = getProperty(xmlNode, KEY_LV_FOOTER);
    334         String layout = getProperty(xmlNode, KEY_LV_ITEM);
    335         if (layout != null || header != null || footer != null) {
    336             int count = 12;
    337             // If we're dealing with a grid view, multiply the list item count
    338             // by the number of columns to ensure we have enough items
    339             if (xmlNode instanceof Element && xmlNode.getNodeName().endsWith(GRID_VIEW)) {
    340                 Element element = (Element) xmlNode;
    341                 String columns = element.getAttributeNS(ANDROID_URI, ATTR_NUM_COLUMNS);
    342                 int multiplier = 2;
    343                 if (columns != null && columns.length() > 0 &&
    344                         !columns.equals(VALUE_AUTO_FIT)) {
    345                     try {
    346                         int c = Integer.parseInt(columns);
    347                         if (c >= 1 && c <= 10) {
    348                             multiplier = c;
    349                         }
    350                     } catch (NumberFormatException nufe) {
    351                         // some unexpected numColumns value: just stick with 2 columns for
    352                         // preview purposes
    353                     }
    354                 }
    355                 count *= multiplier;
    356             }
    357 
    358             return getNodeBinding(viewObject, header, footer, layout, count);
    359         }
    360 
    361         return null;
    362     }
    363 
    364     private static AdapterBinding getNodeBinding(Object viewObject,
    365             String header, String footer, String layout, int count) {
    366         if (layout != null || header != null || footer != null) {
    367             AdapterBinding binding = new AdapterBinding(count);
    368 
    369             if (header != null) {
    370                 boolean isFramework = header.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX);
    371                 binding.addHeader(new ResourceReference(stripLayoutPrefix(header),
    372                         isFramework));
    373             }
    374 
    375             if (footer != null) {
    376                 boolean isFramework = footer.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX);
    377                 binding.addFooter(new ResourceReference(stripLayoutPrefix(footer),
    378                         isFramework));
    379             }
    380 
    381             if (layout != null) {
    382                 boolean isFramework = layout.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX);
    383                 if (isFramework) {
    384                     layout = layout.substring(ANDROID_LAYOUT_RESOURCE_PREFIX.length());
    385                 } else if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) {
    386                     layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length());
    387                 }
    388 
    389                 binding.addItem(new DataBindingItem(layout, isFramework, 1));
    390             } else if (viewObject != null) {
    391                 String listFqcn = ProjectCallback.getListAdapterViewFqcn(viewObject.getClass());
    392                 if (listFqcn != null) {
    393                     if (listFqcn.endsWith(EXPANDABLE_LIST_VIEW)) {
    394                         binding.addItem(
    395                                 new DataBindingItem(DEFAULT_EXPANDABLE_LIST_ITEM,
    396                                 true /* isFramework */, 1));
    397                     } else {
    398                         binding.addItem(
    399                                 new DataBindingItem(DEFAULT_LIST_ITEM,
    400                                 true /* isFramework */, 1));
    401                     }
    402                 }
    403             } else {
    404                 binding.addItem(
    405                         new DataBindingItem(DEFAULT_LIST_ITEM,
    406                         true /* isFramework */, 1));
    407             }
    408             return binding;
    409         }
    410 
    411         return null;
    412     }
    413 }
    414