aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiElementDetail.java
blob: 2aa56a826e8c4efab456a8c1880df29b3a78eff8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.eclipse.org/org/documents/epl-v10.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ide.eclipse.adt.internal.editors.ui.tree;

import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart;
import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.ui.forms.IDetailsPage;
import org.eclipse.ui.forms.IFormPart;
import org.eclipse.ui.forms.IManagedForm;
import org.eclipse.ui.forms.events.ExpansionEvent;
import org.eclipse.ui.forms.events.IExpansionListener;
import org.eclipse.ui.forms.widgets.FormText;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.forms.widgets.Section;
import org.eclipse.ui.forms.widgets.SharedScrolledComposite;
import org.eclipse.ui.forms.widgets.TableWrapData;
import org.eclipse.ui.forms.widgets.TableWrapLayout;

import java.util.Collection;
import java.util.HashSet;

/**
 * Details page for the {@link UiElementNode} nodes in the tree view.
 * <p/>
 * See IDetailsBase for more details.
 */
class UiElementDetail implements IDetailsPage {

    /** The master-detail part, composed of a main tree and an auxiliary detail part */
    private ManifestSectionPart mMasterPart;

    private Section mMasterSection;
    private UiElementNode mCurrentUiElementNode;
    private Composite mCurrentTable;
    private boolean mIsDirty;

    private IManagedForm mManagedForm;

    private final UiTreeBlock mTree;

    public UiElementDetail(UiTreeBlock tree) {
        mTree = tree;
        mMasterPart = mTree.getMasterPart();
        mManagedForm = mMasterPart.getManagedForm();
    }

    /* (non-java doc)
     * Initializes the part.
     */
    @Override
    public void initialize(IManagedForm form) {
        mManagedForm = form;
    }

    /* (non-java doc)
     * Creates the contents of the page in the provided parent.
     */
    @Override
    public void createContents(Composite parent) {
        mMasterSection = createMasterSection(parent);
    }

    /* (non-java doc)
     * Called when the provided part has changed selection state.
     * <p/>
     * Only reply when our master part originates the selection.
     */
    @Override
    public void selectionChanged(IFormPart part, ISelection selection) {
        if (part == mMasterPart &&
                !selection.isEmpty() &&
                selection instanceof ITreeSelection) {
            ITreeSelection tree_selection = (ITreeSelection) selection;

            Object first = tree_selection.getFirstElement();
            if (first instanceof UiElementNode) {
                UiElementNode ui_node = (UiElementNode) first;
                createUiAttributeControls(mManagedForm, ui_node);
            }
        }
    }

    /* (non-java doc)
     * Instructs it to commit the new (modified) data back into the model.
     */
    @Override
    public void commit(boolean onSave) {

        mTree.getEditor().wrapEditXmlModel(new Runnable() {
            @Override
            public void run() {
                try {
                    if (mCurrentUiElementNode != null) {
                        mCurrentUiElementNode.commit();
                    }

                    // Finally reset the dirty flag if everything was saved properly
                    mIsDirty = false;
                } catch (Exception e) {
                    AdtPlugin.log(e, "Detail node failed to commit XML attribute!"); //$NON-NLS-1$
                }
            }
        });
    }

    @Override
    public void dispose() {
        // pass
    }


    /* (non-java doc)
     * Returns true if the part has been modified with respect to the data
     * loaded from the model.
     */
    @Override
    public boolean isDirty() {
        if (mCurrentUiElementNode != null && mCurrentUiElementNode.isDirty()) {
            markDirty();
        }
        return mIsDirty;
    }

    @Override
    public boolean isStale() {
        // pass
        return false;
    }

    /**
     * Called by the master part when the tree is refreshed after the framework resources
     * have been reloaded.
     */
    @Override
    public void refresh() {
        if (mCurrentTable != null) {
            mCurrentTable.dispose();
            mCurrentTable = null;
        }
        mCurrentUiElementNode = null;
        mMasterSection.getParent().pack(true /* changed */);
    }

    @Override
    public void setFocus() {
        // pass
    }

    @Override
    public boolean setFormInput(Object input) {
        // pass
        return false;
    }

