public ComponentHelpWidget(final SimpleComponentDescriptor scd) {
    if (imageResource == null) {
      Images images = Ode.getImageBundle();
      imageResource = images.help();
    }
    AbstractImagePrototype.create(imageResource).applyTo(this);
    addClickListener(
        new ClickListener() {
          @Override
          public void onClick(Widget sender) {
            final long MINIMUM_MS_BETWEEN_SHOWS = 250; // .25 seconds

            if (System.currentTimeMillis() - lastClosureTime >= MINIMUM_MS_BETWEEN_SHOWS) {
              new ComponentHelpPopup(scd, sender);
            }
          }
        });
  }
/** Descriptor for components on the component palette panel. This class is immutable. */
public final class SimpleComponentDescriptor {

  // Component display name
  private final String name;

  private final SimpleEditor editor;

  // Help information to display for component
  private final String helpString;

  // Goro documentation category URL piece
  private final String categoryDocUrlString;

  // Whether to show the component on the palette
  private final boolean showOnPalette;

  // Whether the component has a visual representation in the app's UI
  private final boolean nonVisible;

  // an instantiated mockcomponent is currently necessary in order to
  // to get the image, category, and description
  private MockComponent cachedMockComponent = null;

  // Component database: information about components (including their properties and events)
  private static final SimpleComponentDatabase COMPONENT_DATABASE =
      SimpleComponentDatabase.getInstance();

  /* We keep a static map of image names to images in the image bundle so
   * that we can avoid making individual calls to the server for static image
   * that are already in the bundle. This is purely an efficiency optimization
   * for mock non-visible components.
   */
  private static final Images images = Ode.getImageBundle();
  private static final Map<String, ImageResource> bundledImages = Maps.newHashMap();
  private static boolean imagesInitialized = false;

  private static void initBundledImages() {
    bundledImages.put("images/accelerometersensor.png", images.accelerometersensor());
    bundledImages.put("images/activityStarter.png", images.activitystarter());
    bundledImages.put("images/barcodeScanner.png", images.barcodeScanner());
    bundledImages.put("images/bluetooth.png", images.bluetooth());
    bundledImages.put("images/camera.png", images.camera());
    bundledImages.put("images/camcorder.png", images.camcorder());
    bundledImages.put("images/clock.png", images.clock());
    bundledImages.put("images/fusiontables.png", images.fusiontables());
    bundledImages.put("images/gameClient.png", images.gameclient());
    bundledImages.put("images/locationSensor.png", images.locationSensor());
    bundledImages.put("images/notifier.png", images.notifier());
    bundledImages.put("images/legoMindstormsNxt.png", images.legoMindstormsNxt());
    bundledImages.put("images/orientationsensor.png", images.orientationsensor());
    bundledImages.put("images/pedometer.png", images.pedometerComponent());
    bundledImages.put("images/phoneCall.png", images.phonecall());
    bundledImages.put("images/player.png", images.player());
    bundledImages.put("images/soundEffect.png", images.soundeffect());
    bundledImages.put("images/soundRecorder.png", images.soundRecorder());
    bundledImages.put("images/speechRecognizer.png", images.speechRecognizer());
    bundledImages.put("images/textToSpeech.png", images.textToSpeech());
    bundledImages.put("images/texting.png", images.texting());
    bundledImages.put("images/tinyDB.png", images.tinyDB());
    bundledImages.put("images/tinyWebDB.png", images.tinyWebDB());
    bundledImages.put("images/twitter.png", images.twitterComponent());
    bundledImages.put("images/voting.png", images.voting());
    bundledImages.put("images/web.png", images.web());
    imagesInitialized = true;
  }

  /**
   * Creates a new component descriptor.
   *
   * @param name component display name
   */
  public SimpleComponentDescriptor(
      String name,
      SimpleEditor editor,
      String helpString,
      String categoryDocUrlString,
      boolean showOnPalette,
      boolean nonVisible) {
    this.name = name;
    this.editor = editor;
    this.helpString = helpString;
    this.categoryDocUrlString = categoryDocUrlString;
    this.showOnPalette = showOnPalette;
    this.nonVisible = nonVisible;
  }

