// -*- mode: java; c-basic-offset: 2; -*- // Copyright 2009-2011 Google, All Rights reserved // Copyright 2011-2017 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; import static com.google.appinventor.client.Ode.MESSAGES; import com.google.appinventor.client.DesignToolbar; import com.google.appinventor.client.ErrorReporter; import com.google.appinventor.client.Ode; import com.google.appinventor.client.OdeAsyncCallback; import com.google.appinventor.client.boxes.AssetListBox; import com.google.appinventor.client.editor.FileEditor; import com.google.appinventor.client.editor.ProjectEditor; import com.google.appinventor.client.editor.ProjectEditorFactory; import com.google.appinventor.client.editor.simple.SimpleComponentDatabase; import com.google.appinventor.client.explorer.project.ComponentDatabaseChangeListener; import com.google.appinventor.client.explorer.project.Project; import com.google.appinventor.client.explorer.project.ProjectChangeListener; import com.google.appinventor.client.output.OdeLog; import com.google.appinventor.client.properties.json.ClientJsonParser; import com.google.appinventor.common.utils.StringUtils; import com.google.appinventor.shared.properties.json.JSONArray; import com.google.appinventor.shared.properties.json.JSONObject; import com.google.appinventor.shared.properties.json.JSONValue; import com.google.appinventor.shared.rpc.project.ChecksumedFileException; import com.google.appinventor.shared.rpc.project.ChecksumedLoadFile; import com.google.appinventor.shared.rpc.project.ProjectNode; import com.google.appinventor.shared.rpc.project.ProjectRootNode; import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidBlocksNode; import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidComponentsFolder; import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidFormNode; import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidProjectNode; import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidSourceNode; import com.google.appinventor.shared.storage.StorageUtil; import com.google.appinventor.shared.youngandroid.YoungAndroidSourceAnalyzer; import com.google.common.collect.Maps; import com.google.gwt.core.client.Scheduler; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.rpc.AsyncCallback; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Project editor for Young Android projects. Each instance corresponds to * one project that has been opened in this App Inventor session. * Also responsible for managing screens list for this project in * the DesignToolbar. * * @author lizlooney@google.com (Liz Looney) * @author sharon@google.com (Sharon Perl) - added logic for screens in * DesignToolbar */ public final class YaProjectEditor extends ProjectEditor implements ProjectChangeListener, ComponentDatabaseChangeListener{ // FileEditors in a YA project come in sets. Every form in the project has // a YaFormEditor for editing the UI, and a YaBlocksEditor for editing the // blocks representation of the program logic. Some day it may also have an // editor for the textual representation of the program logic. private class EditorSet { YaFormEditor formEditor = null; YaBlocksEditor blocksEditor = null; } // Maps form name -> editors for this form private final HashMap<String, EditorSet> editorMap = Maps.newHashMap(); // List of External Components private final List<String> externalComponents = new ArrayList<String>(); // Mapping of package names to extensions defined by the package (n > 1) private final Map<String, Set<String>> externalCollections = new HashMap<>(); // Number of external component descriptors loaded since there is no longer a 1-1 correspondence private volatile int numExternalComponentsLoaded = 0; // List of ComponentDatabaseChangeListeners private final List<ComponentDatabaseChangeListener> componentDatabaseChangeListeners = new ArrayList<ComponentDatabaseChangeListener>(); //State variables to help determine whether we are ready to load Project private boolean externalComponentsLoaded = false; // Database of component type descriptions private final SimpleComponentDatabase COMPONENT_DATABASE; // State variables to help determine whether we are ready to show Screen1 // Automatically select the Screen1 form editor when we have finished loading // both the form and blocks editors for Screen1 and we have added the // screen to the DesignToolbar. Since the loading happens asynchronously, // there are multiple points when we may be ready to show the screen, and // we shouldn't try to show it before everything is ready. private boolean screen1FormLoaded = false; private boolean screen1BlocksLoaded = false; private boolean screen1Added = false; /** * Returns a project editor factory for {@code YaProjectEditor}s. * * @return a project editor factory for {@code YaProjectEditor}s. */ public static ProjectEditorFactory getFactory() { return new ProjectEditorFactory() { @Override public ProjectEditor createProjectEditor(ProjectRootNode projectRootNode) { return new YaProjectEditor(projectRootNode); } }; } public YaProjectEditor(ProjectRootNode projectRootNode) { super(projectRootNode); project.addProjectChangeListener(this); COMPONENT_DATABASE = SimpleComponentDatabase.getInstance(projectId); } private void loadBlocksEditor(String formNamePassedIn) { final String formName = formNamePassedIn; final YaBlocksEditor newBlocksEditor = editorMap.get(formName).blocksEditor; newBlocksEditor.loadFile(new Command() { @Override public void execute() { YaBlocksEditor newBlocksEditor = editorMap.get(formName).blocksEditor; int pos = Collections.binarySearch(fileIds, newBlocksEditor.getFileId(), getFileIdComparator()); if (pos < 0) { pos = -pos - 1; } insertFileEditor(newBlocksEditor, pos); if (isScreen1(formName)) { screen1BlocksLoaded = true; if (readyToShowScreen1()) { OdeLog.log("YaProjectEditor.addBlocksEditor.loadFile.execute: switching to screen " + formName + " for project " + newBlocksEditor.getProjectId()); Ode.getInstance().getDesignToolbar().switchToScreen(newBlocksEditor.getProjectId(), formName, DesignToolbar.View.FORM); } } } }); } /** * Project process is completed before loadProject is started! * Currently Project process loads all External Components into Component Database */ @Override public void processProject() { resetExternalComponents(); loadExternalComponents(); callLoadProject(); } // Note: When we add the blocks editors in the loop below we do not actually // have them load the blocks file. Instead we trigger the load of a blocks file // in the callback for the loading of its associated forms file. This is important // because we have to ensure that the component type data is available when the // blocks are loaded! private void loadProject() { // add form editors first, then blocks editors because the blocks editors // need access to their corresponding form editors to set up properly for (ProjectNode source : projectRootNode.getAllSourceNodes()) { if (source instanceof YoungAndroidFormNode) { addFormEditor((YoungAndroidFormNode) source); } } for (ProjectNode source : projectRootNode.getAllSourceNodes()) { if (source instanceof YoungAndroidBlocksNode) { addBlocksEditor((YoungAndroidBlocksNode) source); } } // Add the screens to the design toolbar, along with their associated editors DesignToolbar designToolbar = Ode.getInstance().getDesignToolbar(); for (String formName : editorMap.keySet()) { EditorSet editors = editorMap.get(formName); if (editors.formEditor != null && editors.blocksEditor != null) { designToolbar.addScreen(projectRootNode.getProjectId(), formName, editors.formEditor, editors.blocksEditor); if (isScreen1(formName)) { screen1Added = true; if (readyToShowScreen1()) { // probably not yet but who knows? OdeLog.log("YaProjectEditor.loadProject: switching to screen " + formName + " for project " + projectRootNode.getProjectId()); Ode.getInstance().getDesignToolbar().switchToScreen(projectRootNode.getProjectId(), formName, DesignToolbar.View.FORM); } } } else if (editors.formEditor == null) { OdeLog.wlog("Missing form editor for " + formName); } else { OdeLog.wlog("Missing blocks editor for " + formName); } } } @Override protected void onShow() { OdeLog.log("YaProjectEditor got onShow() for project " + projectId); AssetListBox.getAssetListBox().getAssetList().refreshAssetList(projectId); DesignToolbar designToolbar = Ode.getInstance().getDesignToolbar(); FileEditor selectedFileEditor = getSelectedFileEditor(); if (selectedFileEditor != null) { if (selectedFileEditor instanceof YaFormEditor) { YaFormEditor formEditor = (YaFormEditor) selectedFileEditor; designToolbar.switchToScreen(projectId, formEditor.getForm().getName(), DesignToolbar.View.FORM); } else if (selectedFileEditor instanceof YaBlocksEditor) { YaBlocksEditor blocksEditor = (YaBlocksEditor) selectedFileEditor; designToolbar.switchToScreen(projectId, blocksEditor.getForm().getName(), DesignToolbar.View.BLOCKS); } else { // shouldn't happen! OdeLog.elog("YaProjectEditor got onShow when selectedFileEditor" + " is not a form editor or a blocks editor!"); ErrorReporter.reportError("Internal error: can't switch file editors."); } } } @Override protected void onHide() { OdeLog.log("YaProjectEditor: got onHide"); AssetListBox.getAssetListBox().getAssetList().refreshAssetList(0); FileEditor selectedFileEditor = getSelectedFileEditor(); if (selectedFileEditor != null) { selectedFileEditor.onHide(); } } @Override protected void onUnload() { OdeLog.log("YaProjectEditor: got onUnload"); super.onUnload(); for (EditorSet editors : editorMap.values()) { editors.blocksEditor.prepareForUnload(); } } // ProjectChangeListener methods @Override public void onProjectLoaded(Project project) { } @Override public void onProjectNodeAdded(Project project, ProjectNode node) { String formName = null; if (node instanceof YoungAndroidFormNode) { if (getFileEditor(node.getFileId()) == null) { addFormEditor((YoungAndroidFormNode) node); formName = ((YoungAndroidFormNode) node).getFormName(); } } else if (node instanceof YoungAndroidBlocksNode) { if (getFileEditor(node.getFileId()) == null) { addBlocksEditor((YoungAndroidBlocksNode) node); formName = ((YoungAndroidBlocksNode) node).getFormName(); } } if (formName != null) { // see if we have both editors yet EditorSet editors = editorMap.get(formName); if (editors.formEditor != null && editors.blocksEditor != null) { Ode.getInstance().getDesignToolbar().addScreen(node.getProjectId(), formName, editors.formEditor, editors.blocksEditor); } } } @Override public void onProjectNodeRemoved(Project project, ProjectNode node) { // remove blocks and/or form editor if applicable. Remove screen from // DesignToolbar. If the partner node to this one (blocks or form) was already // removed, calling DesignToolbar.removeScreen a second time will be a no-op. OdeLog.log("YaProjectEditor: got onProjectNodeRemoved for project " + project.getProjectId() + ", node " + node.getFileId()); String formName = null; if (node instanceof YoungAndroidFormNode) { formName = ((YoungAndroidFormNode) node).getFormName(); removeFormEditor(formName); } else if (node instanceof YoungAndroidBlocksNode) { formName = ((YoungAndroidBlocksNode) node).getFormName(); removeBlocksEditor(formName); } } /* * Returns the YaBlocksEditor for the given form name in this project */ public YaBlocksEditor getBlocksFileEditor(String formName) { if (editorMap.containsKey(formName)) { return editorMap.get(formName).blocksEditor; } else { return null; } } /* * Returns the YaFormEditor for the given form name in this project */ public YaFormEditor getFormFileEditor(String formName) { if (editorMap.containsKey(formName)) { return editorMap.get(formName).formEditor; } else { return null; } } /** * @return a list of component instance names */ public List<String> getComponentInstances(String formName) { List<String> components = new ArrayList<String>(); EditorSet editorSet = editorMap.get(formName); if (editorSet == null) { return components; } components.addAll(editorSet.formEditor.getComponents().keySet()); return components; } public List<String> getComponentInstances() { List<String> components = new ArrayList<String>(); for (String formName : editorMap.keySet()) { components.addAll(getComponentInstances(formName)); } return components; } // Private methods private static Comparator<String> getFileIdComparator() { // File editors (YaFormEditors and YaBlocksEditors) are sorted so that Screen1 always comes // first and others are in alphabetical order. Within each pair, the YaFormEditor is // immediately before the YaBlocksEditor. return new Comparator<String>() { @Override public int compare(String fileId1, String fileId2) { boolean isForm1 = fileId1.endsWith(YoungAndroidSourceAnalyzer.FORM_PROPERTIES_EXTENSION); boolean isForm2 = fileId2.endsWith(YoungAndroidSourceAnalyzer.FORM_PROPERTIES_EXTENSION); // Give priority to screen1. if (YoungAndroidSourceNode.isScreen1(fileId1)) { if (YoungAndroidSourceNode.isScreen1(fileId2)) { // They are both named screen1. The form editor should come before the blocks editor. if (isForm1) { return isForm2 ? 0 : -1; } else { return isForm2 ? 1 : 0; } } else { // Only fileId1 is named screen1. return -1; } } else if (YoungAndroidSourceNode.isScreen1(fileId2)) { // Only fileId2 is named screen1. return 1; } String fileId1WithoutExtension = StorageUtil.trimOffExtension(fileId1); String fileId2WithoutExtension = StorageUtil.trimOffExtension(fileId2); int compare = fileId1WithoutExtension.compareTo(fileId2WithoutExtension); if (compare != 0) { return compare; } // They are both the same name without extension. The form editor should come before the // blocks editor. if (isForm1) { return isForm2 ? 0 : -1; } else { return isForm2 ? 1 : 0; } } }; } private void addFormEditor(YoungAndroidFormNode formNode) { final YaFormEditor newFormEditor = new YaFormEditor(this, formNode); final String formName = formNode.getFormName(); OdeLog.log("Adding form editor for " + formName); if (editorMap.containsKey(formName)) { // This happens if the blocks editor was already added. editorMap.get(formName).formEditor = newFormEditor; } else { EditorSet editors = new EditorSet(); editors.formEditor = newFormEditor; editorMap.put(formName, editors); } newFormEditor.loadFile(new Command() { @Override public void execute() { int pos = Collections.binarySearch(fileIds, newFormEditor.getFileId(), getFileIdComparator()); if (pos < 0) { pos = -pos - 1; } insertFileEditor(newFormEditor, pos); if (isScreen1(formName)) { screen1FormLoaded = true; if (readyToShowScreen1()) { OdeLog.log("YaProjectEditor.addFormEditor.loadFile.execute: switching to screen " + formName + " for project " + newFormEditor.getProjectId()); Ode.getInstance().getDesignToolbar().switchToScreen(newFormEditor.getProjectId(), formName, DesignToolbar.View.FORM); } } loadBlocksEditor(formName); } }); } private boolean readyToShowScreen1() { return screen1FormLoaded && screen1BlocksLoaded && screen1Added; } private boolean readyToLoadProject() { return externalComponentsLoaded; } private void addBlocksEditor(YoungAndroidBlocksNode blocksNode) { final YaBlocksEditor newBlocksEditor = new YaBlocksEditor(this, blocksNode); final String formName = blocksNode.getFormName(); OdeLog.log("Adding blocks editor for " + formName); if (editorMap.containsKey(formName)) { // This happens if the form editor was already added. editorMap.get(formName).blocksEditor = newBlocksEditor; } else { EditorSet editors = new EditorSet(); editors.blocksEditor = newBlocksEditor; editorMap.put(formName, editors); } } private void removeFormEditor(String formName) { if (editorMap.containsKey(formName)) { EditorSet editors = editorMap.get(formName); if (editors.blocksEditor == null) { editorMap.remove(formName); } else { editors.formEditor = null; } } } private void removeBlocksEditor(String formName) { if (editorMap.containsKey(formName)) { EditorSet editors = editorMap.get(formName); if (editors.formEditor == null) { editorMap.remove(formName); } else { editors.blocksEditor = null; } } } public void addComponent(final ProjectNode node, final Command afterComponentAdded) { final ProjectNode compNode = node; final String fileId = compNode.getFileId(); AsyncCallback<ChecksumedLoadFile> callback = new OdeAsyncCallback<ChecksumedLoadFile>(MESSAGES.loadError()) { @Override public void onSuccess(ChecksumedLoadFile result) { String jsonFileContent; try { jsonFileContent = result.getContent(); } catch (ChecksumedFileException e) { this.onFailure(e); return; } JSONValue value = new ClientJsonParser().parse(jsonFileContent); COMPONENT_DATABASE.addComponentDatabaseListener(YaProjectEditor.this); if (value instanceof JSONArray) { JSONArray componentList = value.asArray(); COMPONENT_DATABASE.addComponents(componentList); for (JSONValue component : componentList.getElements()) { String name = component.asObject().get("type").asString().getString(); if (componentList.size() > 1) { // this is a extension collection String packageName = name.substring(0, name.lastIndexOf('.')); if (!externalCollections.containsKey(packageName)) { externalCollections.put(packageName, new HashSet<String>()); } externalCollections.get(packageName).add(name); name = packageName; } if (!externalComponents.contains(name)) { externalComponents.add(name); } } } else { JSONObject componentJSONObject = value.asObject(); COMPONENT_DATABASE.addComponent(componentJSONObject); // In case of upgrade, we do not need to add entry if (!externalComponents.contains(componentJSONObject.get("type").toString())) { externalComponents.add(componentJSONObject.get("type").toString()); } } numExternalComponentsLoaded++; if (afterComponentAdded != null) { afterComponentAdded.execute(); } } @Override public void onFailure(Throwable caught) { if (caught instanceof ChecksumedFileException) { Ode.getInstance().recordCorruptProject(projectId, fileId, caught.getMessage()); } super.onFailure(caught); } }; Ode.getInstance().getProjectService().load2(projectId, fileId, callback); } /** * To remove Component Files from the Project! * @param componentTypes */ public void removeComponent(Map<String, String> componentTypes) { final Ode ode = Ode.getInstance(); final YoungAndroidComponentsFolder componentsFolder = ((YoungAndroidProjectNode) project.getRootNode()).getComponentsFolder(); Set<String> removedPackages = new HashSet<String>(); for (String componentType : componentTypes.keySet()) { String typeName = componentTypes.get(componentType); if (!externalComponents.contains(typeName)) { typeName = typeName.substring(0, typeName.lastIndexOf('.')); if (removedPackages.contains(typeName)) { continue; } removedPackages.add(typeName); } final String directory = componentsFolder.getFileId() + "/" + typeName + "/"; ode.getProjectService().deleteFolder(ode.getSessionId(), this.projectId, directory, new AsyncCallback<Long>() { @Override public void onFailure(Throwable throwable) { } @Override public void onSuccess(Long date) { Iterable<ProjectNode> nodes = componentsFolder.getChildren(); for (ProjectNode node : nodes) { if (node.getFileId().startsWith(directory)) { ode.getProjectManager().getProject(node).deleteNode(node); ode.updateModificationDate(node.getProjectId(), date); } } } }); } } private void callLoadProject() { Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() { @Override public void execute() { if (!readyToLoadProject()) { // wait till project is processed Scheduler.get().scheduleDeferred(this); } else { loadProject(); } } }); } private void loadExternalComponents() { //Get the list of all ComponentNodes to be Added List<ProjectNode> componentNodes = new ArrayList<ProjectNode>(); YoungAndroidComponentsFolder componentsFolder = ((YoungAndroidProjectNode) project.getRootNode()).getComponentsFolder(); if (componentsFolder != null) { for (ProjectNode node : componentsFolder.getChildren()) { // Find all components that are json files. final String nodeName = node.getName(); if (nodeName.endsWith(".json") && StringUtils.countMatches(node.getFileId(), "/") == 3 ) { componentNodes.add(node); } } } final int componentCount = componentNodes.size(); for (ProjectNode componentNode : componentNodes) { addComponent(componentNode, new Command() { @Override public void execute() { if (componentCount == numExternalComponentsLoaded) { // true for the last component added externalComponentsLoaded = true; } } }); } if (componentCount == 0) { externalComponentsLoaded = true; // to hint that we are ready to load } } private void resetExternalComponents() { COMPONENT_DATABASE.addComponentDatabaseListener(this); COMPONENT_DATABASE.resetDatabase(); externalComponents.clear(); numExternalComponentsLoaded = 0; } private static boolean isScreen1(String formName) { return formName.equals(YoungAndroidSourceNode.SCREEN1_FORM_NAME); } public void addComponentDatbaseListener(ComponentDatabaseChangeListener cdbChangeListener) { componentDatabaseChangeListeners.add(cdbChangeListener); } public void removeComponentDatbaseListener(ComponentDatabaseChangeListener cdbChangeListener) { componentDatabaseChangeListeners.remove(cdbChangeListener); } public void clearComponentDatabaseListeners() { componentDatabaseChangeListeners.clear(); } @Override public void onComponentTypeAdded(List<String> componentTypes) { COMPONENT_DATABASE.removeComponentDatabaseListener(this); for (ComponentDatabaseChangeListener cdbChangeListener : componentDatabaseChangeListeners) { cdbChangeListener.onComponentTypeAdded(componentTypes); } for (String formName : editorMap.keySet()) { EditorSet editors = editorMap.get(formName); editors.formEditor.onComponentTypeAdded(componentTypes); editors.blocksEditor.onComponentTypeAdded(componentTypes); } } @Override public boolean beforeComponentTypeRemoved(List<String> componentTypes) { boolean result = true; Set<String> removedTypes = new HashSet<>(componentTypes); // aggregate types in the same package for (String type : removedTypes) { String packageName = COMPONENT_DATABASE.getComponentType(type); packageName = packageName.substring(0, packageName.lastIndexOf('.')); if (externalCollections.containsKey(packageName)) { for (String siblingType : externalCollections.get(packageName)) { componentTypes.add(siblingType.substring(siblingType.lastIndexOf('.') + 1)); } } } for (ComponentDatabaseChangeListener cdbChangeListener : componentDatabaseChangeListeners) { result = result & cdbChangeListener.beforeComponentTypeRemoved(componentTypes); } for (String formName : editorMap.keySet()) { EditorSet editors = editorMap.get(formName); result = result & editors.formEditor.beforeComponentTypeRemoved(componentTypes); result = result & editors.blocksEditor.beforeComponentTypeRemoved(componentTypes); } return result; } @Override public void onComponentTypeRemoved(Map<String, String> componentTypes) { COMPONENT_DATABASE.removeComponentDatabaseListener(this); for (ComponentDatabaseChangeListener cdbChangeListener : componentDatabaseChangeListeners) { cdbChangeListener.onComponentTypeRemoved(componentTypes); } for (String formName : editorMap.keySet()) { EditorSet editors = editorMap.get(formName); editors.formEditor.onComponentTypeRemoved(componentTypes); editors.blocksEditor.onComponentTypeRemoved(componentTypes); } removeComponent(componentTypes); } @Override public void onResetDatabase() { COMPONENT_DATABASE.removeComponentDatabaseListener(this); for (ComponentDatabaseChangeListener cdbChangeListener : componentDatabaseChangeListeners) { cdbChangeListener.onResetDatabase(); } for (String formName : editorMap.keySet()) { EditorSet editors = editorMap.get(formName); editors.formEditor.onResetDatabase(); editors.blocksEditor.onResetDatabase(); } } }