blob: 2aa56a826e8c4efab456a8c1880df29b3a78eff8 [file] [log] [blame]
/*
* 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();
}
}
}