  /**
   * Returns the display name of the component.
   *
   * @return component display name
   */
  public String getName() {
    return name;
  }

  /**
   * Returns the help string for the component. For more detail, see javadoc for {@link
   * com.google.appinventor.client.editor.simple.ComponentDatabase#getHelpString(String)}.
   *
   * @return helpful message about the component
   */
  public String getHelpString() {
    return helpString;
  }

  /**
   * Returns the categoryDocUrl string for the component. For more detail, see javadoc for {@link
   * com.google.appinventor.client.editor.simple.ComponentDatabase#getCategoryDocUrlString(String)}.
   *
   * @return helpful message about the component
   */
  public String getCategoryDocUrlString() {
    return categoryDocUrlString;
  }

  /**
   * Returns whether this component should be shown on the palette. For more detail, see javadoc for
   * {@link com.google.appinventor.client.editor.simple.ComponentDatabase#getHelpString(String)}.
   *
   * @return whether the component should be shown on the palette
   */
  public boolean getShowOnPalette() {
    return showOnPalette;
  }

  /**
   * Returns whether this component is visible in the app's UI. For more detail, see javadoc for
   * {@link com.google.appinventor.client.editor.simple.ComponentDatabase#getHelpString(String)}.
   *
   * @return whether the component is non-visible
   */
  public boolean getNonVisible() {
    return nonVisible;
  }

  /**
   * Returns an image for display on the component palette.
   *
   * @return image for component
   */
  public Image getImage() {
    if (nonVisible) {
      return getImageFromPath(COMPONENT_DATABASE.getIconName(name));
    } else {
      return getCachedMockComponent(name, editor).getIconImage();
    }
  }

  /**
   * Returns a draggable image for the component. Used when dragging a component from the palette
   * onto the form.
   *
   * @return draggable widget for component
   */
  public Widget getDragWidget() {
    return createMockComponent(name, editor);
  }

  /**
   * Instantiates the corresponding mock component.
   *
   * @return mock component
   */
  public MockComponent createMockComponentFromPalette() {
    MockComponent mockComponent = createMockComponent(name, editor);
    mockComponent.onCreateFromPalette();
    return mockComponent;
  }

  /** Gets cached mock component; creates if necessary. */
  private MockComponent getCachedMockComponent(String name, SimpleEditor editor) {
    if (cachedMockComponent == null) {
      cachedMockComponent = createMockComponent(name, editor);
    }
    return cachedMockComponent;
  }

  public static Image getImageFromPath(String iconPath) {
    if (!imagesInitialized) {
      initBundledImages();
    }
    if (bundledImages.containsKey(iconPath)) {
      return new Image(bundledImages.get(iconPath));
    } else {
      return new Image(iconPath);
    }
  }

