// -*- mode: java; c-basic-offset: 2; -*-
// Copyright © 2009-2011 Google, All Rights reserved
// Copyright © 2011-2016 Massachusetts Institute of Technology, 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 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.boxes.BlockSelectorBox;
import com.google.appinventor.client.boxes.PaletteBox;
import com.google.appinventor.client.editor.FileEditor;
import com.google.appinventor.client.editor.simple.SimpleComponentDatabase;
import com.google.appinventor.client.editor.simple.components.FormChangeListener;
import com.google.appinventor.client.editor.simple.components.MockComponent;
import com.google.appinventor.client.editor.simple.components.MockForm;
import com.google.appinventor.client.editor.simple.palette.DropTargetProvider;
import com.google.appinventor.client.editor.youngandroid.BlocklyPanel.BlocklyWorkspaceChangeListener;
import com.google.appinventor.client.editor.youngandroid.events.EventHelper;
import com.google.appinventor.client.editor.youngandroid.palette.YoungAndroidPalettePanel;
import com.google.appinventor.client.explorer.SourceStructureExplorer;
import com.google.appinventor.client.explorer.SourceStructureExplorerItem;
import com.google.appinventor.client.explorer.project.ComponentDatabaseChangeListener;
import com.google.appinventor.client.output.OdeLog;
import com.google.appinventor.client.widgets.dnd.DropTarget;
import com.google.appinventor.shared.rpc.project.ChecksumedFileException;
import com.google.appinventor.shared.rpc.project.ChecksumedLoadFile;
import com.google.appinventor.shared.rpc.project.FileDescriptorWithContent;
import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidBlocksNode;
import com.google.appinventor.shared.youngandroid.YoungAndroidSourceAnalyzer;
import com.google.common.collect.Maps;
import com.google.gwt.core.client.Callback;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.TreeItem;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.google.appinventor.client.Ode.MESSAGES;
/**
* Editor for Young Android Blocks (.blk) files.
*
* @author lizlooney@google.com (Liz Looney)
* @author sharon@google.com (Sharon Perl) added Blockly functionality
*/
public final class YaBlocksEditor extends FileEditor
implements FormChangeListener, BlockDrawerSelectionListener, ComponentDatabaseChangeListener, BlocklyWorkspaceChangeListener {
// A constant to substract from the total height of the Viewer window, set through
// the computed height of the user's window (Window.getClientHeight())
// This is an approximation of the size of the header navigation panel
private static final int VIEWER_WINDOW_OFFSET = 170;
// Database of component type descriptions
private final SimpleComponentDatabase COMPONENT_DATABASE;
// Keep a map from projectid_formname -> YaBlocksEditor for handling blocks workspace changed
// callbacks from the BlocklyPanel objects. This has to be static because it is used by
// static methods that are called from the Javascript Blockly world.
private static final Map<String, YaBlocksEditor> formToBlocksEditor = Maps.newHashMap();
// projectid_formname for this blocks editor. Our index into the static formToBlocksEditor map.
private String fullFormName;
private final YoungAndroidBlocksNode blocksNode;
// References to other panels that we need to control.
private final SourceStructureExplorer sourceStructureExplorer;
// Panel that is used as the content of the palette box
private final YoungAndroidPalettePanel palettePanel;
// Blocks area. Note that the blocks area is a part of the "document" in the
// browser (via the deckPanel in the ProjectEditor). So if the document changes (which happens
// when we switch projects) we will lose the blocks editor state, even though
// YaBlocksEditor objects are kept around when switching projects. If we come
// back to this blocks editor after having switched projects, the blocksArea
// will get reinitialized.
private final BlocklyPanel blocksArea;
// True once we've finished loading the current file.
private boolean loadComplete = false;
// if selectedDrawer != null, it is either "component_" + instance name or
// "builtin_" + drawer name
private String selectedDrawer = null;
// Keep a list of components that we know about. Need this to detect when a call to add a
// component is adding one that we already have (which can happen when a component gets
// moved from one container to another). In that case we do not want to add it to the
// blocks area again.
private Set<String> componentUuids = new HashSet<String>();
// The form editor associated with this blocks editor
private YaFormEditor myFormEditor;
YaBlocksEditor(YaProjectEditor projectEditor, YoungAndroidBlocksNode blocksNode) {
super(projectEditor, blocksNode);
this.blocksNode = blocksNode;
COMPONENT_DATABASE = SimpleComponentDatabase.getInstance(getProjectId());
fullFormName = blocksNode.getProjectId() + "_" + blocksNode.getFormName();
formToBlocksEditor.put(fullFormName, this);
blocksArea = new BlocklyPanel(this, fullFormName); // [lyn, 2014/10/28] pass in editor so can extract form json from it
blocksArea.setWidth("100%");
// This code seems to be using a rather old layout, so we cannot simply pass 100% for height.
// Instead, it needs to be calculated from the client's window, and a listener added to Window
// We use VIEWER_WINDOW_OFFSET as an approximation of the size of the top navigation bar
// New layouts don't need all this messing; see comments on selected answer at:
// http://stackoverflow.com/questions/86901/creating-a-fluid-panel-in-gwt-to-fill-the-page
blocksArea.setHeight(Window.getClientHeight() - VIEWER_WINDOW_OFFSET + "px");
Window.addResizeHandler(new ResizeHandler() {
public void onResize(ResizeEvent event) {
int height = event.getHeight();
blocksArea.setHeight(height - VIEWER_WINDOW_OFFSET + "px");
}
});
initWidget(blocksArea);
blocksArea.populateComponentTypes(COMPONENT_DATABASE.getComponentsJSONString());
// Get references to the source structure explorer
sourceStructureExplorer = BlockSelectorBox.getBlockSelectorBox().getSourceStructureExplorer();
// Listen for selection events for built-in drawers
BlockSelectorBox.getBlockSelectorBox().addBlockDrawerSelectionListener(this);
// Create palettePanel, which will be used as the content of the PaletteBox.
myFormEditor = projectEditor.getFormFileEditor(blocksNode.getFormName());
if (myFormEditor != null) {
palettePanel = new YoungAndroidPalettePanel(myFormEditor);
palettePanel.loadComponents(new DropTargetProvider() {
// TODO(sharon): make the tree in the BlockSelectorBox a drop target
@Override
public DropTarget[] getDropTargets() {
return new DropTarget[0];
}
});
palettePanel.setSize("100%", "100%");
} else {
palettePanel = null;
OdeLog.wlog("Can't get form editor for blocks: " + getFileId());
}
}
// FileEditor methods
@Override
public void loadFile(final Command afterFileLoaded) {
final long projectId = getProjectId();
final String fileId = getFileId();
OdeAsyncCallback<ChecksumedLoadFile> callback = new OdeAsyncCallback<ChecksumedLoadFile>(MESSAGES.loadError()) {
@Override
public void onSuccess(ChecksumedLoadFile result) {
String blkFileContent;
try {
blkFileContent = result.getContent();
} catch (ChecksumedFileException e) {
this.onFailure(e);
return;
}
String formJson = myFormEditor.preUpgradeJsonString(); // [lyn, 2014/10/27] added formJson for upgrading
try {
blocksArea.loadBlocksContent(formJson, blkFileContent);
blocksArea.addChangeListener(YaBlocksEditor.this);
} catch(LoadBlocksException e) {
setBlocksDamaged(fullFormName);
ErrorReporter.reportError(MESSAGES.blocksNotSaved(fullFormName));
}
loadComplete = true;
selectedDrawer = null;
if (afterFileLoaded != null) {
afterFileLoaded.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);
}
@Override
public String getTabText() {
return MESSAGES.blocksEditorTabName(blocksNode.getFormName());
}
@Override
public void onShow() {
OdeLog.log("YaBlocksEditor: got onShow() for " + getFileId());
super.onShow();
loadBlocksEditor();
sendComponentData(); // Send Blockly the component information for generating Yail
}
/*
* Updates the the whole designer: form, palette, source structure explorer, assets list, and
* properties panel.
*/
private void loadBlocksEditor() {
// Set the palette box's content.
if (palettePanel != null) {
PaletteBox paletteBox = PaletteBox.getPaletteBox();
paletteBox.setContent(palettePanel);
}
PaletteBox.getPaletteBox().setVisible(false);
// Update the source structure explorer with the tree of this form's components.
MockForm form = getForm();
if (form != null) {
// start with no component selected in sourceStructureExplorer. We
// don't want a component drawer open in the blocks editor when we
// come back to it.
updateBlocksTree(form, null);
Ode.getInstance().getWorkColumns().remove(Ode.getInstance().getStructureAndAssets()
.getWidget(2));
Ode.getInstance().getWorkColumns().insert(Ode.getInstance().getStructureAndAssets(), 1);
Ode.getInstance().getStructureAndAssets().insert(BlockSelectorBox.getBlockSelectorBox(), 0);
BlockSelectorBox.getBlockSelectorBox().setVisible(true);
AssetListBox.getAssetListBox().setVisible(true);
blocksArea.injectWorkspace();
hideComponentBlocks();
} else {
OdeLog.wlog("Can't get form editor for blocks: " + getFileId());
}
}
@Override
public void onHide() {
// When an editor is detached, if we are the "current" editor,
// set the current editor to null and clean up the UI.
// Note: I'm not sure it is possible that we would not be the "current"
// editor when this is called, but we check just to be safe.
OdeLog.log("YaBlocksEditor: got onHide() for " + getFileId());
if (Ode.getInstance().getCurrentFileEditor() == this) {
super.onHide();
unloadBlocksEditor();
} else {
OdeLog.wlog("YaBlocksEditor.onHide: Not doing anything since we're not the "
+ "current file editor!");
}
}
@Override
public void onClose() {
// our partner YaFormEditor added us as a FormChangeListener, but we remove ourself.
getForm().removeFormChangeListener(this);
BlockSelectorBox.getBlockSelectorBox().removeBlockDrawerSelectionListener(this);
formToBlocksEditor.remove(fullFormName);
}
public static void toggleWarning() {
BlocklyPanel.switchWarningVisibility();
for(YaBlocksEditor editor : formToBlocksEditor.values()){
editor.blocksArea.toggleWarning();
}
}
private void unloadBlocksEditor() {
// TODO(sharon): do something about form change listener?
// Clear the palette box.
PaletteBox paletteBox = PaletteBox.getPaletteBox();
paletteBox.clear();
paletteBox.setVisible(true);
Ode.getInstance().getWorkColumns().remove(Ode.getInstance().getStructureAndAssets().getWidget(0));
Ode.getInstance().getWorkColumns().insert(Ode.getInstance().getStructureAndAssets(), 3);
Ode.getInstance().getStructureAndAssets().insert(BlockSelectorBox.getBlockSelectorBox(), 0);
BlockSelectorBox.getBlockSelectorBox().setVisible(false);
AssetListBox.getAssetListBox().setVisible(true);
// Clear and hide the blocks selector tree
sourceStructureExplorer.clearTree();
hideComponentBlocks();
blocksArea.hideChaff();
}
@Override
public void onWorkspaceChange(BlocklyPanel panel, JavaScriptObject event) {
OdeLog.log("Got blocks area changed for " + fullFormName);
if (!EventHelper.isTransient(event)) {
Ode.getInstance().getEditorManager().scheduleAutoSave(this);
}
sendComponentData();
}
@Override
public void getBlocksImage(Callback<String, String> callback) {
blocksArea.getBlocksImage(callback);
}
public synchronized void sendComponentData() {
try {
blocksArea.sendComponentData(myFormEditor.encodeFormAsJsonString(true),
packageNameFromPath(getFileId()));
} catch (YailGenerationException e) {
e.printStackTrace();
}
}
private void updateBlocksTree(MockForm form, SourceStructureExplorerItem itemToSelect) {
TreeItem items[] = new TreeItem[3];
items[0] = BlockSelectorBox.getBlockSelectorBox().getBuiltInBlocksTree();
items[1] = form.buildComponentsTree();
items[2] = BlockSelectorBox.getBlockSelectorBox().getGenericComponentsTree(form);
sourceStructureExplorer.updateTree(items, itemToSelect);
}
// Do whatever is needed to save Blockly state when our project is about to be
// detached from the parent document. Note that this is not for saving the blocks file itself.
// We use EditorManager.scheduleAutoSave for that.
public void prepareForUnload() {
blocksArea.saveComponentsAndBlocks();
// blocksArea.saveBackpackContents();
}
@Override
public String getRawFileContent() {
return blocksArea.getBlocksContent();
}
public FileDescriptorWithContent getYail() throws YailGenerationException {
return new FileDescriptorWithContent(getProjectId(), yailFileName(),
blocksArea.getYail(myFormEditor.encodeFormAsJsonString(true),
packageNameFromPath(getFileId())));
}
/**
* Converts a source file path (e.g.,
* src/com/gmail/username/project1/Form.extension) into a package
* name (e.g., com.gmail.username.project1.Form)
* @param path the path to convert.
* @return a dot separated package name.
*/
private static String packageNameFromPath(String path) {
path = path.replaceFirst("src/", "");
int extensionIndex = path.lastIndexOf(".");
if (extensionIndex != -1) {
path = path.substring(0, extensionIndex);
}
return path.replaceAll("/", ".");
}
@Override
public void onSave() {
// Nothing to do after blocks are saved.
}
public static String getComponentInfo(String typeName) {
return SimpleComponentDatabase.getInstance().getTypeDescription(typeName);
}
public static String getComponentsJSONString(long projectId) {
return SimpleComponentDatabase.getInstance(projectId).getComponentsJSONString();
}
public static String getComponentInstanceTypeName(String formName, String instanceName) {
//use form name to get blocks editor
YaBlocksEditor blocksEditor = formToBlocksEditor.get(formName);
//get type name from form editor
return blocksEditor.myFormEditor.getComponentInstanceTypeName(instanceName);
}
public void addComponent(String typeName, String instanceName, String uuid) {
if (componentUuids.add(uuid)) {
blocksArea.addComponent(uuid, instanceName, typeName);
}
}
public void removeComponent(String typeName, String instanceName, String uuid) {
if (componentUuids.remove(uuid)) {
blocksArea.removeComponent(uuid);
}
}
public void renameComponent(String oldName, String newName, String uuid) {
blocksArea.renameComponent(uuid, oldName, newName);
}
public void showComponentBlocks(String instanceName) {
String instanceDrawer = "component_" + instanceName;
if (selectedDrawer == null || !blocksArea.drawerShowing()
|| !selectedDrawer.equals(instanceDrawer)) {
blocksArea.showComponentBlocks(instanceName);
selectedDrawer = instanceDrawer;
} else {
blocksArea.hideDrawer();
selectedDrawer = null;
}
}
public void hideComponentBlocks() {
blocksArea.hideDrawer();
selectedDrawer = null;
}
public void showBuiltinBlocks(String drawerName) {
OdeLog.log("Showing built-in drawer " + drawerName);
String builtinDrawer = "builtin_" + drawerName;
if (selectedDrawer == null || !blocksArea.drawerShowing()
|| !selectedDrawer.equals(builtinDrawer)) {
blocksArea.showBuiltinBlocks(drawerName);
selectedDrawer = builtinDrawer;
} else {
blocksArea.hideDrawer();
selectedDrawer = null;
}
}
public void showGenericBlocks(String drawerName) {
OdeLog.log("Showing generic drawer " + drawerName);
String genericDrawer = "generic_" + drawerName;
if (selectedDrawer == null || !blocksArea.drawerShowing()
|| !selectedDrawer.equals(genericDrawer)) {
blocksArea.showGenericBlocks(drawerName);
selectedDrawer = genericDrawer;
} else {
blocksArea.hideDrawer();
selectedDrawer = null;
}
}
public void hideBuiltinBlocks() {
blocksArea.hideDrawer();
}
public MockForm getForm() {
YaProjectEditor yaProjectEditor = (YaProjectEditor) projectEditor;
YaFormEditor myFormEditor = yaProjectEditor.getFormFileEditor(blocksNode.getFormName());
if (myFormEditor != null) {
return myFormEditor.getForm();
} else {
return null;
}
}
private String yailFileName() {
String fileId = getFileId();
return fileId.replace(YoungAndroidSourceAnalyzer.BLOCKLY_SOURCE_EXTENSION,
YoungAndroidSourceAnalyzer.YAIL_FILE_EXTENSION);
}
// FormChangeListener implementation
// Note: our companion YaFormEditor adds us as a listener on the form
/*
* @see com.google.appinventor.client.editor.simple.components.FormChangeListener#
* onComponentPropertyChanged
* (com.google.appinventor.client.editor.simple.components.MockComponent, java.lang.String,
* java.lang.String)
*/
@Override
public void onComponentPropertyChanged(
MockComponent component, String propertyName, String propertyValue) {
// nothing to do here
}
/*
* @see
* com.google.appinventor.client.editor.simple.components.FormChangeListener#onComponentRemoved
* (com.google.appinventor.client.editor.simple.components.MockComponent, boolean)
*/
@Override
public void onComponentRemoved(MockComponent component, boolean permanentlyDeleted) {
if (permanentlyDeleted) {
removeComponent(component.getType(), component.getName(), component.getUuid());
if (loadComplete) {
updateSourceStructureExplorer();
}
}
}
/*
* @see
* com.google.appinventor.client.editor.simple.components.FormChangeListener#onComponentAdded
* (com.google.appinventor.client.editor.simple.components.MockComponent)
*/
@Override
public void onComponentAdded(MockComponent component) {
addComponent(component.getType(), component.getName(), component.getUuid());
if (loadComplete) {
// Update source structure panel
updateSourceStructureExplorer();
}
}
/*
* @see
* com.google.appinventor.client.editor.simple.components.FormChangeListener#onComponentRenamed
* (com.google.appinventor.client.editor.simple.components.MockComponent, java.lang.String)
*/
@Override
public void onComponentRenamed(MockComponent component, String oldName) {
renameComponent(oldName, component.getName(), component.getUuid());
if (loadComplete) {
updateSourceStructureExplorer();
// renaming could potentially confuse an open drawer so close just in case
hideComponentBlocks();
selectedDrawer = null;
}
}
private void updateSourceStructureExplorer() {
MockForm form = getForm();
if (form != null) {
updateBlocksTree(form, form.getSelectedComponent().getSourceStructureExplorerItem());
}
}
/*
* @see com.google.appinventor.client.editor.simple.components.FormChangeListener#
* onComponentSelectionChange
* (com.google.appinventor.client.editor.simple.components.MockComponent, boolean)
*/
@Override
public void onComponentSelectionChange(MockComponent component, boolean selected) {
// not relevant for blocks editor - this happens on clicks in the mock form areas
}
// BlockDrawerSelectionListener implementation
/*
* @see com.google.appinventor.client.editor.youngandroid.BlockDrawerSelectionListener#
* onBlockDrawerSelected(java.lang.String)
*/
@Override
public void onBuiltinDrawerSelected(String drawerName) {
// Only do something if we are the current file editor
if (Ode.getInstance().getCurrentFileEditor() == this) {
showBuiltinBlocks(drawerName);
}
}
/*
* @see com.google.appinventor.client.editor.youngandroid.BlockDrawerSelectionListener#
* onBlockDrawerSelected(java.lang.String)
*/
@Override
public void onGenericDrawerSelected(String drawerName) {
// Only do something if we are the current file editor
if (Ode.getInstance().getCurrentFileEditor() == this) {
showGenericBlocks(drawerName);
}
}
/*
* Start up the Repl (call into the Blockly.ReplMgr via the BlocklyPanel.
*/
@Override
public void startRepl(boolean alreadyRunning, boolean forEmulator, boolean forUsb) {
blocksArea.startRepl(alreadyRunning, forEmulator, forUsb);
}
/*
* Perform a Hard Reset of the Emulator
*/
public void hardReset() {
blocksArea.hardReset();
}
// Static Function. Find the associated editor for formName and
// set its "damaged" bit. This will cause the editor manager's scheduleAutoSave
// method to ignore this blocks file and not save it out.
public static void setBlocksDamaged(String formName) {
YaBlocksEditor editor = formToBlocksEditor.get(formName);
if (editor != null) {
editor.setDamaged(true);
}
}
/*
* Trigger a Companion Update
*/
@Override
public void updateCompanion() {
blocksArea.updateCompanion();
}
/*
* [lyn, 2014/10/28] Added for accessing current form json from BlocklyPanel
* Encodes the associated form's properties as a JSON encoded string. Used by YaBlocksEditor as well,
* to send the form info to the blockly world during code generation.
*/
protected String encodeFormAsJsonString(boolean forYail) {
return myFormEditor.encodeFormAsJsonString(forYail);
}
@Override
public void onComponentTypeAdded(List<String> componentTypes) {
blocksArea.populateComponentTypes(COMPONENT_DATABASE.getComponentsJSONString());
blocksArea.verifyAllBlocks();
}
@Override
public boolean beforeComponentTypeRemoved(List<String> componentTypes) {
return true;
}
@Override
public void onComponentTypeRemoved(Map<String, String> componentTypes) {
blocksArea.populateComponentTypes(COMPONENT_DATABASE.getComponentsJSONString());
blocksArea.verifyAllBlocks();
}
@Override
public void onResetDatabase() {
blocksArea.populateComponentTypes(COMPONENT_DATABASE.getComponentsJSONString());
blocksArea.verifyAllBlocks();
}
@Override
public void makeActiveWorkspace() {
blocksArea.makeActive();
}
}