1 /*
2  * Copyright (C) 2011 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.gle2;
18 
19 import static com.android.SdkConstants.DOT_PNG;
20 import static com.android.SdkConstants.FQCN_DATE_PICKER;
21 import static com.android.SdkConstants.FQCN_EXPANDABLE_LIST_VIEW;
22 import static com.android.SdkConstants.FQCN_LIST_VIEW;
23 import static com.android.SdkConstants.FQCN_TIME_PICKER;
24 
25 import com.android.annotations.NonNull;
26 import com.android.annotations.Nullable;
27 import com.android.ide.common.rendering.LayoutLibrary;
28 import com.android.ide.common.rendering.api.Capability;
29 import com.android.ide.common.rendering.api.RenderSession;
30 import com.android.ide.common.rendering.api.ResourceValue;
31 import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
32 import com.android.ide.common.rendering.api.StyleResourceValue;
33 import com.android.ide.common.rendering.api.ViewInfo;
34 import com.android.ide.common.resources.ResourceResolver;
35 import com.android.ide.eclipse.adt.AdtPlugin;
36 import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
37 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
38 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
39 import com.android.ide.eclipse.adt.internal.editors.layout.gre.PaletteMetadataDescriptor;
40 import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
41 import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository.RenderMode;
42 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
43 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
44 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
45 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
46 import com.android.sdklib.IAndroidTarget;
47 import com.android.utils.Pair;
48 
49 import org.eclipse.core.runtime.IPath;
50 import org.eclipse.core.runtime.IStatus;
51 import org.eclipse.jface.resource.ImageDescriptor;
52 import org.eclipse.swt.graphics.RGB;
53 import org.w3c.dom.Document;
54 import org.w3c.dom.Element;
55 import org.w3c.dom.Node;
56 import org.w3c.dom.NodeList;
57 
58 import java.awt.image.BufferedImage;
59 import java.io.BufferedInputStream;
60 import java.io.File;
61 import java.io.FileInputStream;
62 import java.io.IOException;
63 import java.io.InputStream;
64 import java.net.MalformedURLException;
65 import java.util.ArrayList;
66 import java.util.Collections;
67 import java.util.List;
68 import java.util.Properties;
69 
70 import javax.imageio.ImageIO;
71 
72 /**
73  * Factory which can provide preview icons for android views of a particular SDK and
74  * editor's configuration chooser
75  */
76 public class PreviewIconFactory {
77     private PaletteControl mPalette;
78     private RGB mBackground;
79     private RGB mForeground;
80     private File mImageDir;
81 
82     private static final String PREVIEW_INFO_FILE = "preview.properties"; //$NON-NLS-1$
83 
PreviewIconFactory(PaletteControl palette)84     public PreviewIconFactory(PaletteControl palette) {
85         mPalette = palette;
86     }
87 
88     /**
89      * Resets the state in the preview icon factory such that it will re-fetch information
90      * like the theme and SDK (the icons themselves are cached in a directory across IDE
91      * session though)
92      */
reset()93     public void reset() {
94         mImageDir = null;
95         mBackground = null;
96         mForeground = null;
97     }
98 
99     /**
100      * Deletes all the persistent state for the current settings such that it will be regenerated
101      */
refresh()102     public void refresh() {
103         File imageDir = getImageDir(false);
104         if (imageDir != null && imageDir.exists()) {
105             File[] files = imageDir.listFiles();
106             for (File file : files) {
107                 file.delete();
108             }
109             imageDir.delete();
110             reset();
111         }
112     }
113 
114     /**
115      * Returns an image descriptor for the given element descriptor, or null if no image
116      * could be computed. The rendering parameters (SDK, theme etc) correspond to those
117      * stored in the associated palette.
118      *
119      * @param desc the element descriptor to get an image for
120      * @return an image descriptor, or null if no image could be rendered
121      */
getImageDescriptor(ElementDescriptor desc)122     public ImageDescriptor getImageDescriptor(ElementDescriptor desc) {
123         File imageDir = getImageDir(false);
124         if (!imageDir.exists()) {
125             render();
126         }
127         File file = new File(imageDir, getFileName(desc));
128         if (file.exists()) {
129             try {
130                 return ImageDescriptor.createFromURL(file.toURI().toURL());
131             } catch (MalformedURLException e) {
132                 AdtPlugin.log(e, "Could not create image descriptor for %s", file);
133             }
134         }
135 
136         return null;
137     }
138 
139     /**
140      * Partition the elements in the document according to their rendering preferences;
141      * elements that should be skipped are removed, elements that should be rendered alone
142      * are placed in their own list, etc
143      *
144      * @param document the document containing render fragments for the various elements
145      * @return
146      */
partitionRenderElements(Document document)147     private List<List<Element>> partitionRenderElements(Document document) {
148         List<List<Element>> elements = new ArrayList<List<Element>>();
149 
150         List<Element> shared = new ArrayList<Element>();
151         Element root = document.getDocumentElement();
152         elements.add(shared);
153 
154         ViewMetadataRepository repository = ViewMetadataRepository.get();
155 
156         NodeList children = root.getChildNodes();
157         for (int i = 0, n = children.getLength(); i < n; i++) {
158             Node node = children.item(i);
159             if (node.getNodeType() == Node.ELEMENT_NODE) {
160                 Element element = (Element) node;
161                 String fqn = repository.getFullClassName(element);
162                 assert fqn.length() > 0 : element.getNodeName();
163                 RenderMode renderMode = repository.getRenderMode(fqn);
164 
165                 // Temporary special cases
166                 if (fqn.equals(FQCN_LIST_VIEW) || fqn.equals(FQCN_EXPANDABLE_LIST_VIEW)) {
167                     if (!mPalette.getEditor().renderingSupports(Capability.ADAPTER_BINDING)) {
168                         renderMode = RenderMode.SKIP;
169                     }
170                 } else if (fqn.equals(FQCN_DATE_PICKER) || fqn.equals(FQCN_TIME_PICKER)) {
171                     IAndroidTarget renderingTarget = mPalette.getEditor().getRenderingTarget();
172                     // In Honeycomb, these widgets only render properly in the Holo themes.
173                     int apiLevel = renderingTarget.getVersion().getApiLevel();
174                     if (apiLevel == 11) {
175                         String themeName = mPalette.getCurrentTheme();
176                         if (themeName == null || !themeName.startsWith("Theme.Holo")) { //$NON-NLS-1$
177                             // Note - it's possible that the the theme is some other theme
178                             // such as a user theme which inherits from Theme.Holo and that
179                             // the render -would- have worked, but it's harder to detect that
180                             // scenario, so we err on the side of caution and just show an
181                             // icon + name for the time widgets.
182                             renderMode = RenderMode.SKIP;
183                         }
184                     } else if (apiLevel >= 12) {
185                         // Currently broken, even for Holo.
186                         renderMode = RenderMode.SKIP;
187                     } // apiLevel <= 10 is fine
188                 }
189 
190                 if (renderMode == RenderMode.ALONE) {
191                     elements.add(Collections.singletonList(element));
192                 } else if (renderMode == RenderMode.NORMAL) {
193                     shared.add(element);
194                 } else {
195                     assert renderMode == RenderMode.SKIP;
196                 }
197             }
198         }
199 
200         return elements;
201     }
202 
203     /**
204      * Renders ALL the widgets and then extracts image data for each view and saves it on
205      * disk
206      */
render()207     private boolean render() {
208         File imageDir = getImageDir(true);
209 
210         GraphicalEditorPart editor = mPalette.getEditor();
211         LayoutEditorDelegate layoutEditorDelegate = editor.getEditorDelegate();
212         LayoutLibrary layoutLibrary = editor.getLayoutLibrary();
213         Integer overrideBgColor = null;
214         if (layoutLibrary != null) {
215             if (layoutLibrary.supports(Capability.CUSTOM_BACKGROUND_COLOR)) {
216                 Pair<RGB, RGB> themeColors = getColorsFromTheme();
217                 RGB bg = themeColors.getFirst();
218                 RGB fg = themeColors.getSecond();
219                 if (bg != null) {
220                     storeBackground(imageDir, bg, fg);
221                     overrideBgColor = Integer.valueOf(ImageUtils.rgbToInt(bg, 0xFF));
222                 }
223             }
224         }
225 
226         ViewMetadataRepository repository = ViewMetadataRepository.get();
227         Document document = repository.getRenderingConfigDoc();
228 
229         if (document == null) {
230             return false;
231         }
232 
233         // Construct UI model from XML
234         AndroidTargetData data = layoutEditorDelegate.getEditor().getTargetData();
235         DocumentDescriptor documentDescriptor;
236         if (data == null) {
237             documentDescriptor = new DocumentDescriptor("temp", null/*children*/);//$NON-NLS-1$
238         } else {
239             documentDescriptor = data.getLayoutDescriptors().getDescriptor();
240         }
241         UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode();
242         model.setEditor(layoutEditorDelegate.getEditor());
243         model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider());
244 
245         Element documentElement = document.getDocumentElement();
246         List<List<Element>> elements = partitionRenderElements(document);
247         for (List<Element> elementGroup : elements) {
248             // Replace the document elements with the current element group
249             while (documentElement.getFirstChild() != null) {
250                 documentElement.removeChild(documentElement.getFirstChild());
251             }
252             for (Element element : elementGroup) {
253                 documentElement.appendChild(element);
254             }
255 
256             model.loadFromXmlNode(document);
257 
258             RenderSession session = null;
259             NodeList childNodes = documentElement.getChildNodes();
260             try {
261                 // Important to get these sizes large enough for clients that don't support
262                 // RenderMode.FULL_EXPAND such as 1.6
263                 int width = 200;
264                 int height = childNodes.getLength() == 1 ? 400 : 1600;
265 
266                 session = RenderService.create(editor)
267                     .setModel(model)
268                     .setOverrideRenderSize(width, height)
269                     .setRenderingMode(RenderingMode.FULL_EXPAND)
270                     .setLog(editor.createRenderLogger("palette"))
271                     .setOverrideBgColor(overrideBgColor)
272                     .setDecorations(false)
273                     .createRenderSession();
274             } catch (Throwable t) {
275                 // If there are internal errors previewing the components just revert to plain
276                 // icons and labels
277                 continue;
278             }
279 
280             if (session != null) {
281                 if (session.getResult().isSuccess()) {
282                     BufferedImage image = session.getImage();
283                     if (image != null && image.getWidth() > 0 && image.getHeight() > 0) {
284 
285                         // Fallback for older platforms where we couldn't do background rendering
286                         // at the beginning of this method
287                         if (mBackground == null) {
288                             Pair<RGB, RGB> themeColors = getColorsFromTheme();
289                             RGB bg = themeColors.getFirst();
290                             RGB fg = themeColors.getSecond();
291 
292                             if (bg == null) {
293                                 // Just use a pixel from the rendering instead.
294                                 int p = image.getRGB(image.getWidth() - 1, image.getHeight() - 1);
295                                 // However, in this case we don't trust the foreground color
296                                 // even if one was found in the themes; pick one that is guaranteed
297                                 // to contrast with the background
298                                 bg = ImageUtils.intToRgb(p);
299                                 if (ImageUtils.getBrightness(ImageUtils.rgbToInt(bg, 255)) < 128) {
300                                     fg = new RGB(255, 255, 255);
301                                 } else {
302                                     fg = new RGB(0, 0, 0);
303                                 }
304                             }
305                             storeBackground(imageDir, bg, fg);
306                             assert mBackground != null;
307                         }
308 
309                         List<ViewInfo> viewInfoList = session.getRootViews();
310                         if (viewInfoList != null && viewInfoList.size() > 0) {
311                             // We don't render previews under a <merge> so there should
312                             // only be one root.
313                             ViewInfo firstRoot = viewInfoList.get(0);
314                             int parentX = firstRoot.getLeft();
315                             int parentY = firstRoot.getTop();
316                             List<ViewInfo> infos = firstRoot.getChildren();
317                             for (ViewInfo info : infos) {
318                                 Object cookie = info.getCookie();
319                                 if (!(cookie instanceof UiElementNode)) {
320                                     continue;
321                                 }
322                                 UiElementNode node = (UiElementNode) cookie;
323                                 String fileName = getFileName(node);
324                                 File file = new File(imageDir, fileName);
325                                 if (file.exists()) {
326                                     // On Windows, perhaps we need to rename instead?
327                                     file.delete();
328                                 }
329                                 int x1 = parentX + info.getLeft();
330                                 int y1 = parentY + info.getTop();
331                                 int x2 = parentX + info.getRight();
332                                 int y2 = parentY + info.getBottom();
333                                 if (x1 != x2 && y1 != y2) {
334                                     savePreview(file, image, x1, y1, x2, y2);
335                                 }
336                             }
337                         }
338                     }
339                 } else {
340                     StringBuilder sb = new StringBuilder();
341                     for (int i = 0, n = childNodes.getLength(); i < n; i++) {
342                         Node node = childNodes.item(i);
343                         if (node instanceof Element) {
344                             Element e = (Element) node;
345                             String fqn = repository.getFullClassName(e);
346                             fqn = fqn.substring(fqn.lastIndexOf('.') + 1);
347                             if (sb.length() > 0) {
348                                 sb.append(", "); //$NON-NLS-1$
349                             }
350                             sb.append(fqn);
351                         }
352                     }
353                     AdtPlugin.log(IStatus.WARNING, "Failed to render set of icons for %1$s",
354                             sb.toString());
355 
356                     if (session.getResult().getException() != null) {
357                         AdtPlugin.log(session.getResult().getException(),
358                                 session.getResult().getErrorMessage());
359                     } else if (session.getResult().getErrorMessage() != null) {
360                         AdtPlugin.log(IStatus.WARNING, session.getResult().getErrorMessage());
361                     }
362                 }
363 
364                 session.dispose();
365             }
366         }
367 
368         mPalette.getEditor().recomputeLayout();
369 
370         return true;
371     }
372 
373     /**
374      * Look up the background and foreground colors from the theme. May not find either
375      * the background or foreground or both, but will always return a pair of possibly
376      * null colors.
377      *
378      * @return a pair of possibly null color descriptions
379      */
380     @NonNull
getColorsFromTheme()381     private Pair<RGB, RGB> getColorsFromTheme() {
382         RGB background = null;
383         RGB foreground = null;
384 
385         ResourceResolver resources = mPalette.getEditor().getResourceResolver();
386         if (resources == null) {
387             return Pair.of(background, foreground);
388         }
389         StyleResourceValue theme = resources.getCurrentTheme();
390         if (theme != null) {
391             background = resolveThemeColor(resources, "windowBackground"); //$NON-NLS-1$
392             if (background == null) {
393                 background = renderDrawableResource("windowBackground"); //$NON-NLS-1$
394                 // This causes some harm with some themes: We'll find a color, say black,
395                 // that isn't actually rendered in the theme. Better to use null here,
396                 // which will cause the caller to pick a pixel from the observed background
397                 // instead.
398                 //if (background == null) {
399                 //    background = resolveThemeColor(resources, "colorBackground"); //$NON-NLS-1$
400                 //}
401             }
402             foreground = resolveThemeColor(resources, "textColorPrimary"); //$NON-NLS-1$
403         }
404 
405         // Ensure that the foreground color is suitably distinct from the background color
406         if (background != null) {
407             int bgRgb = ImageUtils.rgbToInt(background, 0xFF);
408             int backgroundBrightness = ImageUtils.getBrightness(bgRgb);
409             if (foreground == null) {
410                 if (backgroundBrightness < 128) {
411                     foreground = new RGB(255, 255, 255);
412                 } else {
413                     foreground = new RGB(0, 0, 0);
414                 }
415             } else {
416                 int fgRgb = ImageUtils.rgbToInt(foreground, 0xFF);
417                 int foregroundBrightness = ImageUtils.getBrightness(fgRgb);
418                 if (Math.abs(backgroundBrightness - foregroundBrightness) < 64) {
419                     if (backgroundBrightness < 128) {
420                         foreground = new RGB(255, 255, 255);
421                     } else {
422                         foreground = new RGB(0, 0, 0);
423                     }
424                 }
425             }
426         }
427 
428         return Pair.of(background, foreground);
429     }
430 
431     /**
432      * Renders the given resource which should refer to a drawable and returns a
433      * representative color value for the drawable (such as the color in the center)
434      *
435      * @param themeItemName the item in the theme to be looked up and rendered
436      * @return a color representing a typical color in the drawable
437      */
renderDrawableResource(String themeItemName)438     private RGB renderDrawableResource(String themeItemName) {
439         GraphicalEditorPart editor = mPalette.getEditor();
440         ResourceResolver resources = editor.getResourceResolver();
441         ResourceValue resourceValue = resources.findItemInTheme(themeItemName);
442         BufferedImage image = RenderService.create(editor)
443             .setOverrideRenderSize(100, 100)
444             .renderDrawable(resourceValue);
445         if (image != null) {
446             // Use the middle pixel as the color since that works better for gradients;
447             // solid colors work too.
448             int rgb = image.getRGB(image.getWidth() / 2, image.getHeight() / 2);
449             return ImageUtils.intToRgb(rgb);
450         }
451 
452         return null;
453     }
454 
resolveThemeColor(ResourceResolver resources, String resourceName)455     private static RGB resolveThemeColor(ResourceResolver resources, String resourceName) {
456         ResourceValue textColor = resources.findItemInTheme(resourceName);
457         return ResourceHelper.resolveColor(resources, textColor);
458     }
459 
getFileName(ElementDescriptor descriptor)460     private String getFileName(ElementDescriptor descriptor) {
461         if (descriptor instanceof PaletteMetadataDescriptor) {
462             PaletteMetadataDescriptor pmd = (PaletteMetadataDescriptor) descriptor;
463             StringBuilder sb = new StringBuilder();
464             String name = pmd.getUiName();
465             // Strip out whitespace, parentheses, etc.
466             for (int i = 0, n = name.length(); i < n; i++) {
467                 char c = name.charAt(i);
468                 if (Character.isLetter(c)) {
469                     sb.append(c);
470                 }
471             }
472             return sb.toString() + DOT_PNG;
473         }
474         return descriptor.getUiName() + DOT_PNG;
475     }
476 
getFileName(UiElementNode node)477     private String getFileName(UiElementNode node) {
478         ViewMetadataRepository repository = ViewMetadataRepository.get();
479         String fqn = repository.getFullClassName((Element) node.getXmlNode());
480         return fqn.substring(fqn.lastIndexOf('.') + 1) + DOT_PNG;
481     }
482 
483     /**
484      * Cleans up a name by removing punctuation and whitespace etc to make
485      * it a better filename
486      * @param name the name to clean
487      * @return a cleaned up name
488      */
489     @NonNull
cleanup(@ullable String name)490     private static String cleanup(@Nullable String name) {
491         if (name == null) {
492             return "";
493         }
494 
495         // Extract just the characters (no whitespace, parentheses, punctuation etc)
496         // to ensure that the filename is pretty portable
497         StringBuilder sb = new StringBuilder(name.length());
498         for (int i = 0; i < name.length(); i++) {
499             char c = name.charAt(i);
500             if (Character.isJavaIdentifierPart(c)) {
501                 sb.append(Character.toLowerCase(c));
502             }
503         }
504 
505         return sb.toString();
506     }
507 
508     /** Returns the location of a directory containing image previews (which may not exist) */
getImageDir(boolean create)509     private File getImageDir(boolean create) {
510         if (mImageDir == null) {
511             // Location for plugin-related state data
512             IPath pluginState = AdtPlugin.getDefault().getStateLocation();
513 
514             // We have multiple directories - one for each combination of SDK, theme and device
515             // (and later, possibly other qualifiers).
516             // These are created -lazily-.
517             String targetName = mPalette.getCurrentTarget().hashString();
518             String androidTargetNamePrefix = "android-";
519             String themeNamePrefix = "Theme.";
520             if (targetName.startsWith(androidTargetNamePrefix)) {
521                 targetName = targetName.substring(androidTargetNamePrefix.length());
522             }
523             String themeName = mPalette.getCurrentTheme();
524             if (themeName == null) {
525                 themeName = "Theme"; //$NON-NLS-1$
526             }
527             if (themeName.startsWith(themeNamePrefix)) {
528                 themeName = themeName.substring(themeNamePrefix.length());
529             }
530             targetName = cleanup(targetName);
531             themeName = cleanup(themeName);
532             String deviceName = cleanup(mPalette.getCurrentDevice());
533             String dirName = String.format("palette-preview-r16b-%s-%s-%s", targetName,
534                     themeName, deviceName);
535             IPath dirPath = pluginState.append(dirName);
536 
537             mImageDir = new File(dirPath.toOSString());
538         }
539 
540         if (create && !mImageDir.exists()) {
541             mImageDir.mkdirs();
542         }
543 
544         return mImageDir;
545     }
546 
savePreview(File output, BufferedImage image, int left, int top, int right, int bottom)547     private void savePreview(File output, BufferedImage image,
548             int left, int top, int right, int bottom) {
549         try {
550             BufferedImage im = ImageUtils.subImage(image, left, top, right, bottom);
551             ImageIO.write(im, "PNG", output); //$NON-NLS-1$
552         } catch (IOException e) {
553             AdtPlugin.log(e, "Failed writing palette file");
554         }
555     }
556 
storeBackground(File imageDir, RGB bg, RGB fg)557     private void storeBackground(File imageDir, RGB bg, RGB fg) {
558         mBackground = bg;
559         mForeground = fg;
560         File file = new File(imageDir, PREVIEW_INFO_FILE);
561         String colors = String.format(
562                 "background=#%02x%02x%02x\nforeground=#%02x%02x%02x\n", //$NON-NLS-1$
563                 bg.red, bg.green, bg.blue,
564                 fg.red, fg.green, fg.blue);
565         AdtPlugin.writeFile(file, colors);
566     }
567 
getBackgroundColor()568     public RGB getBackgroundColor() {
569         if (mBackground == null) {
570             initColors();
571         }
572 
573         return mBackground;
574     }
575 
getForegroundColor()576     public RGB getForegroundColor() {
577         if (mForeground == null) {
578             initColors();
579         }
580 
581         return mForeground;
582     }
583 
initColors()584     public void initColors() {
585         try {
586             // Already initialized? Foreground can be null which would call
587             // initColors again and again, but background is never null after
588             // initialization so we use it as the have-initialized flag.
589             if (mBackground != null) {
590                 return;
591             }
592 
593             File imageDir = getImageDir(false);
594             if (!imageDir.exists()) {
595                 render();
596 
597                 // Initialized as part of the render
598                 if (mBackground != null) {
599                     return;
600                 }
601             }
602 
603             File file = new File(imageDir, PREVIEW_INFO_FILE);
604             if (file.exists()) {
605                 Properties properties = new Properties();
606                 InputStream is = null;
607                 try {
608                     is = new BufferedInputStream(new FileInputStream(file));
609                     properties.load(is);
610                 } catch (IOException e) {
611                     AdtPlugin.log(e, "Can't read preview properties");
612                 } finally {
613                     if (is != null) {
614                         try {
615                             is.close();
616                         } catch (IOException e) {
617                             // Nothing useful can be done.
618                         }
619                     }
620                 }
621 
622                 String colorString = (String) properties.get("background"); //$NON-NLS-1$
623                 if (colorString != null) {
624                     int rgb = ImageUtils.getColor(colorString.trim());
625                     mBackground = ImageUtils.intToRgb(rgb);
626                 }
627                 colorString = (String) properties.get("foreground"); //$NON-NLS-1$
628                 if (colorString != null) {
629                     int rgb = ImageUtils.getColor(colorString.trim());
630                     mForeground = ImageUtils.intToRgb(rgb);
631                 }
632             }
633 
634             if (mBackground == null) {
635                 mBackground = new RGB(0, 0, 0);
636             }
637             // mForeground is allowed to be null.
638         } catch (Throwable t) {
639             AdtPlugin.log(t, "Cannot initialize preview color settings");
640         }
641     }
642 }
643