// -*- mode: java; c-basic-offset: 2; -*- // Copyright 2009-2011 Google, All Rights reserved // Copyright 2011-2014 MIT, All rights reserved // Released under the Apache License, Version 2.0 // http://www.apache.org/licenses/LICENSE-2.0 package com.google.appinventor.client.editor.youngandroid.palette; import com.google.appinventor.client.ComponentsTranslation; import com.google.appinventor.client.editor.simple.SimpleComponentDatabase; import com.google.appinventor.client.editor.simple.components.MockComponent; import com.google.appinventor.client.editor.simple.components.utils.PropertiesUtil; import com.google.appinventor.client.editor.simple.palette.DropTargetProvider; import com.google.appinventor.client.editor.simple.palette.SimpleComponentDescriptor; import com.google.appinventor.client.editor.simple.palette.SimplePaletteItem; import com.google.appinventor.client.editor.simple.palette.SimplePalettePanel; import com.google.appinventor.client.editor.youngandroid.YaFormEditor; import com.google.appinventor.client.explorer.project.ComponentDatabaseChangeListener; import com.google.appinventor.client.wizards.ComponentImportWizard; import com.google.appinventor.common.version.AppInventorFeatures; import com.google.appinventor.components.common.ComponentCategory; import com.google.gwt.core.client.JsArrayString; import com.google.gwt.core.client.Scheduler; import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ChangeEvent; import com.google.gwt.event.dom.client.ChangeHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.user.client.ui.Anchor; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HasHorizontalAlignment; import com.google.gwt.user.client.ui.StackPanel; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.event.dom.client.KeyUpEvent; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyPressHandler; import com.google.gwt.event.dom.client.KeyUpHandler; import com.google.gwt.event.dom.client.KeyDownHandler; import jsinterop.annotations.JsOverlay; import jsinterop.annotations.JsType; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.*; import static com.google.appinventor.client.Ode.MESSAGES; /** * Panel showing Simple components which can be dropped onto the Young Android * visual designer panel. * * @author [email protected] (Liz Looney) */ public class YoungAndroidPalettePanel extends Composite implements SimplePalettePanel, ComponentDatabaseChangeListener { // Component database: information about components (including their properties and events) private final SimpleComponentDatabase COMPONENT_DATABASE; // Associated editor private final YaFormEditor editor; private final Map<ComponentCategory, PaletteHelper> paletteHelpers; private final StackPanel stackPalette; private final Map<ComponentCategory, VerticalPanel> categoryPanels; // store Component Type along with SimplePaleteItem to enable removal of components private final Map<String, SimplePaletteItem> simplePaletteItems; private DropTargetProvider dropTargetProvider; private List<Integer> categoryOrder; // panel that holds all palette items final VerticalPanel panel; // Map translated component names to English names private final Map<String, String> translationMap; private final TextBox searchText; private final VerticalPanel searchResults; private JsArrayString arrayString = (JsArrayString) JsArrayString.createArray(); private String lastSearch = ""; private Map<String, SimplePaletteItem> searchSimplePaletteItems = new HashMap<String, SimplePaletteItem>(); @SuppressWarnings("checkstyle:LineLength") private native NativeArray filter(String match)/*-{ return [email protected]YoungAndroidPalettePanel::arrayString.filter(function(x) { return x.indexOf(match) >= 0 }); }-*/; @SuppressWarnings("checkstyle:LineLength") private native void sort()/*-{ [email protected]YoungAndroidPalettePanel::arrayString.sort(); }-*/; private Scheduler.ScheduledCommand rebuild = null; private void requestRebuildList() { if (rebuild != null) { return; } rebuild = new Scheduler.ScheduledCommand() { @Override public void execute() { arrayString.setLength(0); for (String s : translationMap.keySet()) { arrayString.push(s); } sort(); // Refresh the list by repeating the search doSearch(true); rebuild = null; } }; Scheduler.get().scheduleDeferred(rebuild); } @JsType public static class NativeArray extends JsArrayString implements Iterable<String> { protected NativeArray() { } @Override @JsOverlay public final Iterator<String> iterator() { return new Iterator<String>() { int index = 0; @Override public boolean hasNext() { return index < NativeArray.this.length(); } @Override public String next() { return NativeArray.this.get(index++); } }; } } /** * Creates a new component palette panel. * * @param editor parent editor of this panel */ public YoungAndroidPalettePanel(YaFormEditor editor) { this.editor = editor; COMPONENT_DATABASE = SimpleComponentDatabase.getInstance(editor.getProjectId()); stackPalette = new StackPanel(); paletteHelpers = new HashMap<ComponentCategory, PaletteHelper>(); // If a category has a palette helper, add it to the paletteHelpers map here. paletteHelpers.put(ComponentCategory.LEGOMINDSTORMS, new LegoPaletteHelper()); categoryPanels = new HashMap<ComponentCategory, VerticalPanel>(); simplePaletteItems = new HashMap<String, SimplePaletteItem>(); categoryOrder = new ArrayList<Integer>(); translationMap = new HashMap<String, String>(); panel = new VerticalPanel(); panel.setWidth("100%"); for (String component : COMPONENT_DATABASE.getComponentNames()) { String translationName = ComponentsTranslation.getComponentName(component).toLowerCase(); arrayString.push(translationName); translationMap.put(translationName, component); } searchText = new TextBox(); searchText.setWidth("100%"); searchText.getElement().setPropertyString("placeholder", MESSAGES.searchComponents()); searchText.getElement().setAttribute("style", "width: 100%; box-sizing: border-box;"); searchText.addKeyUpHandler(new SearchKeyUpHandler()); searchText.addKeyPressHandler(new ReturnKeyHandler()); searchText.addKeyDownHandler(new EscapeKeyDownHandler()); searchText.addBlurHandler(new BlurHandler() { @Override public void onBlur(BlurEvent event) { doSearch(); } }); searchText.addChangeHandler(new ChangeHandler() { @Override public void onChange(ChangeEvent event) { doSearch(); } }); panel.setSpacing(3); panel.add(searchText); panel.setWidth("100%"); searchResults = new VerticalPanel(); searchResults.setWidth("100%"); stackPalette.setWidth("100%"); initWidget(panel); panel.add(searchResults); panel.add(stackPalette); for (ComponentCategory category : ComponentCategory.values()) { if (showCategory(category)) { VerticalPanel categoryPanel = new VerticalPanel(); categoryPanel.setWidth("100%"); categoryPanels.put(category, categoryPanel); // The production version will not include a mapping for Extension because // only compile-time categories are included. This allows us to i18n the // Extension title for the palette. String title = ComponentCategory.EXTENSION.equals(category) ? MESSAGES.extensionComponentPallette() : ComponentsTranslation.getCategoryName(category.getName()); stackPalette.add(categoryPanel, title); } } initExtensionPanel(); } /** * Automatic search and list results as users input the string */ private class SearchKeyUpHandler implements KeyUpHandler { @Override public void onKeyUp(KeyUpEvent event) { doSearch(); } } /** * Users press escapte button, results and searchText will be cleared */ private class EscapeKeyDownHandler implements KeyDownHandler { @Override public void onKeyDown(KeyDownEvent event) { if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) { searchResults.clear(); searchText.setText(""); } } } /** * Users press enter button, results will be added to searchResults panel */ private class ReturnKeyHandler implements KeyPressHandler { @Override public void onKeyPress(KeyPressEvent event) { switch (event.getCharCode()) { case KeyCodes.KEY_END: case KeyCodes.KEY_DELETE: case KeyCodes.KEY_BACKSPACE: doSearch(); break; } } } private void doSearch() { doSearch(false); } /** * User clicks on searchButton and results will be added to searchResults panel */ private void doSearch(boolean force) { String search_str = searchText.getText().trim().toLowerCase(); if (search_str.equals(lastSearch) && !force) { // nothing to do here. return; } // Empty strings will return nothing if (search_str.length() != 0) { long start = System.currentTimeMillis(); // Remove previous search results searchResults.clear(); Iterable<String> allComponents = filter(search_str); for (String name : allComponents) { if (translationMap.containsKey(name)) { final String codeName = translationMap.get(name); if (simplePaletteItems.containsKey(codeName)) { searchResults.add(searchSimplePaletteItems.get(codeName)); } } } } else { searchResults.clear(); } lastSearch = search_str; } private static boolean showCategory(ComponentCategory category) { if (category == ComponentCategory.UNINITIALIZED) { return false; } if (category == ComponentCategory.INTERNAL && !AppInventorFeatures.showInternalComponentsCategory()) { return false; } return true; } /** * Loads all components to be shown on this palette. Specifically, for * each component (except for those whose category is UNINITIALIZED, or * whose category is INTERNAL and we're running on a production server, * or who are specifically marked as not to be shown on the palette), * this creates a corresponding {@link SimplePaletteItem} with the passed * {@link DropTargetProvider} and adds it to the panel corresponding to * its category. * * @param dropTargetProvider provider of targets that palette items can be * dropped on */ @Override public void loadComponents(DropTargetProvider dropTargetProvider) { this.dropTargetProvider = dropTargetProvider; for (String component : COMPONENT_DATABASE.getComponentNames()) { this.addComponent(component); } } public void loadComponents() { for (String component : COMPONENT_DATABASE.getComponentNames()) { this.addComponent(component); } } @Override public void configureComponent(MockComponent mockComponent) { String componentType = mockComponent.getType(); PropertiesUtil.populateProperties(mockComponent, COMPONENT_DATABASE.getPropertyDefinitions(componentType), editor); } /** * Loads a single Component to Palette. Used for adding Components. */ @Override public void addComponent(String componentTypeName) { if (simplePaletteItems.containsKey(componentTypeName)) { // We are upgrading removeComponent(componentTypeName); } int version = COMPONENT_DATABASE.getComponentVersion(componentTypeName); String versionName = COMPONENT_DATABASE.getComponentVersionName(componentTypeName); String dateBuilt = COMPONENT_DATABASE.getComponentBuildDate(componentTypeName); String helpString = COMPONENT_DATABASE.getHelpString(componentTypeName); String helpUrl = COMPONENT_DATABASE.getHelpUrl(componentTypeName); String categoryDocUrlString = COMPONENT_DATABASE.getCategoryDocUrlString(componentTypeName); String categoryString = COMPONENT_DATABASE.getCategoryString(componentTypeName); Boolean showOnPalette = COMPONENT_DATABASE.getShowOnPalette(componentTypeName); Boolean nonVisible = COMPONENT_DATABASE.getNonVisible(componentTypeName); Boolean external = COMPONENT_DATABASE.getComponentExternal(componentTypeName); ComponentCategory category = ComponentCategory.valueOf(categoryString); if (showOnPalette && showCategory(category)) { SimplePaletteItem item = new SimplePaletteItem( new SimpleComponentDescriptor(componentTypeName, editor, version, versionName, dateBuilt, helpString, helpUrl, categoryDocUrlString, showOnPalette, nonVisible, external), dropTargetProvider); simplePaletteItems.put(componentTypeName, item); addPaletteItem(item, category); // Make a second copy for the search mechanism item = new SimplePaletteItem( new SimpleComponentDescriptor(componentTypeName, editor, version, versionName, dateBuilt, helpString, helpUrl, categoryDocUrlString, showOnPalette, nonVisible, external), dropTargetProvider); // Handle extensions if (external) { translationMap.put(componentTypeName.toLowerCase(), componentTypeName); requestRebuildList(); } searchSimplePaletteItems.put(componentTypeName, item); } } public void removeComponent(String componentTypeName) { String categoryString = COMPONENT_DATABASE.getCategoryString(componentTypeName); ComponentCategory category = ComponentCategory.valueOf(categoryString); if (simplePaletteItems.containsKey(componentTypeName)) { removePaletteItem(simplePaletteItems.get(componentTypeName), category); simplePaletteItems.remove(componentTypeName); } if (category == ComponentCategory.EXTENSION) { searchSimplePaletteItems.remove(componentTypeName); translationMap.remove(componentTypeName); requestRebuildList(); } } /* * Adds a component entry to the palette. */ private void addPaletteItem(SimplePaletteItem component, ComponentCategory category) { VerticalPanel panel = categoryPanels.get(category); if (panel == null) { panel = addComponentCategory(category); } PaletteHelper paletteHelper = paletteHelpers.get(category); if (paletteHelper != null) { paletteHelper.addPaletteItem(panel, component); } else { panel.add(component); } } private VerticalPanel addComponentCategory(ComponentCategory category) { VerticalPanel panel = new VerticalPanel(); panel.setWidth("100%"); categoryPanels.put(category, panel); // The production version will not include a mapping for Extension because // only compile-time categories are included. This allows us to i18n the // Extension title for the palette. int insert_index = Collections.binarySearch(categoryOrder, category.ordinal()); insert_index = - insert_index - 1; stackPalette.insert(panel, insert_index); String title = ""; if (ComponentCategory.EXTENSION.equals(category)) { title = MESSAGES.extensionComponentPallette(); initExtensionPanel(); } else { title = ComponentsTranslation.getCategoryName(category.getName()); } stackPalette.setStackText(insert_index, title); categoryOrder.add(insert_index, category.ordinal()); // When the categories are loaded, we want the first one open, which will almost always be User Interface stackPalette.showStack(0); return panel; } private void removePaletteItem(SimplePaletteItem component, ComponentCategory category) { VerticalPanel panel = categoryPanels.get(category); panel.remove(component); if (panel.getWidgetCount() < 1) { stackPalette.remove(panel); categoryPanels.remove(category); } } private void initExtensionPanel() { Anchor addComponentAnchor = new Anchor(MESSAGES.importExtensionMenuItem()); addComponentAnchor.setStylePrimaryName("ode-ExtensionAnchor"); addComponentAnchor.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { new ComponentImportWizard().center(); } }); categoryPanels.get(ComponentCategory.EXTENSION).add(addComponentAnchor); categoryPanels.get(ComponentCategory.EXTENSION).setCellHorizontalAlignment( addComponentAnchor, HasHorizontalAlignment.ALIGN_CENTER); } @Override public void onComponentTypeAdded(List<String> componentTypes) { for (String componentType : componentTypes) { this.addComponent(componentType); } } @Override public boolean beforeComponentTypeRemoved(List<String> componentTypes) { boolean result = true; for (String componentType : componentTypes) { this.removeComponent(componentType); } return result; } @Override public void onComponentTypeRemoved(Map<String, String> componentTypes) { } @Override public void onResetDatabase() { reloadComponents(); } @Override public void clearComponents() { for (ComponentCategory category : categoryPanels.keySet()) { VerticalPanel panel = categoryPanels.get(category); panel.clear(); stackPalette.remove(panel); } for (PaletteHelper pal : paletteHelpers.values()) { pal.clear(); } categoryPanels.clear(); paletteHelpers.clear(); categoryOrder.clear(); simplePaletteItems.clear(); } // Intended for use by Blocks Toolkit, which needs to be able to refresh without // bothering the loaded extensions public void clearComponentsExceptExtension() { for (ComponentCategory category : categoryPanels.keySet()) { if (!ComponentCategory.EXTENSION.equals(category)) { VerticalPanel panel = categoryPanels.get(category); panel.clear(); stackPalette.remove(panel); } } for (PaletteHelper pal : paletteHelpers.values()) { pal.clear(); } categoryPanels.clear(); paletteHelpers.clear(); categoryOrder.clear(); simplePaletteItems.clear(); } @Override public void reloadComponents() { clearComponents(); loadComponents(); } }