Home | History | Annotate | Download | only in properties
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.editors.layout.properties;
     18 
     19 import static com.android.SdkConstants.ANDROID_PREFIX;
     20 import static com.android.SdkConstants.ANDROID_THEME_PREFIX;
     21 import static com.android.SdkConstants.ATTR_ID;
     22 import static com.android.SdkConstants.DOT_PNG;
     23 import static com.android.SdkConstants.DOT_XML;
     24 import static com.android.SdkConstants.NEW_ID_PREFIX;
     25 import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
     26 import static com.android.SdkConstants.PREFIX_THEME_REF;
     27 import static com.android.ide.common.layout.BaseViewRule.stripIdPrefix;
     28 
     29 import com.android.annotations.NonNull;
     30 import com.android.ide.common.api.IAttributeInfo;
     31 import com.android.ide.common.api.IAttributeInfo.Format;
     32 import com.android.ide.common.layout.BaseViewRule;
     33 import com.android.ide.common.rendering.api.ResourceValue;
     34 import com.android.ide.common.resources.ResourceRepository;
     35 import com.android.ide.common.resources.ResourceResolver;
     36 import com.android.ide.eclipse.adt.AdtPlugin;
     37 import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
     38 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
     39 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
     40 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils;
     41 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
     42 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderService;
     43 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager;
     44 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils;
     45 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
     46 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
     47 import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceWizard;
     48 import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult;
     49 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
     50 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
     51 import com.android.ide.eclipse.adt.internal.ui.ReferenceChooserDialog;
     52 import com.android.ide.eclipse.adt.internal.ui.ResourceChooser;
     53 import com.android.ide.eclipse.adt.internal.ui.ResourcePreviewHelper;
     54 import com.android.resources.ResourceType;
     55 import com.google.common.collect.Maps;
     56 
     57 import org.eclipse.core.resources.IProject;
     58 import org.eclipse.core.runtime.CoreException;
     59 import org.eclipse.core.runtime.QualifiedName;
     60 import org.eclipse.jface.dialogs.IDialogConstants;
     61 import org.eclipse.jface.dialogs.MessageDialogWithToggle;
     62 import org.eclipse.jface.preference.IPreferenceStore;
     63 import org.eclipse.jface.window.Window;
     64 import org.eclipse.swt.graphics.Color;
     65 import org.eclipse.swt.graphics.GC;
     66 import org.eclipse.swt.graphics.Image;
     67 import org.eclipse.swt.graphics.ImageData;
     68 import org.eclipse.swt.graphics.Point;
     69 import org.eclipse.swt.graphics.RGB;
     70 import org.eclipse.swt.widgets.Shell;
     71 import org.eclipse.wb.draw2d.IColorConstants;
     72 import org.eclipse.wb.internal.core.model.property.Property;
     73 import org.eclipse.wb.internal.core.model.property.editor.AbstractTextPropertyEditor;
     74 import org.eclipse.wb.internal.core.model.property.editor.presentation.ButtonPropertyEditorPresentation;
     75 import org.eclipse.wb.internal.core.model.property.editor.presentation.PropertyEditorPresentation;
     76 import org.eclipse.wb.internal.core.model.property.table.PropertyTable;
     77 import org.eclipse.wb.internal.core.utils.ui.DrawUtils;
     78 
     79 import java.awt.image.BufferedImage;
     80 import java.io.File;
     81 import java.io.IOException;
     82 import java.util.ArrayList;
     83 import java.util.EnumSet;
     84 import java.util.List;
     85 import java.util.Map;
     86 
     87 import javax.imageio.ImageIO;
     88 
     89 /**
     90  * Special property editor used for the {@link XmlProperty} instances which handles
     91  * editing the XML properties, rendering defaults by looking up the actual colors and images,
     92  */
     93 class XmlPropertyEditor extends AbstractTextPropertyEditor {
     94     public static final XmlPropertyEditor INSTANCE = new XmlPropertyEditor();
     95     private static final int SAMPLE_SIZE = 10;
     96     private static final int SAMPLE_MARGIN = 3;
     97 
     98     protected XmlPropertyEditor() {
     99     }
    100 
    101     private final PropertyEditorPresentation mPresentation =
    102             new ButtonPropertyEditorPresentation() {
    103         @Override
    104         protected void onClick(PropertyTable propertyTable, Property property) throws Exception {
    105             openDialog(propertyTable, property);
    106         }
    107     };
    108 
    109     @Override
    110     public PropertyEditorPresentation getPresentation() {
    111         return mPresentation;
    112     }
    113 
    114     @Override
    115     public String getText(Property property) throws Exception {
    116         Object value = property.getValue();
    117         if (value instanceof String) {
    118             return (String) value;
    119         }
    120         return null;
    121     }
    122 
    123     @Override
    124     protected String getEditorText(Property property) throws Exception {
    125         return getText(property);
    126     }
    127 
    128     @Override
    129     public void paint(Property property, GC gc, int x, int y, int width, int height)
    130             throws Exception {
    131         String text = getText(property);
    132         if (text != null) {
    133             ResourceValue resValue = null;
    134             String resolvedText = null;
    135 
    136             // TODO: Use the constants for @, ?, @android: etc
    137             if (text.startsWith("@") || text.startsWith("?")) { //$NON-NLS-1$ //$NON-NLS-2$
    138                 // Yes, try to resolve it in order to show better info
    139                 XmlProperty xmlProperty = (XmlProperty) property;
    140                 GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor();
    141                 if (graphicalEditor != null) {
    142                     ResourceResolver resolver = graphicalEditor.getResourceResolver();
    143                     boolean isFramework = text.startsWith(ANDROID_PREFIX)
    144                             || text.startsWith(ANDROID_THEME_PREFIX);
    145                     resValue = resolver.findResValue(text, isFramework);
    146                     while (resValue != null && resValue.getValue() != null) {
    147                         String value = resValue.getValue();
    148                         if (value.startsWith(PREFIX_RESOURCE_REF)
    149                                 || value.startsWith(PREFIX_THEME_REF)) {
    150                             // TODO: do I have to strip off the @ too?
    151                             isFramework = isFramework
    152                                     || value.startsWith(ANDROID_PREFIX)
    153                                     || value.startsWith(ANDROID_THEME_PREFIX);
    154                             ResourceValue v = resolver.findResValue(text, isFramework);
    155                             if (v != null && !value.equals(v.getValue())) {
    156                                 resValue = v;
    157                             } else {
    158                                 break;
    159                             }
    160                         } else {
    161                             break;
    162                         }
    163                     }
    164                 }
    165             } else if (text.startsWith("#") && text.matches("#\\p{XDigit}+")) { //$NON-NLS-1$
    166                 resValue = new ResourceValue(ResourceType.COLOR, property.getName(), text, false);
    167             }
    168 
    169             if (resValue != null && resValue.getValue() != null) {
    170                 String value = resValue.getValue();
    171                 // Decide whether it's a color, an image, a nine patch etc
    172                 // and decide how to render it
    173                 if (value.startsWith("#") || value.endsWith(DOT_XML) //$NON-NLS-1$
    174                         && value.contains("res/color")) { //$NON-NLS-1$ // TBD: File.separator?
    175                     XmlProperty xmlProperty = (XmlProperty) property;
    176                     GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor();
    177                     if (graphicalEditor != null) {
    178                         ResourceResolver resolver = graphicalEditor.getResourceResolver();
    179                         RGB rgb = ResourceHelper.resolveColor(resolver, resValue);
    180                         if (rgb != null) {
    181                             Color color = new Color(gc.getDevice(), rgb);
    182                             // draw color sample
    183                             Color oldBackground = gc.getBackground();
    184                             Color oldForeground = gc.getForeground();
    185                             try {
    186                                 int width_c = SAMPLE_SIZE;
    187                                 int height_c = SAMPLE_SIZE;
    188                                 int x_c = x;
    189                                 int y_c = y + (height - height_c) / 2;
    190                                 // update rest bounds
    191                                 int delta = SAMPLE_SIZE + SAMPLE_MARGIN;
    192                                 x += delta;
    193                                 width -= delta;
    194                                 // fill
    195                                 gc.setBackground(color);
    196                                 gc.fillRectangle(x_c, y_c, width_c, height_c);
    197                                 // draw line
    198                                 gc.setForeground(IColorConstants.gray);
    199                                 gc.drawRectangle(x_c, y_c, width_c, height_c);
    200                             } finally {
    201                                 gc.setBackground(oldBackground);
    202                                 gc.setForeground(oldForeground);
    203                             }
    204                             color.dispose();
    205                         }
    206                     }
    207                 } else {
    208                     Image swtImage = null;
    209                     if (value.endsWith(DOT_XML) && value.contains("res/drawable")) { // TBD: Filesep?
    210                         Map<String, Image> cache = getImageCache(property);
    211                         swtImage = cache.get(value);
    212                         if (swtImage == null) {
    213                             XmlProperty xmlProperty = (XmlProperty) property;
    214                             GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor();
    215                             RenderService service = RenderService.create(graphicalEditor);
    216                             service.setOverrideRenderSize(SAMPLE_SIZE, SAMPLE_SIZE);
    217                             BufferedImage drawable = service.renderDrawable(resValue);
    218                             if (drawable != null) {
    219                                 swtImage = SwtUtils.convertToSwt(gc.getDevice(), drawable,
    220                                         true /*transferAlpha*/, -1);
    221                                 cache.put(value, swtImage);
    222                             }
    223                         }
    224                     } else if (value.endsWith(DOT_PNG)) {
    225                         // TODO: 9-patch handling?
    226                         //if (text.endsWith(DOT_9PNG)) {
    227                         //    // 9-patch image: How do we paint this?
    228                         //    URL url = new File(text).toURI().toURL();
    229                         //    NinePatch ninepatch = NinePatch.load(url, false /* ?? */);
    230                         //    BufferedImage image = ninepatch.getImage();
    231                         //}
    232                         Map<String, Image> cache = getImageCache(property);
    233                         swtImage = cache.get(value);
    234                         if (swtImage == null) {
    235                             File file = new File(value);
    236                             if (file.exists()) {
    237                                 try {
    238                                     BufferedImage awtImage = ImageIO.read(file);
    239                                     if (awtImage != null && awtImage.getWidth() > 0
    240                                             && awtImage.getHeight() > 0) {
    241                                         awtImage = ImageUtils.cropBlank(awtImage, null);
    242                                         if (awtImage != null) {
    243                                             // Scale image
    244                                             int imageWidth = awtImage.getWidth();
    245                                             int imageHeight = awtImage.getHeight();
    246                                             int maxWidth = 3 * height;
    247 
    248                                             if (imageWidth > maxWidth || imageHeight > height) {
    249                                                 double scale = height / (double) imageHeight;
    250                                                 int scaledWidth = (int) (imageWidth * scale);
    251                                                 if (scaledWidth > maxWidth) {
    252                                                     scale = maxWidth / (double) imageWidth;
    253                                                 }
    254                                                 awtImage = ImageUtils.scale(awtImage, scale,
    255                                                         scale);
    256                                             }
    257                                             swtImage = SwtUtils.convertToSwt(gc.getDevice(),
    258                                                     awtImage, true /*transferAlpha*/, -1);
    259                                         }
    260                                     }
    261                                 } catch (IOException e) {
    262                                     AdtPlugin.log(e, value);
    263                                 }
    264                             }
    265                             cache.put(value, swtImage);
    266                         }
    267 
    268                     } else if (value != null) {
    269                         // It's a normal string: if different from the text, paint
    270                         // it in parentheses, e.g.
    271                         //   @string/foo: Foo Bar (probably cropped)
    272                         if (!value.equals(text) && !value.equals("@null")) { //$NON-NLS-1$
    273                             resolvedText = value;
    274                         }
    275                     }
    276 
    277                     if (swtImage != null) {
    278                         // Make a square the size of the height
    279                         ImageData imageData = swtImage.getImageData();
    280                         int imageWidth = imageData.width;
    281                         int imageHeight = imageData.height;
    282                         if (imageWidth > 0 && imageHeight > 0) {
    283                             gc.drawImage(swtImage, x, y + (height - imageHeight) / 2);
    284                             int delta = imageWidth + SAMPLE_MARGIN;
    285                             x += delta;
    286                             width -= delta;
    287                         }
    288                     }
    289                 }
    290             }
    291 
    292             DrawUtils.drawStringCV(gc, text, x, y, width, height);
    293 
    294             if (resolvedText != null && resolvedText.length() > 0) {
    295                 Point size = gc.stringExtent(text);
    296                 x += size.x;
    297                 width -= size.x;
    298 
    299                 x += SAMPLE_MARGIN;
    300                 width -= SAMPLE_MARGIN;
    301 
    302                 if (width > 0) {
    303                     Color oldForeground = gc.getForeground();
    304                     try {
    305                         gc.setForeground(PropertyTable.COLOR_PROPERTY_FG_DEFAULT);
    306                         DrawUtils.drawStringCV(gc, '(' + resolvedText + ')', x, y, width, height);
    307                     } finally {
    308                         gc.setForeground(oldForeground);
    309                     }
    310                 }
    311             }
    312         }
    313     }
    314 
    315     @Override
    316     protected boolean setEditorText(Property property, String text) throws Exception {
    317         Object oldValue = property.getValue();
    318         String old = oldValue != null ? oldValue.toString() : null;
    319 
    320         // If users enters a new id without specifying the @id/@+id prefix, insert it
    321         boolean isId = isIdProperty(property);
    322         if (isId && !text.startsWith(PREFIX_RESOURCE_REF)) {
    323             text = NEW_ID_PREFIX + text;
    324         }
    325 
    326         // Handle id refactoring: if you change an id, may want to update references too.
    327         // Ask user.
    328         if (isId && property instanceof XmlProperty
    329                 && old != null && !old.isEmpty()
    330                 && text != null && !text.isEmpty()
    331                 && !text.equals(old)) {
    332             XmlProperty xmlProperty = (XmlProperty) property;
    333             IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
    334             String refactorPref = store.getString(AdtPrefs.PREFS_REFACTOR_IDS);
    335             boolean performRefactor = false;
    336             Shell shell = AdtPlugin.getShell();
    337             if (refactorPref == null
    338                     || refactorPref.isEmpty()
    339                     || refactorPref.equals(MessageDialogWithToggle.PROMPT)) {
    340                 MessageDialogWithToggle dialog =
    341                         MessageDialogWithToggle.openYesNoCancelQuestion(
    342                     shell,
    343                     "Update References?",
    344                     "Update all references as well? " +
    345                     "This will update all XML references and Java R field references.",
    346                     "Do not show again",
    347                     false,
    348                     store,
    349                     AdtPrefs.PREFS_REFACTOR_IDS);
    350                 switch (dialog.getReturnCode()) {
    351                     case IDialogConstants.CANCEL_ID:
    352                         return false;
    353                     case IDialogConstants.YES_ID:
    354                         performRefactor = true;
    355                         break;
    356                     case IDialogConstants.NO_ID:
    357                         performRefactor = false;
    358                         break;
    359                 }
    360             } else {
    361                 performRefactor = refactorPref.equals(MessageDialogWithToggle.ALWAYS);
    362             }
    363             if (performRefactor) {
    364                 CommonXmlEditor xmlEditor = xmlProperty.getXmlEditor();
    365                 if (xmlEditor != null) {
    366                     IProject project = xmlEditor.getProject();
    367                     if (project != null && shell != null) {
    368                         RenameResourceWizard.renameResource(shell, project,
    369                                 ResourceType.ID, stripIdPrefix(old), stripIdPrefix(text), false);
    370                     }
    371                 }
    372             }
    373         }
    374 
    375         property.setValue(text);
    376 
    377         return true;
    378     }
    379 
    380     private static boolean isIdProperty(Property property) {
    381         XmlProperty xmlProperty = (XmlProperty) property;
    382         return xmlProperty.getDescriptor().getXmlLocalName().equals(ATTR_ID);
    383     }
    384 
    385     private void openDialog(PropertyTable propertyTable, Property property) throws Exception {
    386         XmlProperty xmlProperty = (XmlProperty) property;
    387         IAttributeInfo attributeInfo = xmlProperty.getDescriptor().getAttributeInfo();
    388 
    389         if (isIdProperty(property)) {
    390             Object value = xmlProperty.getValue();
    391             if (value != null && !value.toString().isEmpty()) {
    392                 GraphicalEditorPart editor = xmlProperty.getGraphicalEditor();
    393                 if (editor != null) {
    394                     LayoutCanvas canvas = editor.getCanvasControl();
    395                     SelectionManager manager = canvas.getSelectionManager();
    396 
    397                     NodeProxy primary = canvas.getNodeFactory().create(xmlProperty.getNode());
    398                     if (primary != null) {
    399                         RenameResult result = manager.performRename(primary, null);
    400                         if (result.isCanceled()) {
    401                             return;
    402                         } else if (!result.isUnavailable()) {
    403                             String name = result.getName();
    404                             String id = NEW_ID_PREFIX + BaseViewRule.stripIdPrefix(name);
    405                             xmlProperty.setValue(id);
    406                             return;
    407                         }
    408                     }
    409                 }
    410             }
    411 
    412             // When editing the id attribute, don't offer a resource chooser: usually
    413             // you want to enter a *new* id here
    414             attributeInfo = null;
    415         }
    416 
    417         boolean referenceAllowed = false;
    418         if (attributeInfo != null) {
    419             EnumSet<Format> formats = attributeInfo.getFormats();
    420             ResourceType type = null;
    421             List<ResourceType> types = null;
    422             if (formats.contains(Format.FLAG)) {
    423                 String[] flagValues = attributeInfo.getFlagValues();
    424                 if (flagValues != null) {
    425                     FlagXmlPropertyDialog dialog =
    426                         new FlagXmlPropertyDialog(propertyTable.getShell(),
    427                                 "Select Flag Values", false /* radio */,
    428                                 flagValues, xmlProperty);
    429 
    430                     dialog.open();
    431                     return;
    432                 }
    433             } else if (formats.contains(Format.ENUM)) {
    434                 String[] enumValues = attributeInfo.getEnumValues();
    435                 if (enumValues != null) {
    436                     FlagXmlPropertyDialog dialog =
    437                         new FlagXmlPropertyDialog(propertyTable.getShell(),
    438                                 "Select Enum Value", true /* radio */,
    439                                 enumValues, xmlProperty);
    440                     dialog.open();
    441                     return;
    442                 }
    443             } else {
    444                 for (Format format : formats) {
    445                     ResourceType t = format.getResourceType();
    446                     if (t != null) {
    447                         if (type != null) {
    448                             if (types == null) {
    449                                 types = new ArrayList<ResourceType>();
    450                                 types.add(type);
    451                             }
    452                             types.add(t);
    453                         }
    454                         type = t;
    455                     } else if (format == Format.REFERENCE) {
    456                         referenceAllowed = true;
    457                     }
    458                 }
    459             }
    460             if (types != null || referenceAllowed) {
    461                 // Multiple resource types (such as string *and* boolean):
    462                 // just use a reference chooser
    463                 GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor();
    464                 if (graphicalEditor != null) {
    465                     LayoutEditorDelegate delegate = graphicalEditor.getEditorDelegate();
    466                     IProject project = delegate.getEditor().getProject();
    467                     if (project != null) {
    468                         // get the resource repository for this project and the system resources.
    469                         ResourceRepository projectRepository =
    470                             ResourceManager.getInstance().getProjectResources(project);
    471                         Shell shell = AdtPlugin.getShell();
    472                         ReferenceChooserDialog dlg = new ReferenceChooserDialog(
    473                                 project,
    474                                 projectRepository,
    475                                 shell);
    476                         dlg.setPreviewHelper(new ResourcePreviewHelper(dlg, graphicalEditor));
    477 
    478                         String currentValue = (String) property.getValue();
    479                         dlg.setCurrentResource(currentValue);
    480 
    481                         if (dlg.open() == Window.OK) {
    482                             String resource = dlg.getCurrentResource();
    483                             if (resource != null) {
    484                                 // Returns null for cancel, "" for clear and otherwise a new value
    485                                 if (resource.length() > 0) {
    486                                     property.setValue(resource);
    487                                 } else {
    488                                     property.setValue(null);
    489                                 }
    490                             }
    491                         }
    492 
    493                         return;
    494                     }
    495                 }
    496             } else if (type != null) {
    497                 // Single resource type: use a resource chooser
    498                 GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor();
    499                 if (graphicalEditor != null) {
    500                     String currentValue = (String) property.getValue();
    501                     // TODO: Add validator factory?
    502                     String resource = ResourceChooser.chooseResource(graphicalEditor,
    503                             type, currentValue, null /* validator */);
    504                     // Returns null for cancel, "" for clear and otherwise a new value
    505                     if (resource != null) {
    506                         if (resource.length() > 0) {
    507                             property.setValue(resource);
    508                         } else {
    509                             property.setValue(null);
    510                         }
    511                     }
    512                 }
    513 
    514                 return;
    515             }
    516         }
    517 
    518         // Fallback: Just use a plain string editor
    519         StringXmlPropertyDialog dialog =
    520                 new StringXmlPropertyDialog(propertyTable.getShell(), property);
    521         if (dialog.open() == Window.OK) {
    522             // TODO: Do I need to activate?
    523         }
    524     }
    525 
    526     /** Qualified name for the per-project persistent property include-map */
    527     private final static QualifiedName CACHE_NAME = new QualifiedName(AdtPlugin.PLUGIN_ID,
    528             "property-images");//$NON-NLS-1$
    529 
    530     @NonNull
    531     private static Map<String, Image> getImageCache(@NonNull Property property) {
    532         XmlProperty xmlProperty = (XmlProperty) property;
    533         GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor();
    534         IProject project = graphicalEditor.getProject();
    535         try {
    536             Map<String, Image> cache = (Map<String, Image>) project.getSessionProperty(CACHE_NAME);
    537             if (cache == null) {
    538                 cache = Maps.newHashMap();
    539                 project.setSessionProperty(CACHE_NAME, cache);
    540             }
    541 
    542             return cache;
    543         } catch (CoreException e) {
    544             AdtPlugin.log(e, null);
    545             return Maps.newHashMap();
    546         }
    547     }
    548 }
    549