    /**
     * Creates a TableWrapLayout in the DetailsPage, which in turns contains a Section.
     *
     * All the UI should be created in a layout which parent is the mSection itself.
     * The hierarchy is:
     * <pre>
     * DetailPage
     * + TableWrapLayout
     *   + Section (with title/description && fill_grab horizontal)
     *     + TableWrapLayout [*]
     *       + Labels/Forms/etc... [*]
     * </pre>
     * Both items marked with [*] are created by the derived classes to fit their needs.
     *
     * @param parent Parent of the mSection (from createContents)
     * @return The new Section
     */
    private Section createMasterSection(Composite parent) {
        TableWrapLayout layout = new TableWrapLayout();
        layout.topMargin = 0;
        parent.setLayout(layout);

        FormToolkit toolkit = mManagedForm.getToolkit();
        Section section = toolkit.createSection(parent, Section.TITLE_BAR);
        section.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.TOP));
        return section;
    }

    /**
     * Create the ui attribute controls to edit the attributes for the given
     * ElementDescriptor.
     * <p/>
     * This is called by the constructor.
     * Derived classes can override this if necessary.
     *
     * @param managedForm The managed form
     */
    private void createUiAttributeControls(
            final IManagedForm managedForm,
            final UiElementNode ui_node) {

        final ElementDescriptor elem_desc = ui_node.getDescriptor();
        mMasterSection.setText(String.format("Attributes for %1$s", ui_node.getShortDescription()));

        if (mCurrentUiElementNode != ui_node) {
            // Before changing the table, commit all dirty state.
            if (mIsDirty) {
                commit(false);
            }
            if (mCurrentTable != null) {
                mCurrentTable.dispose();
                mCurrentTable = null;
            }

            // To iterate over all attributes, we use the {@link ElementDescriptor} instead
            // of the {@link UiElementNode} because the attributes order is guaranteed in the
            // descriptor but not in the node itself.
            AttributeDescriptor[] attr_desc_list = ui_node.getAttributeDescriptors();

            // If the attribute list contains at least one SeparatorAttributeDescriptor,
            // sub-sections will be used. This needs to be known early as it influences the
            // creation of the master table.
            boolean useSubsections = false;
            for (AttributeDescriptor attr_desc : attr_desc_list) {
                if (attr_desc instanceof SeparatorAttributeDescriptor) {
                    // Sub-sections will be used. The default sections should no longer be
                    useSubsections = true;
                    break;
                }
            }

            FormToolkit toolkit = managedForm.getToolkit();
            Composite masterTable = SectionHelper.createTableLayout(mMasterSection,
                    toolkit, useSubsections ? 1 : 2 /* numColumns */);
            mCurrentTable = masterTable;

            mCurrentUiElementNode = ui_node;

            if (elem_desc.getTooltip() != null) {
                String tooltip;
                if (Sdk.getCurrent() != null &&
                        Sdk.getCurrent().getDocumentationBaseUrl() != null) {
                    tooltip = DescriptorsUtils.formatFormText(elem_desc.getTooltip(),
                            elem_desc,
                            Sdk.getCurrent().getDocumentationBaseUrl());
                } else {
                    tooltip = elem_desc.getTooltip();
                }

                try {
                    FormText text = SectionHelper.createFormText(masterTable, toolkit,
                            true /* isHtml */, tooltip, true /* setupLayoutData */);
                    text.addHyperlinkListener(mTree.getEditor().createHyperlinkListener());
                    Image icon = elem_desc.getCustomizedIcon();
                    if (icon != null) {
                        text.setImage(DescriptorsUtils.IMAGE_KEY, icon);
                    }
                } catch(Exception e) {
                    // The FormText parser is really really basic and will fail as soon as the
                    // HTML javadoc is ever so slightly malformatted.
                    AdtPlugin.log(e,
                            "Malformed javadoc, rejected by FormText for node %1$s: '%2$s'", //$NON-NLS-1$
                            ui_node.getDescriptor().getXmlName(),
                            tooltip);

                    // Fallback to a pure text tooltip, no fancy HTML
                    tooltip = DescriptorsUtils.formatTooltip(elem_desc.getTooltip());
                    SectionHelper.createLabel(masterTable, toolkit, tooltip, tooltip);
                }
            }

            Composite table = useSubsections ? null : masterTable;

            for (AttributeDescriptor attr_desc : attr_desc_list) {
                if (attr_desc instanceof XmlnsAttributeDescriptor) {
                    // Do not show hidden attributes
                    continue;
                } else if (table == null || attr_desc instanceof SeparatorAttributeDescriptor) {
                    String title = null;
                    if (attr_desc instanceof SeparatorAttributeDescriptor) {
                        // xmlName is actually the label of the separator
                        title = attr_desc.getXmlLocalName();
                    } else {
                        title = String.format("Attributes from %1$s", elem_desc.getUiName());
                    }

                    table = createSubSectionTable(toolkit, masterTable, title);
                    if (attr_desc instanceof SeparatorAttributeDescriptor) {
                        continue;
                    }
                }

                UiAttributeNode ui_attr = ui_node.findUiAttribute(attr_desc);

                if (ui_attr != null) {
                    ui_attr.createUiControl(table, managedForm);

                    if (ui_attr.getCurrentValue() != null &&
                            ui_attr.getCurrentValue().length() > 0) {
                        ((Section) table.getParent()).setExpanded(true);
                    }
                } else {
                    // The XML has an extra unknown attribute.
                    // This is not expected to happen so it is ignored.
                    AdtPlugin.log(IStatus.INFO,
                            "Attribute %1$s not declared in node %2$s, ignored.", //$NON-NLS-1$
                            attr_desc.getXmlLocalName(),
                            ui_node.getDescriptor().getXmlName());
                }
            }

            // Create a sub-section for the unknown attributes.
            // It is initially hidden till there are some attributes to show here.
            final Composite unknownTable = createSubSectionTable(toolkit, masterTable,
                    "Unknown XML Attributes");
            unknownTable.getParent().setVisible(false); // set section to not visible
            final HashSet<UiAttributeNode> reference = new HashSet<UiAttributeNode>();

            final IUiUpdateListener updateListener = new IUiUpdateListener() {
                @Override
                public void uiElementNodeUpdated(UiElementNode uiNode, UiUpdateState state) {
                    if (state == UiUpdateState.ATTR_UPDATED) {
                        updateUnknownAttributesSection(uiNode, unknownTable, managedForm,
                                reference);
                    }
                }
            };
            ui_node.addUpdateListener(updateListener);

            // remove the listener when the UI is disposed
            unknownTable.addDisposeListener(new DisposeListener() {
                @Override
                public void widgetDisposed(DisposeEvent e) {
                    ui_node.removeUpdateListener(updateListener);
                }
            });

            updateUnknownAttributesSection(ui_node, unknownTable, managedForm, reference);

            mMasterSection.getParent().pack(true /* changed */);
        }
    }

    /**
     * Create a sub Section and its embedding wrapper table with 2 columns.
     * @return The table, child of a new section.
     */
    private Composite createSubSectionTable(FormToolkit toolkit,
            Composite masterTable, String title) {

        // The Section composite seems to ignore colspan when assigned a TableWrapData so
        // if the parent is a table with more than one column an extra table with one column
        // is inserted to respect colspan.
        int parentNumCol = ((TableWrapLayout) masterTable.getLayout()).numColumns;
        if (parentNumCol > 1) {
            masterTable = SectionHelper.createTableLayout(masterTable, toolkit, 1);
            TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB);
            twd.maxWidth = AndroidXmlEditor.TEXT_WIDTH_HINT;
            twd.colspan = parentNumCol;
            masterTable.setLayoutData(twd);
        }

        Composite table;
        Section section = toolkit.createSection(masterTable,
                Section.TITLE_BAR | Section.TWISTIE);

        // Add an expansion listener that will trigger a reflow on the parent
        // ScrolledPageBook (which is actually a SharedScrolledComposite). This will
        // recompute the correct size and adjust the scrollbar as needed.
        section.addExpansionListener(new IExpansionListener() {
            @Override
            public void expansionStateChanged(ExpansionEvent e) {
                reflowMasterSection();
            }

            @Override
            public void expansionStateChanging(ExpansionEvent e) {
                // pass
            }
        });

        section.setText(title);
        section.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB,
                                                TableWrapData.TOP));
        table = SectionHelper.createTableLayout(section, toolkit, 2 /* numColumns */);
        return table;
    }

    /**
     * Reflow the parent ScrolledPageBook (which is actually a SharedScrolledComposite).
     * This will recompute the correct size and adjust the scrollbar as needed.
     */
    private void reflowMasterSection() {
        for(Composite c = mMasterSection; c != null; c = c.getParent()) {
            if (c instanceof SharedScrolledComposite) {
                ((SharedScrolledComposite) c).reflow(true /* flushCache */);
                break;
            }
        }
    }

    /**
     * Updates the unknown attributes section for the UI Node.
     */
    private void updateUnknownAttributesSection(UiElementNode ui_node,
            final Composite unknownTable, final IManagedForm managedForm,
            HashSet<UiAttributeNode> reference) {
        Collection<UiAttributeNode> ui_attrs = ui_node.getUnknownUiAttributes();
        Section section = ((Section) unknownTable.getParent());
        boolean needs_reflow = false;

        // The table was created hidden, show it if there are unknown attributes now
        if (ui_attrs.size() > 0 && !section.isVisible()) {
            section.setVisible(true);
            needs_reflow = true;
        }

        // Compare the new attribute set with the old "reference" one
        boolean has_differences = ui_attrs.size() != reference.size();
        if (!has_differences) {
            for (UiAttributeNode ui_attr : ui_attrs) {
                if (!reference.contains(ui_attr)) {
                    has_differences = true;
                    break;
                }
            }
        }

        if (has_differences) {
            needs_reflow = true;
            reference.clear();

            // Remove all children of the table
            for (Control c : unknownTable.getChildren()) {
                c.dispose();
            }

            // Recreate all attributes UI
            for (UiAttributeNode ui_attr : ui_attrs) {
                reference.add(ui_attr);
                ui_attr.createUiControl(unknownTable, managedForm);

                if (ui_attr.getCurrentValue() != null && ui_attr.getCurrentValue().length() > 0) {
                    section.setExpanded(true);
                }
            }
        }

        if (needs_reflow) {
            reflowMasterSection();
        }
    }

    /**
     * Marks the part dirty. Called as a result of user interaction with the widgets in the
     * section.
     */
    private void markDirty() {
        if (!mIsDirty) {
            mIsDirty = true;
            mManagedForm.dirtyStateChanged();
        }
    }
}