  /** Instantiates mock component by name. */
  public static MockComponent createMockComponent(String name, SimpleEditor editor) {
    if (COMPONENT_DATABASE.getNonVisible(name)) {
      return new MockNonVisibleComponent(
          editor, name, getImageFromPath(COMPONENT_DATABASE.getIconName(name)));
    } else if (name.equals(MockButton.TYPE)) {
      return new MockButton(editor);
    } else if (name.equals(MockCanvas.TYPE)) {
      return new MockCanvas(editor);
    } else if (name.equals(MockCheckBox.TYPE)) {
      return new MockCheckBox(editor);
    } else if (name.equals(MockImage.TYPE)) {
      return new MockImage(editor);
    } else if (name.equals(MockLabel.TYPE)) {
      return new MockLabel(editor);
    } else if (name.equals(MockPasswordTextBox.TYPE)) {
      return new MockPasswordTextBox(editor);
    } else if (name.equals(MockRadioButton.TYPE)) {
      return new MockRadioButton(editor);
    } else if (name.equals(MockTextBox.TYPE)) {
      return new MockTextBox(editor);
    } else if (name.equals(MockContactPicker.TYPE)) {
      return new MockContactPicker(editor);
    } else if (name.equals(MockPhoneNumberPicker.TYPE)) {
      return new MockPhoneNumberPicker(editor);
    } else if (name.equals(MockEmailPicker.TYPE)) {
      return new MockEmailPicker(editor);
    } else if (name.equals(MockListPicker.TYPE)) {
      return new MockListPicker(editor);
    } else if (name.equals(MockHorizontalArrangement.TYPE)) {
      return new MockHorizontalArrangement(editor);
    } else if (name.equals(MockVerticalArrangement.TYPE)) {
      return new MockVerticalArrangement(editor);
    } else if (name.equals(MockTableArrangement.TYPE)) {
      return new MockTableArrangement(editor);
    } else if (name.equals(MockImageSprite.TYPE)) {
      return new MockImageSprite(editor);
    } else if (name.equals(MockBall.TYPE)) {
      return new MockBall(editor);
    } else if (name.equals(MockImagePicker.TYPE)) {
      return new MockImagePicker(editor);
    } else if (name.equals(MockVideoPlayer.TYPE)) {
      return new MockVideoPlayer(editor);
    } else if (name.equals(MockWebViewer.TYPE)) {
      return new MockWebViewer(editor);
    } else {
      // TODO(user): add 3rd party mock component proxy here
      throw new UnsupportedOperationException("unknown component: " + name);
    }
  }
}
/**
 * Box implementation for block selector. Shows a tree containing the built-in blocks as well as the
 * components for the current form. Clicking on an item opens the blocks drawer for the item (or
 * closes it if it was already open). This box shares screen real estate with the
 * SourceStructureBox. It uses a SourceStructureExplorer to handle the components part of the tree.
 *
 * @author [email protected] (Sharon Perl)
 */
public final class BlockSelectorBox extends Box {
  private static class BlockSelectorItem implements SourceStructureExplorerItem {
    BlockSelectorItem() {}

    @Override
    public void onSelected() {}

    @Override
    public void onStateChange(boolean open) {}

    @Override
    public boolean canRename() {
      return false;
    }

    @Override
    public void rename() {}

    @Override
    public boolean canDelete() {
      return false;
    }

    @Override
    public void delete() {}
  }

  // Singleton block selector box instance
  // Starts out not visible. call setVisible(true) to make it visible
  private static final BlockSelectorBox INSTANCE = new BlockSelectorBox();

  private static final String BUILTIN_DRAWER_NAMES[] = {
    "Control", "Logic", "Math", "Text", "Lists", "Colors", "Variables", "Procedures"
  };

  private static final Images images = Ode.getImageBundle();
  private static final Map<String, ImageResource> bundledImages = Maps.newHashMap();

  // Source structure explorer (for components and built-in blocks)
  private final SourceStructureExplorer sourceStructureExplorer;

  private List<BlockDrawerSelectionListener> drawerListeners;

  /**
   * Return the singleton BlockSelectorBox box.
   *
   * @return block selector box
   */
  public static BlockSelectorBox getBlockSelectorBox() {
    return INSTANCE;
  }

  /** Creates new block selector box. */
  private BlockSelectorBox() {
    super(
        MESSAGES.blockSelectorBoxCaption(),
        300, // height
        false, // minimizable
        false); // removable

    sourceStructureExplorer = new SourceStructureExplorer();

    setContent(sourceStructureExplorer);
    setVisible(false);
    drawerListeners = new ArrayList<BlockDrawerSelectionListener>();
  }

  private static void initBundledImages() {
    bundledImages.put("Control", images.control());
    bundledImages.put("Logic", images.logic());
    bundledImages.put("Math", images.math());
    bundledImages.put("Text", images.text());
    bundledImages.put("Lists", images.lists());
    bundledImages.put("Colors", images.colors());
    bundledImages.put("Variables", images.variables());
    bundledImages.put("Procedures", images.procedures());
  }

  /**
   * Returns source structure explorer associated with box.
   *
   * @return source structure explorer
   */
  public SourceStructureExplorer getSourceStructureExplorer() {
    return sourceStructureExplorer;
  }

