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