diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties')
13 files changed, 3297 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/BooleanXmlPropertyEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/BooleanXmlPropertyEditor.java new file mode 100644 index 000000000..d6ff4d51d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/BooleanXmlPropertyEditor.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2012 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.layout.properties; + +import static com.android.SdkConstants.VALUE_FALSE; +import static com.android.SdkConstants.VALUE_TRUE; + +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.wb.internal.core.DesignerPlugin; +import org.eclipse.wb.internal.core.model.property.Property; +import org.eclipse.wb.internal.core.model.property.table.PropertyTable; +import org.eclipse.wb.internal.core.utils.ui.DrawUtils; + +/** + * Handle an XML property which represents booleans. + * + * Similar to the WindowBuilder PropertyEditor, but operates on Strings rather + * than Booleans (which means it is a tri-state boolean: true, false, not set) + */ +public class BooleanXmlPropertyEditor extends XmlPropertyEditor { + public static final BooleanXmlPropertyEditor INSTANCE = new BooleanXmlPropertyEditor(); + + private static final Image mTrueImage = DesignerPlugin.getImage("properties/true.png"); + private static final Image mFalseImage = DesignerPlugin.getImage("properties/false.png"); + private static final Image mNullImage = + DesignerPlugin.getImage("properties/BooleanNull.png"); + private static final Image mUnknownImage = + DesignerPlugin.getImage("properties/BooleanUnknown.png"); + + private BooleanXmlPropertyEditor() { + } + + @Override + public void paint(Property property, GC gc, int x, int y, int width, int height) + throws Exception { + Object value = property.getValue(); + assert value == null || value instanceof String; + if (value == null || value instanceof String) { + String text = (String) value; + Image image; + if (VALUE_TRUE.equals(text)) { + image = mTrueImage; + } else if (VALUE_FALSE.equals(text)) { + image = mFalseImage; + } else if (text == null) { + image = mNullImage; + } else { + // Probably something like a reference, e.g. @boolean/foo + image = mUnknownImage; + } + + // draw image + DrawUtils.drawImageCV(gc, image, x, y, height); + + // prepare new position/width + int imageWidth = image.getBounds().width + 2; + width -= imageWidth; + + // draw text + if (text != null) { + x += imageWidth; + DrawUtils.drawStringCV(gc, text, x, y, width, height); + } + } + } + + @Override + public boolean activate(PropertyTable propertyTable, Property property, Point location) + throws Exception { + // check that user clicked on image + if (location == null || location.x < mTrueImage.getBounds().width + 2) { + cycleValue(property); + } + // don't activate + return false; + } + + @Override + public void doubleClick(Property property, Point location) throws Exception { + cycleValue(property); + } + + /** + * Cycles through the values + */ + private void cycleValue(Property property) throws Exception { + Object value = property.getValue(); + if (value == null || value instanceof String) { + // Cycle null => true => false => null + String text = (String) value; + if (VALUE_TRUE.equals(text)) { + property.setValue(VALUE_FALSE); + } else if (VALUE_FALSE.equals(text)) { + property.setValue(null); + } else { + property.setValue(VALUE_TRUE); + } + } else { + assert false; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/EnumXmlPropertyEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/EnumXmlPropertyEditor.java new file mode 100644 index 000000000..f1a3f2aaa --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/EnumXmlPropertyEditor.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2012 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.layout.properties; + +import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ListAttributeDescriptor; + +import org.eclipse.wb.core.controls.CCombo3; +import org.eclipse.wb.internal.core.model.property.Property; +import org.eclipse.wb.internal.core.model.property.editor.AbstractComboPropertyEditor; +import org.eclipse.wb.internal.core.model.property.editor.ITextValuePropertyEditor; + +class EnumXmlPropertyEditor extends AbstractComboPropertyEditor implements + ITextValuePropertyEditor { + public static final EnumXmlPropertyEditor INSTANCE = new EnumXmlPropertyEditor(); + + private EnumXmlPropertyEditor() { + } + + @Override + protected String getText(Property property) throws Exception { + Object value = property.getValue(); + if (value == null) { + return ""; + } else if (value instanceof String) { + return (String) value; + } else if (value == Property.UNKNOWN_VALUE) { + return "<varies>"; + } else { + return ""; + } + } + + private String[] getItems(Property property) { + XmlProperty xmlProperty = (XmlProperty) property; + AttributeDescriptor descriptor = xmlProperty.getDescriptor(); + assert descriptor instanceof ListAttributeDescriptor; + ListAttributeDescriptor list = (ListAttributeDescriptor) descriptor; + return list.getValues(); + } + + @Override + protected void addItems(Property property, CCombo3 combo) throws Exception { + for (String item : getItems(property)) { + combo.add(item); + } + } + + @Override + protected void selectItem(Property property, CCombo3 combo) throws Exception { + combo.setText(getText(property)); + } + + @Override + protected void toPropertyEx(Property property, CCombo3 combo, int index) throws Exception { + property.setValue(getItems(property)[index]); + } + + @Override + public void setText(Property property, String text) throws Exception { + property.setValue(text); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/FlagXmlPropertyDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/FlagXmlPropertyDialog.java new file mode 100644 index 000000000..5e1e7029f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/FlagXmlPropertyDialog.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2012 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.layout.properties; + +import com.android.annotations.NonNull; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.google.common.base.Splitter; + +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.viewers.CheckStateChangedEvent; +import org.eclipse.jface.viewers.CheckboxTableViewer; +import org.eclipse.jface.viewers.ICheckStateListener; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.wb.internal.core.utils.execution.ExecutionUtils; +import org.eclipse.wb.internal.core.utils.execution.RunnableEx; +import org.eclipse.wb.internal.core.utils.ui.dialogs.ResizableDialog; + +import java.util.ArrayList; +import java.util.List; + +class FlagXmlPropertyDialog extends ResizableDialog +implements IStructuredContentProvider, ICheckStateListener, SelectionListener, KeyListener { + private final String mTitle; + private final XmlProperty mProperty; + private final String[] mFlags; + private final boolean mIsRadio; + + private Table mTable; + private CheckboxTableViewer mViewer; + + FlagXmlPropertyDialog( + @NonNull Shell parentShell, + @NonNull String title, + boolean isRadio, + @NonNull String[] flags, + @NonNull XmlProperty property) { + super(parentShell, AdtPlugin.getDefault()); + mTitle = title; + mIsRadio = isRadio; + mFlags = flags; + mProperty = property; + } + + @Override + protected void configureShell(Shell newShell) { + super.configureShell(newShell); + newShell.setText(mTitle); + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite container = (Composite) super.createDialogArea(parent); + + mViewer = CheckboxTableViewer.newCheckList(container, + SWT.BORDER | SWT.FULL_SELECTION | SWT.HIDE_SELECTION); + mTable = mViewer.getTable(); + mTable.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); + + Composite workaround = PropertyFactory.addWorkaround(container); + if (workaround != null) { + workaround.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1)); + } + + mViewer.setContentProvider(this); + mViewer.setInput(mFlags); + + String current = mProperty.getStringValue(); + if (current != null) { + Object[] checked = null; + if (mIsRadio) { + checked = new String[] { current }; + } else { + List<String> flags = new ArrayList<String>(); + for (String s : Splitter.on('|').omitEmptyStrings().trimResults().split(current)) { + flags.add(s); + } + checked = flags.toArray(new String[flags.size()]); + } + mViewer.setCheckedElements(checked); + } + if (mFlags.length > 0) { + mTable.setSelection(0); + } + + if (mIsRadio) { + // Enforce single-item selection + mViewer.addCheckStateListener(this); + } + mTable.addSelectionListener(this); + mTable.addKeyListener(this); + + return container; + } + + @Override + protected void createButtonsForButtonBar(Composite parent) { + createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); + createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false); + } + + @Override + protected Point getDefaultSize() { + return new Point(450, 400); + } + + @Override + protected void okPressed() { + // Apply the value + ExecutionUtils.runLog(new RunnableEx() { + @Override + public void run() throws Exception { + StringBuilder sb = new StringBuilder(30); + for (Object o : mViewer.getCheckedElements()) { + if (sb.length() > 0) { + sb.append('|'); + } + sb.append((String) o); + } + String value = sb.length() > 0 ? sb.toString() : null; + mProperty.setValue(value); + } + }); + + // close dialog + super.okPressed(); + } + + // ---- Implements IStructuredContentProvider ---- + + @Override + public Object[] getElements(Object inputElement) { + return (Object []) inputElement; + } + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + // ---- Implements ICheckStateListener ---- + + @Override + public void checkStateChanged(CheckStateChangedEvent event) { + // Try to disable other elements that conflict with this + boolean isChecked = event.getChecked(); + if (isChecked) { + Object selected = event.getElement(); + for (Object other : mViewer.getCheckedElements()) { + if (other != selected) { + mViewer.setChecked(other, false); + } + } + } else { + + } + } + + // ---- Implements SelectionListener ---- + + @Override + public void widgetSelected(SelectionEvent e) { + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem item = (TableItem) e.item; + item.setChecked(!item.getChecked()); + } + } + + // ---- Implements KeyListener ---- + + @Override + public void keyPressed(KeyEvent e) { + // Let space toggle checked state + if (e.keyCode == ' ' /* SWT.SPACE requires Eclipse 3.7 */) { + if (mTable.getSelectionCount() == 1) { + TableItem item = mTable.getSelection()[0]; + item.setChecked(!item.getChecked()); + } + } + } + + @Override + public void keyReleased(KeyEvent e) { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyFactory.java new file mode 100644 index 000000000..2b8cfbf43 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyFactory.java @@ -0,0 +1,750 @@ +/* + * Copyright (C) 2012 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.layout.properties; + +import static com.android.SdkConstants.ATTR_ID; +import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN; +import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; + +import com.android.annotations.Nullable; +import com.android.ide.common.api.IAttributeInfo; +import com.android.ide.common.api.IAttributeInfo.Format; +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.SeparatorAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; +import com.android.tools.lint.detector.api.LintUtils; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.browser.IWebBrowser; +import org.eclipse.wb.internal.core.editor.structure.property.PropertyListIntersector; +import org.eclipse.wb.internal.core.model.property.ComplexProperty; +import org.eclipse.wb.internal.core.model.property.Property; +import org.eclipse.wb.internal.core.model.property.category.PropertyCategory; +import org.eclipse.wb.internal.core.model.property.editor.PropertyEditor; +import org.eclipse.wb.internal.core.model.property.editor.presentation.ButtonPropertyEditorPresentation; + +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * The {@link PropertyFactory} creates (and caches) the set of {@link Property} + * instances applicable to a given node. It's also responsible for ordering + * these, and sometimes combining them into {@link ComplexProperty} category + * nodes. + * <p> + * TODO: For any properties that are *set* in XML, they should NOT be labeled as + * advanced (which would make them disappear) + */ +public class PropertyFactory { + /** Disable cache during development only */ + @SuppressWarnings("unused") + private static final boolean CACHE_ENABLED = true || !LintUtils.assertionsEnabled(); + static { + if (!CACHE_ENABLED) { + System.err.println("WARNING: The property cache is disabled"); + } + } + + private static final Property[] NO_PROPERTIES = new Property[0]; + + private static final int PRIO_FIRST = -100000; + private static final int PRIO_SECOND = PRIO_FIRST + 10; + private static final int PRIO_LAST = 100000; + + private final GraphicalEditorPart mGraphicalEditorPart; + private Map<UiViewElementNode, Property[]> mCache = + new WeakHashMap<UiViewElementNode, Property[]>(); + private UiViewElementNode mCurrentViewCookie; + + /** Sorting orders for the properties */ + public enum SortingMode { + NATURAL, + BY_ORIGIN, + ALPHABETICAL; + } + + /** The default sorting mode */ + public static final SortingMode DEFAULT_MODE = SortingMode.BY_ORIGIN; + + private SortingMode mSortMode = DEFAULT_MODE; + private SortingMode mCacheSortMode; + + public PropertyFactory(GraphicalEditorPart graphicalEditorPart) { + mGraphicalEditorPart = graphicalEditorPart; + } + + /** + * Get the properties for the given list of selection items. + * + * @param items the {@link CanvasViewInfo} instances to get an intersected + * property list for + * @return the properties for the given items + */ + public Property[] getProperties(List<CanvasViewInfo> items) { + mCurrentViewCookie = null; + + if (items == null || items.size() == 0) { + return NO_PROPERTIES; + } else if (items.size() == 1) { + CanvasViewInfo item = items.get(0); + mCurrentViewCookie = item.getUiViewNode(); + + return getProperties(item); + } else { + // intersect properties + PropertyListIntersector intersector = new PropertyListIntersector(); + for (CanvasViewInfo node : items) { + intersector.intersect(getProperties(node)); + } + + return intersector.getProperties(); + } + } + + private Property[] getProperties(CanvasViewInfo item) { + UiViewElementNode node = item.getUiViewNode(); + if (node == null) { + return NO_PROPERTIES; + } + + if (mCacheSortMode != mSortMode) { + mCacheSortMode = mSortMode; + mCache.clear(); + } + + Property[] properties = mCache.get(node); + if (!CACHE_ENABLED) { + properties = null; + } + if (properties == null) { + Collection<? extends Property> propertyList = getProperties(node); + if (propertyList == null) { + properties = new Property[0]; + } else { + properties = propertyList.toArray(new Property[propertyList.size()]); + } + mCache.put(node, properties); + } + return properties; + } + + + protected Collection<? extends Property> getProperties(UiViewElementNode node) { + ViewMetadataRepository repository = ViewMetadataRepository.get(); + ViewElementDescriptor viewDescriptor = (ViewElementDescriptor) node.getDescriptor(); + String fqcn = viewDescriptor.getFullClassName(); + Set<String> top = new HashSet<String>(repository.getTopAttributes(fqcn)); + AttributeDescriptor[] attributeDescriptors = node.getAttributeDescriptors(); + + List<XmlProperty> properties = new ArrayList<XmlProperty>(attributeDescriptors.length); + int priority = 0; + for (final AttributeDescriptor descriptor : attributeDescriptors) { + // TODO: Filter out non-public properties!! + // (They shouldn't be in the descriptors at all) + + assert !(descriptor instanceof SeparatorAttributeDescriptor); // No longer inserted + if (descriptor instanceof XmlnsAttributeDescriptor) { + continue; + } + + PropertyEditor editor = XmlPropertyEditor.INSTANCE; + IAttributeInfo info = descriptor.getAttributeInfo(); + if (info != null) { + EnumSet<Format> formats = info.getFormats(); + if (formats.contains(Format.BOOLEAN)) { + editor = BooleanXmlPropertyEditor.INSTANCE; + } else if (formats.contains(Format.ENUM)) { + // We deliberately don't use EnumXmlPropertyEditor.INSTANCE here, + // since some attributes (such as layout_width) can have not just one + // of the enum values but custom values such as "42dp" as well. And + // furthermore, we don't even bother limiting this to formats.size()==1, + // since the editing experience with the enum property editor is + // more limited than the text editor plus enum completer anyway + // (for example, you can't type to filter the values, and clearing + // the value is harder.) + } + } + + XmlProperty property = new XmlProperty(editor, this, node, descriptor); + // Assign ids sequentially. This ensures that the properties will mostly keep their + // relative order (such as placing width before height), even though we will regroup + // some (such as properties in the same category, and the layout params etc) + priority += 10; + + PropertyCategory category = PropertyCategory.NORMAL; + String name = descriptor.getXmlLocalName(); + if (top.contains(name) || PropertyMetadata.isPreferred(name)) { + category = PropertyCategory.PREFERRED; + property.setPriority(PRIO_FIRST + priority); + } else { + property.setPriority(priority); + + // Prefer attributes defined on the specific type of this + // widget + // NOTE: This doesn't work very well for TextViews + /* IAttributeInfo attributeInfo = descriptor.getAttributeInfo(); + if (attributeInfo != null && fqcn.equals(attributeInfo.getDefinedBy())) { + category = PropertyCategory.PREFERRED; + } else*/ if (PropertyMetadata.isAdvanced(name)) { + category = PropertyCategory.ADVANCED; + } + } + if (category != null) { + property.setCategory(category); + } + properties.add(property); + } + + switch (mSortMode) { + case BY_ORIGIN: + return sortByOrigin(node, properties); + + case ALPHABETICAL: + return sortAlphabetically(node, properties); + + default: + case NATURAL: + return sortNatural(node, properties); + } + } + + protected Collection<? extends Property> sortAlphabetically( + UiViewElementNode node, + List<XmlProperty> properties) { + Collections.sort(properties, Property.ALPHABETICAL); + return properties; + } + + protected Collection<? extends Property> sortByOrigin( + UiViewElementNode node, + List<XmlProperty> properties) { + List<Property> collapsed = new ArrayList<Property>(properties.size()); + List<Property> layoutProperties = Lists.newArrayListWithExpectedSize(20); + List<Property> marginProperties = null; + List<Property> deprecatedProperties = null; + Map<String, ComplexProperty> categoryToProperty = new HashMap<String, ComplexProperty>(); + Multimap<String, Property> categoryToProperties = ArrayListMultimap.create(); + + if (properties.isEmpty()) { + return properties; + } + + ViewElementDescriptor parent = (ViewElementDescriptor) properties.get(0).getDescriptor() + .getParent(); + Map<String, Integer> categoryPriorities = Maps.newHashMap(); + int nextCategoryPriority = 100; + while (parent != null) { + categoryPriorities.put(parent.getFullClassName(), nextCategoryPriority += 100); + parent = parent.getSuperClassDesc(); + } + + for (int i = 0, max = properties.size(); i < max; i++) { + XmlProperty property = properties.get(i); + + AttributeDescriptor descriptor = property.getDescriptor(); + if (descriptor.isDeprecated()) { + if (deprecatedProperties == null) { + deprecatedProperties = Lists.newArrayListWithExpectedSize(10); + } + deprecatedProperties.add(property); + continue; + } + + String firstName = descriptor.getXmlLocalName(); + if (firstName.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) { + if (firstName.startsWith(ATTR_LAYOUT_MARGIN)) { + if (marginProperties == null) { + marginProperties = Lists.newArrayListWithExpectedSize(5); + } + marginProperties.add(property); + } else { + layoutProperties.add(property); + } + continue; + } + + if (firstName.equals(ATTR_ID)) { + // Add id to the front (though the layout parameters will be added to + // the front of this at the end) + property.setPriority(PRIO_FIRST); + collapsed.add(property); + continue; + } + + if (property.getCategory() == PropertyCategory.PREFERRED) { + collapsed.add(property); + // Fall through: these are *duplicated* inside their defining categories! + // However, create a new instance of the property, such that the propertysheet + // doesn't see the same property instance twice (when selected, it will highlight + // both, etc.) Also, set the category to Normal such that we don't draw attention + // to it again. We want it to appear in both places such that somebody looking + // within a category will always find it there, even if for this specific + // view type it's a common attribute and replicated up at the top. + XmlProperty oldProperty = property; + property = new XmlProperty(oldProperty.getEditor(), this, node, + oldProperty.getDescriptor()); + property.setPriority(oldProperty.getPriority()); + } + + IAttributeInfo attributeInfo = descriptor.getAttributeInfo(); + if (attributeInfo != null && attributeInfo.getDefinedBy() != null) { + String category = attributeInfo.getDefinedBy(); + ComplexProperty complex = categoryToProperty.get(category); + if (complex == null) { + complex = new ComplexProperty( + category.substring(category.lastIndexOf('.') + 1), + "[]", + null /* properties */); + categoryToProperty.put(category, complex); + Integer categoryPriority = categoryPriorities.get(category); + if (categoryPriority != null) { + complex.setPriority(categoryPriority); + } else { + // Descriptor for an attribute whose definedBy does *not* + // correspond to one of the known superclasses of this widget. + // This sometimes happens; for example, a RatingBar will pull in + // an ImageView's minWidth attribute. Probably an error in the + // metadata, but deal with it gracefully here. + categoryPriorities.put(category, nextCategoryPriority += 100); + complex.setPriority(nextCategoryPriority); + } + } + categoryToProperties.put(category, property); + continue; + } else { + collapsed.add(property); + } + } + + // Update the complex properties + for (String category : categoryToProperties.keySet()) { + Collection<Property> subProperties = categoryToProperties.get(category); + if (subProperties.size() > 1) { + ComplexProperty complex = categoryToProperty.get(category); + assert complex != null : category; + Property[] subArray = new Property[subProperties.size()]; + complex.setProperties(subProperties.toArray(subArray)); + //complex.setPriority(subArray[0].getPriority()); + + collapsed.add(complex); + + boolean allAdvanced = true; + boolean isPreferred = false; + for (Property p : subProperties) { + PropertyCategory c = p.getCategory(); + if (c != PropertyCategory.ADVANCED) { + allAdvanced = false; + } + if (c == PropertyCategory.PREFERRED) { + isPreferred = true; + } + } + if (isPreferred) { + complex.setCategory(PropertyCategory.PREFERRED); + } else if (allAdvanced) { + complex.setCategory(PropertyCategory.ADVANCED); + } + } else if (subProperties.size() == 1) { + collapsed.add(subProperties.iterator().next()); + } + } + + if (layoutProperties.size() > 0 || marginProperties != null) { + if (marginProperties != null) { + XmlProperty[] m = + marginProperties.toArray(new XmlProperty[marginProperties.size()]); + Property marginProperty = new ComplexProperty( + "Margins", + "[]", + m); + layoutProperties.add(marginProperty); + marginProperty.setPriority(PRIO_LAST); + + for (XmlProperty p : m) { + p.setParent(marginProperty); + } + } + Property[] l = layoutProperties.toArray(new Property[layoutProperties.size()]); + Arrays.sort(l, Property.PRIORITY); + Property property = new ComplexProperty( + "Layout Parameters", + "[]", + l); + for (Property p : l) { + if (p instanceof XmlProperty) { + ((XmlProperty) p).setParent(property); + } + } + property.setCategory(PropertyCategory.PREFERRED); + collapsed.add(property); + property.setPriority(PRIO_SECOND); + } + + if (deprecatedProperties != null && deprecatedProperties.size() > 0) { + Property property = new ComplexProperty( + "Deprecated", + "(Deprecated Properties)", + deprecatedProperties.toArray(new Property[deprecatedProperties.size()])); + property.setPriority(PRIO_LAST); + collapsed.add(property); + } + + Collections.sort(collapsed, Property.PRIORITY); + + return collapsed; + } + + protected Collection<? extends Property> sortNatural( + UiViewElementNode node, + List<XmlProperty> properties) { + Collections.sort(properties, Property.ALPHABETICAL); + List<Property> collapsed = new ArrayList<Property>(properties.size()); + List<Property> layoutProperties = Lists.newArrayListWithExpectedSize(20); + List<Property> marginProperties = null; + List<Property> deprecatedProperties = null; + Map<String, ComplexProperty> categoryToProperty = new HashMap<String, ComplexProperty>(); + Multimap<String, Property> categoryToProperties = ArrayListMultimap.create(); + + for (int i = 0, max = properties.size(); i < max; i++) { + XmlProperty property = properties.get(i); + + AttributeDescriptor descriptor = property.getDescriptor(); + if (descriptor.isDeprecated()) { + if (deprecatedProperties == null) { + deprecatedProperties = Lists.newArrayListWithExpectedSize(10); + } + deprecatedProperties.add(property); + continue; + } + + String firstName = descriptor.getXmlLocalName(); + if (firstName.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) { + if (firstName.startsWith(ATTR_LAYOUT_MARGIN)) { + if (marginProperties == null) { + marginProperties = Lists.newArrayListWithExpectedSize(5); + } + marginProperties.add(property); + } else { + layoutProperties.add(property); + } + continue; + } + + if (firstName.equals(ATTR_ID)) { + // Add id to the front (though the layout parameters will be added to + // the front of this at the end) + property.setPriority(PRIO_FIRST); + collapsed.add(property); + continue; + } + + String category = PropertyMetadata.getCategory(firstName); + if (category != null) { + ComplexProperty complex = categoryToProperty.get(category); + if (complex == null) { + complex = new ComplexProperty( + category, + "[]", + null /* properties */); + categoryToProperty.put(category, complex); + complex.setPriority(property.getPriority()); + } + categoryToProperties.put(category, property); + continue; + } + + // Index of second word in the first name, so in fooBar it's 3 (index of 'B') + int firstNameIndex = firstName.length(); + for (int k = 0, kn = firstName.length(); k < kn; k++) { + if (Character.isUpperCase(firstName.charAt(k))) { + firstNameIndex = k; + break; + } + } + + // Scout forwards and see how many properties we can combine + int j = i + 1; + if (property.getCategory() != PropertyCategory.PREFERRED + && !property.getDescriptor().isDeprecated()) { + for (; j < max; j++) { + XmlProperty next = properties.get(j); + String nextName = next.getName(); + if (nextName.regionMatches(0, firstName, 0, firstNameIndex) + // Also make sure we begin the second word at the next + // character; if not, we could have something like + // scrollBar + // scrollingBehavior + && nextName.length() > firstNameIndex + && Character.isUpperCase(nextName.charAt(firstNameIndex))) { + + // Deprecated attributes, and preferred attributes, should not + // be pushed into normal clusters (preferred stay top-level + // and sort to the top, deprecated are all put in the same cluster at + // the end) + + if (next.getCategory() == PropertyCategory.PREFERRED) { + break; + } + if (next.getDescriptor().isDeprecated()) { + break; + } + + // This property should be combined with the previous + // property + } else { + break; + } + } + } + if (j - i > 1) { + // Combining multiple properties: all the properties from i + // through j inclusive + XmlProperty[] subprops = new XmlProperty[j - i]; + for (int k = i, index = 0; k < j; k++, index++) { + subprops[index] = properties.get(k); + } + Arrays.sort(subprops, Property.PRIORITY); + + // See if we can compute a LONGER base than just the first word. + // For example, if we have "lineSpacingExtra" and "lineSpacingMultiplier" + // we'd like the base to be "lineSpacing", not "line". + int common = firstNameIndex; + for (int k = firstNameIndex + 1, n = firstName.length(); k < n; k++) { + if (Character.isUpperCase(firstName.charAt(k))) { + common = k; + break; + } + } + if (common > firstNameIndex) { + for (int k = 0, n = subprops.length; k < n; k++) { + String nextName = subprops[k].getName(); + if (nextName.regionMatches(0, firstName, 0, common) + // Also make sure we begin the second word at the next + // character; if not, we could have something like + // scrollBar + // scrollingBehavior + && nextName.length() > common + && Character.isUpperCase(nextName.charAt(common))) { + // New prefix is okay + } else { + common = firstNameIndex; + break; + } + } + firstNameIndex = common; + } + + String base = firstName.substring(0, firstNameIndex); + base = DescriptorsUtils.capitalize(base); + Property complexProperty = new ComplexProperty( + base, + "[]", + subprops); + complexProperty.setPriority(subprops[0].getPriority()); + //complexProperty.setCategory(PropertyCategory.PREFERRED); + collapsed.add(complexProperty); + boolean allAdvanced = true; + boolean isPreferred = false; + for (XmlProperty p : subprops) { + p.setParent(complexProperty); + PropertyCategory c = p.getCategory(); + if (c != PropertyCategory.ADVANCED) { + allAdvanced = false; + } + if (c == PropertyCategory.PREFERRED) { + isPreferred = true; + } + } + if (isPreferred) { + complexProperty.setCategory(PropertyCategory.PREFERRED); + } else if (allAdvanced) { + complexProperty.setCategory(PropertyCategory.PREFERRED); + } + } else { + // Add the individual properties (usually 1, sometimes 2 + for (int k = i; k < j; k++) { + collapsed.add(properties.get(k)); + } + } + + i = j - 1; // -1: compensate in advance for the for-loop adding 1 + } + + // Update the complex properties + for (String category : categoryToProperties.keySet()) { + Collection<Property> subProperties = categoryToProperties.get(category); + if (subProperties.size() > 1) { + ComplexProperty complex = categoryToProperty.get(category); + assert complex != null : category; + Property[] subArray = new Property[subProperties.size()]; + complex.setProperties(subProperties.toArray(subArray)); + complex.setPriority(subArray[0].getPriority()); + collapsed.add(complex); + + boolean allAdvanced = true; + boolean isPreferred = false; + for (Property p : subProperties) { + PropertyCategory c = p.getCategory(); + if (c != PropertyCategory.ADVANCED) { + allAdvanced = false; + } + if (c == PropertyCategory.PREFERRED) { + isPreferred = true; + } + } + if (isPreferred) { + complex.setCategory(PropertyCategory.PREFERRED); + } else if (allAdvanced) { + complex.setCategory(PropertyCategory.ADVANCED); + } + } else if (subProperties.size() == 1) { + collapsed.add(subProperties.iterator().next()); + } + } + + if (layoutProperties.size() > 0 || marginProperties != null) { + if (marginProperties != null) { + XmlProperty[] m = + marginProperties.toArray(new XmlProperty[marginProperties.size()]); + Property marginProperty = new ComplexProperty( + "Margins", + "[]", + m); + layoutProperties.add(marginProperty); + marginProperty.setPriority(PRIO_LAST); + + for (XmlProperty p : m) { + p.setParent(marginProperty); + } + } + Property[] l = layoutProperties.toArray(new Property[layoutProperties.size()]); + Arrays.sort(l, Property.PRIORITY); + Property property = new ComplexProperty( + "Layout Parameters", + "[]", + l); + for (Property p : l) { + if (p instanceof XmlProperty) { + ((XmlProperty) p).setParent(property); + } + } + property.setCategory(PropertyCategory.PREFERRED); + collapsed.add(property); + property.setPriority(PRIO_SECOND); + } + + if (deprecatedProperties != null && deprecatedProperties.size() > 0) { + Property property = new ComplexProperty( + "Deprecated", + "(Deprecated Properties)", + deprecatedProperties.toArray(new Property[deprecatedProperties.size()])); + property.setPriority(PRIO_LAST); + collapsed.add(property); + } + + Collections.sort(collapsed, Property.PRIORITY); + + return collapsed; + } + + @Nullable + GraphicalEditorPart getGraphicalEditor() { + return mGraphicalEditorPart; + } + + // HACK: This should be passed into each property instead + public Object getCurrentViewObject() { + return mCurrentViewCookie; + } + + public void setSortingMode(SortingMode sortingMode) { + mSortMode = sortingMode; + } + + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=388574 + public static Composite addWorkaround(Composite parent) { + if (ButtonPropertyEditorPresentation.isInWorkaround) { + Composite top = new Composite(parent, SWT.NONE); + top.setLayout(new GridLayout(1, false)); + Label label = new Label(top, SWT.WRAP); + label.setText( + "This dialog is shown instead of an inline text editor as a\n" + + "workaround for an Eclipse bug specific to OSX Mountain Lion.\n" + + "It should be fixed in Eclipse 4.3."); + label.setForeground(top.getDisplay().getSystemColor(SWT.COLOR_RED)); + GridData data = new GridData(); + data.grabExcessVerticalSpace = false; + data.grabExcessHorizontalSpace = false; + data.horizontalAlignment = GridData.FILL; + data.verticalAlignment = GridData.BEGINNING; + label.setLayoutData(data); + + Link link = new Link(top, SWT.NO_FOCUS); + link.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1)); + link.setText("<a>https://bugs.eclipse.org/bugs/show_bug.cgi?id=388574</a>"); + link.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + try { + IWorkbench workbench = PlatformUI.getWorkbench(); + IWebBrowser browser = workbench.getBrowserSupport().getExternalBrowser(); + browser.openURL(new URL(event.text)); + } catch (Exception e) { + String message = String.format( + "Could not open browser. Vist\n%1$s\ninstead.", + event.text); + MessageDialog.openError(((Link)event.getSource()).getShell(), + "Browser Error", message); + } + } + }); + + return top; + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyMetadata.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyMetadata.java new file mode 100644 index 000000000..b230aa99d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyMetadata.java @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2012 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.layout.properties; + +import static com.android.SdkConstants.ATTR_CONTENT_DESCRIPTION; +import static com.android.SdkConstants.ATTR_HINT; +import static com.android.SdkConstants.ATTR_TEXT; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; + +import java.util.HashSet; +import java.util.Set; + +/** Extra metadata about properties not available from the descriptors (yet) */ +class PropertyMetadata { + static boolean isAdvanced(@NonNull String name) { + return sAdvanced.contains(name); + } + + static boolean isPreferred(@NonNull String name) { + return sPreferred.contains(name); + } + + @Nullable + static String getCategory(@NonNull String name) { + //return sCategories.get(name); + assert false : "Disabled to save memory since this method is not currently used."; + return null; + } + + private static final int ADVANCED_MAP_SIZE = 134; + private static final Set<String> sAdvanced = new HashSet<String>(ADVANCED_MAP_SIZE); + static { + // This metadata about which attributes are "advanced" was generated as follows: + // First, I ran the sdk/attribute_stats project with the --list argument to dump out + // *all* referenced XML attributes found in layouts, run against a bunch of + // sample Android code (development/samples, packages/apps, vendor, etc. + // + // Then I iterated over the LayoutDescriptors' ViewElementDescriptors' + // AttributeDescriptors, and basically diffed the two: any attribute descriptor name + // which was *not* found in any of the representative layouts is added here + // as an advanced property. + // + // Then I manually edited in some attributes that were referenced in the sample + // layouts but which I still consider to be advanced: + // -- nothing right now + + // I also manually *removed* some entries from the below list: + // drawableBottom (the others, drawableTop, drawableLeft and drawableRight were all + // NOT on the list so keep bottom off for symmetry) + // rating (useful when you deal with a RatingsBar component) + + + // Automatically generated, see above: + sAdvanced.add("alwaysDrawnWithCache"); + sAdvanced.add("animationCache"); + sAdvanced.add("animationDuration"); + sAdvanced.add("animationResolution"); + sAdvanced.add("baseline"); + sAdvanced.add("bufferType"); + sAdvanced.add("calendarViewShown"); + sAdvanced.add("completionHint"); + sAdvanced.add("completionHintView"); + sAdvanced.add("completionThreshold"); + sAdvanced.add("cursorVisible"); + sAdvanced.add("dateTextAppearance"); + sAdvanced.add("dial"); + sAdvanced.add("digits"); + sAdvanced.add("disableChildrenWhenDisabled"); + sAdvanced.add("disabledAlpha"); + sAdvanced.add("drawableAlpha"); + sAdvanced.add("drawableEnd"); + sAdvanced.add("drawableStart"); + sAdvanced.add("drawingCacheQuality"); + sAdvanced.add("dropDownAnchor"); + sAdvanced.add("dropDownHeight"); + sAdvanced.add("dropDownHorizontalOffset"); + sAdvanced.add("dropDownSelector"); + sAdvanced.add("dropDownVerticalOffset"); + sAdvanced.add("dropDownWidth"); + sAdvanced.add("editorExtras"); + sAdvanced.add("ems"); + sAdvanced.add("endYear"); + sAdvanced.add("eventsInterceptionEnabled"); + sAdvanced.add("fadeDuration"); + sAdvanced.add("fadeEnabled"); + sAdvanced.add("fadeOffset"); + sAdvanced.add("fadeScrollbars"); + sAdvanced.add("filterTouchesWhenObscured"); + sAdvanced.add("firstDayOfWeek"); + sAdvanced.add("flingable"); + sAdvanced.add("focusedMonthDateColor"); + sAdvanced.add("foregroundInsidePadding"); + sAdvanced.add("format"); + sAdvanced.add("gestureColor"); + sAdvanced.add("gestureStrokeAngleThreshold"); + sAdvanced.add("gestureStrokeLengthThreshold"); + sAdvanced.add("gestureStrokeSquarenessThreshold"); + sAdvanced.add("gestureStrokeType"); + sAdvanced.add("gestureStrokeWidth"); + sAdvanced.add("hand_hour"); + sAdvanced.add("hand_minute"); + sAdvanced.add("hapticFeedbackEnabled"); + sAdvanced.add("id"); + sAdvanced.add("imeActionId"); + sAdvanced.add("imeActionLabel"); + sAdvanced.add("indeterminateDrawable"); + sAdvanced.add("indeterminateDuration"); + sAdvanced.add("inputMethod"); + sAdvanced.add("interpolator"); + sAdvanced.add("isScrollContainer"); + sAdvanced.add("keepScreenOn"); + sAdvanced.add("layerType"); + sAdvanced.add("layoutDirection"); + sAdvanced.add("maxDate"); + sAdvanced.add("minDate"); + sAdvanced.add("mode"); + sAdvanced.add("numeric"); + sAdvanced.add("paddingEnd"); + sAdvanced.add("paddingStart"); + sAdvanced.add("persistentDrawingCache"); + sAdvanced.add("phoneNumber"); + sAdvanced.add("popupBackground"); + sAdvanced.add("popupPromptView"); + sAdvanced.add("privateImeOptions"); + sAdvanced.add("quickContactWindowSize"); + //sAdvanced.add("rating"); + sAdvanced.add("requiresFadingEdge"); + sAdvanced.add("rotation"); + sAdvanced.add("rotationX"); + sAdvanced.add("rotationY"); + sAdvanced.add("saveEnabled"); + sAdvanced.add("scaleX"); + sAdvanced.add("scaleY"); + sAdvanced.add("scrollX"); + sAdvanced.add("scrollY"); + sAdvanced.add("scrollbarAlwaysDrawHorizontalTrack"); + sAdvanced.add("scrollbarDefaultDelayBeforeFade"); + sAdvanced.add("scrollbarFadeDuration"); + sAdvanced.add("scrollbarSize"); + sAdvanced.add("scrollbarThumbHorizontal"); + sAdvanced.add("scrollbarThumbVertical"); + sAdvanced.add("scrollbarTrackHorizontal"); + sAdvanced.add("scrollbarTrackVertical"); + sAdvanced.add("secondaryProgress"); + sAdvanced.add("selectedDateVerticalBar"); + sAdvanced.add("selectedWeekBackgroundColor"); + sAdvanced.add("selectionDivider"); + sAdvanced.add("selectionDividerHeight"); + sAdvanced.add("showWeekNumber"); + sAdvanced.add("shownWeekCount"); + sAdvanced.add("solidColor"); + sAdvanced.add("soundEffectsEnabled"); + sAdvanced.add("spinnerMode"); + sAdvanced.add("spinnersShown"); + sAdvanced.add("startYear"); + sAdvanced.add("switchMinWidth"); + sAdvanced.add("switchPadding"); + sAdvanced.add("switchTextAppearance"); + sAdvanced.add("textColorHighlight"); + sAdvanced.add("textCursorDrawable"); + sAdvanced.add("textDirection"); + sAdvanced.add("textEditNoPasteWindowLayout"); + sAdvanced.add("textEditPasteWindowLayout"); + sAdvanced.add("textEditSideNoPasteWindowLayout"); + sAdvanced.add("textEditSidePasteWindowLayout"); + sAdvanced.add("textEditSuggestionItemLayout"); + sAdvanced.add("textIsSelectable"); + sAdvanced.add("textOff"); + sAdvanced.add("textOn"); + sAdvanced.add("textScaleX"); + sAdvanced.add("textSelectHandle"); + sAdvanced.add("textSelectHandleLeft"); + sAdvanced.add("textSelectHandleRight"); + sAdvanced.add("thumbOffset"); + sAdvanced.add("thumbTextPadding"); + sAdvanced.add("tint"); + sAdvanced.add("track"); + sAdvanced.add("transformPivotX"); + sAdvanced.add("transformPivotY"); + sAdvanced.add("translationX"); + sAdvanced.add("translationY"); + sAdvanced.add("uncertainGestureColor"); + sAdvanced.add("unfocusedMonthDateColor"); + sAdvanced.add("unselectedAlpha"); + sAdvanced.add("verticalScrollbarPosition"); + sAdvanced.add("weekDayTextAppearance"); + sAdvanced.add("weekNumberColor"); + sAdvanced.add("weekSeparatorLineColor"); + + assert sAdvanced.size() == ADVANCED_MAP_SIZE : sAdvanced.size(); + + } + + private static final int PREFERRED_MAP_SIZE = 7; + private static final Set<String> sPreferred = new HashSet<String>(PREFERRED_MAP_SIZE); + static { + // Manual registrations of attributes that should be treated as preferred if + // they are available on a widget even if they don't show up in the top 10% of + // usages (which the view metadata provides) + sPreferred.add(ATTR_TEXT); + sPreferred.add(ATTR_CONTENT_DESCRIPTION); + sPreferred.add(ATTR_HINT); + sPreferred.add("indeterminate"); + sPreferred.add("progress"); + sPreferred.add("rating"); + sPreferred.add("max"); + assert sPreferred.size() == PREFERRED_MAP_SIZE : sPreferred.size(); + } + + /* + private static final int CATEGORY_MAP_SIZE = 62; + private static final Map<String, String> sCategories = + new HashMap<String, String>(CATEGORY_MAP_SIZE); + static { + sCategories.put("requiresFadingEdge", "Scrolling"); + sCategories.put("fadingEdgeLength", "Scrolling"); + sCategories.put("scrollbarSize", "Scrolling"); + sCategories.put("scrollbarThumbVertical", "Scrolling"); + sCategories.put("scrollbarThumbHorizontal", "Scrolling"); + sCategories.put("scrollbarTrackHorizontal", "Scrolling"); + sCategories.put("scrollbarTrackVertical", "Scrolling"); + sCategories.put("scrollbarAlwaysDrawHorizontalTrack", "Scrolling"); + sCategories.put("scrollbarAlwaysDrawVerticalTrack", "Scrolling"); + sCategories.put("scrollViewStyle", "Scrolling"); + sCategories.put("scrollbars", "Scrolling"); + sCategories.put("scrollingCache", "Scrolling"); + sCategories.put("scrollHorizontally", "Scrolling"); + sCategories.put("scrollbarFadeDuration", "Scrolling"); + sCategories.put("scrollbarDefaultDelayBeforeFade", "Scrolling"); + sCategories.put("fastScrollEnabled", "Scrolling"); + sCategories.put("smoothScrollbar", "Scrolling"); + sCategories.put("isScrollContainer", "Scrolling"); + sCategories.put("fadeScrollbars", "Scrolling"); + sCategories.put("overScrollMode", "Scrolling"); + sCategories.put("overScrollHeader", "Scrolling"); + sCategories.put("overScrollFooter", "Scrolling"); + sCategories.put("verticalScrollbarPosition", "Scrolling"); + sCategories.put("fastScrollAlwaysVisible", "Scrolling"); + sCategories.put("fastScrollThumbDrawable", "Scrolling"); + sCategories.put("fastScrollPreviewBackgroundLeft", "Scrolling"); + sCategories.put("fastScrollPreviewBackgroundRight", "Scrolling"); + sCategories.put("fastScrollTrackDrawable", "Scrolling"); + sCategories.put("fastScrollOverlayPosition", "Scrolling"); + sCategories.put("horizontalScrollViewStyle", "Scrolling"); + sCategories.put("fastScrollTextColor", "Scrolling"); + sCategories.put("scrollbarSize", "Scrolling"); + sCategories.put("scrollbarSize", "Scrolling"); + sCategories.put("scrollbarSize", "Scrolling"); + sCategories.put("scrollbarSize", "Scrolling"); + sCategories.put("scrollbarSize", "Scrolling"); + + // TODO: All the styles: radioButtonStyle, ratingBarStyle, progressBarStyle, ... + + sCategories.put("focusable", "Focus"); + sCategories.put("focusableInTouchMode", "Focus"); + sCategories.put("nextFocusLeft", "Focus"); + sCategories.put("nextFocusRight", "Focus"); + sCategories.put("nextFocusUp", "Focus"); + sCategories.put("nextFocusDown", "Focus"); + sCategories.put("descendantFocusability", "Focus"); + sCategories.put("selectAllOnFocus", "Focus"); + sCategories.put("nextFocusForward", "Focus"); + sCategories.put("colorFocusedHighlight", "Focus"); + + sCategories.put("rotation", "Transforms"); + sCategories.put("scrollX", "Transforms"); + sCategories.put("scrollY", "Transforms"); + sCategories.put("rotationX", "Transforms"); + sCategories.put("rotationY", "Transforms"); + sCategories.put("transformPivotX", "Transforms"); + sCategories.put("transformPivotY", "Transforms"); + sCategories.put("translationX", "Transforms"); + sCategories.put("translationY", "Transforms"); + sCategories.put("scaleX", "Transforms"); + sCategories.put("scaleY", "Transforms"); + + sCategories.put("width", "Size"); + sCategories.put("height", "Size"); + sCategories.put("minWidth", "Size"); + sCategories.put("minHeight", "Size"); + + sCategories.put("longClickable", "Clicks"); + sCategories.put("onClick", "Clicks"); + sCategories.put("clickable", "Clicks"); + sCategories.put("hapticFeedbackEnabled", "Clicks"); + + sCategories.put("duplicateParentState", "State"); + sCategories.put("addStatesFromChildren", "State"); + + assert sCategories.size() == CATEGORY_MAP_SIZE : sCategories.size(); + } + */ + +// private static final int PRIO_CLZ_LAYOUT = 1000; +// private static final int PRIO_CLZ_TEXT = 2000; +// private static final int PRIO_CLZ_DRAWABLE = 3000; +// private static final int PRIO_CLZ_ANIMATION = 4000; +// private static final int PRIO_CLZ_FOCUS = 5000; +// +// private static final int PRIORITY_MAP_SIZE = 100; +// private static final Map<String, Integer> sPriorities = +// new HashMap<String, Integer>(PRIORITY_MAP_SIZE); +// static { +// // TODO: I should put all the properties roughly based on their original order: this +// // will correspond to the rough order they came in with +// // TODO: How can I make similar complex properties show up adjacent; e.g. min and max +// sPriorities.put("min", PRIO_CLZ_LAYOUT); +// sPriorities.put("max", PRIO_CLZ_LAYOUT); +// +// assert sPriorities.size() == PRIORITY_MAP_SIZE : sPriorities.size(); +// } + + // TODO: Emit metadata into a file +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertySheetPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertySheetPage.java new file mode 100644 index 000000000..58fddc0ee --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertySheetPage.java @@ -0,0 +1,403 @@ +/* + * Copyright (C) 2012 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.layout.properties; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; +import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertyFactory.SortingMode; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IMenuListener; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IStatusLineManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.TreeSelection; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.part.Page; +import org.eclipse.ui.views.properties.IPropertySheetPage; +import org.eclipse.wb.internal.core.editor.structure.IPage; +import org.eclipse.wb.internal.core.model.property.Property; +import org.eclipse.wb.internal.core.model.property.table.IPropertyExceptionHandler; +import org.eclipse.wb.internal.core.model.property.table.PropertyTable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * Property sheet page used when the graphical layout editor is chosen + */ +public class PropertySheetPage extends Page + implements IPropertySheetPage, IUiUpdateListener, IPage { + private PropertyTable mPropertyTable; + private final GraphicalEditorPart mEditor; + private Property mActiveProperty; + private Action mDefaultValueAction; + private Action mShowAdvancedPropertiesAction; + private Action mSortAlphaAction; + private Action mCollapseAll; + private Action mExpandAll; + private List<CanvasViewInfo> mSelection; + + private static final String EXPAND_DISABLED_ICON = "expandall-disabled"; //$NON-NLS-1$ + private static final String EXPAND_ICON = "expandall"; //$NON-NLS-1$ + private static final String DEFAULT_ICON = "properties_default"; //$NON-NLS-1$ + private static final String ADVANCED_ICON = "filter_advanced_properties"; //$NON-NLS-1$ + private static final String ALPHA_ICON = "sort_alpha"; //$NON-NLS-1$ + // TODO: goto-definition.png + + /** + * Constructs a new {@link PropertySheetPage} associated with the given + * editor + * + * @param editor the editor associated with this property sheet page + */ + public PropertySheetPage(GraphicalEditorPart editor) { + mEditor = editor; + } + + private PropertyFactory getPropertyFactory() { + return mEditor.getPropertyFactory(); + } + + @Override + public void createControl(Composite parent) { + assert parent != null; + mPropertyTable = new PropertyTable(parent, SWT.NONE); + mPropertyTable.setExceptionHandler(new IPropertyExceptionHandler() { + @Override + public void handle(Throwable e) { + AdtPlugin.log(e, null); + } + }); + mPropertyTable.setDefaultCollapsedNames(Arrays.asList( + "Deprecated", + "Layout Parameters", + "Layout Parameters|Margins")); + + createActions(); + setPropertyTableContextMenu(); + } + + @Override + public void selectionChanged(IWorkbenchPart part, ISelection selection) { + if (selection instanceof TreeSelection + && mPropertyTable != null && !mPropertyTable.isDisposed()) { + TreeSelection treeSelection = (TreeSelection) selection; + + // We get a lot of repeated selection requests for the same selection + // as before, so try to eliminate these + if (mSelection != null) { + if (mSelection.isEmpty()) { + if (treeSelection.isEmpty()) { + return; + } + } else { + int selectionCount = treeSelection.size(); + if (selectionCount == mSelection.size()) { + boolean same = true; + Iterator<?> iterator = treeSelection.iterator(); + for (int i = 0, n = selectionCount; i < n && iterator.hasNext(); i++) { + Object next = iterator.next(); + if (next instanceof CanvasViewInfo) { + CanvasViewInfo info = (CanvasViewInfo) next; + if (info != mSelection.get(i)) { + same = false; + break; + } + } else { + same = false; + break; + } + } + if (same) { + return; + } + } + } + } + + stopTrackingSelection(); + + if (treeSelection.isEmpty()) { + mSelection = Collections.emptyList(); + } else { + int selectionCount = treeSelection.size(); + List<CanvasViewInfo> newSelection = new ArrayList<CanvasViewInfo>(selectionCount); + Iterator<?> iterator = treeSelection.iterator(); + while (iterator.hasNext()) { + Object next = iterator.next(); + if (next instanceof CanvasViewInfo) { + CanvasViewInfo info = (CanvasViewInfo) next; + newSelection.add(info); + } + } + mSelection = newSelection; + } + + startTrackingSelection(); + + refreshProperties(); + } + } + + @Override + public void dispose() { + stopTrackingSelection(); + super.dispose(); + } + + private void startTrackingSelection() { + if (mSelection != null && !mSelection.isEmpty()) { + for (CanvasViewInfo item : mSelection) { + UiViewElementNode node = item.getUiViewNode(); + if (node != null) { + node.addUpdateListener(this); + } + } + } + } + + private void stopTrackingSelection() { + if (mSelection != null && !mSelection.isEmpty()) { + for (CanvasViewInfo item : mSelection) { + UiViewElementNode node = item.getUiViewNode(); + if (node != null) { + node.removeUpdateListener(this); + } + } + } + mSelection = null; + } + + // Implements IUiUpdateListener + @Override + public void uiElementNodeUpdated(UiElementNode node, UiUpdateState state) { + refreshProperties(); + } + + @Override + public Control getControl() { + return mPropertyTable; + } + + @Override + public void setFocus() { + mPropertyTable.setFocus(); + } + + @Override + public void makeContributions(IMenuManager menuManager, + IToolBarManager toolBarManager, IStatusLineManager statusLineManager) { + toolBarManager.add(mShowAdvancedPropertiesAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mSortAlphaAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mDefaultValueAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mExpandAll); + toolBarManager.add(mCollapseAll); + toolBarManager.add(new Separator()); + } + + private void createActions() { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + IconFactory iconFactory = IconFactory.getInstance(); + + mExpandAll = new PropertySheetAction( + IAction.AS_PUSH_BUTTON, + "Expand All", + ACTION_EXPAND, + iconFactory.getImageDescriptor(EXPAND_ICON), + iconFactory.getImageDescriptor(EXPAND_DISABLED_ICON)); + + mCollapseAll = new PropertySheetAction( + IAction.AS_PUSH_BUTTON, + "Collapse All", + ACTION_COLLAPSE, + sharedImages.getImageDescriptor(ISharedImages.IMG_ELCL_COLLAPSEALL), + sharedImages.getImageDescriptor(ISharedImages.IMG_ELCL_COLLAPSEALL_DISABLED)); + + mShowAdvancedPropertiesAction = new PropertySheetAction( + IAction.AS_CHECK_BOX, + "Show Advanced Properties", + ACTION_SHOW_ADVANCED, + iconFactory.getImageDescriptor(ADVANCED_ICON), + null); + + mSortAlphaAction = new PropertySheetAction( + IAction.AS_CHECK_BOX, + "Sort Alphabetically", + ACTION_SORT_ALPHA, + iconFactory.getImageDescriptor(ALPHA_ICON), + null); + + mDefaultValueAction = new PropertySheetAction( + IAction.AS_PUSH_BUTTON, + "Restore Default Value", + ACTION_DEFAULT_VALUE, + iconFactory.getImageDescriptor(DEFAULT_ICON), + null); + + // Listen on the selection in the property sheet so we can update the + // Restore Default Value action + ISelectionChangedListener listener = new ISelectionChangedListener() { + @Override + public void selectionChanged(SelectionChangedEvent event) { + StructuredSelection selection = (StructuredSelection) event.getSelection(); + mActiveProperty = (Property) selection.getFirstElement(); + updateDefaultValueAction(); + } + }; + mPropertyTable.addSelectionChangedListener(listener); + } + + /** + * Updates the state of {@link #mDefaultValueAction}. + */ + private void updateDefaultValueAction() { + if (mActiveProperty != null) { + try { + mDefaultValueAction.setEnabled(mActiveProperty.isModified()); + } catch (Exception e) { + AdtPlugin.log(e, null); + } + } else { + mDefaultValueAction.setEnabled(false); + } + } + + /** + * Sets the context menu for {@link #mPropertyTable}. + */ + private void setPropertyTableContextMenu() { + final MenuManager manager = new MenuManager(); + manager.setRemoveAllWhenShown(true); + manager.addMenuListener(new IMenuListener() { + @Override + public void menuAboutToShow(IMenuManager m) { + // dispose items to avoid caching + for (MenuItem item : manager.getMenu().getItems()) { + item.dispose(); + } + // apply new items + fillContextMenu(); + } + + private void fillContextMenu() { + manager.add(mDefaultValueAction); + manager.add(mSortAlphaAction); + manager.add(mShowAdvancedPropertiesAction); + } + }); + + mPropertyTable.setMenu(manager.createContextMenu(mPropertyTable)); + } + + /** + * Shows {@link Property}'s of current objects. + */ + private void refreshProperties() { + PropertyFactory factory = getPropertyFactory(); + mPropertyTable.setInput(factory.getProperties(mSelection)); + updateDefaultValueAction(); + } + + // ---- Actions ---- + + private static final int ACTION_DEFAULT_VALUE = 1; + private static final int ACTION_SHOW_ADVANCED = 2; + private static final int ACTION_COLLAPSE = 3; + private static final int ACTION_EXPAND = 4; + private static final int ACTION_SORT_ALPHA = 5; + + private class PropertySheetAction extends Action { + private final int mAction; + + private PropertySheetAction(int style, String label, int action, + ImageDescriptor imageDesc, ImageDescriptor disabledImageDesc) { + super(label, style); + mAction = action; + setImageDescriptor(imageDesc); + if (disabledImageDesc != null) { + setDisabledImageDescriptor(disabledImageDesc); + } + setToolTipText(label); + } + + @Override + public void run() { + switch (mAction) { + case ACTION_COLLAPSE: { + mPropertyTable.collapseAll(); + break; + } + case ACTION_EXPAND: { + mPropertyTable.expandAll(); + break; + } + case ACTION_SHOW_ADVANCED: { + boolean show = mShowAdvancedPropertiesAction.isChecked(); + mPropertyTable.setShowAdvancedProperties(show); + break; + } + case ACTION_SORT_ALPHA: { + boolean isAlphabetical = mSortAlphaAction.isChecked(); + getPropertyFactory().setSortingMode( + isAlphabetical ? SortingMode.ALPHABETICAL : PropertyFactory.DEFAULT_MODE); + refreshProperties(); + break; + } + case ACTION_DEFAULT_VALUE: + try { + mActiveProperty.setValue(Property.UNKNOWN_VALUE); + } catch (Exception e) { + // Ignore warnings from setters + } + break; + default: + assert false : mAction; + } + } + } + + @Override + public void setToolBar(IToolBarManager toolBarManager) { + makeContributions(null, toolBarManager, null); + toolBarManager.update(false); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyValueCompleter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyValueCompleter.java new file mode 100644 index 000000000..f2bf07312 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyValueCompleter.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2012 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.layout.properties; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; + +class PropertyValueCompleter extends ValueCompleter { + private final XmlProperty mProperty; + + PropertyValueCompleter(XmlProperty property) { + mProperty = property; + } + + @Override + @Nullable + protected CommonXmlEditor getEditor() { + return mProperty.getXmlEditor(); + } + + @Override + @NonNull + protected AttributeDescriptor getDescriptor() { + return mProperty.getDescriptor(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/ResourceValueCompleter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/ResourceValueCompleter.java new file mode 100644 index 000000000..081ec8069 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/ResourceValueCompleter.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2012 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.layout.properties; + +import static com.android.SdkConstants.ANDROID_PKG; +import static com.android.SdkConstants.ANDROID_PREFIX; +import static com.android.SdkConstants.ANDROID_THEME_PREFIX; +import static com.android.SdkConstants.NEW_ID_PREFIX; +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.PREFIX_THEME_REF; + +import com.android.ide.common.resources.ResourceItem; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiResourceAttributeNode; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.resources.ResourceType; +import com.android.utils.SdkUtils; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.fieldassist.ContentProposal; +import org.eclipse.jface.fieldassist.IContentProposal; +import org.eclipse.jface.fieldassist.IContentProposalProvider; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Resource value completion for the given property + * <p> + * TODO: + * <ul> + * <li>also offer other values seen in the app + * <li>also offer previously set values for this property + * <li>also complete on properties + * </ul> + */ +class ResourceValueCompleter implements IContentProposalProvider { + protected final XmlProperty xmlProperty; + + ResourceValueCompleter(XmlProperty xmlProperty) { + this.xmlProperty = xmlProperty; + } + + @Override + public IContentProposal[] getProposals(String contents, int position) { + if (contents.startsWith(PREFIX_RESOURCE_REF)) { + CommonXmlEditor editor = this.xmlProperty.getXmlEditor(); + if (editor != null) { + String[] matches = computeResourceStringMatches( + editor, + this.xmlProperty.mDescriptor, contents.substring(0, position)); + List<IContentProposal> proposals = null; + if (matches != null && matches.length > 0) { + proposals = new ArrayList<IContentProposal>(matches.length); + for (String match : matches) { + proposals.add(new ContentProposal(match)); + } + return proposals.toArray(new IContentProposal[proposals.size()]); + } + } + } + + return new IContentProposal[0]; + } + + /** + * Similar to {@link UiResourceAttributeNode#computeResourceStringMatches} + * but computes complete results up front rather than dividing it up into + * smaller chunks like @{code @android:}, {@code string/}, and {@code ok}. + */ + static String[] computeResourceStringMatches(AndroidXmlEditor editor, + AttributeDescriptor attributeDescriptor, String prefix) { + List<String> results = new ArrayList<String>(200); + + // System matches: only do this if the value already matches at least @a, + // and doesn't start with something that can't possibly be @android + if (prefix.startsWith("@a") && //$NON-NLS-1$ + prefix.regionMatches(true /* ignoreCase */, 0, ANDROID_PREFIX, 0, + Math.min(prefix.length() - 1, ANDROID_PREFIX.length()))) { + AndroidTargetData data = editor.getTargetData(); + if (data != null) { + ResourceRepository repository = data.getFrameworkResources(); + addMatches(repository, prefix, true /* isSystem */, results); + } + } else if (prefix.startsWith("?") && //$NON-NLS-1$ + prefix.regionMatches(true /* ignoreCase */, 0, ANDROID_THEME_PREFIX, 0, + Math.min(prefix.length() - 1, ANDROID_THEME_PREFIX.length()))) { + AndroidTargetData data = editor.getTargetData(); + if (data != null) { + ResourceRepository repository = data.getFrameworkResources(); + addMatches(repository, prefix, true /* isSystem */, results); + } + } + + + // When completing project resources skip framework resources unless + // the prefix possibly completes both, such as "@an" which can match + // both the project resource @animator as well as @android:string + if (!prefix.startsWith("@and") && !prefix.startsWith("?and")) { //$NON-NLS-1$ //$NON-NLS-2$ + IProject project = editor.getProject(); + if (project != null) { + // get the resource repository for this project and the system resources. + ResourceManager manager = ResourceManager.getInstance(); + ResourceRepository repository = manager.getProjectResources(project); + if (repository != null) { + // We have a style name and a repository. Find all resources that match this + // type and recreate suggestions out of them. + addMatches(repository, prefix, false /* isSystem */, results); + } + + } + } + + if (attributeDescriptor != null) { + UiResourceAttributeNode.sortAttributeChoices(attributeDescriptor, results); + } else { + Collections.sort(results); + } + + return results.toArray(new String[results.size()]); + } + + private static void addMatches(ResourceRepository repository, String prefix, boolean isSystem, + List<String> results) { + int typeStart = isSystem + ? ANDROID_PREFIX.length() : PREFIX_RESOURCE_REF.length(); + + for (ResourceType type : repository.getAvailableResourceTypes()) { + if (prefix.regionMatches(typeStart, type.getName(), 0, + Math.min(type.getName().length(), prefix.length() - typeStart))) { + StringBuilder sb = new StringBuilder(); + if (prefix.length() == 0 || prefix.startsWith(PREFIX_RESOURCE_REF)) { + sb.append(PREFIX_RESOURCE_REF); + } else { + if (type != ResourceType.ATTR) { + continue; + } + sb.append(PREFIX_THEME_REF); + } + + if (type == ResourceType.ID && prefix.startsWith(NEW_ID_PREFIX)) { + sb.append('+'); + } + + if (isSystem) { + sb.append(ANDROID_PKG).append(':'); + } + + sb.append(type.getName()).append('/'); + String base = sb.toString(); + + int nameStart = typeStart + type.getName().length() + 1; // +1: add "/" divider + String namePrefix = + prefix.length() <= nameStart ? "" : prefix.substring(nameStart); + for (ResourceItem item : repository.getResourceItemsOfType(type)) { + String name = item.getName(); + if (SdkUtils.startsWithIgnoreCase(name, namePrefix)) { + results.add(base + name); + } + } + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/StringXmlPropertyDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/StringXmlPropertyDialog.java new file mode 100644 index 000000000..fb7e45902 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/StringXmlPropertyDialog.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2012 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.layout.properties; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.wb.internal.core.model.property.Property; +import org.eclipse.wb.internal.core.model.property.editor.string.StringPropertyDialog; + +class StringXmlPropertyDialog extends StringPropertyDialog { + StringXmlPropertyDialog(Shell parentShell, Property property) throws Exception { + super(parentShell, property); + } + + @Override + protected boolean isMultiLine() { + return false; + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite area = (Composite) super.createDialogArea(parent); + + Composite workaround = PropertyFactory.addWorkaround(area); + if (workaround != null) { + workaround.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1)); + } + + return area; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/ValueCompleter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/ValueCompleter.java new file mode 100644 index 000000000..5559349fc --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/ValueCompleter.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2012 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.layout.properties; + +import static com.android.SdkConstants.ATTR_TEXT_SIZE; +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.PREFIX_THEME_REF; +import static com.android.SdkConstants.UNIT_DP; +import static com.android.SdkConstants.UNIT_SP; +import static com.android.SdkConstants.VALUE_FALSE; +import static com.android.SdkConstants.VALUE_TRUE; +import static com.android.ide.common.api.IAttributeInfo.Format.BOOLEAN; +import static com.android.ide.common.api.IAttributeInfo.Format.DIMENSION; +import static com.android.ide.common.api.IAttributeInfo.Format.ENUM; +import static com.android.ide.common.api.IAttributeInfo.Format.FLAG; +import static com.android.ide.common.api.IAttributeInfo.Format.FLOAT; +import static com.android.ide.common.api.IAttributeInfo.Format.INTEGER; +import static com.android.ide.common.api.IAttributeInfo.Format.REFERENCE; +import static com.android.ide.common.api.IAttributeInfo.Format.STRING; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.api.IAttributeInfo; +import com.android.ide.common.api.IAttributeInfo.Format; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; +import com.android.utils.SdkUtils; + +import org.eclipse.jface.fieldassist.ContentProposal; +import org.eclipse.jface.fieldassist.IContentProposal; +import org.eclipse.jface.fieldassist.IContentProposalProvider; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +/** + * An {@link IContentProposalProvider} which completes possible property values + * for Android properties, completing resource strings, flag values, enum + * values, as well as dimension units. + */ +abstract class ValueCompleter implements IContentProposalProvider { + @Nullable + protected abstract CommonXmlEditor getEditor(); + + @NonNull + protected abstract AttributeDescriptor getDescriptor(); + + @Override + public IContentProposal[] getProposals(String contents, int position) { + AttributeDescriptor descriptor = getDescriptor(); + IAttributeInfo info = descriptor.getAttributeInfo(); + EnumSet<Format> formats = info.getFormats(); + + List<IContentProposal> proposals = new ArrayList<IContentProposal>(); + + String prefix = contents; // TODO: Go back to position inside the array? + + // TODO: If the user is typing in a number, or a number plus a prefix of a dimension unit, + // then propose that number plus the completed dimension unit (using sp for text, dp + // for other properties and maybe both if I'm not sure) + if (formats.contains(STRING) + && !contents.isEmpty() + && (formats.size() > 1 && formats.contains(REFERENCE) || + formats.size() > 2) + && !contents.startsWith(PREFIX_RESOURCE_REF) + && !contents.startsWith(PREFIX_THEME_REF)) { + proposals.add(new ContentProposal(contents)); + } + + if (!contents.isEmpty() && Character.isDigit(contents.charAt(0)) + && (formats.contains(DIMENSION) + || formats.contains(INTEGER) + || formats.contains(FLOAT))) { + StringBuilder sb = new StringBuilder(); + for (int i = 0, n = contents.length(); i < n; i++) { + char c = contents.charAt(i); + if (Character.isDigit(c)) { + sb.append(c); + } else { + break; + } + } + + String number = sb.toString(); + if (formats.contains(Format.DIMENSION)) { + if (descriptor.getXmlLocalName().equals(ATTR_TEXT_SIZE)) { + proposals.add(new ContentProposal(number + UNIT_SP)); + } + proposals.add(new ContentProposal(number + UNIT_DP)); + } else if (formats.contains(Format.INTEGER)) { + proposals.add(new ContentProposal(number)); + } + // Perhaps offer other units too -- see AndroidContentAssist.sDimensionUnits + } + + if (formats.contains(REFERENCE) || contents.startsWith(PREFIX_RESOURCE_REF) + || contents.startsWith(PREFIX_THEME_REF)) { + CommonXmlEditor editor = getEditor(); + if (editor != null) { + String[] matches = ResourceValueCompleter.computeResourceStringMatches( + editor, + descriptor, contents.substring(0, position)); + for (String match : matches) { + proposals.add(new ContentProposal(match)); + } + } + } + + if (formats.contains(FLAG)) { + String[] values = info.getFlagValues(); + if (values != null) { + // Flag completion + int flagStart = prefix.lastIndexOf('|'); + String prepend = null; + if (flagStart != -1) { + prepend = prefix.substring(0, flagStart + 1); + prefix = prefix.substring(flagStart + 1).trim(); + } + + boolean exactMatch = false; + for (String value : values) { + if (prefix.equals(value)) { + exactMatch = true; + proposals.add(new ContentProposal(contents)); + + break; + } + } + + if (exactMatch) { + prepend = contents + '|'; + prefix = ""; + } + + for (String value : values) { + if (SdkUtils.startsWithIgnoreCase(value, prefix)) { + if (prepend != null && prepend.contains(value)) { + continue; + } + String match; + if (prepend != null) { + match = prepend + value; + } else { + match = value; + } + proposals.add(new ContentProposal(match)); + } + } + } + } else if (formats.contains(ENUM)) { + String[] values = info.getEnumValues(); + if (values != null) { + for (String value : values) { + if (SdkUtils.startsWithIgnoreCase(value, prefix)) { + proposals.add(new ContentProposal(value)); + } + } + + for (String value : values) { + if (!SdkUtils.startsWithIgnoreCase(value, prefix)) { + proposals.add(new ContentProposal(value)); + } + } + } + } else if (formats.contains(BOOLEAN)) { + proposals.add(new ContentProposal(VALUE_TRUE)); + proposals.add(new ContentProposal(VALUE_FALSE)); + } + + return proposals.toArray(new IContentProposal[proposals.size()]); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlProperty.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlProperty.java new file mode 100644 index 000000000..a320b682d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlProperty.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2012 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.layout.properties; + +import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN; +import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.api.IAttributeInfo; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; +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.layout.gle2.GraphicalEditorPart; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; + +import org.eclipse.jface.fieldassist.IContentProposal; +import org.eclipse.jface.fieldassist.IContentProposalProvider; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.views.properties.IPropertyDescriptor; +import org.eclipse.wb.internal.core.model.property.Property; +import org.eclipse.wb.internal.core.model.property.editor.PropertyEditor; +import org.eclipse.wb.internal.core.model.property.table.PropertyTooltipProvider; +import org.eclipse.wb.internal.core.model.property.table.PropertyTooltipTextProvider; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; + +import java.util.Map; + +/** + * An Android XML property + */ +class XmlProperty extends Property { + private PropertyFactory mFactory; + final AttributeDescriptor mDescriptor; + private UiViewElementNode mNode; + private Property mParent; + + XmlProperty( + @NonNull PropertyEditor editor, + @NonNull PropertyFactory factory, + @NonNull UiViewElementNode node, + @NonNull AttributeDescriptor descriptor) { + super(editor); + mFactory = factory; + mNode = node; + mDescriptor = descriptor; + } + + @NonNull + public PropertyFactory getFactory() { + return mFactory; + } + + @NonNull + public UiViewElementNode getNode() { + return mNode; + } + + @NonNull + public AttributeDescriptor getDescriptor() { + return mDescriptor; + } + + @Override + @NonNull + public String getName() { + return mDescriptor.getXmlLocalName(); + } + + @Override + @NonNull + public String getTitle() { + String name = mDescriptor.getXmlLocalName(); + int nameLength = name.length(); + + if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) { + if (name.startsWith(ATTR_LAYOUT_MARGIN) + && nameLength > ATTR_LAYOUT_MARGIN.length()) { + name = name.substring(ATTR_LAYOUT_MARGIN.length()); + } else { + name = name.substring(ATTR_LAYOUT_RESOURCE_PREFIX.length()); + } + } + + // Capitalize + name = DescriptorsUtils.capitalize(name); + + // If we're nested within a complex property, say "Line Spacing", don't + // include "Line Spacing " as a prefix for each property here + if (mParent != null) { + String parentTitle = mParent.getTitle(); + if (name.startsWith(parentTitle)) { + int parentTitleLength = parentTitle.length(); + if (parentTitleLength < nameLength) { + if (nameLength > parentTitleLength && + Character.isWhitespace(name.charAt(parentTitleLength))) { + parentTitleLength++; + } + name = name.substring(parentTitleLength); + } + } + } + + return name; + } + + @Override + public <T> T getAdapter(Class<T> adapter) { + // tooltip + if (adapter == PropertyTooltipProvider.class) { + return adapter.cast(new PropertyTooltipTextProvider() { + @Override + protected String getText(Property p) throws Exception { + if (mDescriptor instanceof IPropertyDescriptor) { + IPropertyDescriptor d = (IPropertyDescriptor) mDescriptor; + return d.getDescription(); + } + + return null; + } + }); + } else if (adapter == IContentProposalProvider.class) { + IAttributeInfo info = mDescriptor.getAttributeInfo(); + if (info != null) { + return adapter.cast(new PropertyValueCompleter(this)); + } + // Fallback: complete values on resource values + return adapter.cast(new ResourceValueCompleter(this)); + } else if (adapter == ILabelProvider.class) { + return adapter.cast(new LabelProvider() { + @Override + public Image getImage(Object element) { + return AdtPlugin.getAndroidLogo(); + } + + @Override + public String getText(Object element) { + return ((IContentProposal) element).getLabel(); + } + }); + } + return super.getAdapter(adapter); + } + + @Override + public boolean isModified() throws Exception { + Object s = null; + try { + Element element = (Element) mNode.getXmlNode(); + if (element == null) { + return false; + } + String name = mDescriptor.getXmlLocalName(); + String uri = mDescriptor.getNamespaceUri(); + if (uri != null) { + return element.hasAttributeNS(uri, name); + } else { + return element.hasAttribute(name); + } + } catch (Exception e) { + // pass + } + return s != null && s.toString().length() > 0; + } + + @Nullable + public String getStringValue() { + Element element = (Element) mNode.getXmlNode(); + if (element == null) { + return null; + } + String name = mDescriptor.getXmlLocalName(); + String uri = mDescriptor.getNamespaceUri(); + Attr attr; + if (uri != null) { + attr = element.getAttributeNodeNS(uri, name); + } else { + attr = element.getAttributeNode(name); + } + if (attr != null) { + return attr.getValue(); + } + + Object viewObject = getFactory().getCurrentViewObject(); + if (viewObject != null) { + GraphicalEditorPart graphicalEditor = getGraphicalEditor(); + if (graphicalEditor == null) { + return null; + } + ViewHierarchy views = graphicalEditor.getCanvasControl().getViewHierarchy(); + Map<String, String> defaultProperties = views.getDefaultProperties(viewObject); + if (defaultProperties != null) { + return defaultProperties.get(name); + } + } + + return null; + } + + @Override + @Nullable + public Object getValue() throws Exception { + return getStringValue(); + } + + @Override + public void setValue(Object value) throws Exception { + CommonXmlEditor editor = getXmlEditor(); + if (editor == null) { + return; + } + final String attribute = mDescriptor.getXmlLocalName(); + final String xmlValue = value != null && value != UNKNOWN_VALUE ? value.toString() : null; + editor.wrapUndoEditXmlModel( + String.format("Set \"%1$s\" to \"%2$s\"", attribute, xmlValue), + new Runnable() { + @Override + public void run() { + mNode.setAttributeValue(attribute, + mDescriptor.getNamespaceUri(), xmlValue, true /*override*/); + mNode.commitDirtyAttributesToXml(); + } + }); + } + + @Override + @NonNull + public Property getComposite(Property[] properties) { + return XmlPropertyComposite.create(properties); + } + + @Nullable + GraphicalEditorPart getGraphicalEditor() { + return mFactory.getGraphicalEditor(); + } + + @Nullable + CommonXmlEditor getXmlEditor() { + GraphicalEditorPart graphicalEditor = getGraphicalEditor(); + if (graphicalEditor != null) { + return graphicalEditor.getEditorDelegate().getEditor(); + } + + return null; + } + + @Nullable + public Property getParent() { + return mParent; + } + + public void setParent(@Nullable Property parent) { + mParent = parent; + } + + @Override + public String toString() { + return getName() + ":" + getPriority(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyComposite.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyComposite.java new file mode 100644 index 000000000..af9e13b3e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyComposite.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2012 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.layout.properties; + +import com.android.annotations.NonNull; +import com.google.common.base.Objects; + +import org.eclipse.wb.internal.core.model.property.Property; + +import java.util.Arrays; + +/** + * Property holding multiple instances of the same {@link XmlProperty} (but + * bound to difference objects. This is used when multiple objects are selected + * in the layout editor and the common properties are shown; editing a value + * will (via {@link #setValue(Object)}) set it on all selected objects. + * <p> + * Similar to + * org.eclipse.wb.internal.core.model.property.GenericPropertyComposite + */ +class XmlPropertyComposite extends XmlProperty { + private static final Object NO_VALUE = new Object(); + + private final XmlProperty[] mProperties; + + public XmlPropertyComposite(XmlProperty primary, XmlProperty[] properties) { + super( + primary.getEditor(), + primary.getFactory(), + primary.getNode(), + primary.getDescriptor()); + mProperties = properties; + } + + @Override + @NonNull + public String getTitle() { + return mProperties[0].getTitle(); + } + + @Override + public int hashCode() { + return mProperties.length; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof XmlPropertyComposite) { + XmlPropertyComposite property = (XmlPropertyComposite) obj; + return Arrays.equals(mProperties, property.mProperties); + } + + return false; + } + + @Override + public boolean isModified() throws Exception { + for (Property property : mProperties) { + if (property.isModified()) { + return true; + } + } + + return false; + } + + @Override + public Object getValue() throws Exception { + Object value = NO_VALUE; + for (Property property : mProperties) { + Object propertyValue = property.getValue(); + if (value == NO_VALUE) { + value = propertyValue; + } else if (!Objects.equal(value, propertyValue)) { + return UNKNOWN_VALUE; + } + } + + return value; + } + + @Override + public void setValue(final Object value) throws Exception { + // TBD: Wrap in ExecutionUtils.run? + for (Property property : mProperties) { + property.setValue(value); + } + } + + @NonNull + public static XmlPropertyComposite create(Property... properties) { + // Cast from Property into XmlProperty + XmlProperty[] xmlProperties = new XmlProperty[properties.length]; + for (int i = 0; i < properties.length; i++) { + Property property = properties[i]; + xmlProperties[i] = (XmlProperty) property; + } + + XmlPropertyComposite composite = new XmlPropertyComposite(xmlProperties[0], xmlProperties); + composite.setCategory(xmlProperties[0].getCategory()); + return composite; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyEditor.java new file mode 100644 index 000000000..87fb0e6ed --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyEditor.java @@ -0,0 +1,548 @@ +/* + * Copyright (C) 2012 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.layout.properties; + +import static com.android.SdkConstants.ANDROID_PREFIX; +import static com.android.SdkConstants.ANDROID_THEME_PREFIX; +import static com.android.SdkConstants.ATTR_ID; +import static com.android.SdkConstants.DOT_PNG; +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.NEW_ID_PREFIX; +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.PREFIX_THEME_REF; +import static com.android.ide.common.layout.BaseViewRule.stripIdPrefix; + +import com.android.annotations.NonNull; +import com.android.ide.common.api.IAttributeInfo; +import com.android.ide.common.api.IAttributeInfo.Format; +import com.android.ide.common.layout.BaseViewRule; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.common.resources.ResourceResolver; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderService; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceWizard; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult; +import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; +import com.android.ide.eclipse.adt.internal.ui.ReferenceChooserDialog; +import com.android.ide.eclipse.adt.internal.ui.ResourceChooser; +import com.android.ide.eclipse.adt.internal.ui.ResourcePreviewHelper; +import com.android.resources.ResourceType; +import com.google.common.collect.Maps; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.MessageDialogWithToggle; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.wb.draw2d.IColorConstants; +import org.eclipse.wb.internal.core.model.property.Property; +import org.eclipse.wb.internal.core.model.property.editor.AbstractTextPropertyEditor; +import org.eclipse.wb.internal.core.model.property.editor.presentation.ButtonPropertyEditorPresentation; +import org.eclipse.wb.internal.core.model.property.editor.presentation.PropertyEditorPresentation; +import org.eclipse.wb.internal.core.model.property.table.PropertyTable; +import org.eclipse.wb.internal.core.utils.ui.DrawUtils; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +import javax.imageio.ImageIO; + +/** + * Special property editor used for the {@link XmlProperty} instances which handles + * editing the XML properties, rendering defaults by looking up the actual colors and images, + */ +class XmlPropertyEditor extends AbstractTextPropertyEditor { + public static final XmlPropertyEditor INSTANCE = new XmlPropertyEditor(); + private static final int SAMPLE_SIZE = 10; + private static final int SAMPLE_MARGIN = 3; + + protected XmlPropertyEditor() { + } + + private final PropertyEditorPresentation mPresentation = + new ButtonPropertyEditorPresentation() { + @Override + protected void onClick(PropertyTable propertyTable, Property property) throws Exception { + openDialog(propertyTable, property); + } + }; + + @Override + public PropertyEditorPresentation getPresentation() { + return mPresentation; + } + + @Override + public String getText(Property property) throws Exception { + Object value = property.getValue(); + if (value instanceof String) { + return (String) value; + } + return null; + } + + @Override + protected String getEditorText(Property property) throws Exception { + return getText(property); + } + + @Override + public void paint(Property property, GC gc, int x, int y, int width, int height) + throws Exception { + String text = getText(property); + if (text != null) { + ResourceValue resValue = null; + String resolvedText = null; + + // TODO: Use the constants for @, ?, @android: etc + if (text.startsWith("@") || text.startsWith("?")) { //$NON-NLS-1$ //$NON-NLS-2$ + // Yes, try to resolve it in order to show better info + XmlProperty xmlProperty = (XmlProperty) property; + GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); + if (graphicalEditor != null) { + ResourceResolver resolver = graphicalEditor.getResourceResolver(); + boolean isFramework = text.startsWith(ANDROID_PREFIX) + || text.startsWith(ANDROID_THEME_PREFIX); + resValue = resolver.findResValue(text, isFramework); + while (resValue != null && resValue.getValue() != null) { + String value = resValue.getValue(); + if (value.startsWith(PREFIX_RESOURCE_REF) + || value.startsWith(PREFIX_THEME_REF)) { + // TODO: do I have to strip off the @ too? + isFramework = isFramework + || value.startsWith(ANDROID_PREFIX) + || value.startsWith(ANDROID_THEME_PREFIX); + ResourceValue v = resolver.findResValue(text, isFramework); + if (v != null && !value.equals(v.getValue())) { + resValue = v; + } else { + break; + } + } else { + break; + } + } + } + } else if (text.startsWith("#") && text.matches("#\\p{XDigit}+")) { //$NON-NLS-1$ + resValue = new ResourceValue(ResourceType.COLOR, property.getName(), text, false); + } + + if (resValue != null && resValue.getValue() != null) { + String value = resValue.getValue(); + // Decide whether it's a color, an image, a nine patch etc + // and decide how to render it + if (value.startsWith("#") || value.endsWith(DOT_XML) //$NON-NLS-1$ + && value.contains("res/color")) { //$NON-NLS-1$ // TBD: File.separator? + XmlProperty xmlProperty = (XmlProperty) property; + GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); + if (graphicalEditor != null) { + ResourceResolver resolver = graphicalEditor.getResourceResolver(); + RGB rgb = ResourceHelper.resolveColor(resolver, resValue); + if (rgb != null) { + Color color = new Color(gc.getDevice(), rgb); + // draw color sample + Color oldBackground = gc.getBackground(); + Color oldForeground = gc.getForeground(); + try { + int width_c = SAMPLE_SIZE; + int height_c = SAMPLE_SIZE; + int x_c = x; + int y_c = y + (height - height_c) / 2; + // update rest bounds + int delta = SAMPLE_SIZE + SAMPLE_MARGIN; + x += delta; + width -= delta; + // fill + gc.setBackground(color); + gc.fillRectangle(x_c, y_c, width_c, height_c); + // draw line + gc.setForeground(IColorConstants.gray); + gc.drawRectangle(x_c, y_c, width_c, height_c); + } finally { + gc.setBackground(oldBackground); + gc.setForeground(oldForeground); + } + color.dispose(); + } + } + } else { + Image swtImage = null; + if (value.endsWith(DOT_XML) && value.contains("res/drawable")) { // TBD: Filesep? + Map<String, Image> cache = getImageCache(property); + swtImage = cache.get(value); + if (swtImage == null) { + XmlProperty xmlProperty = (XmlProperty) property; + GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); + RenderService service = RenderService.create(graphicalEditor); + service.setOverrideRenderSize(SAMPLE_SIZE, SAMPLE_SIZE); + BufferedImage drawable = service.renderDrawable(resValue); + if (drawable != null) { + swtImage = SwtUtils.convertToSwt(gc.getDevice(), drawable, + true /*transferAlpha*/, -1); + cache.put(value, swtImage); + } + } + } else if (value.endsWith(DOT_PNG)) { + // TODO: 9-patch handling? + //if (text.endsWith(DOT_9PNG)) { + // // 9-patch image: How do we paint this? + // URL url = new File(text).toURI().toURL(); + // NinePatch ninepatch = NinePatch.load(url, false /* ?? */); + // BufferedImage image = ninepatch.getImage(); + //} + Map<String, Image> cache = getImageCache(property); + swtImage = cache.get(value); + if (swtImage == null) { + File file = new File(value); + if (file.exists()) { + try { + BufferedImage awtImage = ImageIO.read(file); + if (awtImage != null && awtImage.getWidth() > 0 + && awtImage.getHeight() > 0) { + awtImage = ImageUtils.cropBlank(awtImage, null); + if (awtImage != null) { + // Scale image + int imageWidth = awtImage.getWidth(); + int imageHeight = awtImage.getHeight(); + int maxWidth = 3 * height; + + if (imageWidth > maxWidth || imageHeight > height) { + double scale = height / (double) imageHeight; + int scaledWidth = (int) (imageWidth * scale); + if (scaledWidth > maxWidth) { + scale = maxWidth / (double) imageWidth; + } + awtImage = ImageUtils.scale(awtImage, scale, + scale); + } + swtImage = SwtUtils.convertToSwt(gc.getDevice(), + awtImage, true /*transferAlpha*/, -1); + } + } + } catch (IOException e) { + AdtPlugin.log(e, value); + } + } + cache.put(value, swtImage); + } + + } else if (value != null) { + // It's a normal string: if different from the text, paint + // it in parentheses, e.g. + // @string/foo: Foo Bar (probably cropped) + if (!value.equals(text) && !value.equals("@null")) { //$NON-NLS-1$ + resolvedText = value; + } + } + + if (swtImage != null) { + // Make a square the size of the height + ImageData imageData = swtImage.getImageData(); + int imageWidth = imageData.width; + int imageHeight = imageData.height; + if (imageWidth > 0 && imageHeight > 0) { + gc.drawImage(swtImage, x, y + (height - imageHeight) / 2); + int delta = imageWidth + SAMPLE_MARGIN; + x += delta; + width -= delta; + } + } + } + } + + DrawUtils.drawStringCV(gc, text, x, y, width, height); + + if (resolvedText != null && resolvedText.length() > 0) { + Point size = gc.stringExtent(text); + x += size.x; + width -= size.x; + + x += SAMPLE_MARGIN; + width -= SAMPLE_MARGIN; + + if (width > 0) { + Color oldForeground = gc.getForeground(); + try { + gc.setForeground(PropertyTable.COLOR_PROPERTY_FG_DEFAULT); + DrawUtils.drawStringCV(gc, '(' + resolvedText + ')', x, y, width, height); + } finally { + gc.setForeground(oldForeground); + } + } + } + } + } + + @Override + protected boolean setEditorText(Property property, String text) throws Exception { + Object oldValue = property.getValue(); + String old = oldValue != null ? oldValue.toString() : null; + + // If users enters a new id without specifying the @id/@+id prefix, insert it + boolean isId = isIdProperty(property); + if (isId && !text.startsWith(PREFIX_RESOURCE_REF)) { + text = NEW_ID_PREFIX + text; + } + + // Handle id refactoring: if you change an id, may want to update references too. + // Ask user. + if (isId && property instanceof XmlProperty + && old != null && !old.isEmpty() + && text != null && !text.isEmpty() + && !text.equals(old)) { + XmlProperty xmlProperty = (XmlProperty) property; + IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + String refactorPref = store.getString(AdtPrefs.PREFS_REFACTOR_IDS); + boolean performRefactor = false; + Shell shell = AdtPlugin.getShell(); + if (refactorPref == null + || refactorPref.isEmpty() + || refactorPref.equals(MessageDialogWithToggle.PROMPT)) { + MessageDialogWithToggle dialog = + MessageDialogWithToggle.openYesNoCancelQuestion( + shell, + "Update References?", + "Update all references as well? " + + "This will update all XML references and Java R field references.", + "Do not show again", + false, + store, + AdtPrefs.PREFS_REFACTOR_IDS); + switch (dialog.getReturnCode()) { + case IDialogConstants.CANCEL_ID: + return false; + case IDialogConstants.YES_ID: + performRefactor = true; + break; + case IDialogConstants.NO_ID: + performRefactor = false; + break; + } + } else { + performRefactor = refactorPref.equals(MessageDialogWithToggle.ALWAYS); + } + if (performRefactor) { + CommonXmlEditor xmlEditor = xmlProperty.getXmlEditor(); + if (xmlEditor != null) { + IProject project = xmlEditor.getProject(); + if (project != null && shell != null) { + RenameResourceWizard.renameResource(shell, project, + ResourceType.ID, stripIdPrefix(old), stripIdPrefix(text), false); + } + } + } + } + + property.setValue(text); + + return true; + } + + private static boolean isIdProperty(Property property) { + XmlProperty xmlProperty = (XmlProperty) property; + return xmlProperty.getDescriptor().getXmlLocalName().equals(ATTR_ID); + } + + private void openDialog(PropertyTable propertyTable, Property property) throws Exception { + XmlProperty xmlProperty = (XmlProperty) property; + IAttributeInfo attributeInfo = xmlProperty.getDescriptor().getAttributeInfo(); + + if (isIdProperty(property)) { + Object value = xmlProperty.getValue(); + if (value != null && !value.toString().isEmpty()) { + GraphicalEditorPart editor = xmlProperty.getGraphicalEditor(); + if (editor != null) { + LayoutCanvas canvas = editor.getCanvasControl(); + SelectionManager manager = canvas.getSelectionManager(); + + NodeProxy primary = canvas.getNodeFactory().create(xmlProperty.getNode()); + if (primary != null) { + RenameResult result = manager.performRename(primary, null); + if (result.isCanceled()) { + return; + } else if (!result.isUnavailable()) { + String name = result.getName(); + String id = NEW_ID_PREFIX + BaseViewRule.stripIdPrefix(name); + xmlProperty.setValue(id); + return; + } + } + } + } + + // When editing the id attribute, don't offer a resource chooser: usually + // you want to enter a *new* id here + attributeInfo = null; + } + + boolean referenceAllowed = false; + if (attributeInfo != null) { + EnumSet<Format> formats = attributeInfo.getFormats(); + ResourceType type = null; + List<ResourceType> types = null; + if (formats.contains(Format.FLAG)) { + String[] flagValues = attributeInfo.getFlagValues(); + if (flagValues != null) { + FlagXmlPropertyDialog dialog = + new FlagXmlPropertyDialog(propertyTable.getShell(), + "Select Flag Values", false /* radio */, + flagValues, xmlProperty); + + dialog.open(); + return; + } + } else if (formats.contains(Format.ENUM)) { + String[] enumValues = attributeInfo.getEnumValues(); + if (enumValues != null) { + FlagXmlPropertyDialog dialog = + new FlagXmlPropertyDialog(propertyTable.getShell(), + "Select Enum Value", true /* radio */, + enumValues, xmlProperty); + dialog.open(); + return; + } + } else { + for (Format format : formats) { + ResourceType t = format.getResourceType(); + if (t != null) { + if (type != null) { + if (types == null) { + types = new ArrayList<ResourceType>(); + types.add(type); + } + types.add(t); + } + type = t; + } else if (format == Format.REFERENCE) { + referenceAllowed = true; + } + } + } + if (types != null || referenceAllowed) { + // Multiple resource types (such as string *and* boolean): + // just use a reference chooser + GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); + if (graphicalEditor != null) { + LayoutEditorDelegate delegate = graphicalEditor.getEditorDelegate(); + IProject project = delegate.getEditor().getProject(); + if (project != null) { + // get the resource repository for this project and the system resources. + ResourceRepository projectRepository = + ResourceManager.getInstance().getProjectResources(project); + Shell shell = AdtPlugin.getShell(); + ReferenceChooserDialog dlg = new ReferenceChooserDialog( + project, + projectRepository, + shell); + dlg.setPreviewHelper(new ResourcePreviewHelper(dlg, graphicalEditor)); + + String currentValue = (String) property.getValue(); + dlg.setCurrentResource(currentValue); + + if (dlg.open() == Window.OK) { + String resource = dlg.getCurrentResource(); + if (resource != null) { + // Returns null for cancel, "" for clear and otherwise a new value + if (resource.length() > 0) { + property.setValue(resource); + } else { + property.setValue(null); + } + } + } + + return; + } + } + } else if (type != null) { + // Single resource type: use a resource chooser + GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); + if (graphicalEditor != null) { + String currentValue = (String) property.getValue(); + // TODO: Add validator factory? + String resource = ResourceChooser.chooseResource(graphicalEditor, + type, currentValue, null /* validator */); + // Returns null for cancel, "" for clear and otherwise a new value + if (resource != null) { + if (resource.length() > 0) { + property.setValue(resource); + } else { + property.setValue(null); + } + } + } + + return; + } + } + + // Fallback: Just use a plain string editor + StringXmlPropertyDialog dialog = + new StringXmlPropertyDialog(propertyTable.getShell(), property); + if (dialog.open() == Window.OK) { + // TODO: Do I need to activate? + } + } + + /** Qualified name for the per-project persistent property include-map */ + private final static QualifiedName CACHE_NAME = new QualifiedName(AdtPlugin.PLUGIN_ID, + "property-images");//$NON-NLS-1$ + + @NonNull + private static Map<String, Image> getImageCache(@NonNull Property property) { + XmlProperty xmlProperty = (XmlProperty) property; + GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); + IProject project = graphicalEditor.getProject(); + try { + Map<String, Image> cache = (Map<String, Image>) project.getSessionProperty(CACHE_NAME); + if (cache == null) { + cache = Maps.newHashMap(); + project.setSessionProperty(CACHE_NAME, cache); + } + + return cache; + } catch (CoreException e) { + AdtPlugin.log(e, null); + return Maps.newHashMap(); + } + } +} |