  /**
   * Constructs a tree item for built-in blocks.
   *
   * @return tree item
   */
  public TreeItem getBuiltInBlocksTree() {
    initBundledImages();
    TreeItem builtinNode =
        new TreeItem(new HTML("<span>" + MESSAGES.builtinBlocksLabel() + "</span>"));
    for (final String drawerName : BUILTIN_DRAWER_NAMES) {
      Image drawerImage = new Image(bundledImages.get(drawerName));
      TreeItem itemNode =
          new TreeItem(
              new HTML("<span>" + drawerImage + getBuiltinDrawerNames(drawerName) + "</span>"));
      SourceStructureExplorerItem sourceItem =
          new BlockSelectorItem() {
            @Override
            public void onSelected() {
              fireBuiltinDrawerSelected(drawerName);
            }
          };
      itemNode.setUserObject(sourceItem);
      builtinNode.addItem(itemNode);
    }
    builtinNode.setState(true);
    return builtinNode;
  }

  /** Given the drawerName, return the name in current language setting */
  private String getBuiltinDrawerNames(String drawerName) {
    String name;

    OdeLog.wlog("getBuiltinDrawerNames: drawerName = " + drawerName);

    if (drawerName.equals("Control")) {
      name = MESSAGES.builtinControlLabel();
    } else if (drawerName.equals("Logic")) {
      name = MESSAGES.builtinLogicLabel();
    } else if (drawerName.equals("Math")) {
      name = MESSAGES.builtinMathLabel();
    } else if (drawerName.equals("Text")) {
      name = MESSAGES.builtinTextLabel();
    } else if (drawerName.equals("Lists")) {
      name = MESSAGES.builtinListsLabel();
    } else if (drawerName.equals("Colors")) {
      name = MESSAGES.builtinColorsLabel();
    } else if (drawerName.equals("Variables")) {
      name = MESSAGES.builtinVariablesLabel();
    } else if (drawerName.equals("Procedures")) {
      name = MESSAGES.builtinProceduresLabel();
    } else {
      name = drawerName;
    }
    return name;
  }

  /**
   * Constructs a tree item for generic ("Advanced") component blocks for component types that
   * appear in form.
   *
   * @param form only component types that appear in this Form will be included
   * @return tree item for this form
   */
  public TreeItem getGenericComponentsTree(MockForm form) {
    Map<String, String> typesAndIcons = Maps.newHashMap();
    form.collectTypesAndIcons(typesAndIcons);
    TreeItem advanced = new TreeItem(new HTML("<span>" + MESSAGES.anyComponentLabel() + "</span>"));
    List<String> typeList = new ArrayList<String>(typesAndIcons.keySet());
    Collections.sort(typeList);
    for (final String typeName : typeList) {
      TreeItem itemNode =
          new TreeItem(
              new HTML(
                  "<span>"
                      + typesAndIcons.get(typeName)
                      + MESSAGES.textAnyComponentLabel()
                      + TranslationDesignerPallete.getCorrespondingString(typeName)
                      + "</span>"));
      SourceStructureExplorerItem sourceItem =
          new BlockSelectorItem() {
            @Override
            public void onSelected() {
              fireGenericDrawerSelected(typeName);
            }
          };
      itemNode.setUserObject(sourceItem);
      advanced.addItem(itemNode);
    }
    return advanced;
  }

  public void addBlockDrawerSelectionListener(BlockDrawerSelectionListener listener) {
    drawerListeners.add(listener);
  }

  public void removeBlockDrawerSelectionListener(BlockDrawerSelectionListener listener) {
    drawerListeners.remove(listener);
  }

  private void fireBuiltinDrawerSelected(String drawerName) {
    for (BlockDrawerSelectionListener listener : drawerListeners) {
      listener.onBuiltinDrawerSelected(drawerName);
    }
  }

  private void fireGenericDrawerSelected(String drawerName) {
    for (BlockDrawerSelectionListener listener : drawerListeners) {
      listener.onGenericDrawerSelected(drawerName);
    }
  }
}