| /* |
| * 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(); |
| } |
| } |
| } |
| |
| |