Home | History | Annotate | Download | only in tree
      1 /*
      2  * Copyright (C) 2007 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.ui.tree;
     18 
     19 import com.android.ide.eclipse.adt.AdtPlugin;
     20 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
     21 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
     22 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
     23 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
     24 import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
     25 import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
     26 import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
     27 import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart;
     28 import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener;
     29 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
     30 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
     31 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     32 
     33 import org.eclipse.core.runtime.IStatus;
     34 import org.eclipse.jface.viewers.ISelection;
     35 import org.eclipse.jface.viewers.ITreeSelection;
     36 import org.eclipse.swt.events.DisposeEvent;
     37 import org.eclipse.swt.events.DisposeListener;
     38 import org.eclipse.swt.graphics.Image;
     39 import org.eclipse.swt.widgets.Composite;
     40 import org.eclipse.swt.widgets.Control;
     41 import org.eclipse.ui.forms.IDetailsPage;
     42 import org.eclipse.ui.forms.IFormPart;
     43 import org.eclipse.ui.forms.IManagedForm;
     44 import org.eclipse.ui.forms.events.ExpansionEvent;
     45 import org.eclipse.ui.forms.events.IExpansionListener;
     46 import org.eclipse.ui.forms.widgets.FormText;
     47 import org.eclipse.ui.forms.widgets.FormToolkit;
     48 import org.eclipse.ui.forms.widgets.Section;
     49 import org.eclipse.ui.forms.widgets.SharedScrolledComposite;
     50 import org.eclipse.ui.forms.widgets.TableWrapData;
     51 import org.eclipse.ui.forms.widgets.TableWrapLayout;
     52 
     53 import java.util.Collection;
     54 import java.util.HashSet;
     55 
     56 /**
     57  * Details page for the {@link UiElementNode} nodes in the tree view.
     58  * <p/>
     59  * See IDetailsBase for more details.
     60  */
     61 class UiElementDetail implements IDetailsPage {
     62 
     63     /** The master-detail part, composed of a main tree and an auxiliary detail part */
     64     private ManifestSectionPart mMasterPart;
     65 
     66     private Section mMasterSection;
     67     private UiElementNode mCurrentUiElementNode;
     68     private Composite mCurrentTable;
     69     private boolean mIsDirty;
     70 
     71     private IManagedForm mManagedForm;
     72 
     73     private final UiTreeBlock mTree;
     74 
     75     public UiElementDetail(UiTreeBlock tree) {
     76         mTree = tree;
     77         mMasterPart = mTree.getMasterPart();
     78         mManagedForm = mMasterPart.getManagedForm();
     79     }
     80 
     81     /* (non-java doc)
     82      * Initializes the part.
     83      */
     84     @Override
     85     public void initialize(IManagedForm form) {
     86         mManagedForm = form;
     87     }
     88 
     89     /* (non-java doc)
     90      * Creates the contents of the page in the provided parent.
     91      */
     92     @Override
     93     public void createContents(Composite parent) {
     94         mMasterSection = createMasterSection(parent);
     95     }
     96 
     97     /* (non-java doc)
     98      * Called when the provided part has changed selection state.
     99      * <p/>
    100      * Only reply when our master part originates the selection.
    101      */
    102     @Override
    103     public void selectionChanged(IFormPart part, ISelection selection) {
    104         if (part == mMasterPart &&
    105                 !selection.isEmpty() &&
    106                 selection instanceof ITreeSelection) {
    107             ITreeSelection tree_selection = (ITreeSelection) selection;
    108 
    109             Object first = tree_selection.getFirstElement();
    110             if (first instanceof UiElementNode) {
    111                 UiElementNode ui_node = (UiElementNode) first;
    112                 createUiAttributeControls(mManagedForm, ui_node);
    113             }
    114         }
    115     }
    116 
    117     /* (non-java doc)
    118      * Instructs it to commit the new (modified) data back into the model.
    119      */
    120     @Override
    121     public void commit(boolean onSave) {
    122 
    123         mTree.getEditor().wrapEditXmlModel(new Runnable() {
    124             @Override
    125             public void run() {
    126                 try {
    127                     if (mCurrentUiElementNode != null) {
    128                         mCurrentUiElementNode.commit();
    129                     }
    130 
    131                     // Finally reset the dirty flag if everything was saved properly
    132                     mIsDirty = false;
    133                 } catch (Exception e) {
    134                     AdtPlugin.log(e, "Detail node failed to commit XML attribute!"); //$NON-NLS-1$
    135                 }
    136             }
    137         });
    138     }
    139 
    140     @Override
    141     public void dispose() {
    142         // pass
    143     }
    144 
    145 
    146     /* (non-java doc)
    147      * Returns true if the part has been modified with respect to the data
    148      * loaded from the model.
    149      */
    150     @Override
    151     public boolean isDirty() {
    152         if (mCurrentUiElementNode != null && mCurrentUiElementNode.isDirty()) {
    153             markDirty();
    154         }
    155         return mIsDirty;
    156     }
    157 
    158     @Override
    159     public boolean isStale() {
    160         // pass
    161         return false;
    162     }
    163 
    164     /**
    165      * Called by the master part when the tree is refreshed after the framework resources
    166      * have been reloaded.
    167      */
    168     @Override
    169     public void refresh() {
    170         if (mCurrentTable != null) {
    171             mCurrentTable.dispose();
    172             mCurrentTable = null;
    173         }
    174         mCurrentUiElementNode = null;
    175         mMasterSection.getParent().pack(true /* changed */);
    176     }
    177 
    178     @Override
    179     public void setFocus() {
    180         // pass
    181     }
    182 
    183     @Override
    184     public boolean setFormInput(Object input) {
    185         // pass
    186         return false;
    187     }
    188 
    189     /**
    190      * Creates a TableWrapLayout in the DetailsPage, which in turns contains a Section.
    191      *
    192      * All the UI should be created in a layout which parent is the mSection itself.
    193      * The hierarchy is:
    194      * <pre>
    195      * DetailPage
    196      * + TableWrapLayout
    197      *   + Section (with title/description && fill_grab horizontal)
    198      *     + TableWrapLayout [*]
    199      *       + Labels/Forms/etc... [*]
    200      * </pre>
    201      * Both items marked with [*] are created by the derived classes to fit their needs.
    202      *
    203      * @param parent Parent of the mSection (from createContents)
    204      * @return The new Section
    205      */
    206     private Section createMasterSection(Composite parent) {
    207         TableWrapLayout layout = new TableWrapLayout();
    208         layout.topMargin = 0;
    209         parent.setLayout(layout);
    210 
    211         FormToolkit toolkit = mManagedForm.getToolkit();
    212         Section section = toolkit.createSection(parent, Section.TITLE_BAR);
    213         section.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.TOP));
    214         return section;
    215     }
    216 
    217     /**
    218      * Create the ui attribute controls to edit the attributes for the given
    219      * ElementDescriptor.
    220      * <p/>
    221      * This is called by the constructor.
    222      * Derived classes can override this if necessary.
    223      *
    224      * @param managedForm The managed form
    225      */
    226     private void createUiAttributeControls(
    227             final IManagedForm managedForm,
    228             final UiElementNode ui_node) {
    229 
    230         final ElementDescriptor elem_desc = ui_node.getDescriptor();
    231         mMasterSection.setText(String.format("Attributes for %1$s", ui_node.getShortDescription()));
    232 
    233         if (mCurrentUiElementNode != ui_node) {
    234             // Before changing the table, commit all dirty state.
    235             if (mIsDirty) {
    236                 commit(false);
    237             }
    238             if (mCurrentTable != null) {
    239                 mCurrentTable.dispose();
    240                 mCurrentTable = null;
    241             }
    242 
    243             // To iterate over all attributes, we use the {@link ElementDescriptor} instead
    244             // of the {@link UiElementNode} because the attributes order is guaranteed in the
    245             // descriptor but not in the node itself.
    246             AttributeDescriptor[] attr_desc_list = ui_node.getAttributeDescriptors();
    247 
    248             // If the attribute list contains at least one SeparatorAttributeDescriptor,
    249             // sub-sections will be used. This needs to be known early as it influences the
    250             // creation of the master table.
    251             boolean useSubsections = false;
    252             for (AttributeDescriptor attr_desc : attr_desc_list) {
    253                 if (attr_desc instanceof SeparatorAttributeDescriptor) {
    254                     // Sub-sections will be used. The default sections should no longer be
    255                     useSubsections = true;
    256                     break;
    257                 }
    258             }
    259 
    260             FormToolkit toolkit = managedForm.getToolkit();
    261             Composite masterTable = SectionHelper.createTableLayout(mMasterSection,
    262                     toolkit, useSubsections ? 1 : 2 /* numColumns */);
    263             mCurrentTable = masterTable;
    264 
    265             mCurrentUiElementNode = ui_node;
    266 
    267             if (elem_desc.getTooltip() != null) {
    268                 String tooltip;
    269                 if (Sdk.getCurrent() != null &&
    270                         Sdk.getCurrent().getDocumentationBaseUrl() != null) {
    271                     tooltip = DescriptorsUtils.formatFormText(elem_desc.getTooltip(),
    272                             elem_desc,
    273                             Sdk.getCurrent().getDocumentationBaseUrl());
    274                 } else {
    275                     tooltip = elem_desc.getTooltip();
    276                 }
    277 
    278                 try {
    279                     FormText text = SectionHelper.createFormText(masterTable, toolkit,
    280                             true /* isHtml */, tooltip, true /* setupLayoutData */);
    281                     text.addHyperlinkListener(mTree.getEditor().createHyperlinkListener());
    282                     Image icon = elem_desc.getCustomizedIcon();
    283                     if (icon != null) {
    284                         text.setImage(DescriptorsUtils.IMAGE_KEY, icon);
    285                     }
    286                 } catch(Exception e) {
    287                     // The FormText parser is really really basic and will fail as soon as the
    288                     // HTML javadoc is ever so slightly malformatted.
    289                     AdtPlugin.log(e,
    290                             "Malformed javadoc, rejected by FormText for node %1$s: '%2$s'", //$NON-NLS-1$
    291                             ui_node.getDescriptor().getXmlName(),
    292                             tooltip);
    293 
    294                     // Fallback to a pure text tooltip, no fancy HTML
    295                     tooltip = DescriptorsUtils.formatTooltip(elem_desc.getTooltip());
    296                     SectionHelper.createLabel(masterTable, toolkit, tooltip, tooltip);
    297                 }
    298             }
    299 
    300             Composite table = useSubsections ? null : masterTable;
    301 
    302             for (AttributeDescriptor attr_desc : attr_desc_list) {
    303                 if (attr_desc instanceof XmlnsAttributeDescriptor) {
    304                     // Do not show hidden attributes
    305                     continue;
    306                 } else if (table == null || attr_desc instanceof SeparatorAttributeDescriptor) {
    307                     String title = null;
    308                     if (attr_desc instanceof SeparatorAttributeDescriptor) {
    309                         // xmlName is actually the label of the separator
    310                         title = attr_desc.getXmlLocalName();
    311                     } else {
    312                         title = String.format("Attributes from %1$s", elem_desc.getUiName());
    313                     }
    314 
    315                     table = createSubSectionTable(toolkit, masterTable, title);
    316                     if (attr_desc instanceof SeparatorAttributeDescriptor) {
    317                         continue;
    318                     }
    319                 }
    320 
    321                 UiAttributeNode ui_attr = ui_node.findUiAttribute(attr_desc);
    322 
    323                 if (ui_attr != null) {
    324                     ui_attr.createUiControl(table, managedForm);
    325 
    326                     if (ui_attr.getCurrentValue() != null &&
    327                             ui_attr.getCurrentValue().length() > 0) {
    328                         ((Section) table.getParent()).setExpanded(true);
    329                     }
    330                 } else {
    331                     // The XML has an extra unknown attribute.
    332                     // This is not expected to happen so it is ignored.
    333                     AdtPlugin.log(IStatus.INFO,
    334                             "Attribute %1$s not declared in node %2$s, ignored.", //$NON-NLS-1$
    335                             attr_desc.getXmlLocalName(),
    336                             ui_node.getDescriptor().getXmlName());
    337                 }
    338             }
    339 
    340             // Create a sub-section for the unknown attributes.
    341             // It is initially hidden till there are some attributes to show here.
    342             final Composite unknownTable = createSubSectionTable(toolkit, masterTable,
    343                     "Unknown XML Attributes");
    344             unknownTable.getParent().setVisible(false); // set section to not visible
    345             final HashSet<UiAttributeNode> reference = new HashSet<UiAttributeNode>();
    346 
    347             final IUiUpdateListener updateListener = new IUiUpdateListener() {
    348                 @Override
    349                 public void uiElementNodeUpdated(UiElementNode uiNode, UiUpdateState state) {
    350                     if (state == UiUpdateState.ATTR_UPDATED) {
    351                         updateUnknownAttributesSection(uiNode, unknownTable, managedForm,
    352                                 reference);
    353                     }
    354                 }
    355             };
    356             ui_node.addUpdateListener(updateListener);
    357 
    358             // remove the listener when the UI is disposed
    359             unknownTable.addDisposeListener(new DisposeListener() {
    360                 @Override
    361                 public void widgetDisposed(DisposeEvent e) {
    362                     ui_node.removeUpdateListener(updateListener);
    363                 }
    364             });
    365 
    366             updateUnknownAttributesSection(ui_node, unknownTable, managedForm, reference);
    367 
    368             mMasterSection.getParent().pack(true /* changed */);
    369         }
    370     }
    371 
    372     /**
    373      * Create a sub Section and its embedding wrapper table with 2 columns.
    374      * @return The table, child of a new section.
    375      */
    376     private Composite createSubSectionTable(FormToolkit toolkit,
    377             Composite masterTable, String title) {
    378 
    379         // The Section composite seems to ignore colspan when assigned a TableWrapData so
    380         // if the parent is a table with more than one column an extra table with one column
    381         // is inserted to respect colspan.
    382         int parentNumCol = ((TableWrapLayout) masterTable.getLayout()).numColumns;
    383         if (parentNumCol > 1) {
    384             masterTable = SectionHelper.createTableLayout(masterTable, toolkit, 1);
    385             TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB);
    386             twd.maxWidth = AndroidXmlEditor.TEXT_WIDTH_HINT;
    387             twd.colspan = parentNumCol;
    388             masterTable.setLayoutData(twd);
    389         }
    390 
    391         Composite table;
    392         Section section = toolkit.createSection(masterTable,
    393                 Section.TITLE_BAR | Section.TWISTIE);
    394 
    395         // Add an expansion listener that will trigger a reflow on the parent
    396         // ScrolledPageBook (which is actually a SharedScrolledComposite). This will
    397         // recompute the correct size and adjust the scrollbar as needed.
    398         section.addExpansionListener(new IExpansionListener() {
    399             @Override
    400             public void expansionStateChanged(ExpansionEvent e) {
    401                 reflowMasterSection();
    402             }
    403 
    404             @Override
    405             public void expansionStateChanging(ExpansionEvent e) {
    406                 // pass
    407             }
    408         });
    409 
    410         section.setText(title);
    411         section.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB,
    412                                                 TableWrapData.TOP));
    413         table = SectionHelper.createTableLayout(section, toolkit, 2 /* numColumns */);
    414         return table;
    415     }
    416 
    417     /**
    418      * Reflow the parent ScrolledPageBook (which is actually a SharedScrolledComposite).
    419      * This will recompute the correct size and adjust the scrollbar as needed.
    420      */
    421     private void reflowMasterSection() {
    422         for(Composite c = mMasterSection; c != null; c = c.getParent()) {
    423             if (c instanceof SharedScrolledComposite) {
    424                 ((SharedScrolledComposite) c).reflow(true /* flushCache */);
    425                 break;
    426             }
    427         }
    428     }
    429 
    430     /**
    431      * Updates the unknown attributes section for the UI Node.
    432      */
    433     private void updateUnknownAttributesSection(UiElementNode ui_node,
    434             final Composite unknownTable, final IManagedForm managedForm,
    435             HashSet<UiAttributeNode> reference) {
    436         Collection<UiAttributeNode> ui_attrs = ui_node.getUnknownUiAttributes();
    437         Section section = ((Section) unknownTable.getParent());
    438         boolean needs_reflow = false;
    439 
    440         // The table was created hidden, show it if there are unknown attributes now
    441         if (ui_attrs.size() > 0 && !section.isVisible()) {
    442             section.setVisible(true);
    443             needs_reflow = true;
    444         }
    445 
    446         // Compare the new attribute set with the old "reference" one
    447         boolean has_differences = ui_attrs.size() != reference.size();
    448         if (!has_differences) {
    449             for (UiAttributeNode ui_attr : ui_attrs) {
    450                 if (!reference.contains(ui_attr)) {
    451                     has_differences = true;
    452                     break;
    453                 }
    454             }
    455         }
    456 
    457         if (has_differences) {
    458             needs_reflow = true;
    459             reference.clear();
    460 
    461             // Remove all children of the table
    462             for (Control c : unknownTable.getChildren()) {
    463                 c.dispose();
    464             }
    465 
    466             // Recreate all attributes UI
    467             for (UiAttributeNode ui_attr : ui_attrs) {
    468                 reference.add(ui_attr);
    469                 ui_attr.createUiControl(unknownTable, managedForm);
    470 
    471                 if (ui_attr.getCurrentValue() != null && ui_attr.getCurrentValue().length() > 0) {
    472                     section.setExpanded(true);
    473                 }
    474             }
    475         }
    476 
    477         if (needs_reflow) {
    478             reflowMasterSection();
    479         }
    480     }
    481 
    482     /**
    483      * Marks the part dirty. Called as a result of user interaction with the widgets in the
    484      * section.
    485      */
    486     private void markDirty() {
    487         if (!mIsDirty) {
    488             mIsDirty = true;
    489             mManagedForm.dirtyStateChanged();
    490         }
    491     }
    492 }
    493 
    494 
    495