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 
UiElementDetail(UiTreeBlock tree)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
initialize(IManagedForm form)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
createContents(Composite parent)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
selectionChanged(IFormPart part, ISelection selection)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
commit(boolean onSave)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
dispose()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
isDirty()151     public boolean isDirty() {
152         if (mCurrentUiElementNode != null && mCurrentUiElementNode.isDirty()) {
153             markDirty();
154         }
155         return mIsDirty;
156     }
157 
158     @Override
isStale()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
refresh()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
setFocus()179     public void setFocus() {
180         // pass
181     }
182 
183     @Override
setFormInput(Object input)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      */
createMasterSection(Composite parent)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      */
createUiAttributeControls( final IManagedForm managedForm, final UiElementNode ui_node)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      */
createSubSectionTable(FormToolkit toolkit, Composite masterTable, String title)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      */
reflowMasterSection()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      */
updateUnknownAttributesSection(UiElementNode ui_node, final Composite unknownTable, final IManagedForm managedForm, HashSet<UiAttributeNode> reference)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      */
markDirty()486     private void markDirty() {
487         if (!mIsDirty) {
488             mIsDirty = true;
489             mManagedForm.dirtyStateChanged();
490         }
491     }
492 }
493 
494 
495